From c207fd1454b64f47e2e66cd2f578a2b21b06f019 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期三, 15 七月 2026 17:39:42 +0800
Subject: [PATCH] 分账退款fix

---
 ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java       |   39 +++++++++++-
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java     |   16 ++++-
 ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java   |   47 +++++++++++++++
 ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java           |   28 +++++++++
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java |   18 ++++++
 ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java               |   10 ++-
 6 files changed, 145 insertions(+), 13 deletions(-)

diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
index 45cd128..a3fb2df 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
@@ -130,15 +130,14 @@
         if (!"SUCCESS".equals(shareRecord.getOnlineShareStatus())) {
             throw new IllegalArgumentException("仅支持已成功的工行分账记录退款");
         }
-        if (isBlank(shareRecord.getSeqNo()) || isBlank(shareRecord.getBusiType())
-                || isBlank(shareRecord.getOriMerId()) || shareRecord.getOriTrxDate() == null
+        if (shareRecord.getOriTrxDate() == null
                 || isBlank(shareRecord.getSubMerId()) || isBlank(shareRecord.getSubMerPrtclNo())) {
             throw new IllegalArgumentException("分账记录缺少工行退款参数");
         }
         IcbcSubOrderRefundReq request = new IcbcSubOrderRefundReq();
         request.setChargingOrderCode(order.getCode());
-        request.setSeqNo(shareRecord.getSeqNo());
-        request.setBusiType(shareRecord.getBusiType());
+        request.setSeqNo(firstNotBlank(shareRecord.getSeqNo(), order.getCode()));
+        request.setBusiType(firstNotBlank(shareRecord.getBusiType(), "1"));
         request.setOriMerId(shareRecord.getOriMerId());
         request.setOriTrxDate(shareRecord.getOriTrxDate() == null ? null : shareRecord.getOriTrxDate().toString());
         request.setSubOrderId(shareRecord.getSubOrderId());
@@ -167,4 +166,13 @@
     private static boolean isBlank(String value) {
         return value == null || value.trim().isEmpty();
     }
+
+    private static String firstNotBlank(String... values) {
+        for (String value : values) {
+            if (!isBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
 }
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
index f69f7d4..9194907 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
@@ -13,6 +13,7 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -94,6 +95,23 @@
     }
 
     @Test
+    void buildsLegacySubOrderRefundWithoutStoredOriginalMerchantId() {
+        TChargingOrderShareRecord record = shareRecord();
+        record.setSeqNo(null);
+        record.setBusiType(null);
+        record.setOriMerId(null);
+
+        IcbcSubOrderRefundReq request = IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+                paidOrder(), record, new BigDecimal("1.90"),
+                "ICBCSUBREFUNDLEGACY", "ICBCSUBREFUNDSERIALLEGACY");
+
+        assertEquals("CD202607110001", request.getSeqNo());
+        assertEquals("1", request.getBusiType());
+        assertNull(request.getOriMerId());
+        assertEquals(new BigDecimal("1.90"), request.getRefundAmount());
+    }
+
+    @Test
     void rejectsAmountWithMoreThanTwoDecimalPlaces() {
         IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
                 () -> IcbcAdminFundOperationSupport.requireAmount(new BigDecimal("0.004")));
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
index 52bdbb1..f9b427c 100644
--- a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
@@ -219,6 +219,7 @@
         Map<String, String[]> requestParams = new LinkedHashMap<>(request.getParameterMap());
         String rawBody = requestParams.isEmpty() ? readRequestBody(request) : "";
         mergeJsonBody(requestParams, rawBody);
+        boolean success = false;
         log.info("工行支付回调原始请求,method:{},uri:{},query:{},contentType:{},params:{},body:{},headers:{}",
                 request.getMethod(),
                 request.getRequestURI(),
@@ -231,10 +232,11 @@
             Map<String, String> callback = icbcPayService.parseNotify(requestParams);
             log.info("工行支付回调解析结果:{}", JSON.toJSONString(callback));
             dispatchChargingOrderCallback(callback);
+            success = true;
         } catch (Exception e) {
-            log.error("工行支付回调处理失败,已记录原始请求并返回success避免重复通知", e);
+            log.error("工行支付回调处理失败,已记录原始请求并返回fail等待重试", e);
         } finally {
-            writeCallbackResponse(response);
+            writeCallbackResponse(response, success);
         }
     }
 
