longz
2026-07-09 d10e18bc5b40cbc943736f1d1cb62aa859bb0116
支付对接
2个文件已添加
13个文件已修改
789 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java
@@ -24,6 +24,18 @@
    @ApiModelProperty("当前积分")
    private Integer points;
    @ApiModelProperty("微信openid")
    private String wxOpenid;
    @ApiModelProperty("支付宝openid")
    private String aliOpenid;
    @ApiModelProperty("支付宝userId")
    private String aliUserId;
    @ApiModelProperty("支付宝unionId")
    private String aliUnionId;
    @ApiModelProperty("当前余额")
    private BigDecimal balance;
    @ApiModelProperty("当前优惠卷数量")
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java
@@ -30,11 +30,21 @@
    private String body;
    /**
     * 原微信/支付宝 openId,骨架阶段透传给工行适配层。
     * 工行 open_id。微信小程序传微信 openId;支付宝小程序传支付宝 16 位 userId。
     */
    private String openId;
    /**
     * 工行 union_id,预留给支付宝生活号等 access_type=8 场景。
     */
    private String unionId;
    /**
     * 小程序设备标识,用于工行 deviceInfo/decive_info。
     */
    private String deviceInfo;
    /**
     * 兼容原支付方式:1=微信,2=支付宝,3=支付宝H5。
     */
    private Integer paymentType;
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java
New file
@@ -0,0 +1,27 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 工行聚合支付小订单拆分退款请求。
 */
@Data
public class IcbcSubOrderRefundReq {
    private String chargingOrderCode;
    private String seqNo;
    private String busiType;
    private String oriMerId;
    private String oriTrxDate;
    private String subOrderId;
    private String retSubOrderId;
    private String outtrxSerialNo;
    private String subMerId;
    private String subMerPrtclNo;
    private BigDecimal refundAmount;
    private String refundSource;
    private String merAcct;
    private String merSpInfor;
    private String termId;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行聚合支付小订单拆分退款响应。
 */
@Data
public class IcbcSubOrderRefundResp {
    private String chargingOrderCode;
    private String subOrderId;
    private String retSubOrderId;
    private String outtrxSerialNo;
    private String orderId;
    private String subOrderSeq;
    private String subOrderTrxid;
    private String refundAmount;
    private String status;
    private String rawResponse;
}
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
@@ -54,4 +54,24 @@
     * 地址
     */
    private String address;
    /**
     * 微信小程序 openId
     */
    private String wxOpenid;
    /**
     * 支付宝小程序 openId
     */
    private String aliOpenid;
    /**
     * 支付宝 userId。
     */
    private String aliUserId;
    /**
     * 支付宝 unionId,工行支付宝小程序支付 union_id 使用。
     */
    private String aliUnionId;
}
ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml
@@ -12,7 +12,7 @@
  profiles:
    # 环境配置
#    active: dev
    active: prod
    active: dev
---
spring:
  config:
@@ -22,7 +22,7 @@
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 192.168.110.169:8848
        server-addr: 192.168.110.80:8848
        service: ${spring.application.name}
        group: DEFAULT_GROUP
        namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -30,7 +30,7 @@
        password: nacos
      config:
        # 配置中心地址
        server-addr: 192.168.110.169:8848
        server-addr: 192.168.110.80:8848
        namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
        group: DEFAULT_GROUP
        username: nacos
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
@@ -9,6 +9,7 @@
import com.alipay.api.response.AlipaySystemOauthTokenResponse;
import com.alipay.api.response.AlipayUserInfoShareResponse;
import com.alipay.api.response.AlipayUserUserinfoShareResponse;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.account.ali.Constant.AliConstant;
@@ -28,6 +29,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -56,8 +58,16 @@
        // 登录
        AlipaySystemOauthTokenResponse response = new AliAppletTools(aliProperties).login(appletUserLogin.getCode());
        if (response.isSuccess()) {
            // 获取用户信息
//            AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken());
            String aliUnionId = firstNotBlank(readStringProperty(response, "getUnionId"), readStringProperty(response, "getUnion_id"),
                    getResponseBodyValue(response, "union_id"), getResponseBodyValue(response, "unionId"));
            String aliUserId = firstNotBlank(response.getUserId(), getResponseBodyValue(response, "user_id"), getResponseBodyValue(response, "userId"));
            log.info("支付宝授权换取用户标识成功,openId:{},userId:{},unionId:{}", response.getOpenId(), aliUserId, aliUnionId);
            AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken());
            log.info("支付宝用户信息接口返回:{}", JSON.toJSONString(userInfo));
            aliUserId = firstNotBlank(aliUserId, userInfo == null ? null : userInfo.getUserId(),
                    getResponseBodyValue(userInfo, "user_id"), getResponseBodyValue(userInfo, "userId"));
            aliUnionId = firstNotBlank(aliUnionId, readStringProperty(userInfo, "getUnionId"), readStringProperty(userInfo, "getUnion_id"),
                    getResponseBodyValue(userInfo, "union_id"), getResponseBodyValue(userInfo, "unionId"));
            if(StringUtils.isEmpty(appletUserLogin.getToken())){
                return AjaxResult.success("已拒绝授权");
            }
