From 36358accc9febf8150fc5f1cc0589f9132532c01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:00:21 +0000 Subject: [PATCH 1/4] Initial plan From 089104548a8580dbeeda12bea90e390992f314bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:12:14 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E9=AA=8C=E8=AF=81=E6=9C=AA=E7=9F=A5=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=AF=BC=E8=87=B4=E7=AD=BE=E5=90=8D=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- ...xPayOrderNotifyResultToMapCompareTest.java | 105 +++++++++++++++ .../WxPayOrderNotifyResultToMapTest.java | 91 +++++++++++++ .../notify/WxPayOrderNotifySignatureTest.java | 120 ++++++++++++++++++ .../WxPayOrderNotifyUnknownFieldTest.java | 96 ++++++++++++++ 4 files changed, 412 insertions(+) create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java new file mode 100644 index 0000000000..7f3ffe0315 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java @@ -0,0 +1,105 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; +import com.github.binarywang.wxpay.util.SignUtils; +import com.github.binarywang.wxpay.util.XmlConfig; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.*; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +/** + * 比较两种 toMap() 方法实现的差异 + */ +public class WxPayOrderNotifyResultToMapCompareTest { + + @Test + public void testCompareToMapMethods() throws Exception { + // 从 issue 的截图中提取的 XML 数据 + String xml = "" + + "wx58ff40508696691f" + + "ICBC_DEBIT" + + "1" + + "CNY" + + "N" + + "1545462911" + + "1761723102373" + + "o1gdd16CZCi6yYvkn6j9EB_1TObM" + + "20251029153140" + + "SUCCESS" + + "SUCCESS" + + "03F5C68CA8F2E30855077FA3FC21EBEA" + + "20251029153852" + + "1" + + "JSAPI" + + "4200002882220251029816273963B" + + ""; + + WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); + + // WxPayOrderNotifyResult 重写的 toMap(),使用 SignUtils.xmlBean2Map(this) + Map beanMap = result.toMap(); + + // BaseWxPayResult.toMap() 的方式,直接从 XML 解析 + Map xmlMap = parseXmlToMap(xml); + + System.out.println("=== WxPayOrderNotifyResult.toMap() (SignUtils.xmlBean2Map) ==="); + printSignString(beanMap); + + System.out.println("\n=== 直接从 XML 解析的 Map ==="); + printSignString(xmlMap); + + // 找出差异 + System.out.println("\n=== 差异 ==="); + Set allKeys = new TreeSet<>(); + allKeys.addAll(beanMap.keySet()); + allKeys.addAll(xmlMap.keySet()); + + for (String key : allKeys) { + String beanValue = beanMap.get(key); + String xmlValue = xmlMap.get(key); + if (!Objects.equals(beanValue, xmlValue)) { + System.out.println(key + ": beanMap=" + beanValue + ", xmlMap=" + xmlValue); + } + } + } + + private static Map parseXmlToMap(String xml) throws Exception { + Map result = new HashMap<>(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setExpandEntityReferences(false); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + Document doc = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); + + NodeList list = (NodeList) XPathFactory.newInstance().newXPath() + .compile("/xml/*") + .evaluate(doc, XPathConstants.NODESET); + + int len = list.getLength(); + for (int i = 0; i < len; i++) { + result.put(list.item(i).getNodeName(), list.item(i).getTextContent()); + } + return result; + } + + private static void printSignString(Map params) { + List noSignParams = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); + StringBuilder sb = new StringBuilder(); + for (String key : new TreeMap<>(params).keySet()) { + String value = params.get(key); + if (value != null && !value.isEmpty() && !noSignParams.contains(key)) { + sb.append(key).append("=").append(value).append("&"); + } + } + sb.append("key=YOUR_MCH_KEY"); + System.out.println(sb.toString()); + } +} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java new file mode 100644 index 0000000000..f26b8c927b --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java @@ -0,0 +1,91 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; +import com.github.binarywang.wxpay.util.XmlConfig; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Map; + +/** + * 测试 WxPayOrderNotifyResult.toMap() 方法是否正确包含所有 XML 字段 + */ +public class WxPayOrderNotifyResultToMapTest { + + @Test + public void testToMapContainsAllFields() { + // 从 issue 的截图中提取的 XML 数据 + String xml = "" + + "wx58ff40508696691f" + + "ICBC_DEBIT" + + "1" + + "CNY" + + "N" + + "1545462911" + + "1761723102373" + + "o1gdd16CZCi6yYvkn6j9EB_1TObM" + + "20251029153140" + + "SUCCESS" + + "SUCCESS" + + "03F5C68CA8F2E30855077FA3FC21EBEA" + + "20251029153852" + + "1" + + "JSAPI" + + "4200002882220251029816273963B" + + ""; + + WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); + Map map = result.toMap(); + + System.out.println("toMap() 结果:"); + map.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> System.out.println(" " + entry.getKey() + " = " + entry.getValue())); + + // 验证关键字段是否存在 + Assert.assertTrue(map.containsKey("is_subscribe"), "toMap() 应该包含 is_subscribe 字段"); + Assert.assertEquals(map.get("is_subscribe"), "N", "is_subscribe 的值应该是 N"); + + Assert.assertTrue(map.containsKey("bank_type"), "toMap() 应该包含 bank_type 字段"); + Assert.assertEquals(map.get("bank_type"), "ICBC_DEBIT", "bank_type 的值应该是 ICBC_DEBIT"); + } + + @Test + public void testToMapWithFastMode() { + String xml = "" + + "wx58ff40508696691f" + + "ICBC_DEBIT" + + "1" + + "CNY" + + "N" + + "1545462911" + + "1761723102373" + + "o1gdd16CZCi6yYvkn6j9EB_1TObM" + + "20251029153140" + + "SUCCESS" + + "SUCCESS" + + "03F5C68CA8F2E30855077FA3FC21EBEA" + + "20251029153852" + + "1" + + "JSAPI" + + "4200002882220251029816273963B" + + ""; + + XmlConfig.fastMode = true; + try { + WxPayOrderNotifyResult result = BaseWxPayResult.fromXML(xml, WxPayOrderNotifyResult.class); + Map map = result.toMap(); + + System.out.println("fastMode toMap() 结果:"); + map.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> System.out.println(" " + entry.getKey() + " = " + entry.getValue())); + + // 验证关键字段是否存在 + Assert.assertTrue(map.containsKey("is_subscribe"), "fastMode toMap() 应该包含 is_subscribe 字段"); + Assert.assertEquals(map.get("is_subscribe"), "N", "is_subscribe 的值应该是 N"); + } finally { + XmlConfig.fastMode = false; + } + } +} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java new file mode 100644 index 0000000000..ca711f1b19 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java @@ -0,0 +1,120 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.util.SignUtils; +import com.github.binarywang.wxpay.util.XmlConfig; +import org.apache.commons.codec.digest.DigestUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +/** + * 测试签名验证逻辑 + */ +public class WxPayOrderNotifySignatureTest { + + private static final String MCH_KEY = "testmchkey1234567890123456789012"; // 32位测试密钥 + private static final List NO_SIGN_PARAMS = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); + + @Test + public void testSignatureVerification() throws Exception { + // 创建一个测试用的 XML,并手动计算正确的签名 + Map params = new LinkedHashMap<>(); + params.put("appid", "wx58ff40508696691f"); + params.put("bank_type", "ICBC_DEBIT"); + params.put("cash_fee", "1"); + params.put("fee_type", "CNY"); + params.put("is_subscribe", "N"); + params.put("mch_id", "1545462911"); + params.put("nonce_str", "1761723102373"); + params.put("openid", "o1gdd16CZCi6yYvkn6j9EB_1TObM"); + params.put("out_trade_no", "20251029153140"); + params.put("result_code", "SUCCESS"); + params.put("return_code", "SUCCESS"); + params.put("time_end", "20251029153852"); + params.put("total_fee", "1"); + params.put("trade_type", "JSAPI"); + params.put("transaction_id", "4200002882220251029816273963B"); + + // 计算正确的签名 + String correctSign = createSign(params, WxPayConstants.SignType.MD5, MCH_KEY); + params.put("sign", correctSign); + + // 创建 XML + StringBuilder xmlBuilder = new StringBuilder(""); + for (Map.Entry entry : params.entrySet()) { + xmlBuilder.append("<").append(entry.getKey()).append(">") + .append(entry.getValue()) + .append(""); + } + xmlBuilder.append(""); + String xml = xmlBuilder.toString(); + + System.out.println("测试 XML:"); + System.out.println(xml); + System.out.println("计算的签名: " + correctSign); + + // 测试普通模式 + System.out.println("\n=== 普通模式 ==="); + WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); + Map beanMap = result.toMap(); + + System.out.println("toMap() 结果 (用于签名验证):"); + TreeMap sortedMap = new TreeMap<>(beanMap); + for (Map.Entry entry : sortedMap.entrySet()) { + if (!NO_SIGN_PARAMS.contains(entry.getKey())) { + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); + } + } + + // 验证签名 + String verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); + System.out.println("原始签名: " + result.getSign()); + System.out.println("计算签名: " + verifySign); + Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配"); + + // 测试 fastMode + System.out.println("\n=== Fast Mode ==="); + XmlConfig.fastMode = true; + try { + result = BaseWxPayResult.fromXML(xml, WxPayOrderNotifyResult.class); + beanMap = result.toMap(); + + System.out.println("fastMode toMap() 结果 (用于签名验证):"); + sortedMap = new TreeMap<>(beanMap); + for (Map.Entry entry : sortedMap.entrySet()) { + if (!NO_SIGN_PARAMS.contains(entry.getKey())) { + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); + } + } + + verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); + System.out.println("原始签名: " + result.getSign()); + System.out.println("计算签名: " + verifySign); + Assert.assertEquals(verifySign, result.getSign(), "fastMode 签名应该匹配"); + } finally { + XmlConfig.fastMode = false; + } + } + + private static String createSign(Map params, String signType, String signKey) { + StringBuilder toSign = new StringBuilder(); + for (String key : new TreeMap<>(params).keySet()) { + String value = params.get(key); + if (value != null && !value.isEmpty() && !NO_SIGN_PARAMS.contains(key)) { + toSign.append(key).append("=").append(value).append("&"); + } + } + toSign.append("key=").append(signKey); + return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); + } +} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java new file mode 100644 index 0000000000..dfbf99d3b9 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java @@ -0,0 +1,96 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.util.SignUtils; +import com.github.binarywang.wxpay.util.XmlConfig; +import org.apache.commons.codec.digest.DigestUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.*; + +/** + * 测试当 XML 包含未知字段时,签名验证是否正常 + */ +public class WxPayOrderNotifyUnknownFieldTest { + + private static final String MCH_KEY = "testmchkey1234567890123456789012"; + private static final List NO_SIGN_PARAMS = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); + + @Test + public void testSignatureWithUnknownField() throws Exception { + // 创建一个测试用的 XML,包含一个未知字段 (未在 WxPayOrderNotifyResult 中定义) + Map params = new LinkedHashMap<>(); + params.put("appid", "wx58ff40508696691f"); + params.put("bank_type", "ICBC_DEBIT"); + params.put("cash_fee", "1"); + params.put("fee_type", "CNY"); + params.put("is_subscribe", "N"); + params.put("mch_id", "1545462911"); + params.put("nonce_str", "1761723102373"); + params.put("openid", "o1gdd16CZCi6yYvkn6j9EB_1TObM"); + params.put("out_trade_no", "20251029153140"); + params.put("result_code", "SUCCESS"); + params.put("return_code", "SUCCESS"); + params.put("time_end", "20251029153852"); + params.put("total_fee", "1"); + params.put("trade_type", "JSAPI"); + params.put("transaction_id", "4200002882220251029816273963B"); + // 添加一个未知字段 + params.put("unknown_field", "unknown_value"); + + // 计算正确的签名 (包含未知字段) + String correctSign = createSign(params, WxPayConstants.SignType.MD5, MCH_KEY); + params.put("sign", correctSign); + + // 创建 XML + StringBuilder xmlBuilder = new StringBuilder(""); + for (Map.Entry entry : params.entrySet()) { + xmlBuilder.append("<").append(entry.getKey()).append(">") + .append(entry.getValue()) + .append(""); + } + xmlBuilder.append(""); + String xml = xmlBuilder.toString(); + + System.out.println("测试 XML (包含未知字段 unknown_field):"); + System.out.println(xml); + System.out.println("正确的签名 (包含未知字段计算): " + correctSign); + + // 解析 XML + WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); + Map beanMap = result.toMap(); + + System.out.println("\ntoMap() 结果:"); + TreeMap sortedMap = new TreeMap<>(beanMap); + for (Map.Entry entry : sortedMap.entrySet()) { + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); + } + + // 检查 unknown_field 是否存在 + boolean hasUnknownField = beanMap.containsKey("unknown_field"); + System.out.println("\ntoMap() 是否包含 unknown_field: " + hasUnknownField); + + // 验证签名 + String verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); + System.out.println("原始签名: " + result.getSign()); + System.out.println("计算签名: " + verifySign); + + // 这个测试应该会失败,因为 toMap() 丢失了 unknown_field + Assert.assertTrue(hasUnknownField, "toMap() 应该包含 unknown_field"); + Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配"); + } + + private static String createSign(Map params, String signType, String signKey) { + StringBuilder toSign = new StringBuilder(); + for (String key : new TreeMap<>(params).keySet()) { + String value = params.get(key); + if (value != null && !value.isEmpty() && !NO_SIGN_PARAMS.contains(key)) { + toSign.append(key).append("=").append(value).append("&"); + } + } + toSign.append("key=").append(signKey); + return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); + } +} From 640e771ae592ecda7b140a54ab3c3c9e43376b31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:16:26 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20V2=20=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=9B=9E=E8=B0=83=E7=AD=BE=E5=90=8D=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:当微信支付 V2 回调 XML 包含未在 WxPayOrderNotifyResult 中定义的字段时, toMap() 方法使用 SignUtils.xmlBean2Map(this) 会丢失这些字段,导致签名验证失败。 解决方案:修改 WxPayOrderNotifyResult.toMap() 方法,使用父类的 toMap() 方法 直接从原始 XML 解析所有字段,确保包含所有字段用于签名验证。 Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../bean/notify/WxPayOrderNotifyResult.java | 4 +- ...xPayOrderNotifyResultToMapCompareTest.java | 105 --------------- .../WxPayOrderNotifyResultToMapTest.java | 91 ------------- .../notify/WxPayOrderNotifySignatureTest.java | 120 ------------------ .../WxPayOrderNotifyUnknownFieldTest.java | 18 ++- 5 files changed, 16 insertions(+), 322 deletions(-) delete mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java delete mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java delete mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResult.java index 27e8c1e1ec..8f16e5d4e4 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResult.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResult.java @@ -342,7 +342,9 @@ public static WxPayOrderNotifyResult fromXML(String xmlString) { @Override public Map toMap() { - Map resultMap = SignUtils.xmlBean2Map(this); + // 使用父类的 toMap() 方法,直接从原始 XML 解析所有字段, + // 确保包含未在 Java Bean 中定义的字段,避免签名验证失败 + Map resultMap = super.toMap(); if (this.getCouponCount() != null && this.getCouponCount() > 0) { for (int i = 0; i < this.getCouponCount(); i++) { WxPayOrderNotifyCoupon coupon = couponList.get(i); diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java deleted file mode 100644 index 7f3ffe0315..0000000000 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapCompareTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.github.binarywang.wxpay.bean.notify; - -import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; -import com.github.binarywang.wxpay.util.SignUtils; -import com.github.binarywang.wxpay.util.XmlConfig; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.ByteArrayInputStream; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.util.*; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathFactory; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; - -/** - * 比较两种 toMap() 方法实现的差异 - */ -public class WxPayOrderNotifyResultToMapCompareTest { - - @Test - public void testCompareToMapMethods() throws Exception { - // 从 issue 的截图中提取的 XML 数据 - String xml = "" + - "wx58ff40508696691f" + - "ICBC_DEBIT" + - "1" + - "CNY" + - "N" + - "1545462911" + - "1761723102373" + - "o1gdd16CZCi6yYvkn6j9EB_1TObM" + - "20251029153140" + - "SUCCESS" + - "SUCCESS" + - "03F5C68CA8F2E30855077FA3FC21EBEA" + - "20251029153852" + - "1" + - "JSAPI" + - "4200002882220251029816273963B" + - ""; - - WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); - - // WxPayOrderNotifyResult 重写的 toMap(),使用 SignUtils.xmlBean2Map(this) - Map beanMap = result.toMap(); - - // BaseWxPayResult.toMap() 的方式,直接从 XML 解析 - Map xmlMap = parseXmlToMap(xml); - - System.out.println("=== WxPayOrderNotifyResult.toMap() (SignUtils.xmlBean2Map) ==="); - printSignString(beanMap); - - System.out.println("\n=== 直接从 XML 解析的 Map ==="); - printSignString(xmlMap); - - // 找出差异 - System.out.println("\n=== 差异 ==="); - Set allKeys = new TreeSet<>(); - allKeys.addAll(beanMap.keySet()); - allKeys.addAll(xmlMap.keySet()); - - for (String key : allKeys) { - String beanValue = beanMap.get(key); - String xmlValue = xmlMap.get(key); - if (!Objects.equals(beanValue, xmlValue)) { - System.out.println(key + ": beanMap=" + beanValue + ", xmlMap=" + xmlValue); - } - } - } - - private static Map parseXmlToMap(String xml) throws Exception { - Map result = new HashMap<>(); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setExpandEntityReferences(false); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - Document doc = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); - - NodeList list = (NodeList) XPathFactory.newInstance().newXPath() - .compile("/xml/*") - .evaluate(doc, XPathConstants.NODESET); - - int len = list.getLength(); - for (int i = 0; i < len; i++) { - result.put(list.item(i).getNodeName(), list.item(i).getTextContent()); - } - return result; - } - - private static void printSignString(Map params) { - List noSignParams = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); - StringBuilder sb = new StringBuilder(); - for (String key : new TreeMap<>(params).keySet()) { - String value = params.get(key); - if (value != null && !value.isEmpty() && !noSignParams.contains(key)) { - sb.append(key).append("=").append(value).append("&"); - } - } - sb.append("key=YOUR_MCH_KEY"); - System.out.println(sb.toString()); - } -} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java deleted file mode 100644 index f26b8c927b..0000000000 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyResultToMapTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.github.binarywang.wxpay.bean.notify; - -import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; -import com.github.binarywang.wxpay.util.XmlConfig; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.util.Map; - -/** - * 测试 WxPayOrderNotifyResult.toMap() 方法是否正确包含所有 XML 字段 - */ -public class WxPayOrderNotifyResultToMapTest { - - @Test - public void testToMapContainsAllFields() { - // 从 issue 的截图中提取的 XML 数据 - String xml = "" + - "wx58ff40508696691f" + - "ICBC_DEBIT" + - "1" + - "CNY" + - "N" + - "1545462911" + - "1761723102373" + - "o1gdd16CZCi6yYvkn6j9EB_1TObM" + - "20251029153140" + - "SUCCESS" + - "SUCCESS" + - "03F5C68CA8F2E30855077FA3FC21EBEA" + - "20251029153852" + - "1" + - "JSAPI" + - "4200002882220251029816273963B" + - ""; - - WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); - Map map = result.toMap(); - - System.out.println("toMap() 结果:"); - map.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach(entry -> System.out.println(" " + entry.getKey() + " = " + entry.getValue())); - - // 验证关键字段是否存在 - Assert.assertTrue(map.containsKey("is_subscribe"), "toMap() 应该包含 is_subscribe 字段"); - Assert.assertEquals(map.get("is_subscribe"), "N", "is_subscribe 的值应该是 N"); - - Assert.assertTrue(map.containsKey("bank_type"), "toMap() 应该包含 bank_type 字段"); - Assert.assertEquals(map.get("bank_type"), "ICBC_DEBIT", "bank_type 的值应该是 ICBC_DEBIT"); - } - - @Test - public void testToMapWithFastMode() { - String xml = "" + - "wx58ff40508696691f" + - "ICBC_DEBIT" + - "1" + - "CNY" + - "N" + - "1545462911" + - "1761723102373" + - "o1gdd16CZCi6yYvkn6j9EB_1TObM" + - "20251029153140" + - "SUCCESS" + - "SUCCESS" + - "03F5C68CA8F2E30855077FA3FC21EBEA" + - "20251029153852" + - "1" + - "JSAPI" + - "4200002882220251029816273963B" + - ""; - - XmlConfig.fastMode = true; - try { - WxPayOrderNotifyResult result = BaseWxPayResult.fromXML(xml, WxPayOrderNotifyResult.class); - Map map = result.toMap(); - - System.out.println("fastMode toMap() 结果:"); - map.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach(entry -> System.out.println(" " + entry.getKey() + " = " + entry.getValue())); - - // 验证关键字段是否存在 - Assert.assertTrue(map.containsKey("is_subscribe"), "fastMode toMap() 应该包含 is_subscribe 字段"); - Assert.assertEquals(map.get("is_subscribe"), "N", "is_subscribe 的值应该是 N"); - } finally { - XmlConfig.fastMode = false; - } - } -} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java deleted file mode 100644 index ca711f1b19..0000000000 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifySignatureTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.github.binarywang.wxpay.bean.notify; - -import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; -import com.github.binarywang.wxpay.constant.WxPayConstants; -import com.github.binarywang.wxpay.util.SignUtils; -import com.github.binarywang.wxpay.util.XmlConfig; -import org.apache.commons.codec.digest.DigestUtils; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.util.*; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathFactory; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; - -/** - * 测试签名验证逻辑 - */ -public class WxPayOrderNotifySignatureTest { - - private static final String MCH_KEY = "testmchkey1234567890123456789012"; // 32位测试密钥 - private static final List NO_SIGN_PARAMS = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); - - @Test - public void testSignatureVerification() throws Exception { - // 创建一个测试用的 XML,并手动计算正确的签名 - Map params = new LinkedHashMap<>(); - params.put("appid", "wx58ff40508696691f"); - params.put("bank_type", "ICBC_DEBIT"); - params.put("cash_fee", "1"); - params.put("fee_type", "CNY"); - params.put("is_subscribe", "N"); - params.put("mch_id", "1545462911"); - params.put("nonce_str", "1761723102373"); - params.put("openid", "o1gdd16CZCi6yYvkn6j9EB_1TObM"); - params.put("out_trade_no", "20251029153140"); - params.put("result_code", "SUCCESS"); - params.put("return_code", "SUCCESS"); - params.put("time_end", "20251029153852"); - params.put("total_fee", "1"); - params.put("trade_type", "JSAPI"); - params.put("transaction_id", "4200002882220251029816273963B"); - - // 计算正确的签名 - String correctSign = createSign(params, WxPayConstants.SignType.MD5, MCH_KEY); - params.put("sign", correctSign); - - // 创建 XML - StringBuilder xmlBuilder = new StringBuilder(""); - for (Map.Entry entry : params.entrySet()) { - xmlBuilder.append("<").append(entry.getKey()).append(">") - .append(entry.getValue()) - .append(""); - } - xmlBuilder.append(""); - String xml = xmlBuilder.toString(); - - System.out.println("测试 XML:"); - System.out.println(xml); - System.out.println("计算的签名: " + correctSign); - - // 测试普通模式 - System.out.println("\n=== 普通模式 ==="); - WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); - Map beanMap = result.toMap(); - - System.out.println("toMap() 结果 (用于签名验证):"); - TreeMap sortedMap = new TreeMap<>(beanMap); - for (Map.Entry entry : sortedMap.entrySet()) { - if (!NO_SIGN_PARAMS.contains(entry.getKey())) { - System.out.println(" " + entry.getKey() + " = " + entry.getValue()); - } - } - - // 验证签名 - String verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); - System.out.println("原始签名: " + result.getSign()); - System.out.println("计算签名: " + verifySign); - Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配"); - - // 测试 fastMode - System.out.println("\n=== Fast Mode ==="); - XmlConfig.fastMode = true; - try { - result = BaseWxPayResult.fromXML(xml, WxPayOrderNotifyResult.class); - beanMap = result.toMap(); - - System.out.println("fastMode toMap() 结果 (用于签名验证):"); - sortedMap = new TreeMap<>(beanMap); - for (Map.Entry entry : sortedMap.entrySet()) { - if (!NO_SIGN_PARAMS.contains(entry.getKey())) { - System.out.println(" " + entry.getKey() + " = " + entry.getValue()); - } - } - - verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); - System.out.println("原始签名: " + result.getSign()); - System.out.println("计算签名: " + verifySign); - Assert.assertEquals(verifySign, result.getSign(), "fastMode 签名应该匹配"); - } finally { - XmlConfig.fastMode = false; - } - } - - private static String createSign(Map params, String signType, String signKey) { - StringBuilder toSign = new StringBuilder(); - for (String key : new TreeMap<>(params).keySet()) { - String value = params.get(key); - if (value != null && !value.isEmpty() && !NO_SIGN_PARAMS.contains(key)) { - toSign.append(key).append("=").append(value).append("&"); - } - } - toSign.append("key=").append(signKey); - return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); - } -} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java index dfbf99d3b9..13a9b22301 100644 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java @@ -1,9 +1,6 @@ package com.github.binarywang.wxpay.bean.notify; -import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; import com.github.binarywang.wxpay.constant.WxPayConstants; -import com.github.binarywang.wxpay.util.SignUtils; -import com.github.binarywang.wxpay.util.XmlConfig; import org.apache.commons.codec.digest.DigestUtils; import org.testng.Assert; import org.testng.annotations.Test; @@ -11,7 +8,18 @@ import java.util.*; /** - * 测试当 XML 包含未知字段时,签名验证是否正常 + * 测试当微信支付回调 XML 包含未在 Java Bean 中定义的字段时,签名验证是否正常。 + *

+ * 问题背景:当微信返回的 XML 包含某些未在 WxPayOrderNotifyResult 中定义的字段时, + * 这些字段会被微信服务器用于签名计算。如果 toMap() 方法丢失了这些字段, + * 则签名验证会失败,抛出 "参数格式校验错误!" 异常。 + *

+ *

+ * 解决方案:修改 WxPayOrderNotifyResult.toMap() 方法,使用父类的 toMap() 方法 + * 直接从原始 XML 解析所有字段,而不是使用 SignUtils.xmlBean2Map(this)。 + *

+ * + * @see Issue链接 */ public class WxPayOrderNotifyUnknownFieldTest { @@ -77,7 +85,7 @@ public void testSignatureWithUnknownField() throws Exception { System.out.println("原始签名: " + result.getSign()); System.out.println("计算签名: " + verifySign); - // 这个测试应该会失败,因为 toMap() 丢失了 unknown_field + // 这个测试验证修复后 toMap() 能正确包含所有字段 Assert.assertTrue(hasUnknownField, "toMap() 应该包含 unknown_field"); Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配"); } From b7aecc6a4277e3b4bb01a18dba382d9fb02bb51f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:18:17 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=9A=84=20Issue=20=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E4=B8=BA=E5=AE=9E=E9=99=85=20Issue=20=E7=BC=96=E5=8F=B7=20#375?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java index 13a9b22301..be35523ec4 100644 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java @@ -19,7 +19,7 @@ * 直接从原始 XML 解析所有字段,而不是使用 SignUtils.xmlBean2Map(this)。 *

* - * @see Issue链接 + * @see Issue #3750 */ public class WxPayOrderNotifyUnknownFieldTest {