longz
2026-07-15 c207fd1454b64f47e2e66cd2f578a2b21b06f019
分账退款fix
1个文件已添加
5个文件已修改
158 ■■■■■ 已修改文件
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImplTest.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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;
    }
}
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")));
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();
    }
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);
    }
ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/controller/IcbcPayControllerTest.java
New file
@@ -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());
    }
}
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());
    }
}