longz
2026-07-15 21347ba3bb614e713d8f00f1e083cdb61e2b5d3d
支付回调fix
5个文件已修改
92 ■■■■■ 已修改文件
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/SiteController.java
@@ -676,6 +676,9 @@
    @ResponseBody
    @PostMapping("/getSiteByIds")
    public R<List<Site>> getSiteByIds(@RequestBody List<Integer> ids){
        if (CollectionUtils.isEmpty(ids)) {
            return R.ok(Collections.emptyList());
        }
        List<Site> sites = siteService.list(new LambdaQueryWrapper<Site>().in(Site::getId, ids).eq(Site::getDelFlag, 0));
        return R.ok(sites);
    }
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
@@ -1,7 +1,10 @@
package com.ruoyi.order.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.order.api.model.TChargingOrder;
import com.ruoyi.order.api.model.TChargingOrderShareRecord;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
@@ -13,15 +16,34 @@
    }
    static void validateOrder(TChargingOrder order) {
        validateIcbcOrder(order);
        if (!Integer.valueOf(2).equals(order.getRechargePaymentStatus())) {
            throw new IllegalArgumentException("订单尚未支付成功");
        }
    }
    static void validateIcbcOrder(TChargingOrder order) {
        if (order == null) {
            throw new IllegalArgumentException("充电订单不存在");
        }
        if (!"ICBC".equals(order.getSourceName())) {
            throw new IllegalArgumentException("仅支持工商银行支付订单");
        }
        if (!Integer.valueOf(2).equals(order.getRechargePaymentStatus())) {
            throw new IllegalArgumentException("订单尚未支付成功");
    }
    static boolean isBankPaid(IcbcPaymentResp payment) {
        if (payment == null) {
            return false;
        }
        try {
            JSONObject response = JSON.parseObject(payment.getRawResponse());
            if (response != null && response.getIntValue("returnCode") == 0
                    && "0".equals(response.getString("pay_status"))) {
                return true;
            }
        } catch (Exception ignored) {
        }
        return "0".equals(payment.getStatus());
    }
    static BigDecimal requireAmount(BigDecimal amount) {
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -1105,7 +1105,7 @@
    @Override
    public IcbcPaymentResp queryIcbcAdminPayment(String orderCode) {
        TChargingOrder order = loadIcbcAdminOrder(orderCode);
        TChargingOrder order = loadIcbcAdminOrder(orderCode, false);
        return requireFeignData(icbcPaymentClient.query(order.getCode()), "工行支付查询失败");
    }
@@ -1114,7 +1114,7 @@
        if (request == null) {
            throw new ServiceException("请求参数不能为空");
        }
        TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode());
        TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode(), false);
        BigDecimal amount;
        try {
            amount = IcbcAdminFundOperationSupport.requireAmount(request.getAmount());
@@ -1132,8 +1132,13 @@
        }
        try {
            IcbcPaymentResp payment = requireFeignData(icbcPaymentClient.query(order.getCode()), "工行支付查询失败");
            if (!IcbcAdminFundOperationSupport.isBankPaid(payment)) {
                throw new ServiceException("工行查询未确认该订单支付成功,禁止退款");
            }
            String orderId = extractIcbcOrderId(payment);
            order.setRechargeSerialNumber(orderId);
            // 仅用于本次请求通过退款参数校验,不更新本地订单状态。
            order.setRechargePaymentStatus(2);
            RBucket<String> pendingRefund = redissonClient.getBucket("icbcAdminFund:pending:main:" + order.getCode());
            String generatedRefundNo = "ICBCADMINREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
            String pendingValue = pendingRefund.get();
@@ -1163,7 +1168,7 @@
    @Override
    public List<IcbcAdminSubOrderVO> queryIcbcAdminSubOrders(String orderCode) {
        TChargingOrder order = loadIcbcAdminOrder(orderCode);
        TChargingOrder order = loadIcbcAdminOrder(orderCode, true);
        List<TChargingOrderShareRecord> records = chargingOrderShareRecordMapper.selectList(
                new LambdaQueryWrapper<TChargingOrderShareRecord>()
                        .eq(TChargingOrderShareRecord::getChargingOrderId, order.getId())
@@ -1190,7 +1195,7 @@
        if (request == null || request.getShareRecordId() == null) {
            throw new ServiceException("请选择需要退款的分账子订单");
        }
        TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode());
        TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode(), true);
        TChargingOrderShareRecord shareRecord = chargingOrderShareRecordMapper.selectById(request.getShareRecordId());
        if (shareRecord == null || !order.getId().equals(shareRecord.getChargingOrderId())) {
            throw new ServiceException("分账记录不属于当前订单");
@@ -1248,7 +1253,7 @@
        }
    }
    private TChargingOrder loadIcbcAdminOrder(String orderCode) {
    private TChargingOrder loadIcbcAdminOrder(String orderCode, boolean requireLocalPaid) {
        if (!StringUtils.hasLength(orderCode)) {
            throw new ServiceException("订单号不能为空");
        }
@@ -1257,7 +1262,11 @@
                .eq(TChargingOrder::getDelFlag, 0)
                .last("limit 1"));
        try {
            IcbcAdminFundOperationSupport.validateOrder(order);
            if (requireLocalPaid) {
                IcbcAdminFundOperationSupport.validateOrder(order);
            } else {
                IcbcAdminFundOperationSupport.validateIcbcOrder(order);
            }
        } catch (IllegalArgumentException e) {
            throw new ServiceException(e.getMessage());
        }
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
@@ -3,6 +3,7 @@
import com.ruoyi.order.api.model.TChargingOrder;
import com.ruoyi.order.api.model.TChargingOrderShareRecord;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
import org.junit.jupiter.api.Test;
@@ -29,6 +30,28 @@
    }
    @Test
    void acceptsBankPaidOrderWhenLocalCallbackStatusIsStillPending() {
        TChargingOrder order = paidOrder();
        order.setRechargePaymentStatus(1);
        IcbcAdminFundOperationSupport.validateIcbcOrder(order);
        IcbcPaymentResp payment = new IcbcPaymentResp();
        payment.setStatus("0");
        payment.setRawResponse("{\"returnCode\":0,\"pay_status\":\"0\",\"order_id\":\"231011990056000012345\"}");
        assertTrue(IcbcAdminFundOperationSupport.isBankPaid(payment));
    }
    @Test
    void rejectsBankQueryThatDoesNotConfirmPayment() {
        IcbcPaymentResp payment = new IcbcPaymentResp();
        payment.setStatus("1");
        payment.setRawResponse("{\"returnCode\":0,\"pay_status\":\"1\"}");
        assertFalse(IcbcAdminFundOperationSupport.isBankPaid(payment));
    }
    @Test
    void buildsMainOrderRefundFromBusinessOrder() {
        IcbcRefundReq request = IcbcAdminFundOperationSupport.buildRefundRequest(
                paidOrder(), new BigDecimal("0.12"), "ICBCADMINREFUND001");
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
@@ -37,6 +37,7 @@
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.net.URI;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
@@ -581,7 +582,7 @@
            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, sign, properties.getCharset(), publicKey);
            return IcbcSignature.verify(content, signType, publicKey, properties.getCharset(), sign);
        } catch (Exception e) {
            log.error("工行回调验签异常", e);
            return false;
@@ -628,9 +629,8 @@
    private String buildSignContent(Map<String, String> params) {
        Map<String, String> sorted = new TreeMap<>(params);
        String api = sorted.remove("api");
        sorted.remove("sign");
        StringBuilder content = new StringBuilder(firstNotBlank(api, ""));
        StringBuilder content = new StringBuilder(resolveNotifyPath());
        content.append('?');
        boolean hasParam = false;
        for (Map.Entry<String, String> entry : sorted.entrySet()) {
@@ -646,6 +646,19 @@
        return content.toString();
    }
    private String resolveNotifyPath() {
        String notifyUrl = properties.getNotifyUrl();
        if (StringUtils.isEmpty(notifyUrl)) {
            return "";
        }
        try {
            return firstNotBlank(URI.create(notifyUrl).getPath(), notifyUrl);
        } catch (IllegalArgumentException e) {
            log.warn("工行回调地址格式无效,无法提取验签路径:{}", notifyUrl);
            return notifyUrl;
        }
    }
    private boolean isPaidStatus(String status) {
        return Arrays.asList("1", "2", "SUCCESS", "PAID", "PAY_SUCCESS", "TRADE_SUCCESS").contains(status);
    }