Просмотр исходного кода

docs(omg-pay): plan/tasks 顺延 Phase 10 账本重构(追加式 S,T051-T062)

tasks.md 加 Phase 10(账本重构 + 问题 A/B 收尾),12 个 task:
- 前置:parseKvResponse 重复键放宽(T051,queryTrade 安全网前提)、collation 执行(T052)
- reconcile 全量补单:扫描改 EXISTS(T053)、两段式(T054)、三道节流闸(T055)
- 追加式 S 落地:DDL is_active/expire_date/生成列唯一索引(T056)、实体+mapper
  刻意不提供 updateMerchantTradeNo(T057)、create 复用预检 afterCommit 锁(T058)
- 读侧 7 处 getLatestByDdId 迁移(T059)、退款遍历(T060)
- 历史 9 行迁移逐行 queryTrade(T061)、端到端验证+fixture(T062)

plan.md 加账本重构引用(payment-attempt-lifecycle.md + Phase 10)。
基于 payment-attempt-lifecycle.md §6/§8 + 对抗发现前置,wf_06aa691f 否决单行后定追加式 S。

Co-Authored-By: Claude <noreply@anthropic.com>
qmj 2 недель назад
Родитель
Сommit
68009dae39
2 измененных файлов с 32 добавлено и 0 удалено
  1. 2 0
      specs/016-omg-payment/plan.md
  2. 30 0
      specs/016-omg-payment/tasks.md

+ 2 - 0
specs/016-omg-payment/plan.md

@@ -10,6 +10,8 @@
 
 技术决策与备选见 [research.md](research.md),表结构见 [data-model.md](data-model.md),接口契约见 [contracts/api.md](contracts/api.md),验证场景见 [quickstart.md](quickstart.md)。
 
+**追加(2026-08-12):账本重构(问题 A 多 MTN 堆积 + 问题 B 验签/补单修复)落地追加式 S(双轴 is_active ⊥ pay_status),设计见 [payment-attempt-lifecycle.md](payment-attempt-lifecycle.md),任务见 tasks.md Phase 10(T051–T062)。bugB 验签已修(commit `1a424c3` CASE_INSENSITIVE_ORDER,queryTrade 可靠)。**
+
 ## Technical Context
 
 **Language/Version**: Java 21(Spring Boot 3 / RuoYi-Vue-Plus,MyBatis-Plus,Lombok)

+ 30 - 0
specs/016-omg-payment/tasks.md

@@ -167,6 +167,36 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ---
 
