|
@@ -25,11 +25,11 @@
|
|
|
**§8 落地顺序调整**(P0-诊断/P0-修B 已完成):
|
|
**§8 落地顺序调整**(P0-诊断/P0-修B 已完成):
|
|
|
- ✅ P0-诊断 + P0-修B(bugB 验签)+ parseKvResponse 尾随&。
|
|
- ✅ P0-诊断 + P0-修B(bugB 验签)+ parseKvResponse 尾随&。
|
|
|
- ⏳ collation 待执行。
|
|
- ⏳ collation 待执行。
|
|
|
-- ➡️ **接下来**:P0-补单(reconcile 两段式 + 三道节流闸)→ P0-防堆积(create 复用 + DDL is_active/expire_date)→ 读侧(7 处 getLatestByDdId 迁移)→ 退款(listPaidByDdId 遍历)。
|
|
|
|
|
|
|
+- ➡️ **接下来**:P0-补单(reconcile 两段式 + 两道节流闸)→ P0-防堆积(create 复用 + DDL is_active/expire_date)→ 读侧(7 处 getLatestByDdId 迁移)→ 退款(listPaidByDdId 遍历)。
|
|
|
|
|
|
|
|
**对抗发现的前置补强**(加进落地):
|
|
**对抗发现的前置补强**(加进落地):
|
|
|
- parseKvResponse **重复键放宽**(high):S 依赖 queryTrade 做换号/补单安全网,但重复键仍抛(IllegalArgumentException)→ 大响应(ATM/超商 NeedExtraPaidInfo=Y 47 字段)若含重复键 → queryTrade 返 {0,0} → 安全网失效。落地前补(重复键 last-wins + 同 Map 验签避 kill-shot ⑥)+ 抓 stage 真实 fixture。
|
|
- parseKvResponse **重复键放宽**(high):S 依赖 queryTrade 做换号/补单安全网,但重复键仍抛(IllegalArgumentException)→ 大响应(ATM/超商 NeedExtraPaidInfo=Y 47 字段)若含重复键 → queryTrade 返 {0,0} → 安全网失效。落地前补(重复键 last-wins + 同 Map 验签避 kill-shot ⑥)+ 抓 stage 真实 fixture。
|
|
|
-- 三道节流闸(防 OMG 403 自残,§6.4【C】)。
|
|
|
|
|
|
|
+- 两道节流闸(防 OMG 403 自残,§6.4【C】)。
|
|
|
- expire_date 字段(§6.1,paymentInfo 回调落)。
|
|
- expire_date 字段(§6.1,paymentInfo 回调落)。
|
|
|
|
|
|
|
|
**§9 待拍板项**:freshMin / MySQL 版本 / reuse MerchantTradeDate / 节流参数 / AFTEE 窗口等 → 实现/stage 确认(不阻塞 spec)。
|
|
**§9 待拍板项**:freshMin / MySQL 版本 / reuse MerchantTradeDate / 节流参数 / AFTEE 窗口等 → 实现/stage 确认(不阻塞 spec)。
|
|
@@ -189,16 +189,18 @@ ORDER BY p.id ASC LIMIT #{batchSize}
|
|
|
|
|
|
|
|
**【B. 补单主体】** `reconcileByQuery`(L760)把 L769 `getLatestByDdId` 换成两段:
|
|
**【B. 补单主体】** `reconcileByQuery`(L760)把 L769 `getLatestByDdId` 换成两段:
|
|
|
1. **先 `selectLatestPaidByDdId`**:若存在 `pay_status=1` 行且 `order.payStatus==0` → `handlePaymentSuccess` 自愈补推(保留原 L774-781,致命伤 ⑤ 消除);若已付且订单已核销 → 幂等返回 `{1,0}`。
|
|
1. **先 `selectLatestPaidByDdId`**:若存在 `pay_status=1` 行且 `order.payStatus==0` → `handlePaymentSuccess` 自愈补推(保留原 L774-781,致命伤 ⑤ 消除);若已付且订单已核销 → 幂等返回 `{1,0}`。
|
|
|
-2. **否则 `listUnpaidByDdId` 循环**遍历全量 `pay_status=0` 行(is_active 不限,旧行也在内)。每行 `queryTrade` 前过三道节流闸,按 `TradeStatus` 分支:
|
|
|
|
|
|
|
+2. **否则 `listUnpaidByDdId` 循环**遍历全量 `pay_status=0` 行(is_active 不限,旧行也在内)。每行 `queryTrade` 前过两道节流闸,按 `TradeStatus` 分支:
|
|
|
- `1` 且金额/字段校验过 → `applyPaidResult`;任何已付即 break。
|
|
- `1` 且金额/字段校验过 → `applyPaidResult`;任何已付即 break。
|
|
|
- `0` → **no-op**(不 markFail 不撤换,直接 continue,日志 info)。
|
|
- `0` → **no-op**(不 markFail 不撤换,直接 continue,日志 info)。
|
|
|
- `10200095` 且**该行 `trade_no IS NULL`**(从未与 OMG 建立交易)→ `markFail(0→2)`(CAS 允许 2→1 复活,安全)。
|
|
- `10200095` 且**该行 `trade_no IS NULL`**(从未与 OMG 建立交易)→ `markFail(0→2)`(CAS 允许 2→1 复活,安全)。
|
|
|
- `10200095` 且 `trade_no` 非空(已取号,可能 OMG 延迟建案或结算滞后)→ no-op + log.warn,绝不 markFail。
|
|
- `10200095` 且 `trade_no` 非空(已取号,可能 OMG 延迟建案或结算滞后)→ no-op + log.warn,绝不 markFail。
|
|
|
|
|
|
|
|
-**【C. 三道节流闸(防 OMG 403 自残 DoS,致命伤 ③)】**
|
|
|
|
|
-1. **首查延迟**:跳过 `create_time < NOW()-INTERVAL 40 MINUTE` 的行。
|
|
|
|
|
-2. **per-MerchantID 令牌桶**:Redis key `omg:qt:token:{merchantId}`,1 token / 3s,burst 1——`queryTrade` 前 acquire,空则 skip 本行本轮(不阻塞)。
|
|
|
|
|
-3. **per-ddId 扫描间隔**:定时任务 Redis key `omg:reconcile:dd:{ddId}` TTL 180s(每 ddId 最少 3 分钟一轮);`/query` 被动端点 Redis key `omg:query:dd:{ddId}` TTL 60s(窗口内直接返回上次结果或 202,挡用户 2s 轮询放大)。
|
|
|
|
|
|
|
+**【C. 节流闸(防 OMG 403 自残 DoS,致命伤 ③)——两道,频率制】**
|
|
|
|
|
+
|
|
|
|
|
+> ⚠ 原"闸1 首查延迟 40min"已于 2026-08-12 移除(commit fc46fed):是对 OMG 节流的**误读**——OMG 的 403 是"短时间内反复查同一笔"(频率)触发,**不是**"下单后 N 分钟内不能查"。已付款交易可立即查状态(付完即查是行业标准)。原闸1 会让已付客户 notify 丢失时等 40min 才看到状态变更,不可接受。"刚创建未付"由定时任务扫描窗口的 2min 宽限(`create_time <= now-2min`)保护,无需 40min 黑名单。
|
|
|
|
|
+
|
|
|
|
|
+1. **per-MerchantID 令牌桶**:Redis key `omg:qt:token:{merchantId}`,1 token / 3s,burst 1——`queryTrade` 前 acquire,空则 skip 本行本轮(不阻塞)。
|
|
|
|
|
+2. **per-ddId 扫描间隔**:定时任务 Redis key `omg:reconcile:dd:{ddId}` TTL 180s(每 ddId 最少 3 分钟一轮);`/query` 被动端点 Redis key `omg:query:dd:{ddId}` TTL 60s(窗口内直接返回上次结果或 202,挡用户 2s 轮询放大)。
|
|
|
|
|
|
|
|
**【D. 可观测性 + parseKvResponse(致命伤 ⑥)】**
|
|
**【D. 可观测性 + parseKvResponse(致命伤 ⑥)】**
|
|
|
- 三个 `{0,0}` 分支(L789 凭证 null / L797 queryTrade 异常 / L830 TradeStatus=0)拆成结构化 `log.error` 带 ddId/mtcn/source/异常类全名/响应前 500 字节;原始响应写 `ipn_log(type=omg_query_error)` 供事后追查 id=26 根因。
|
|
- 三个 `{0,0}` 分支(L789 凭证 null / L797 queryTrade 异常 / L830 TradeStatus=0)拆成结构化 `log.error` 带 ddId/mtcn/source/异常类全名/响应前 500 字节;原始响应写 `ipn_log(type=omg_query_error)` 供事后追查 id=26 根因。
|
|
@@ -235,7 +237,7 @@ ORDER BY p.id ASC LIMIT #{batchSize}
|
|
|
|-----------|------|
|
|
|-----------|------|
|
|
|
| ① 换号塞进 pay_status → CAS 不命中 | 双轴:轮换只翻 is_active,永不碰 pay_status;TradeStatus=0 一律 no-op |
|
|
| ① 换号塞进 pay_status → CAS 不命中 | 双轴:轮换只翻 is_active,永不碰 pay_status;TradeStatus=0 一律 no-op |
|
|
|
| ② 复用已占用 MTN | `selectActiveForReuse` 守卫 `trade_no IS NULL` + 新鲜期 |
|
|
| ② 复用已占用 MTN | `selectActiveForReuse` 守卫 `trade_no IS NULL` + 新鲜期 |
|
|
|
-| ③ OMG 403 自残 DoS | 三道节流闸(首查≥40min、per-MerchantID 令牌桶 1/3s、per-ddId TTL) |
|
|
|
|
|
|
|
+| ③ OMG 403 自残 DoS | 两道频率闸(per-MerchantID 令牌桶 1/3s、per-ddId TTL);原"首查≥40min"已移除(误读 OMG 节流,403 是频率触发) |
|
|
|
| ④ 7 处 getLatestByDdId 读错行 | 全量迁移表(6.4【E】) |
|
|
| ④ 7 处 getLatestByDdId 读错行 | 全量迁移表(6.4【E】) |
|
|
|
| ⑤ L774 自愈变死代码 | reconcile 两段式,先 selectLatestPaidByDdId 自愈 |
|
|
| ⑤ L774 自愈变死代码 | reconcile 两段式,先 selectLatestPaidByDdId 自愈 |
|
|
|
| ⑥ parseKvResponse 放宽与验签打架 | 重复键不静默丢;真实 fixture 验签;保留缺 CheckMacValue 即抛 |
|
|
| ⑥ parseKvResponse 放宽与验签打架 | 重复键不静默丢;真实 fixture 验签;保留缺 CheckMacValue 即抛 |
|
|
@@ -251,7 +253,7 @@ ORDER BY p.id ASC LIMIT #{batchSize}
|
|
|
|
|
|
|
|
1. **P0-诊断**:三分支日志 + ipn_log(0 改动),复现定位 id=26。
|
|
1. **P0-诊断**:三分支日志 + ipn_log(0 改动),复现定位 id=26。
|
|
|
2. **P0-修 B**:`parseKvResponse` 放宽(尾随&/空值,不丢重复键)+ `catch(Exception)` 改 log.error+UNKNOWN(3) + `OmgPayTest` 补真实 fixture。
|
|
2. **P0-修 B**:`parseKvResponse` 放宽(尾随&/空值,不丢重复键)+ `catch(Exception)` 改 log.error+UNKNOWN(3) + `OmgPayTest` 补真实 fixture。
|
|
|
-3. **P0-补单**:`reconcileByQuery` 两段式 + `selectLeakOrderDdIds` 改 EXISTS + 三道节流闸。
|
|
|
|
|
|
|
+3. **P0-补单**:`reconcileByQuery` 两段式 + `selectLeakOrderDdIds` 改 EXISTS + 两道节流闸。
|
|
|
4. **P0-防堆积**:create 复用预检(`selectActiveForReuse` + `trade_no IS NULL` + 新鲜期)+ afterCommit 锁 + DDL(is_active/expire_date/生成列唯一索引)+ 历史回填(只 is_active)。
|
|
4. **P0-防堆积**:create 复用预检(`selectActiveForReuse` + `trade_no IS NULL` + 新鲜期)+ afterCommit 锁 + DDL(is_active/expire_date/生成列唯一索引)+ 历史回填(只 is_active)。
|
|
|
5. **P0-读侧**:7 处 `getLatestByDdId` 全迁移 + `selectLatestPaidByDdId` 扩 `IN(1,3,4)`。
|
|
5. **P0-读侧**:7 处 `getLatestByDdId` 全迁移 + `selectLatestPaidByDdId` 扩 `IN(1,3,4)`。
|
|
|
6. **退款**:`listPaidByDdId` 遍历逐笔退 + 孤立已付告警。
|
|
6. **退款**:`listPaidByDdId` 遍历逐笔退 + 孤立已付告警。
|
|
@@ -283,7 +285,7 @@ ORDER BY p.id ASC LIMIT #{batchSize}
|
|
|
- `domain/PosStoreOmg.java`(无改)
|
|
- `domain/PosStoreOmg.java`(无改)
|
|
|
|
|
|
|
|
**ruoyi-admin**
|
|
**ruoyi-admin**
|
|
|
-- `app/pay/OmgPayController.java`:create 复用预检 + afterCommit 锁;reconcileByQuery 两段式 + 三道节流闸 + 三分支日志;refund 改 listPaidByDdId 遍历;7 处 getLatestByDdId 迁移
|
|
|
|
|
|
|
+- `app/pay/OmgPayController.java`:create 复用预检 + afterCommit 锁;reconcileByQuery 两段式 + 两道节流闸 + 三分支日志;refund 改 listPaidByDdId 遍历;7 处 getLatestByDdId 迁移
|
|
|
- `app/utils/omg/OmgPay.java`:parseKvResponse 放宽(重复键不丢)
|
|
- `app/utils/omg/OmgPay.java`:parseKvResponse 放宽(重复键不丢)
|
|
|
- `app/task/OmgReconcileTask.java`:节流闸配合(如未在 controller 内全覆盖)
|
|
- `app/task/OmgReconcileTask.java`:节流闸配合(如未在 controller 内全覆盖)
|
|
|
- 新增:queryTrade 节流组件(Redis 令牌桶,per-MerchantID)
|
|
- 新增:queryTrade 节流组件(Redis 令牌桶,per-MerchantID)
|