Explorar el Código

fix(omg-pay): 加 4 候选 CMV 诊断,定位 QueryTradeInfo 验签分歧

线上 receivedCMV(OMG回传)≠ computedCMV(本地算)。查 OMG 官方文档
(01_order/04_order_query)确认「額外回傳參數全部都需要加入檢查碼計算」,
故字段集不是分歧(我们对全 47 字段签名,OMG 也是);且 create 请求(全大写
参数)签名 OMG 接受 → pipeline 对全大写正确,QueryTradeInfo 响应(混合大小写
+ 大量空值)算错 → 分歧收窄到「排序大小写」或「空值处理」(.NET/PHP 实现
细节,官方文档不写),必须实测。

- OmgCheckMacValue 加可配置重载 generate(params,key,iv,keyOrder,excludeEmpty),
  不改原行为(原 generate 委托 naturalOrder + 保留空值)。
- OmgPay 验签失败时一次算 4 种 CMV(大小写敏感/不敏感 × 空值保留/排除),
  日志直接对比 receivedCMV,一次锁定根因;凭证脱敏不外发。

纯诊断,不改验签默认行为。等日志确认哪个候选 == receivedCMV(AC1C1AFF...)
后,再正式改 generate 默认策略 + 补真实 fixture 测试。

Co-Authored-By: Claude <noreply@anthropic.com>
qmj hace 2 semanas
padre
commit
abcd7d4ef9

+ 23 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgCheckMacValue.java

@@ -3,6 +3,7 @@ package com.ruoyi.app.utils.omg;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.util.Comparator;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
@@ -48,8 +49,25 @@ public final class OmgCheckMacValue {
 
     /** 指定摘要算法生成(SHA-256 用于 OMG 生产;MD5 仅供自测对照 ECPay 旧版向量)。 */
     static String generate(Map<String, String> params, String hashKey, String hashIv, String algorithm) {
-        // 1-2. 去 CheckMacValue、去空值、按 key 字母序(TreeMap,大小写敏感)
-        TreeMap<String, String> sorted = new TreeMap<>();
+        return generate(params, hashKey, hashIv, Comparator.naturalOrder(), false, algorithm);
+    }
+
+    /**
+     * 可配置排序与空值策略的生成(诊断用:排查 QueryTradeInfo 响应验签分歧)。
+     *
+     * @param keyOrder     key 排序比较器(大小写敏感 {@code Comparator.naturalOrder()} /
+     *                     不敏感 {@code String.CASE_INSENSITIVE_ORDER})
+     * @param excludeEmpty true=排除空值字段;false=保留空值字段(以 {@code key=} 参与,默认行为)
+     */
+    static String generate(Map<String, String> params, String hashKey, String hashIv,
+                           Comparator<String> keyOrder, boolean excludeEmpty) {
+        return generate(params, hashKey, hashIv, keyOrder, excludeEmpty, "SHA-256");
+    }
+
+    private static String generate(Map<String, String> params, String hashKey, String hashIv,
+                                   Comparator<String> keyOrder, boolean excludeEmpty, String algorithm) {
+        // 1-2. 去 CheckMacValue、去 null/空值(可选)、按 keyOrder 排序
+        TreeMap<String, String> sorted = new TreeMap<>(keyOrder);
         if (params != null) {
             for (Map.Entry<String, String> e : params.entrySet()) {
                 String k = e.getKey();
@@ -60,6 +78,9 @@ public final class OmgCheckMacValue {
                 if (v == null) {
                     continue;
                 }
+                if (excludeEmpty && v.isEmpty()) {
+                    continue;
+                }
                 sorted.put(k, v);
             }
         }

+ 18 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPay.java

@@ -17,6 +17,7 @@ import java.net.URI;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -106,15 +107,14 @@ public class OmgPay {
             verifyResponse(result, cfg, true);
         } catch (RuntimeException e) {
             String receivedCmv = result.get("CheckMacValue");
-            String computedCmv;
-            try {
-                computedCmv = OmgCheckMacValue.generate(result, cfg.getHashKey(), cfg.getHashIv());
-            } catch (Exception ce) {
-                computedCmv = "generate-error:" + ce.getClass().getName() + ":" + ce.getMessage();
-            }
-            log.error("[OMG] queryTrade 验签失败 MerchantTradeNo={} TradeStatus={} RtnCode={} receivedCMV={} computedCMV={} hashKey={} hashIv={} resp={}",
-                    merchantTradeNo, result.get("TradeStatus"), result.get("RtnCode"),
-                    receivedCmv, computedCmv, mask(cfg.getHashKey()), mask(cfg.getHashIv()), resp, e);
+            log.error("[OMG] queryTrade 验签失败 MerchantTradeNo={} TradeStatus={} RtnCode={} receivedCMV={} "
+                    + "nat_inc={} ci_inc={} nat_exc={} ci_exc={} hashKey={} hashIv={} resp={}",
+                    merchantTradeNo, result.get("TradeStatus"), result.get("RtnCode"), receivedCmv,
+                    safeGen(result, cfg, Comparator.naturalOrder(), false),
+                    safeGen(result, cfg, String.CASE_INSENSITIVE_ORDER, false),
+                    safeGen(result, cfg, Comparator.naturalOrder(), true),
+                    safeGen(result, cfg, String.CASE_INSENSITIVE_ORDER, true),
+                    mask(cfg.getHashKey()), mask(cfg.getHashIv()), resp, e);
             throw e;
         }
         verifyCorrelation(result, "MerchantID", cfg.getMerchantId());
@@ -280,4 +280,13 @@ public class OmgPay {
         }
         return s.substring(0, 2) + "***" + s.substring(s.length() - 2) + "(len=" + s.length() + ")";
     }
+
+    /** 诊断用:用指定排序/空值策略重算 CheckMacValue,失败返回 err 标记。 */
+    private String safeGen(Map<String, String> result, OmgPayConfig cfg, Comparator<String> order, boolean excludeEmpty) {
+        try {
+            return OmgCheckMacValue.generate(result, cfg.getHashKey(), cfg.getHashIv(), order, excludeEmpty);
+        } catch (Exception ce) {
+            return "err:" + ce.getClass().getSimpleName();
+        }
+    }
 }