@@ -65,7 +75,8 @@
            String phoneNumber = new AliAppletTools(aliProperties).getPhoneNumber(appletUserLogin.getToken());
            log.info("获取支付宝用户信息:{}", phoneNumber);
            // 用户信息封装
            return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId()));
            return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId(),
                    aliUserId, aliUnionId));
        } else {
             String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
             log.warn("诊断结果:{}",diagnosisUrl);
@@ -73,4 +84,55 @@
        }
    }
    private String getResponseBodyValue(Object response, String key) {
        String body = readStringProperty(response, "getBody");
        if (StringUtils.isEmpty(body)) {
            return null;
        }
        try {
            com.alibaba.fastjson.JSONObject bodyJson = JSON.parseObject(body);
            String value = bodyJson.getString(key);
            if (StringUtils.isNotEmpty(value)) {
                return value;
            }
            for (String nestedKey : bodyJson.keySet()) {
                com.alibaba.fastjson.JSONObject nested = bodyJson.getJSONObject(nestedKey);
                if (nested != null) {
                    value = nested.getString(key);
                    if (StringUtils.isNotEmpty(value)) {
                        return value;
                    }
                }
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }
    private String readStringProperty(Object target, String methodName) {
        if (target == null) {
            return null;
        }
        try {
            Method method = target.getClass().getMethod(methodName);
            Object value = method.invoke(target);
            return value == null ? null : String.valueOf(value);
        } catch (Exception e) {
            return null;
        }
    }
    private String firstNotBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (StringUtils.isNotEmpty(value)) {
                return value;
            }
        }
        return null;
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -851,7 +851,8 @@
    @ApiOperation(value = "个人中心信息", tags = {"小程序-个人中心"})
    @GetMapping(value = "/user/info")
    public R<AppUserInfoDto> info() {
        Long userId = tokenService.getLoginUserApplet().getUserId();
        LoginUserApplet loginUserApplet = tokenService.getLoginUserApplet();
        Long userId = loginUserApplet.getUserId();
        TAppUser byId = appUserService.getById(userId);
        AppUserInfoDto appUserInfoDto = new AppUserInfoDto();
        TVip data = vipClient.getVipInfoByType(2).getData();
@@ -886,6 +887,10 @@
        appUserInfoDto.setUserCars(list);
        appUserInfoDto.setFirstAdd(byId.getFirstAdd()!=null?byId.getFirstAdd():0);
        appUserInfoDto.setPoints(byId.getPoints()==null?0:byId.getPoints());
        appUserInfoDto.setWxOpenid(byId.getWxOpenid());
        appUserInfoDto.setAliOpenid(byId.getAliOpenid());
        appUserInfoDto.setAliUserId(loginUserApplet.getAliUserId());
        appUserInfoDto.setAliUnionId(loginUserApplet.getAliUnionId());
        appUserInfoDto.setBalance(byId.getBalance()==null?BigDecimal.ZERO:byId.getBalance());
        return R.ok(appUserInfoDto);
    }
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
@@ -32,7 +32,7 @@
     * @return
     */
//    Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo);
    Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId);
    Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId);
    /**
     * 封装用户信息和token
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
@@ -88,7 +88,7 @@
    }
    @Override
    public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId) {
    public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId) {
        // 通过手机号查询用户,是否已存在手动导入用户,包含微信用户
        TAppUser appUser = this.getOne(Wrappers.lambdaQuery(TAppUser.class)
                .eq(TAppUser::getPhone, phone)
@@ -129,7 +129,10 @@
        if(Objects.nonNull(inviteUserId)){
            inviteUserService.saveInviteUser(appUser.getId(), inviteUserId);
        }
        return this.getUserInfo(appUser);
        LoginUserApplet loginUserApplet = buildLoginUserApplet(appUser);
        loginUserApplet.setAliUserId(aliUserId);
        loginUserApplet.setAliUnionId(aliUnionId);
        return buildTokenInfo(loginUserApplet);
    }
//    @Override
//    public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo) {
@@ -163,6 +166,10 @@
    @Override
    public Map<String, Object> getUserInfo(TAppUser appUser) {
        return buildTokenInfo(buildLoginUserApplet(appUser));
    }
    private LoginUserApplet buildLoginUserApplet(TAppUser appUser) {
        LoginUserApplet loginUserApplet = new LoginUserApplet();
        if(ObjectUtils.isNotNull(appUser)){
            loginUserApplet.setUserId(appUser.getId());
@@ -171,7 +178,13 @@
            loginUserApplet.setPhone(appUser.getPhone());
            loginUserApplet.setAvatar(appUser.getAvatar());
            loginUserApplet.setAddress(appUser.getProvince()+appUser.getCity());
            loginUserApplet.setWxOpenid(appUser.getWxOpenid());
            loginUserApplet.setAliOpenid(appUser.getAliOpenid());
        }
        return loginUserApplet;
    }
    private Map<String, Object> buildTokenInfo(LoginUserApplet loginUserApplet) {
        Map<String, Object> tokenInfos = new HashMap<>();
        tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
        tokenInfos.put("info",loginUserApplet);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -6257,7 +6257,7 @@
            record.setSeqNo(chargingOrder.getCode());
            record.setBusiType("1");
            record.setOriTrxDate(chargingOrder.getPayTime() == null ? LocalDate.now() : chargingOrder.getPayTime().toLocalDate());
            record.setSubOrderId("ICBCS" + chargingOrder.getCode());
            record.setSubOrderId(chargingOrder.getCode());
        }
        if ("ICBC".equals(payChannel) && partner != null) {
            record.setSubMerId(partner.getIcbcSubMerId());
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
@@ -1,5 +1,7 @@
package com.ruoyi.payment.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
@@ -8,32 +10,178 @@
import com.ruoyi.payment.api.vo.IcbcRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import com.ruoyi.payment.icbc.config.IcbcPayProperties;
import com.ruoyi.payment.icbc.service.IcbcPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.math.BigDecimal;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
 * 工行统一支付入口。
 */