+
+## Phase 10: 账本重构(追加式 S)+ 问题 A/B 收尾(Priority: P1)
+
+**Goal**: 修复问题 A(每次 create 堆积流水,连点 9 行)+ 问题 B 余波(reconcile 漏单/节流),落地追加式 S(双轴 is_active ⊥ pay_status,物理保留历史行接迟到 notify + 审计)。逻辑上每订单 1 条活跃未付行(is_active=1),换号=新增行+旧行 is_active→0(pay_status 不动),mapper 刻意不提供 updateMerchantTradeNo。
+
+**Source**: [payment-attempt-lifecycle.md](payment-attempt-lifecycle.md)(§6 S 方案 + §8 落地顺序 + §0 现状 + §9 待拍板)
+
+**Prerequisites**: ✅ bugB 验签已修(`1a424c3` CASE_INSENSITIVE_ORDER,queryTrade 可靠);✅ parseKvResponse 尾随& 放宽(`7a8e248`);⏳ collation 3 表 CONVERT 待执行(sql.md 2026-08-12 节)。
+
+**Independent Test**: 同订单连点 create 9 次 → 只 1 条 is_active=1 活跃行(不堆积);ATM 取号后过期重新 create → 新行 + 旧行 is_active=0(虚帐仍可付,迟到 notify 落旧行 markSuccess);id=26 已付历史行迁移后 reconcile 补单核销;同门店多单未付 reconcile 不触发 OMG 403。
+
+- [ ] T051 [P] **前置:parseKvResponse 重复键放宽**(`ruoyi-admin/.../utils/omg/OmgPay.java`):重复键 last-wins 不抛(用同一份 Map 重算 CheckMacValue 验签,避 kill-shot ⑥ 解析放宽与验签打架);仅整段无 `=` 或缺 CheckMacValue 才抛;`OmgPayTest` 补重复键用例。S 依赖 queryTrade 做换号/补单安全网,重复键仍抛会让安全网失效。
+- [ ] T052 **collation 对齐执行确认**:执行 `updatesql/sql.md` 2026-08-12 节(pos_order_omg_payment/pos_order_omg_refund/pos_store_omg CONVERT 到 utf8mb4_unicode_ci);执行后 `selectLeakOrderDdIds` JOIN 不再报 Illegal mix of collations。
+- [ ] T053 **reconcile 扫描改全量未付**(`PosOrderOmgPaymentMapper.xml` + Service):`selectLeakOrderDdIds` 改 `EXISTS(任意 pay_status=0 行)` 去重 ddId(不再 MAX(id) 取一条);新增 `listUnpaidByDdId`(全量 pay_status=0,is_active 不限);`selectLatestPaidByDdId` 扩 `IN(1,3,4)`(修退款中/已退返 null);新增 `selectLatestRefundableByDdId`(pay_status IN(1,3,4))、`existsPaidByDdId`。
+- [ ] T054 **reconcileByQuery 两段式**(`OmgPayController.reconcileByQuery`):① 先 selectLatestPaidByDdId(pay_status=1 且 order.payStatus=0 → handlePaymentSuccess 自愈,L774 不能变死代码);② 否则 listUnpaidByDdId 遍历全量未付行 queryTrade:TradeStatus=1 applyPaidResult break / 0 no-op / 10200095 且 trade_no IS NULL markFail / 10200095 且 trade_no 非空 no-op。
+- [ ] T055 **三道节流闸(防 OMG 403)**(新建 queryTrade 节流组件 `ruoyi-admin/.../utils/omg/OmgQueryThrottle.java` + Redis):首查跳过 create_time<NOW()-INTERVAL 40 MINUTE;per-MerchantID 令牌桶 `omg:qt:token:{merchantId}`(1token/3s,burst1)acquire 失败 skip 本行本轮;per-ddId TTL(`omg:reconcile:dd:{ddId}` 180s / `omg:query:dd:{ddId}` 60s)。reconcileByQuery 与 /query 接入。
+- [ ] T056 **DDL 追加式 S 字段**(写 `updatesql/sql.md`,不直接执行):`pos_order_omg_payment` + `expire_date DATETIME NULL`(仅 paymentInfo 回调写,create 不猜)+ `is_active TINYINT NOT NULL DEFAULT 1` + 生成列 `active_dd_id=IF(is_active=1 AND pay_status=0,dd_id,NULL) VIRTUAL` + `UNIQUE uk_omg_active_dd(active_dd_id)`(需 MySQL 5.7.6+,**生产版本待确认**;不够则退化 `idx_omg_dd_active(dd_id,is_active,pay_status)`+app 锁+CAS);历史回填(每 ddId 最新 pay_status=0 置 is_active=1,其余 0;终态行 0;**绝不回填 expire_date**)。
+- [ ] T057 **实体 + mapper**(`PosOrderOmgPayment.java` + Mapper/XML):+ isActive/expireDate 字段(activeDdId 生成列只读可不映射);加 `selectActiveForReuse(ddId,freshMin)`(WHERE dd_id AND is_active=1 AND pay_status=0 AND trade_no IS NULL AND create_time>=NOW()-INTERVAL freshMin MINUTE ORDER BY id DESC LIMIT 1)、`markActiveHistorical(ddId)`(CAS UPDATE is_active=0 WHERE dd_id AND is_active=1 AND pay_status=0,不动 pay_status)、`markPaymentInfoIfOpen` 扩 expire_date 解析;**刻意不提供 updateMerchantTradeNo/updateTradeNo**(从能力杜绝换号丢钱)。
+- [ ] T058 **create 复用预检**(`OmgPayController.create`,保留 @Transactional):Redisson `RLock lock:omg:create:{ddId}` tryLock(等 3s),**锁释放在 TransactionSynchronizationManager.registerSynchronization 的 afterCommit**(仿 WalletService.returnPoints,**禁 try/finally unlock**——否则 TX 提交前泄锁致重复活跃行);selectActiveForReuse 命中复用 MTN(form 用当前时刻重算 CheckMacValue);未命中同 TX:markActiveHistorical(旧 is_active→0)+ createPayment 新行(is_active=1, expire_date=NULL);DuplicateKeyException 重试 3 次(uk_omg_active_dd DB 兜底)。
+- [ ] T059 **读侧 7 处 getLatestByDdId 全迁移**:`getPaymentInfo`/`refundOrderOutcome`/L569 早退/`markRefunding` 并发/`confirmManualRefundOutcome`/`reconcileByQuery`(T054)/`OrderLifecycleService.buildContext`/`validateOmgReconcile`/`validateOmgRefund` → `selectLatestRefundableByDdId`(pay_status IN(1,3,4))或 `listPaidByDdId`;修"订单未支付"早退为 listPaidByDdId 为空才 FAILED。
+- [ ] T060 **退款遍历**(`refundOrderOutcome`):改 `listPaidByDdId` 遍历所有 pay_status=1 且 trade_no 非空行;每行先查 `pos_order_omg_refund` 是否 action=R 且 rtnCode=1 → 跳过;否则 `markRefundingIfPaid`(1→4 CAS)→ `DoAction(Action=R)` 逐笔退(每笔独立 tradeNo);ATM/超商无退款 API 走 manual pending;孤立已付(双付第二笔)写 `ipn_log(type=omg_orphan_paid)` + 运营告警。
+- [ ] T061 **历史 9 行迁移**(orders 如 991786433092835 id=18~26):迁移前对每行 queryTrade 确认(已可靠);TradeStatus=1 的行无论本地 pay_status 一律保留并回填 pay_status=1(如 id=26 OMGFE0494FB7AD1474A8);仅 TradeStatus=10200095 或(0 且过 ExpireDate 且非信用卡)才允许清理;优先 is_active 软标记而非物理删除(lifecycle §9 Q10)。
+- [ ] T062 **端到端验证**(stage):create 连点复用(单活跃 is_active=1)、reconcile 两段式补单(含 id=26)、过期延期支付轮换、退款遍历多笔、三道节流闸不触发 403;抓 stage 真实 QueryTradeInfo 大响应(ATM/超商 NeedExtraPaidInfo=Y)fixture 补 OmgPayTest(重复键验证)。
+
+**Checkpoint**: 问题 A 收敛(连点单活跃)、问题 B 闭环(reconcile 全量补单 + 节流)、追加式 S 落地(is_active 双轴 + mapper 杜绝换号 + 7 处读侧迁移 + 退款遍历);id=26 类历史漏单可补。
+
+**§9 待拍板(实现/stage 确认,不阻塞)**:freshMin(建议 3min,stage 实测同 MTN 重 POST)、生产 MySQL 版本(决定生成列唯一索引)、reuse 保留原 MerchantTradeDate、节流精确参数、AFTEE 窗口(现 168h 不够,建议 720h 或按 expire_date 动态)、/query 轮询 cadence(配合前端 30-60s)。
+
+---
+
 ## Dependencies & Execution Order
 
 ### Phase 依赖