077dba3d98
- 删除原迁移误将 ticket.status=3 (取消/拒绝) 当作已通过迁入的 5 条脏数据
- 补迁 8 条遗漏的 ticket.status=4 已通过单
- content 改为 '历史提现 #<ticket_id>' 支持反查 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 个用户余额失衡。
196 lines
9.6 KiB
SQL
196 lines
9.6 KiB
SQL
-- ============================================================================
|
|
-- 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_id>',支持反查 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 必须形如 '历史提现 #<digits>'
|
|
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;
|