@Slf4j
@RestController
@RequestMapping("/icbc")
public class IcbcPayController {
    private static final String TEST_ORDER_PREFIX = "ICBCTEST";
    private static final String TEST_SUB_MER_ID = "231011990055";
    private static final String TEST_SUB_MER_PRTCL_NO = "2310119900550201";
    @Resource
    private IcbcPayService icbcPayService;
    @Resource
    private IcbcPayProperties icbcPayProperties;
    @Resource
    private ChargingOrderClient chargingOrderClient;
    @PostMapping("/payment")
    public R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req) {
        return R.ok(icbcPayService.payment(req));
        log.info("工行支付请求入参:{}", JSON.toJSONString(req));
        IcbcPaymentResp resp = icbcPayService.payment(req);
        log.info("工行支付返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/test/payment")
    public R<IcbcPaymentResp> testPayment(@RequestBody(required = false) IcbcPaymentReq req) {
        log.info("工行测试支付请求入参:{}", JSON.toJSONString(req));
        IcbcPaymentReq paymentReq = req == null ? new IcbcPaymentReq() : req;
        paymentReq.setOutTradeNo(TEST_ORDER_PREFIX + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
        if (paymentReq.getAmount() == null || paymentReq.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            paymentReq.setAmount(new BigDecimal("0.01"));
        }
        if (paymentReq.getSubject() == null || paymentReq.getSubject().trim().isEmpty()) {
            paymentReq.setSubject("工行测试支付");
        }
        if (paymentReq.getBody() == null || paymentReq.getBody().trim().isEmpty()) {
            paymentReq.setBody("工行测试支付");
        }
        if (paymentReq.getPaymentType() == null) {
            paymentReq.setPaymentType(1);
        }
        log.info("工行测试支付实际下单参数:{}", JSON.toJSONString(paymentReq));
        IcbcPaymentResp resp = icbcPayService.payment(paymentReq);
        log.info("工行测试支付返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/test/query")
    public R<IcbcPaymentResp> testQuery(@RequestBody Map<String, Object> req) {
        String outTradeNo = getString(req, "outTradeNo");
        log.info("工行测试查询请求入参:{}", JSON.toJSONString(req));
        IcbcPaymentResp resp = icbcPayService.query(outTradeNo);
        log.info("工行测试查询返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/test/refund")
    public R<IcbcRefundResp> testRefund(@RequestBody(required = false) IcbcRefundReq req) {
        req = req == null ? new IcbcRefundReq() : req;
        log.info("工行测试退款请求入参:{}", JSON.toJSONString(req));
        if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
            req.setRefundAmount(new BigDecimal("0.01"));
        }
        if (req.getTotalAmount() == null || req.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
            req.setTotalAmount(req.getRefundAmount());
        }
        if (req.getOutRefundNo() == null || req.getOutRefundNo().trim().isEmpty()) {
            req.setOutRefundNo("ICBCREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
        }
        if (req.getReason() == null || req.getReason().trim().isEmpty()) {
            req.setReason("工行测试退款");
        }
        log.info("工行测试退款实际参数:{}", JSON.toJSONString(req));
        IcbcRefundResp resp = icbcPayService.refund(req);
        log.info("工行测试退款返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/test/sub-order/split")
    public R<IcbcSubOrderSplitResp> testSplitSubOrder(@RequestBody(required = false) IcbcSubOrderSplitReq req) {
        req = req == null ? new IcbcSubOrderSplitReq() : req;
        log.info("工行测试分账请求入参:{}", JSON.toJSONString(req));
        if (req.getSubOrderId() == null || req.getSubOrderId().trim().isEmpty()) {
            req.setSubOrderId("ICBCSUB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
        }
        if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) {
            req.setSeqNo(req.getChargingOrderCode());
        }
        if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) {
            req.setBusiType("1");
        }
        if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) {
            req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        }
        if (req.getClassifyAmt() == null || req.getClassifyAmt().compareTo(BigDecimal.ZERO) <= 0) {
            req.setClassifyAmt(new BigDecimal("0.01"));
        }
        if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) {
            req.setSubMerId(TEST_SUB_MER_ID);
        }
        if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) {
            req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO);
        }
        if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) {
            req.setOriMerId(icbcPayProperties.getMerId());
        }
        log.info("工行测试分账实际参数:{}", JSON.toJSONString(req));
        IcbcSubOrderSplitResp resp = icbcPayService.splitSubOrder(req);
        log.info("工行测试分账返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/test/sub-order/refund")
    public R<IcbcSubOrderRefundResp> testRefundSubOrder(@RequestBody(required = false) IcbcSubOrderRefundReq req) {
        req = req == null ? new IcbcSubOrderRefundReq() : req;
        log.info("工行测试分账退款请求入参:{}", JSON.toJSONString(req));
        if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) {
            req.setSeqNo(req.getChargingOrderCode());
        }
        if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) {
            req.setBusiType("1");
        }
        if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) {
            req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        }
        if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
            req.setRefundAmount(new BigDecimal("0.01"));
        }
        if (req.getRetSubOrderId() == null || req.getRetSubOrderId().trim().isEmpty()) {
            req.setRetSubOrderId("ICBCSUBREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
        }
        if (req.getOuttrxSerialNo() == null || req.getOuttrxSerialNo().trim().isEmpty()) {
            req.setOuttrxSerialNo("ICBCSUBREFUNDNO" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
        }
        if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) {
            req.setSubMerId(TEST_SUB_MER_ID);
        }
        if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) {
            req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO);
        }
        if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) {
            req.setOriMerId(icbcPayProperties.getMerId());
        }
        log.info("工行测试分账退款实际参数:{}", JSON.toJSONString(req));
        IcbcSubOrderRefundResp resp = icbcPayService.refundSubOrder(req);
        log.info("工行测试分账退款返回前端:{}", JSON.toJSONString(resp));
        return R.ok(resp);
    }
    @PostMapping("/query")
