Ver Fonte

fix(omg-pay): T051 parseKvResponse 重复键 last-wins 放宽(账本重构前置)

OMG 大响应(NeedExtraPaidInfo=Y)若含重复键,原实现抛 IllegalArgumentException
→ queryTrade catch 吞 {0,0} → 换号/补单安全网失效。改 last-wins(不抛)+
log.warn 记录,验签用同一 map 重算把关。空 key 仍抛,整段无 = 仍抛。

- OmgPay.parseKvResponse:map.putIfAbsent→map.put(last-wins),重复键 log.warn
- OmgPayTest:rejectsDuplicateAndMalformedFields → rejectsMalformedFieldsButAllowsDuplicateKeys
  (重复键改 last-wins 断言取后值,保留无=/空key 抛)
- tasks.md T051 标 [x]

Co-Authored-By: Claude <noreply@anthropic.com>
qmj há 2 semanas atrás
pai
commit
dca4ae74d0

+ 8 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPay.java

@@ -210,8 +210,14 @@ public class OmgPay {
             }
             String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
             String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
-            if (key.isEmpty() || map.putIfAbsent(key, value) != null) {
-                throw new IllegalArgumentException("Duplicate or empty OMG response field");
+            if (key.isEmpty()) {
+                throw new IllegalArgumentException("Empty OMG response field");
+            }
+            // 重复键 last-wins(不抛):OMG 大响应(NeedExtraPaidInfo=Y)若含重复键,
+            // 不让 queryTrade 在此直接失败;log.warn 记录便于排查,验签用同一 map 重算把关。
+            String prev = map.put(key, value);
+            if (prev != null) {
+                log.warn("[OMG] 响应含重复键(取 last-wins): key={}, prev={}, cur={}", key, prev, value);
             }
             if (map.size() > 100) {
                 throw new IllegalArgumentException("Too many OMG response fields");

+ 5 - 3
ruoyi-admin/src/test/java/com/ruoyi/app/utils/omg/OmgPayTest.java

@@ -22,9 +22,11 @@ class OmgPayTest {
     }
 
     @Test
-    void rejectsDuplicateAndMalformedFields() {
-        assertThrows(IllegalArgumentException.class,
-                () -> omgPay.parseKvResponse("RtnCode=1&RtnCode=0"));
+    void rejectsMalformedFieldsButAllowsDuplicateKeys() {
+        // 重复键 last-wins 不抛(T051 放宽:OMG 大响应若含重复键,不让 queryTrade 直接失败;验签用同一 map 把关)
+        Map<String, String> dup = omgPay.parseKvResponse("RtnCode=1&RtnCode=0");
+        assertEquals("0", dup.get("RtnCode")); // last-wins
+        // 无 = / 空 key 仍抛
         assertThrows(IllegalArgumentException.class,
                 () -> omgPay.parseKvResponse("RtnCode=1&broken"));
         assertThrows(IllegalArgumentException.class,

+ 1 - 1
specs/016-omg-payment/tasks.md

@@ -178,7 +178,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 **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 做换号/补单安全网,重复键仍抛会让安全网失效。
+- [x] 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。