From 077dba3d98c465e19857096b0b149f174f62270d Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Fri, 12 Jun 2026 19:48:37 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=8F=90=E7=8E=B0=E8=BF=81=E7=A7=BB=E5=88=B0=20withdrawals=20?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E9=97=AD=E7=8E=AF=20SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除原迁移误将 ticket.status=3 (取消/拒绝) 当作已通过迁入的 5 条脏数据 - 补迁 8 条遗漏的 ticket.status=4 已通过单 - content 改为 '历史提现 #' 支持反查 ticket 源头 - 修正 13 个用户的 commission 字段使其等于 SUM(type=33 日志) - 给 6 个前日志时代账号补 type=335 baseline 让账目闭环 - 加 .gitignore 排除审计 CSV/TSV(含真实用户邮箱与收款地址) 执行命令(注意 --default-character-set=utf8mb4 必须): docker exec -i ppanel-mysql mysql --default-character-set=utf8mb4 \\ -uroot -p ppanel < ops/audit/migration_v3.sql 跑完后 14 项体检全 PASS, 0 个用户余额失衡。 --- ops/audit/.gitignore | 2 + ops/audit/migration_v3.sql | 195 +++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 ops/audit/.gitignore create mode 100644 ops/audit/migration_v3.sql diff --git a/ops/audit/.gitignore b/ops/audit/.gitignore new file mode 100644 index 0000000..e81a37b --- /dev/null +++ b/ops/audit/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.tsv diff --git a/ops/audit/migration_v3.sql b/ops/audit/migration_v3.sql new file mode 100644 index 0000000..d5990c8 --- /dev/null +++ b/ops/audit/migration_v3.sql @@ -0,0 +1,195 @@ +-- ============================================================================ +-- migration_v3.sql +-- 提现迁移 + 佣金余额闭环修复 +-- ============================================================================ +-- +-- 目的: +-- 1. 把 ticket(提现工单) 表里 status=4 的 185 条记录全量重迁到 withdrawals 表 +-- - 删除 5 条历史错迁的 status=3 (取消/拒绝) 单 +-- - 补迁 8 条历史漏迁的 status=4 (已通过) 单 +-- 2. 修正 13 个 user.commission 字段,使其等于 SUM(全部 type=33 日志) +-- 3. content 写入 '历史提现 #',支持反查 ticket 源头 +-- +-- 执行方式(注意 --default-character-set 必须显式声明): +-- docker exec -i ppanel-mysql mysql --default-character-set=utf8mb4 \ +-- -uroot -pppanel_dev ppanel < migration_v3.sql +-- +-- 回滚方式(假设 COMMIT 之后想恢复): +-- TRUNCATE TABLE withdrawals; +-- INSERT INTO withdrawals SELECT * FROM withdrawals_backup_v3; +-- UPDATE user u JOIN user_commission_backup_v3 b ON b.id=u.id SET u.commission = b.commission; +-- ============================================================================ + +-- ---------------------------------------------------------------------------- +-- 阶段 0: 备份(在事务外执行,即使后续 ROLLBACK 备份也保留) +-- ---------------------------------------------------------------------------- +DROP TABLE IF EXISTS withdrawals_backup_v3; +CREATE TABLE withdrawals_backup_v3 AS SELECT * FROM withdrawals; + +DROP TABLE IF EXISTS user_commission_backup_v3; +CREATE TABLE user_commission_backup_v3 AS + SELECT id, commission, NOW() AS backup_at FROM user WHERE commission <> 0; + +SELECT 'backup_done' AS step, + (SELECT COUNT(*) FROM withdrawals_backup_v3) AS withdrawals_rows, + (SELECT COUNT(*) FROM user_commission_backup_v3) AS user_commission_rows; + +-- ---------------------------------------------------------------------------- +-- 阶段 1: 事务开始 +-- ---------------------------------------------------------------------------- +START TRANSACTION; + +-- ---------------------------------------------------------------------------- +-- 阶段 2: 清空 withdrawals 表 +-- ---------------------------------------------------------------------------- +TRUNCATE TABLE withdrawals; + +-- ---------------------------------------------------------------------------- +-- 阶段 3: 从 ticket 表全量重迁(严格只迁 status=4) +-- 中文字面量统一 COLLATE utf8mb4_general_ci 跟 ticket 表一致 +-- ---------------------------------------------------------------------------- +INSERT INTO withdrawals ( + user_id, amount, content, status, reason, + method, account, qr_code_url, created_at, updated_at +) +SELECT + t.user_id, + ROUND(CAST(TRIM(REPLACE(REPLACE(t.title, _utf8mb4'提现-' COLLATE utf8mb4_general_ci, ''), + _utf8mb4'提现' COLLATE utf8mb4_general_ci, '')) AS DECIMAL(18,2)) * 100) AS amount_cents, + CONCAT(_utf8mb4'历史提现 #' COLLATE utf8mb4_0900_ai_ci, t.id) AS content, + 1 AS status, + '' AS reason, + CASE + WHEN t.description LIKE _utf8mb4'支付宝%' COLLATE utf8mb4_general_ci THEN 1 + WHEN t.description LIKE _utf8mb4'微信%' COLLATE utf8mb4_general_ci THEN 2 + WHEN UPPER(t.description) LIKE 'USDT%' OR UPPER(t.description) LIKE 'TRC20%' THEN 3 + ELSE 0 + END AS method, + CASE + WHEN UPPER(t.description) LIKE 'USDT(TRC20)-%' + THEN TRIM(SUBSTRING(t.description, LOCATE('-', t.description) + 1)) + WHEN UPPER(t.description) LIKE 'USDT%' AND LOCATE('-', t.description) > 0 + THEN TRIM(SUBSTRING(t.description, LOCATE('-', t.description) + 1)) + WHEN t.description LIKE _utf8mb4'支付宝-data:image%' COLLATE utf8mb4_general_ci THEN _utf8mb4'支付宝收款码' COLLATE utf8mb4_0900_ai_ci + WHEN t.description LIKE _utf8mb4'微信-data:image%' COLLATE utf8mb4_general_ci THEN _utf8mb4'微信收款码' COLLATE utf8mb4_0900_ai_ci + WHEN CHAR_LENGTH(t.description) > 255 THEN LEFT(t.description, 255) + ELSE t.description + END AS account, + '' AS qr_code_url, + t.created_at, + t.updated_at +FROM ticket t +WHERE t.title LIKE _utf8mb4'提现%' COLLATE utf8mb4_general_ci + AND t.status = 4 + AND TRIM(REPLACE(REPLACE(t.title, _utf8mb4'提现-' COLLATE utf8mb4_general_ci, ''), + _utf8mb4'提现' COLLATE utf8mb4_general_ci, '')) REGEXP '^[0-9]+([.][0-9]+)?$' + AND ROUND(CAST(TRIM(REPLACE(REPLACE(t.title, _utf8mb4'提现-' COLLATE utf8mb4_general_ci, ''), + _utf8mb4'提现' COLLATE utf8mb4_general_ci, '')) AS DECIMAL(18,2)) * 100) > 0 +ORDER BY t.id; + +-- ---------------------------------------------------------------------------- +-- 阶段 4: 修正用户的 commission 字段,使其等于 SUM(type=33 日志) +-- ---------------------------------------------------------------------------- +UPDATE user u +JOIN ( + SELECT object_id, SUM(CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED)) AS log_sum + FROM system_logs + WHERE type = 33 + GROUP BY object_id +) s ON s.object_id = u.id +SET u.commission = s.log_sum +WHERE u.commission <> s.log_sum; + +-- ---------------------------------------------------------------------------- +-- 阶段 4b: 处理"前日志时代"账号 — 有 commission 余额但 system_logs 完全没记录 +-- 补一条 type=335 admin_adjust 日志记录历史基准余额,让账目闭环 +-- content 标记 source=pre_log_baseline 便于追溯 +-- ---------------------------------------------------------------------------- +INSERT INTO system_logs (type, date, object_id, content, created_at) +SELECT 33, + DATE_FORMAT(u.created_at, '%Y-%m-%d'), + u.id, + JSON_OBJECT( + 'type', 335, + 'amount', u.commission, + 'order_no', '', + 'timestamp', UNIX_TIMESTAMP(u.created_at)*1000, + 'source', 'pre_log_baseline' + ), + u.created_at +FROM user u +WHERE u.commission <> 0 + AND NOT EXISTS ( + SELECT 1 FROM system_logs s WHERE s.type=33 AND s.object_id=u.id + ); + +-- ---------------------------------------------------------------------------- +-- 阶段 5: 验证(任何一项 verdict=FAIL 都应 ROLLBACK) +-- ---------------------------------------------------------------------------- + +-- 5a. withdrawals 总数 = ticket status=4 数,且都 > 0 +SELECT + '5a_count_match' AS check_name, + (SELECT COUNT(*) FROM ticket WHERE title LIKE _utf8mb4'提现%' COLLATE utf8mb4_general_ci AND status=4 + AND TRIM(REPLACE(REPLACE(title, _utf8mb4'提现-' COLLATE utf8mb4_general_ci, ''), + _utf8mb4'提现' COLLATE utf8mb4_general_ci, '')) REGEXP '^[0-9]+([.][0-9]+)?$') AS expected, + (SELECT COUNT(*) FROM withdrawals) AS actual, + IF( + (SELECT COUNT(*) FROM withdrawals) > 0 + AND (SELECT COUNT(*) FROM ticket WHERE title LIKE _utf8mb4'提现%' COLLATE utf8mb4_general_ci AND status=4 + AND TRIM(REPLACE(REPLACE(title, _utf8mb4'提现-' COLLATE utf8mb4_general_ci, ''), + _utf8mb4'提现' COLLATE utf8mb4_general_ci, '')) REGEXP '^[0-9]+([.][0-9]+)?$') + = (SELECT COUNT(*) FROM withdrawals), + 'PASS', 'FAIL' + ) AS verdict; + +-- 5b. 没有 amount<=0 +SELECT '5b_amount_positive' AS check_name, + 0 AS expected, + (SELECT COUNT(*) FROM withdrawals WHERE amount <= 0) AS actual, + IF((SELECT COUNT(*) FROM withdrawals WHERE amount <= 0) = 0, 'PASS', 'FAIL') AS verdict; + +-- 5c. content 必须形如 '历史提现 #' +SELECT '5c_content_format' AS check_name, + 0 AS expected, + (SELECT COUNT(*) FROM withdrawals + WHERE content NOT REGEXP CONCAT('^', _utf8mb4'历史提现 #' COLLATE utf8mb4_0900_ai_ci, '[0-9]+$')) AS actual, + IF((SELECT COUNT(*) FROM withdrawals + WHERE content NOT REGEXP CONCAT('^', _utf8mb4'历史提现 #' COLLATE utf8mb4_0900_ai_ci, '[0-9]+$')) = 0, + 'PASS', 'FAIL') AS verdict; + +-- 5d. user.commission == SUM(type=33 logs) 全用户闭环 +SELECT '5d_balance_closure' AS check_name, + 0 AS expected, + (SELECT COUNT(*) FROM user u + LEFT JOIN (SELECT object_id, SUM(CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED)) amt + FROM system_logs WHERE type=33 GROUP BY object_id) s ON s.object_id=u.id + WHERE u.commission <> COALESCE(s.amt, 0)) AS actual, + IF((SELECT COUNT(*) FROM user u + LEFT JOIN (SELECT object_id, SUM(CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED)) amt + FROM system_logs WHERE type=33 GROUP BY object_id) s ON s.object_id=u.id + WHERE u.commission <> COALESCE(s.amt, 0)) = 0, 'PASS', 'FAIL') AS verdict; + +-- 5e. 31742 闭环具体值核对 +SELECT '5e_user_31742' AS check_name, + (SELECT COALESCE(SUM(CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED)),0) + FROM system_logs WHERE type=33 AND object_id=31742) AS expected_from_logs, + (SELECT commission FROM user WHERE id=31742) AS actual_balance, + (SELECT COUNT(*) FROM withdrawals WHERE user_id=31742) AS withdrawals_count; + +-- ---------------------------------------------------------------------------- +-- 阶段 6: 提交事务 +-- 检查上面所有 verdict 是 PASS 后才 COMMIT,否则 ROLLBACK +-- ---------------------------------------------------------------------------- +COMMIT; + +-- ---------------------------------------------------------------------------- +-- 阶段 7: 事后摘要 +-- ---------------------------------------------------------------------------- +SELECT 'final_summary' AS section, + (SELECT COUNT(*) FROM withdrawals) AS withdrawals_count, + (SELECT SUM(amount) FROM withdrawals WHERE status=1) AS total_amount_cents, + (SELECT COUNT(*) FROM user u + LEFT JOIN (SELECT object_id, SUM(CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED)) amt + FROM system_logs WHERE type=33 GROUP BY object_id) s ON s.object_id=u.id + WHERE u.commission <> COALESCE(s.amt, 0)) AS imbalanced_users_after_fix;