@@ -56,25 +204,48 @@
        return R.ok(icbcPayService.splitSubOrder(req));
    }
    @PostMapping("/sub-order/refund")
    public R<IcbcSubOrderRefundResp> refundSubOrder(@RequestBody IcbcSubOrderRefundReq req) {
        return R.ok(icbcPayService.refundSubOrder(req));
    }
    @PostMapping("/sub-order/query")
    public R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req) {
        return R.ok(icbcPayService.querySubOrder(req));
    }
    @PostMapping("/callBack")
    @RequestMapping(value = "/callBack", method = {RequestMethod.GET, RequestMethod.POST})
    public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, String> callback = icbcPayService.parseNotify(request.getParameterMap());
        dispatchChargingOrderCallback(callback);
        PrintWriter writer = response.getWriter();
        writer.println("success");
        writer.flush();
        writer.close();
        Map<String, String[]> requestParams = new LinkedHashMap<>(request.getParameterMap());
        String rawBody = requestParams.isEmpty() ? readRequestBody(request) : "";
        mergeJsonBody(requestParams, rawBody);
        log.info("工行支付回调原始请求,method:{},uri:{},query:{},contentType:{},params:{},body:{},headers:{}",
                request.getMethod(),
                request.getRequestURI(),
                request.getQueryString(),
                request.getContentType(),
                JSON.toJSONString(requestParams),
                rawBody,
                summarizeHeaders(request));
        try {
            Map<String, String> callback = icbcPayService.parseNotify(requestParams);
            log.info("工行支付回调解析结果:{}", JSON.toJSONString(callback));
            dispatchChargingOrderCallback(callback);
        } catch (Exception e) {
            log.error("工行支付回调处理失败,已记录原始请求并返回success避免重复通知", e);
        } finally {
            writeCallbackResponse(response);
        }
    }
    private void dispatchChargingOrderCallback(Map<String, String> callback) {
        String outTradeNo = callback.get("out_trade_no");
        String transactionId = callback.get("transaction_id");
        String attach = callback.get("attach");
        if (outTradeNo != null && outTradeNo.startsWith(TEST_ORDER_PREFIX)) {
            log.info("工行测试支付回调成功,订单号:{},流水号:{},支付类型:{}", outTradeNo, transactionId, attach);
            return;
        }
        Integer paymentType = Integer.valueOf(attach);
        if (Integer.valueOf(1).equals(paymentType)) {
            chargingOrderClient.chargingOrderWXCallback(outTradeNo, transactionId, attach);
@@ -90,4 +261,67 @@
        }
        throw new IllegalArgumentException("工行回调支付类型不支持:" + attach);
    }
    private String getString(Map<String, Object> req, String key) {
        if (req == null || req.get(key) == null) {
            throw new IllegalArgumentException("缺少参数:" + key);
        }
        return String.valueOf(req.get(key));
    }
    private String readRequestBody(HttpServletRequest request) {
        StringBuilder body = new StringBuilder();
        try (BufferedReader reader = request.getReader()) {
            String line;
            while ((line = reader.readLine()) != null) {
                body.append(line);
            }
        } catch (Exception e) {
            log.warn("读取工行支付回调body失败", e);
        }
        return body.toString();
    }
    private void mergeJsonBody(Map<String, String[]> requestParams, String rawBody) {
        if (rawBody == null || rawBody.trim().isEmpty()) {
            return;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(rawBody);
            if (jsonObject == null) {
                return;
            }
            for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                if (entry.getValue() != null && !requestParams.containsKey(entry.getKey())) {
                    requestParams.put(entry.getKey(), new String[]{String.valueOf(entry.getValue())});
                }
            }
        } catch (Exception e) {
            log.warn("工行支付回调body不是JSON格式,body:{}", rawBody);
        }
    }
    private String summarizeHeaders(HttpServletRequest request) {
        Map<String, String> headers = new LinkedHashMap<>();
        Enumeration<String> names = request.getHeaderNames();
        while (names != null && names.hasMoreElements()) {
            String name = names.nextElement();
            if ("user-agent".equalsIgnoreCase(name)
                    || "host".equalsIgnoreCase(name)
                    || "x-forwarded-for".equalsIgnoreCase(name)
                    || "x-real-ip".equalsIgnoreCase(name)) {
                headers.put(name, request.getHeader(name));
            }
        }
        return JSON.toJSONString(headers);
    }
    private void writeCallbackResponse(HttpServletResponse response) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.println("success");
        writer.flush();
        writer.close();
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
@@ -20,13 +20,19 @@
    private String charset = "UTF-8";
    private String format = "json";
    private String gatewayUrl;
    private String orderQueryUrl;
    private String refundUrl;
    private String subOrderSplitUrl;
    private String subOrderRefundUrl;
    private String subOrderQueryUrl;
    private String notifyUrl;
    private String merId;
    private String merPrtclNo;
    private String serviceMerId;
    private String serviceMerPrtclNo;
    private String tpAppId;
    private String wxTpAppId;
    private String aliTpAppId;
    private String spbillCreateIp;
    private String profitSharing;
    private Boolean verifyNotifySign = true;
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java
@@ -6,6 +6,8 @@
import com.ruoyi.payment.api.vo.IcbcRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
@@ -25,6 +27,8 @@
    IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req);
    IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req);
    IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req);
    Map<String, String> parseNotify(Map<String, String[]> requestParams);
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
@@ -2,17 +2,21 @@
import com.alibaba.fastjson.JSON;
import com.icbc.api.DefaultIcbcClient;
import com.icbc.api.IcbcRequest;
import com.icbc.api.IcbcResponse;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2;
import com.icbc.api.request.AggregatePaymentRequestV3;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1;
import com.icbc.api.request.CardbusinessOrderSuborderqueryRequestV1;
import com.icbc.api.internal.util.internal.util.fastjson.JSONArray;
import com.icbc.api.internal.util.internal.util.fastjson.JSONObject;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineOrderqryResponseV2;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineMerrefundResponseV1;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderResponseV1;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1;
import com.icbc.api.response.CardbusinessOrderSuborderqueryResponseV1;
import com.icbc.api.utils.IcbcSignature;
import com.ruoyi.common.core.utils.StringUtils;
@@ -22,6 +26,8 @@
import com.ruoyi.payment.api.vo.IcbcRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import com.ruoyi.payment.icbc.config.IcbcPayProperties;
@@ -50,20 +56,26 @@
    @Override
    public IcbcPaymentResp payment(IcbcPaymentReq req) {
        AggregatePaymentRequestV3 request = new AggregatePaymentRequestV3();
        request.setServiceUrl(properties.getGatewayUrl());
        request.setBizContent(buildAggregatePaymentBiz(req));
        IcbcPaymentResp resp = new IcbcPaymentResp();
        resp.setOrderId(req.getOutTradeNo());
        if (req != null) {
            resp.setOrderId(req.getOutTradeNo());
        }
        try {
            IcbcResponse icbcResponse = buildClient().execute(request);
            resp.setTradeNo(icbcResponse.getMsgId());
            resp.setStatus(icbcResponse.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setPayData(JSON.toJSONString(icbcResponse));
            resp.setRawResponse(JSON.toJSONString(icbcResponse));
            validateCommonConfig();
            validateConfig("icbc.pay.gatewayUrl", properties.getGatewayUrl());
            validatePaymentReq(req);
            CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2 request = new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2();
            request.setServiceUrl(properties.getGatewayUrl());
            request.setBizContent(buildConsumePurchaseBiz(req));
            CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response = executeIcbc("聚合支付下单", request);
            resp.setOrderId(firstNotBlank(response.getOut_trade_no(), req.getOutTradeNo()));
            resp.setTradeNo(firstNotBlank(response.getOrder_id(), response.getMsgId()));
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setPayData(buildConsumePurchasePayData(response, req.getPaymentType()));
            resp.setRawResponse(JSON.toJSONString(response));
        } catch (Exception e) {
            log.error("工行聚合支付调用失败,订单号:{}", req.getOutTradeNo(), e);
            log.error("工行聚合支付调用失败,订单号:{}", req == null ? null : req.getOutTradeNo(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
@@ -72,13 +84,15 @@
    @Override
    public IcbcPaymentResp query(String outTradeNo) {
        validateCommonConfig();
        validateConfig("icbc.pay.orderQueryUrl", properties.getOrderQueryUrl());
        IcbcPaymentResp resp = new IcbcPaymentResp();
        resp.setOrderId(outTradeNo);
        try {
            CardbusinessAggregatepayB2cOnlineOrderqryRequestV2 request = new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2();
            request.setServiceUrl(properties.getGatewayUrl());
            request.setServiceUrl(properties.getOrderQueryUrl());
            request.setBizContent(buildOrderQueryBiz(outTradeNo));
            CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = buildClient().execute(request);
            CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = executeIcbc("聚合支付订单查询", request);
            resp.setOrderId(firstNotBlank(response.getOut_trade_no(), outTradeNo));
            resp.setTradeNo(firstNotBlank(response.getOrder_id(), response.getThird_trade_no(), response.getMsgId()));
            resp.setStatus(firstNotBlank(response.getOrderStatus(), response.getPay_status()));
@@ -104,6 +118,8 @@
    @Override
    public IcbcRefundResp refund(IcbcRefundReq req) {
        validateCommonConfig();
        validateConfig("icbc.pay.refundUrl", properties.getRefundUrl());
        IcbcRefundResp resp = new IcbcRefundResp();
        resp.setOutTradeNo(req.getOutTradeNo());
        resp.setOrderId(req.getOrderId());
@@ -112,7 +128,7 @@
            CardbusinessAggregatepayB2cOnlineMerrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1();
            request.setServiceUrl(properties.getRefundUrl());
            request.setBizContent(buildRefundBiz(req));
            CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = buildClient().execute(request);
            CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = executeIcbc("线上POS退款", request);
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
            resp.setOutTradeNo(firstNotBlank(response.getOutTradeNo(), req.getOutTradeNo()));
@@ -131,6 +147,9 @@
    @Override
    public IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req) {
        validateCommonConfig();
        validateConfig("icbc.pay.merPrtclNo", properties.getMerPrtclNo());
        validateConfig("icbc.pay.subOrderSplitUrl", properties.getSubOrderSplitUrl());
        IcbcSubOrderSplitResp resp = new IcbcSubOrderSplitResp();
        resp.setChargingOrderCode(req.getChargingOrderCode());
        resp.setSubOrderId(req.getSubOrderId());
@@ -138,7 +157,7 @@
            CardbusinessAggregatepayB2cOnlineSersuborderRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1();
            request.setServiceUrl(properties.getSubOrderSplitUrl());
            request.setBizContent(buildSubOrderSplitBiz(req));
            CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = buildClient().execute(request);
            CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = executeIcbc("B2C小订单拆分", request);
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
            fillSplitOutput(resp, response.getOrderSubmitActOutput());
@@ -151,13 +170,43 @@
    }
    @Override
    public IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req) {
        validateCommonConfig();
        validateConfig("icbc.pay.subOrderRefundUrl", properties.getSubOrderRefundUrl());
        IcbcSubOrderRefundResp resp = new IcbcSubOrderRefundResp();
        resp.setChargingOrderCode(req.getChargingOrderCode());
        resp.setSubOrderId(req.getSubOrderId());
        resp.setRetSubOrderId(req.getRetSubOrderId());
        resp.setOuttrxSerialNo(req.getOuttrxSerialNo());
        try {
            CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1();
            request.setServiceUrl(properties.getSubOrderRefundUrl());
            request.setBizContent(buildSubOrderRefundBiz(req));
            CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1 response = executeIcbc("B2C小订单拆分退款", request);
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
            resp.setOrderId(response.getOrder_id());
            resp.setSubOrderSeq(response.getSub_order_seq());
            resp.setSubOrderTrxid(response.getSub_order_trxid());
            resp.setRefundAmount(toFen(req.getRefundAmount()));
        } catch (Exception e) {
            log.error("工行小订单拆分退款失败,订单号:{},子订单号:{},退款流水:{}", req.getChargingOrderCode(), req.getSubOrderId(), req.getOuttrxSerialNo(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req) {
        validateCommonConfig();
        validateConfig("icbc.pay.subOrderQueryUrl", properties.getSubOrderQueryUrl());
        IcbcSubOrderQueryResp resp = new IcbcSubOrderQueryResp();
        try {
            CardbusinessOrderSuborderqueryRequestV1 request = new CardbusinessOrderSuborderqueryRequestV1();
            request.setServiceUrl(properties.getSubOrderQueryUrl());
            request.setBizContent(buildSubOrderQueryBiz(req));
            CardbusinessOrderSuborderqueryResponseV1 response = buildClient().execute(request);
            CardbusinessOrderSuborderqueryResponseV1 response = executeIcbc("子订单查询", request);
            resp.setOrderStatus(response.getOrderStatus());
            resp.setSubOutMerNo(response.getSubOutMerNo());
            resp.setSubOutAgreeNo(response.getSubOutAgreeNo());
@@ -187,7 +236,8 @@
        if (!verifyNotify(params)) {
            throw new IllegalArgumentException("工行回调验签失败");
        }
        String status = firstNotBlank(params.get("orderStatus"), params.get("pay_status"), params.get("status"));
        mergeBizContent(params);
        String status = firstNotBlank(params.get("return_code"), params.get("orderStatus"), params.get("pay_status"), params.get("status"));
        if (StringUtils.isNotEmpty(status) && !isPaidStatus(status)) {
            throw new IllegalArgumentException("工行回调支付状态非成功:" + status);
        }
@@ -210,45 +260,114 @@
    private DefaultIcbcClient buildClient() {
        return new DefaultIcbcClient(
                properties.getAppId(),
                properties.getPrivateKey(),
                properties.getSignType(),
                properties.getCharset(),
                properties.getFormat(),
                properties.getPrivateKey(),
                properties.getIcbcPublicKey());
    }
    private AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz buildAggregatePaymentBiz(IcbcPaymentReq req) {
        AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz biz = new AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz();
        biz.setApp_id(properties.getAppId());
        biz.setCharset(properties.getCharset());
        biz.setFormat(properties.getFormat());
        biz.setSign_type(properties.getSignType());
        biz.setInterfaceVersion("1.0.0");
        biz.setMerId(properties.getMerId());
        biz.setOutTradeNo(req.getOutTradeNo());
        biz.setOrderAmount(toFen(req.getAmount()));
        biz.setTotal_amt(toFen(req.getAmount()));
        biz.setGoodsBody(req.getBody());
        biz.setMerHint(req.getSubject());
    private <T extends IcbcResponse> T executeIcbc(String scene, IcbcRequest<T> request) throws Exception {
        log.info("工行{}请求,url:{},bizContent:{}", scene, request.getServiceUrl(), JSON.toJSONString(request.getBizContent()));
        T response = buildClient().execute(request, System.currentTimeMillis() + "");
        log.info("工行{}响应:{}", scene, JSON.toJSONString(response));
        return response;
    }
    private void validateCommonConfig() {
        validateConfig("icbc.pay.appId", properties.getAppId());
        validateConfig("icbc.pay.privateKey", properties.getPrivateKey());
        validateConfig("icbc.pay.icbcPublicKey", properties.getIcbcPublicKey());
        validateConfig("icbc.pay.merId", properties.getMerId());
    }
    private void validateConfig(String name, String value) {
        if (StringUtils.isEmpty(value)) {
            throw new IllegalStateException("工行配置缺失:" + name);
        }
    }
    private void validatePaymentReq(IcbcPaymentReq req) {
        if (req == null) {
            throw new IllegalArgumentException("工行支付请求不能为空");
        }
        validateConfig("outTradeNo", req.getOutTradeNo());
        if (req.getAmount() == null || req.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("工行支付金额必须大于0");
        }
        if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
            validateConfig("icbc.pay." + (Integer.valueOf(1).equals(req.getPaymentType()) ? "wxTpAppId" : "aliTpAppId"), getTpAppId(req.getPaymentType()));
        }
        if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
            validateConfig("openId", req.getOpenId());
        }
    }
    private CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz buildConsumePurchaseBiz(IcbcPaymentReq req) {
        CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz =
                new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz();
        biz.setMer_id(properties.getMerId());
        setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
        biz.setOut_trade_no(req.getOutTradeNo());
        biz.setIcbc_appid(properties.getAppId());
        biz.setShop_appid(getTpAppId(req.getPaymentType()));
        if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
            biz.setOpen_id(req.getOpenId());
        }
        setAlipayUnionIdIfNeeded(biz, req);
        biz.setPay_mode(toIcbcPayMode(req.getPaymentType()));
        biz.setAccess_type(toIcbcAccessType(req.getPaymentType()));
        biz.setOrig_date_time(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date()));
        biz.setDecive_info(buildDeviceInfo(req));
        biz.setBody(firstNotBlank(req.getBody(), req.getSubject(), "充电充值"));
        biz.setFee_type("001");
        biz.setTotal_fee(toFen(req.getAmount()));
        biz.setSpbill_create_ip(properties.getSpbillCreateIp());
        biz.setMer_url(StringUtils.isNotEmpty(properties.getNotifyUrl()) ? properties.getNotifyUrl() : req.getNotifyUrl());
        biz.setReturn_url(req.getReturnUrl());
        biz.setAttach(String.valueOf(req.getPaymentType()));
        biz.setTpOpenId(req.getOpenId());
        biz.setNotifyUrl(StringUtils.isNotEmpty(properties.getNotifyUrl()) ? properties.getNotifyUrl() : req.getNotifyUrl());
        biz.setReturnUrl(req.getReturnUrl());
        biz.setOrderDate(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
        biz.setChannelId(toIcbcChannel(req.getPaymentType()));
        biz.setSpbillCreateIp(properties.getSpbillCreateIp());
        biz.setTpAppId(properties.getTpAppId());
        // TODO 将 req.profitSharing/properties.profitSharing 按工行分账文档映射到子订单或分账请求。
        biz.setNotify_type("HS");
        biz.setResult_type("0");
        biz.setOrder_apd_inf(req.getSubject());
        return biz;
    }
    private void setAlipayUnionIdIfNeeded(CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz,
                                          IcbcPaymentReq req) {
        if (!Integer.valueOf(2).equals(req.getPaymentType()) && !Integer.valueOf(3).equals(req.getPaymentType())) {
            return;
        }
        if (StringUtils.isNotEmpty(req.getUnionId())) {
            biz.setUnion_id(truncate(req.getUnionId(), 16));
        }
    }
    private String buildConsumePurchasePayData(CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response, Integer paymentType) {
        Map<String, Object> payData = new HashMap<>();
        payData.put("orderId", response.getOrder_id());
        payData.put("outTradeNo", response.getOut_trade_no());
        payData.put("totalAmt", response.getTotal_amt());
        payData.put("payMode", response.getPay_mode());
        payData.put("accessType", response.getAccess_type());
        payData.put("tradeType", response.getTrade_type());
        payData.put("wxDataPackage", response.getWx_data_package());
        payData.put("zfbDataPackage", response.getZfb_data_package());
        payData.put("unionDataPackage", response.getUnion_data_package());
        if (Integer.valueOf(1).equals(paymentType) && StringUtils.isNotEmpty(response.getWx_data_package())) {
            payData.put("package", response.getWx_data_package());
        }
        if ((Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) && StringUtils.isNotEmpty(response.getZfb_data_package())) {
            payData.put("package", response.getZfb_data_package());
        }
        return JSON.toJSONString(payData);
    }
    private CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz buildOrderQueryBiz(String outTradeNo) {
        CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz biz =
                new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz();
        biz.setOut_trade_no(outTradeNo);
        biz.setDeal_flag("0");
        biz.setIcbc_appid(properties.getAppId());
        biz.setMer_id(properties.getMerId());
        biz.setMer_prtcl_no(properties.getMerPrtclNo());
        setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
        return biz;
    }
@@ -259,9 +378,9 @@
        biz.setOut_trade_no(req.getOutTradeNo());
        biz.setOuttrx_serial_no(req.getOutRefundNo());
        biz.setRet_total_amt(toFen(req.getRefundAmount()));
        biz.setTrnsc_ccy("CNY");
        biz.setTrnsc_ccy("001");
        biz.setMer_id(properties.getMerId());
        biz.setMer_prtcl_no(properties.getMerPrtclNo());
        setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
        biz.setIcbc_appid(properties.getAppId());
        biz.setMer_acct(req.getMerAcct());
        biz.setOrder_apd_inf(req.getReason());
@@ -287,11 +406,35 @@
        item.put("mer_sp_infor", req.getMerSpInfor());
        item.put("term_id", req.getTermId());
        orderSubmitActInput.add(item);
        biz.setMerId(properties.getMerId());
        biz.setMerPrtclNo(properties.getMerPrtclNo());
        biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId()));
        biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo()));
        biz.setIcbcAppid(properties.getAppId());
        biz.setOrderNum("1");
        biz.setOrderSubmitActInput(orderSubmitActInput);
        return biz;
    }
    private CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz buildSubOrderRefundBiz(IcbcSubOrderRefundReq req) {
        CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz biz =
                new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz();
        biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId()));
        biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo()));
        biz.setIcbcAppid(properties.getAppId());
        biz.setOperFlag("0");
        biz.setSubOrderId(req.getSubOrderId());
        biz.setSeqNo(req.getSeqNo());
        biz.setBusiType(firstNotBlank(req.getBusiType(), "1"));
        biz.setOriTrxDate(req.getOriTrxDate());
        biz.setOriMerId(firstNotBlank(req.getOriMerId(), properties.getMerId()));
        biz.setSubMerId(req.getSubMerId());
        biz.setSubMerPrtclNo(req.getSubMerPrtclNo());
        biz.setClassifyAmt(toFen(req.getRefundAmount()));
        biz.setRetSubOrderId(req.getRetSubOrderId());
        biz.setOuttrxSerialNo(req.getOuttrxSerialNo());
        biz.setRefundSource(req.getRefundSource());
        biz.setMerAcct(req.getMerAcct());
        biz.setMerSpInfor(req.getMerSpInfor());
        biz.setTermId(req.getTermId());
        return biz;
    }
