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