@@ -316,11 +318,11 @@
         return JSON.toJSONString(headers);
     }
 
-    private void writeCallbackResponse(HttpServletResponse response) throws Exception {
+    private void writeCallbackResponse(HttpServletResponse response, boolean success) throws Exception {
         response.setCharacterEncoding("UTF-8");
         response.setContentType("text/plain;charset=UTF-8");
         PrintWriter writer = response.getWriter();
-        writer.println("success");
+        writer.println(success ? "success" : "fail");
         writer.flush();
         writer.close();
     }
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
index ccd2a40..e477148 100644
--- a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
@@ -37,9 +37,11 @@
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.net.URI;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
@@ -294,8 +296,10 @@
                     params, result, "SUCCESS", null);
             return result;
         } catch (RuntimeException e) {
-            flowRecorder.record("PAY_NOTIFY", firstNotBlank(params.get("out_trade_no"), params.get("outTradeNo")),
-                    parseInteger(firstNotBlank(params.get("attach"), params.get("paymentType"))), null,
+            Map<String, String> auditParams = new HashMap<>(params);
+            mergeBizContent(auditParams);
+            flowRecorder.record("PAY_NOTIFY", firstNotBlank(auditParams.get("out_trade_no"), auditParams.get("outTradeNo")),
+                    parseInteger(firstNotBlank(auditParams.get("attach"), auditParams.get("paymentType"))), null,
                     params, null, "FAIL", e.getMessage());
             throw e;
         }
@@ -581,8 +585,16 @@
             String content = buildSignContent(params);
             String signType = firstNotBlank(params.get("sign_type"), properties.getSignType());
             String publicKey = normalizePublicKey(properties.getIcbcPublicKey());
-            log.info("工行回调验签参数,signType:{},publicKeyLength:{},content:{}", signType, publicKey == null ? 0 : publicKey.length(), content);
-            return IcbcSignature.verify(content, signType, publicKey, properties.getCharset(), sign);
+            String notifyPath = resolveNotifyPath();
+            log.info("工行回调验签参数,notifyPath:{},signType:{},charset:{},publicKeyLength:{},publicKeySha256:{},contentSha256:{},signLength:{}",
+                    notifyPath, signType, properties.getCharset(), publicKey == null ? 0 : publicKey.length(),
+                    sha256Hex(publicKey), sha256Hex(content), sign.length());
+            boolean verified = IcbcSignature.verify(content, signType, publicKey, properties.getCharset(), sign);
+            log.info("工行回调验签结果,verified:{}", verified);
+            if (!verified) {
+                log.warn("工行回调验签未通过,content:{}", content);
+            }
+            return verified;
         } catch (Exception e) {
             log.error("工行回调验签异常", e);
             return false;
@@ -659,6 +671,23 @@
         }
     }
 
+    private String sha256Hex(String value) {
+        if (value == null) {
+            return "";
+        }
+        try {
+            byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
+            StringBuilder hex = new StringBuilder(digest.length * 2);
+            for (byte item : digest) {
+                hex.append(String.format("%02x", item & 0xff));
+            }
+            return hex.toString();
+        } catch (Exception e) {
+            log.warn("计算工行验签摘要失败", e);
+            return "";
+        }
+    }
+
     private boolean isPaidStatus(String status) {
         return Arrays.asList("1", "2", "SUCCESS", "PAID", "PAY_SUCCESS", "TRADE_SUCCESS").contains(status);
     }
diff --git a/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java
new file mode 100644
index 0000000..d7c92a3
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java
@@ -0,0 +1,28 @@
+package com.ruoyi.payment.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class IcbcPayControllerTest {
+
+    @Test
+    void returnsFailWhenCallbackProcessingFails() throws Exception {
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        ReflectionTestUtils.invokeMethod(new IcbcPayController(), "writeCallbackResponse", response, false);
+
+        assertEquals("fail", response.getContentAsString().trim());
+    }
+
+    @Test
+    void returnsSuccessWhenCallbackProcessingSucceeds() throws Exception {
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        ReflectionTestUtils.invokeMethod(new IcbcPayController(), "writeCallbackResponse", response, true);
+
+        assertEquals("success", response.getContentAsString().trim());
+    }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java
index fd6207a..5553cbd 100644
--- a/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java
+++ b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java
@@ -1,5 +1,7 @@
 package com.ruoyi.payment.icbc.service.impl;
 
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
 import com.ruoyi.payment.icbc.config.IcbcPayProperties;
 import org.junit.jupiter.api.Test;
 import org.springframework.test.util.ReflectionTestUtils;
@@ -7,6 +9,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class IcbcPayServiceImplTest {
@@ -36,4 +39,48 @@
 
         assertTrue(Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(service, "verifyNotify", params)));
     }
