-- ============================================================ -- 支付回调幂等加固:t_pay_log.out_trade_no 唯一索引 -- -- 事故背景:2026-10-01 21:06 商户订单号 202610012106065931549256(30 元) -- 微信支付回调被 Redis 幂等锁误判为"重复通知",代码在未做任何入账的情况下 -- 应答了 HTTP 200,微信认为处理成功即停止重试,用户充值永久丢失。 -- 修复后:Redis 锁仅用于削峰,幂等的最终保证是本唯一索引 -- (并发投递时只有一个事务能落库,落库失败的一方整体回滚,不会重复加钱)。 -- ============================================================ -- 【执行前检查 1】是否已存在重复的商户订单号(正常应为 0 行;有结果必须先人工核对处理) -- 若本查询有结果(当前生产库为 10 条),请先执行 database/8-dedupe-pay-log.sql -- 完成"诊断 → 定损 → 备份 → 去重",返回 0 行后再继续本文件。 SELECT out_trade_no, COUNT(*) AS cnt, MIN(id) AS keep_id, GROUP_CONCAT(id) AS ids FROM charge_app.t_pay_log GROUP BY out_trade_no HAVING COUNT(*) > 1; -- 【执行前检查 2】当前索引情况(若已存在 uk_pay_log_out_trade_no 则无需重复执行变更) SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'charge_app' AND TABLE_NAME = 't_pay_log'; -- 【正式变更】商户订单号唯一(微信侧 out_trade_no 本身唯一) ALTER TABLE charge_app.t_pay_log ADD UNIQUE KEY uk_pay_log_out_trade_no (out_trade_no); -- 【验证】NON_UNIQUE 应为 0 SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'charge_app' AND TABLE_NAME = 't_pay_log' AND INDEX_NAME = 'uk_pay_log_out_trade_no'; -- 【上线后对账 1】唯一性:应返回 0 行 SELECT out_trade_no, COUNT(*) AS cnt FROM charge_app.t_pay_log GROUP BY out_trade_no HAVING COUNT(*) > 1; -- 【上线后对账 2】正向:已确认的充值流水必须都能在 t_pay_log 找到同一订单号,应返回 0 行 -- 有结果 = 该笔充值缺少支付记录(可能被清理误删,或历史人工补单),需逐条确认后补齐 pay_log, -- 否则后续再收到该订单回调会走不到幂等快路径而重复入账 SELECT w.id, w.user_id, w.order_no, w.amount, w.status, w.create_time FROM charge_app.t_wallet_detail w LEFT JOIN charge_app.t_pay_log p ON p.out_trade_no = w.order_no WHERE w.type = 1 AND w.status = 1 AND p.id IS NULL; -- 【上线后对账 3】反向:成功的支付记录必须都能找到对应的已确认充值流水,应返回 0 行 -- 有结果 = 微信收了钱但钱包流水没确认(本次事故就是这一类),需按 8-incident-*.sql 的方式补单 SELECT p.id, p.user_id, p.out_trade_no, p.total, p.transaction_id, p.success_time FROM charge_app.t_pay_log p LEFT JOIN charge_app.t_wallet_detail w ON w.order_no = p.out_trade_no AND w.type = 1 AND w.status = 1 WHERE p.trade_state = 'SUCCESS' AND w.id IS NULL;