@@ -332,14 +475,46 @@
        return amount == null ? "0" : amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toPlainString();
    }
    private String toIcbcChannel(Integer paymentType) {
    private String getTpAppId(Integer paymentType) {
        if (Integer.valueOf(1).equals(paymentType)) {
            return "WECHAT";
            return firstNotBlank(properties.getWxTpAppId(), properties.getTpAppId());
        }
        if (isAlipay(paymentType)) {
            return firstNotBlank(properties.getAliTpAppId(), properties.getTpAppId());
        }
        return properties.getTpAppId();
    }
    private boolean isAlipay(Integer paymentType) {
        return Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType);
    }
    private void setMerPrtclNoIfPresent(java.util.function.Consumer<String> setter) {
        if (StringUtils.isNotEmpty(properties.getMerPrtclNo())) {
            setter.accept(properties.getMerPrtclNo());
        }
    }
    private String buildDeviceInfo(IcbcPaymentReq req) {
        String value = firstNotBlank(req.getOpenId(), req.getUnionId(), "USER") + "_" + firstNotBlank(req.getDeviceInfo(), properties.getSpbillCreateIp(), "MINIPROGRAM");
        return value.length() > 32 ? value.substring(0, 32) : value;
    }
    private String toIcbcPayMode(Integer paymentType) {
        if (Integer.valueOf(1).equals(paymentType)) {
            return "9";
        }
        if (Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) {
            return "ALIPAY";
            return "10";
        }
        return "ICBC";
        return "9";
    }
    private String toIcbcAccessType(Integer paymentType) {
        if (Integer.valueOf(3).equals(paymentType)) {
            return "7";
        }
        return "9";
    }
    private boolean verifyNotify(Map<String, String> params) {
@@ -352,25 +527,69 @@
        }
        try {
            String content = buildSignContent(params);
            return IcbcSignature.verify(content, properties.getSignType(), sign, properties.getCharset(), properties.getIcbcPublicKey());
            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);
        } catch (Exception e) {
            log.error("工行回调验签异常", e);
            return false;
        }
    }
    private void mergeBizContent(Map<String, String> params) {
        String bizContent = params.get("biz_content");
        if (StringUtils.isEmpty(bizContent)) {
            return;
        }
        try {
            JSONObject biz = JSONObject.parseObject(bizContent);
            if (biz == null) {
                return;
            }
            for (Map.Entry<String, Object> entry : biz.entrySet()) {
                if (entry.getValue() != null && !params.containsKey(entry.getKey())) {
                    params.put(entry.getKey(), String.valueOf(entry.getValue()));
                }
            }
        } catch (Exception e) {
            log.warn("工行回调biz_content解析失败,bizContent:{}", bizContent, e);
        }
    }
    private String normalizePublicKey(String publicKey) {
        if (publicKey == null) {
            return null;
        }
        String normalized = publicKey;
        String lower = normalized.toLowerCase();
        int start = lower.indexOf("start public key");
        int end = lower.indexOf("end public key");
        if (start >= 0 && end > start) {
            normalized = normalized.substring(start + "start public key".length(), end);
        }
        return normalized
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replace("#", "")
                .replaceAll("\\s+", "");
    }
    private String buildSignContent(Map<String, String> params) {
        Map<String, String> sorted = new TreeMap<>(params);
        String api = sorted.remove("api");
        sorted.remove("sign");
        sorted.remove("sign_type");
        StringBuilder content = new StringBuilder();
        StringBuilder content = new StringBuilder(firstNotBlank(api, ""));
        content.append('?');
        boolean hasParam = false;
        for (Map.Entry<String, String> entry : sorted.entrySet()) {
            if (StringUtils.isEmpty(entry.getValue())) {
                continue;
            }
            if (content.length() > 0) {
            if (hasParam) {
                content.append('&');
            }
            hasParam = true;
            content.append(entry.getKey()).append('=').append(entry.getValue());
        }
        return content.toString();
@@ -388,4 +607,11 @@
        }
        return "";
    }
    private String truncate(String value, int maxLength) {
        if (StringUtils.isEmpty(value) || value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }
}