Files
pj_service_cloud/db/20261102_wallet_recharge_idempotency.md
T

4.8 KiB

批量充值、扣减防重上线说明

本次只修代码和新增防重表。脚本不删除历史流水、不回填防重记录、不调整余额。历史差额、补偿扣款必须另行核账;不能根据 biz_id 重复就直接删除或再次扣钱。

修复范围

  • 提交、编辑、删除、回调和补记先锁定申请。已经提交或存在资金流水的申请不能再次发起、重置审批字段或更改金额明细;同一发票只能绑定一次。
  • 回调在锁内通过“租户 + 申请 + 流程号 + 审核中”条件变更状态,并检查影响行数。状态、发票、整批余额、流水和防重登记使用同一本地事务。任一明细失败全部回滚;钱包缺失不再跳过部分入账。
  • addWalletBalance 在数据库钱包行锁内读取最新余额,检查更新/流水写入结果和整数溢出。类型 2(批量更新余额)与 8(退还各自食堂)按 tenant_id + wallet_id + biz_type + biz_id 防重。正金额、负金额、零金额均覆盖。
  • 类型 1 人工调账仍允许使用同一用户业务号多次操作。未对整张 wallet_transaction 添加 biz_id 唯一限制,也未将本次防重规则扩展到所有业务类型。
  • 补记仅允许审批通过的申请,与正常回调使用相同金额方向、流水类型和入账防重。草稿、审核中、拒绝的申请不能补记。
  • 正元同步改到资金事务提交后,并用独立事务执行;失败记录申请号和租户,不再影响已提交的资金。此同步仍为提交后的同步调用,未新增持久化重试队列,也未保证回调响应在 10 秒内完成。

历史数据兼容

防重表上线时为空。首次收到旧业务号时,会在数据库当前读中查询相同租户/钱包/类型/业务号的历史流水,包括逻辑删除的记录:

情况 行为
无历史流水 余额、流水、防重键一起提交
唯一有效流水,金额相同 登记原流水,直接返回原结果,余额不变
多条流水、金额不同、流水已删除,或防重登记与流水不一致 拒绝处理,整笔回滚并记录异常,需要人工核查

不要通过删除防重表记录或修改审批状态来“重试”。若正元同步失败,应重试同步用户信息;重放一个已处理回调不会再执行同步。恢复历史数据必须以核账结果为依据。

上线顺序

  1. 暂停批量充值/扣减提交、审批回调和补记入口,排空在途请求。保留资金数据备份和当前对账结果。
  2. 连接目标钱包库,在该库执行 20261102_wallet_recharge_idempotency.sql。确认表为 InnoDB,复合主键与脚本一致。脚本可重复执行;不要对历史流水直接添加全局唯一 biz_id。
  3. 更新并重启全部钱包服务实例后恢复入口。新旧版本混用期间,旧实例不会遵守新的防重协议,因此不能在混用时恢复资金写入。
  4. 在预发布环境使用指定测试账户依次验证充值、记账扣款、退还食堂、重复回调、补记,以及中途失败回滚;对照余额、累计金额、流水数量和申请状态。生产验证须使用经业务确认的真实交易,并进行对应对账。
  5. 监控“批量入账金额冲突”“批量入账历史流水异常”“充值回调未找到有效申请”“充值回调失败”和“正元同步失败”日志。异常时保留 tenantId / applyId / processId / taskId / walletId / bizId,核查后处理。

回退应用版本也必须先停写并排空请求;旧版本本身没有防重能力。保留新增防重表,不能把删表作为回退步骤。此改动未修改工作流引擎的通知发送机制;历史重复通知或重新发起审批的具体触发原因仍需生产请求/审批日志确认。

本地验证

WalletRechargeIdempotencyMySqlTest 使用真实 MySQL、InnoDB 行锁、项目租户拦截器/自动填充配置、生产 Mapper SQL 和 Spring 事务代理。仅新建并删除 codex_recharge_* 隔离测试库,不读取或改动生产数据。需显式提供 WALLET_RECHARGE_TEST_JDBC_URL、MYSQL_USER、MYSQL_PWD,没有连接时该组测试会跳过,不能将跳过当作通过。

主要覆盖并发/顺序重复入账(正数、负数、零、类型8)、同钱包不同业务并发、旧事务快照、历史重复/删除/金额冲突、流水失败/钱包更新失败回滚、租户隔离、重复人工调账、重复提交/重复回调、整批和发票状态回滚、补记方向与审批限制、发票重复绑定、提交后同步与同步失败隔离。

2026-11-02 本地验证结果:39 项真实 MySQL 用例及 20 项相关回归用例全部通过(共 59 项,0 失败、0 错误、0 跳过);钱包服务及依赖模块 Maven package 成功。测试库已清理。未部署、未执行生产 SQL。