+
+    @Test
+    void verifiesSecondRealPaymentCallbackWithConfiguredNotifyPath() {
+        IcbcPayProperties properties = new IcbcPayProperties();
+        properties.setCharset("UTF-8");
+        properties.setSignType("RSA2");
+        properties.setNotifyUrl("https://www.lyt18.cn/payment/icbc/callBack");
+        properties.setIcbcPublicKey(
+                "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMpjaWjngB4E3ATh+G1DVAmQnIpiPEFAEDqRfNGAVvvH35yDetqewKi0l7OEceTMN1C6NPym3zStvSoQayjYV+eIcZERkx31KhtFu9clZKgRTyPjdKMIth/wBtPKjL/5+PYalLdomM4ONthrPgnkN4x4R0+D4+EBpXo8gNiAFsNwIDAQAB");
+
+        Map<String, String> params = new HashMap<>();
+        params.put("api", "/api/cardbusiness/aggregatepay/b2c/online/consumepurchase/V1");
+        params.put("app_id", "11000000000000096526");
+        params.put("biz_content", "{\"access_type\":\"9\",\"attach\":\"1\",\"bank_disc_amt\":\"0\",\"card_flag\":\"\",\"card_kind\":\"\",\"card_no\":\"\",\"coupon_amt\":\"0\",\"cust_id\":\"\",\"decr_flag\":\"\",\"ecoupon_amt\":\"0\",\"mer_disc_amt\":\"0\",\"mer_id\":\"231011990056\",\"msg_id\":\"050200136196883020851826964\",\"open_id\":\"ow8NuxGUrqfA23hZNG-1S01p1EuM\",\"order_id\":\"231011990056000522607150004167\",\"out_trade_no\":\"CD78120260715165644735\",\"pay_time\":\"20260715165653\",\"pay_type\":\"9\",\"payment_amt\":\"600\",\"point_amt\":\"0\",\"return_code\":\"0\",\"return_msg\":\"交易成功\",\"third_party_coupon_amt\":\"0\",\"third_party_discount_amt\":\"0\",\"third_trade_no\":\"4500000248202607151296339259\",\"total_amt\":\"600\",\"total_disc_amt\":\"0\"}");
+        params.put("charset", "UTF-8");
+        params.put("format", "json");
+        params.put("from", "icbc-api");
+        params.put("sign", "GR00J4lJPekAVZMhmc70pHodJzY34aTt2oaJBgJGFsYPPvives8Rhrsksj3RFASy7IXEcr8EfexNtPjbbr9WXwuDJa4+uT2aS3b/TkuVR7xJVPXlZ/355z2kVpxLNtBAvbp6R/agWnw0XSG4pps8eVvbENQ8YE+wsjGDbIR5hNI=");
+        params.put("sign_type", "RSA");
+        params.put("timestamp", "2026-07-15 16:56:53");
+
+        IcbcPayServiceImpl service = new IcbcPayServiceImpl();
+        ReflectionTestUtils.setField(service, "properties", properties);
+
+        assertTrue(Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(service, "verifyNotify", params)));
+    }
+
+    @Test
+    void fillsOriginalMerchantIdForLegacySubOrderRefund() {
+        IcbcPayProperties properties = new IcbcPayProperties();
+        properties.setMerId("231011990056");
+        properties.setServiceMerId("231000007053");
+        properties.setServiceMerPrtclNo("23100000705300101");
+
+        IcbcSubOrderRefundReq request = new IcbcSubOrderRefundReq();
+        request.setOriMerId(null);
+
+        IcbcPayServiceImpl service = new IcbcPayServiceImpl();
+        ReflectionTestUtils.setField(service, "properties", properties);
+        CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz biz =
+                ReflectionTestUtils.invokeMethod(service, "buildSubOrderRefundBiz", request);
+
+        assertEquals("231011990056", biz.getOriMerId());
+    }
 }

--
Gitblit v1.7.1