longz
2026-05-30 73df73c66bc540fbeb1c47d14debcd38649a2c25
 集成工行统一支付功能
26个文件已添加
9个文件已修改
1733 ■■■■■ 已修改文件
.gitignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 541 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/icbc-ca.jar 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/readme.md 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/lib/update-info.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/pom.xml 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260529_icbc_partner_profit_sharing.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260530_charging_order_share_record.sql 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/charging_pile_service.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -5,6 +5,7 @@
# Package Files #
*.jar
!ruoyi-service/ruoyi-payment/lib/*.jar
*.war
*.ear
@@ -17,4 +18,4 @@
*.yaml
*.yml
*.log
*.log
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java
@@ -25,6 +25,14 @@
    private String account;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "工行合作商ID")
    private String icbcPartnerId;
    @ApiModelProperty(value = "工行二级商户编号")
    private String icbcSubMerId;
    @ApiModelProperty(value = "工行二级商户协议编号")
    private String icbcSubMerPrtclNo;
    @ApiModelProperty(value = "工行直接分账商户清算账号")
    private String icbcMerAcct;
    @ApiModelProperty(value = "电费分成比例")
    private BigDecimal electricityShareRatio;
    @ApiModelProperty(value = "服务费分成比例")
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java
@@ -69,6 +69,24 @@
    @ApiModelProperty(value = "工行合作商ID")
    private String icbcPartnerId;
    /**
     * 工行二级商户编号
     */
    @TableField("icbc_sub_mer_id")
    @ApiModelProperty(value = "工行二级商户编号")
    private String icbcSubMerId;
    /**
     * 工行二级商户协议编号
     */
    @TableField("icbc_sub_mer_prtcl_no")
    @ApiModelProperty(value = "工行二级商户协议编号")
    private String icbcSubMerPrtclNo;
    /**
     * 工行直接分账商户清算账号
     */
    @TableField("icbc_mer_acct")
    @ApiModelProperty(value = "工行直接分账商户清算账号")
    private String icbcMerAcct;
    /**
     * 电费分成比例
     */
    @TableField("electricity_share_ratio")
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java
New file
@@ -0,0 +1,124 @@
package com.ruoyi.order.api.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.web.domain.BasePojo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * 充电订单分账记录,覆盖余额、微信、支付宝、工行等实际支付渠道。
 */
@Data
@TableName("t_charging_order_share_record")
public class TChargingOrderShareRecord extends BasePojo {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("charging_order_id")
    private Long chargingOrderId;
    @TableField("charging_order_code")
    private String chargingOrderCode;
    @TableField("site_id")
    private Integer siteId;
    @TableField("partner_id")
    private Integer partnerId;
    @TableField("pay_channel")
    private String payChannel;
    @TableField("channel_pay_amount")
    private BigDecimal channelPayAmount;
    @TableField("channel_gift_amount")
    private BigDecimal channelGiftAmount;
    @TableField("channel_pay_ratio")
    private BigDecimal channelPayRatio;
    @TableField("channel_electricity_amount")
    private BigDecimal channelElectricityAmount;
    @TableField("channel_service_amount")
    private BigDecimal channelServiceAmount;
    @TableField("electricity_share_ratio")
    private BigDecimal electricityShareRatio;
    @TableField("service_share_ratio")
    private BigDecimal serviceShareRatio;
    @TableField("electricity_share_amount")
    private BigDecimal electricityShareAmount;
    @TableField("service_share_amount")
    private BigDecimal serviceShareAmount;
    @TableField("share_amount")
    private BigDecimal shareAmount;
    @TableField("need_online_share")
    private Boolean needOnlineShare;
    @TableField("online_share_status")
    private String onlineShareStatus;
    @TableField("seq_no")
    private String seqNo;
    @TableField("busi_type")
    private String busiType;
    @TableField("ori_mer_id")
    private String oriMerId;
    @TableField("ori_trx_date")
    private LocalDate oriTrxDate;
    @TableField("sub_order_id")
    private String subOrderId;
    @TableField("sub_mer_id")
    private String subMerId;
    @TableField("sub_mer_prtcl_no")
    private String subMerPrtclNo;
    @TableField("mer_acct")
    private String merAcct;
    @TableField("icbc_order_id")
    private String icbcOrderId;
    @TableField("sub_order_seq")
    private String subOrderSeq;
    @TableField("sub_order_trxid")
    private String subOrderTrxid;
    @TableField("rec_result")
    private String recResult;
    @TableField("request_json")
    private String requestJson;
    @TableField("response_json")
    private String responseJson;
    @TableField("fail_reason")
    private String failReason;
    @TableField("retry_count")
    private Integer retryCount;
    @TableField("last_retry_time")
    private LocalDateTime lastRetryTime;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java
New file
@@ -0,0 +1,60 @@
package com.ruoyi.payment.api.factory;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.payment.api.feignClient.IcbcPaymentClient;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
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.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
 * 工行支付调用降级处理。
 */
@Component
public class IcbcPaymentFallbackFactory implements FallbackFactory<IcbcPaymentClient> {
    private static final Logger log = LoggerFactory.getLogger(IcbcPaymentFallbackFactory.class);
    @Override
    public IcbcPaymentClient create(Throwable throwable) {
        log.error("工行支付调用失败:{}", throwable.getMessage());
        return new IcbcPaymentClient() {
            @Override
            public R<IcbcPaymentResp> payment(IcbcPaymentReq req) {
                return R.fail("调起工行支付失败:" + throwable.getMessage());
            }
            @Override
            public R<IcbcPaymentResp> query(String outTradeNo) {
                return R.fail("查询工行支付订单失败:" + throwable.getMessage());
            }
            @Override
            public R<IcbcPaymentResp> close(String outTradeNo) {
                return R.fail("关闭工行支付订单失败:" + throwable.getMessage());
            }
            @Override
            public R<IcbcRefundResp> refund(IcbcRefundReq req) {
                return R.fail("工行退款失败:" + throwable.getMessage());
            }
            @Override
            public R<IcbcSubOrderSplitResp> splitSubOrder(IcbcSubOrderSplitReq req) {
                return R.fail("工行小订单拆分失败:" + throwable.getMessage());
            }
            @Override
            public R<IcbcSubOrderQueryResp> querySubOrder(IcbcSubOrderQueryReq req) {
                return R.fail("查询工行子订单失败:" + throwable.getMessage());
            }
        };
    }
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java
New file
@@ -0,0 +1,60 @@
package com.ruoyi.payment.api.feignClient;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.payment.api.factory.IcbcPaymentFallbackFactory;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
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.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 工行统一支付服务。
 */
@FeignClient(contextId = "IcbcPaymentClient", value = ServiceNameConstants.PAYMENT_SERVICE, fallbackFactory = IcbcPaymentFallbackFactory.class)
public interface IcbcPaymentClient {
    /**
     * 调起工行聚合支付。
     */
    @PostMapping("/icbc/payment")
    R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req);
    /**
     * 查询工行支付订单。
     */
    @PostMapping("/icbc/query")
    R<IcbcPaymentResp> query(@RequestParam("outTradeNo") String outTradeNo);
    /**
     * 关闭工行待支付订单。
     */
    @PostMapping("/icbc/close")
    R<IcbcPaymentResp> close(@RequestParam("outTradeNo") String outTradeNo);
    /**
     * 工行聚合支付退款。
     */
    @PostMapping("/icbc/refund")
    R<IcbcRefundResp> refund(@RequestBody IcbcRefundReq req);
    /**
     * 工行小订单拆分。
     */
    @PostMapping("/icbc/sub-order/split")
    R<IcbcSubOrderSplitResp> splitSubOrder(@RequestBody IcbcSubOrderSplitReq req);
    /**
     * 查询工行子订单。
     */
    @PostMapping("/icbc/sub-order/query")
    R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req);
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java
New file
@@ -0,0 +1,56 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 工行统一支付请求。
 */
@Data
public class IcbcPaymentReq {
    /**
     * 业务订单号。
     */
    private String outTradeNo;
    /**
     * 支付金额,单位元。
     */
    private BigDecimal amount;
    /**
     * 订单标题。
     */
    private String subject;
    /**
     * 订单描述。
     */
    private String body;
    /**
     * 原微信/支付宝 openId,骨架阶段透传给工行适配层。
     */
    private String openId;
    /**
     * 兼容原支付方式:1=微信,2=支付宝,3=支付宝H5。
     */
    private Integer paymentType;
    /**
     * 支付结果通知地址。
     */
    private String notifyUrl;
    /**
     * H5 支付回跳地址。
     */
    private String returnUrl;
    /**
     * 分账预留字段,后续接入站点、运营商、平台分账规则后映射到工行请求。
     */
    private String profitSharing;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行统一支付响应。
 */
@Data
public class IcbcPaymentResp {
    /**
     * 业务订单号。
     */
    private String orderId;
    /**
     * 工行侧交易号或支付流水。
     */
    private String tradeNo;
    /**
     * 工行支付状态或本地骨架状态。
     */
    private String status;
    /**
     * 给前端调起支付使用的数据。
     */
    private String payData;
    /**
     * 工行 SDK 原始响应摘要。
     */
    private String rawResponse;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 工行聚合支付退款请求。
 */
@Data
public class IcbcRefundReq {
    private String outTradeNo;
    private String orderId;
    private String outRefundNo;
    private BigDecimal refundAmount;
    private BigDecimal totalAmount;
    private String reason;
    private String merAcct;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java
New file
@@ -0,0 +1,18 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行聚合支付退款响应。
 */
@Data
public class IcbcRefundResp {
    private String outTradeNo;
    private String orderId;
    private String outRefundNo;
    private String refundId;
    private String refundTime;
    private String refundAmount;
    private String status;
    private String rawResponse;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行聚合支付子订单查询请求。
 */
@Data
public class IcbcSubOrderQueryReq {
    private String serNo;
    private String orderNoType;
    private String orderId;
    private String merOrderId;
    private String prtiId;
    private String queryType;
    private String subOrderNo;
    private String merTrnscSerno;
    private String subPrtiId;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java
New file
@@ -0,0 +1,21 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行聚合支付子订单查询响应。
 */
@Data
public class IcbcSubOrderQueryResp {
    private String orderStatus;
    private String subOutMerNo;
    private String subOutAgreeNo;
    private String outTradeNo;
    private String orderId;
    private String subOrderNo;
    private String subMerOrderId;
    private String orderAmnt;
    private String payDate;
    private String status;
    private String rawResponse;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java
New file
@@ -0,0 +1,24 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 工行聚合支付小订单拆分请求。
 */
@Data
public class IcbcSubOrderSplitReq {
    private String chargingOrderCode;
    private String seqNo;
    private String busiType;
    private String oriMerId;
    private String oriTrxDate;
    private String subOrderId;
    private String subMerId;
    private String subMerPrtclNo;
    private BigDecimal classifyAmt;
    private String merAcct;
    private String merSpInfor;
    private String termId;
}
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.payment.api.vo;
import lombok.Data;
/**
 * 工行聚合支付小订单拆分响应。
 */
@Data
public class IcbcSubOrderSplitResp {
    private String chargingOrderCode;
    private String subOrderId;
    private String orderId;
    private String subOrderSeq;
    private String subOrderTrxid;
    private String recResult;
    private String classifyAmt;
    private String subMerId;
    private String status;
    private String rawResponse;
}
ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,4 +1,5 @@
com.ruoyi.payment.api.factory.WxPaymentFallbackFactory
com.ruoyi.payment.api.factory.AliPaymentFallbackFactory
com.ruoyi.payment.api.factory.AppAliPaymentFallbackFactory
com.ruoyi.payment.api.factory.H5AliPaymentFallbackFactory
com.ruoyi.payment.api.factory.H5AliPaymentFallbackFactory
com.ruoyi.payment.api.factory.IcbcPaymentFallbackFactory
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -25,6 +25,7 @@
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@@ -153,8 +154,32 @@
        if(null == partner.getId() && StringUtils.isEmpty(partner.getPassword())){
            return AjaxResult.error("登录密码不能为空");
        }
        if (isInvalidShareRatio(partner.getElectricityShareRatio())) {
            return AjaxResult.error("电费分成比例必须为0-100且最多两位小数");
        }
        if (isInvalidShareRatio(partner.getServiceShareRatio())) {
            return AjaxResult.error("服务费分成比例必须为0-100且最多两位小数");
        }
        if (partner.getElectricityShareRatio() != null || partner.getServiceShareRatio() != null) {
            if (StringUtils.isEmpty(partner.getIcbcSubMerId())) {
                return AjaxResult.error("填写分成比例时工行二级商户编号不能为空");
            }
            if (StringUtils.isEmpty(partner.getIcbcSubMerPrtclNo())) {
                return AjaxResult.error("填写分成比例时工行二级商户协议编号不能为空");
            }
        }
        return AjaxResult.success();
    }
    private boolean isInvalidShareRatio(BigDecimal ratio) {
        if (ratio == null) {
            return false;
        }
        if (ratio.compareTo(BigDecimal.ZERO) < 0 || ratio.compareTo(new BigDecimal("100")) > 0) {
            return true;
        }
        return Math.max(ratio.stripTrailingZeros().scale(), 0) > 2;
    }
    
    
    /**
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
@@ -11,6 +11,9 @@
        <result column="logo_url" property="logoUrl" />
        <result column="rate" property="rate" />
        <result column="icbc_partner_id" property="icbcPartnerId" />
        <result column="icbc_sub_mer_id" property="icbcSubMerId" />
        <result column="icbc_sub_mer_prtcl_no" property="icbcSubMerPrtclNo" />
        <result column="icbc_mer_acct" property="icbcMerAcct" />
        <result column="electricity_share_ratio" property="electricityShareRatio" />
        <result column="service_share_ratio" property="serviceShareRatio" />
        <result column="address" property="address" />
@@ -33,6 +36,10 @@
        address,
        account,
        remark,
        icbc_partner_id as icbcPartnerId,
        icbc_sub_mer_id as icbcSubMerId,
        icbc_sub_mer_prtcl_no as icbcSubMerPrtclNo,
        icbc_mer_acct as icbcMerAcct,
        electricity_share_ratio as electricityShareRatio,
        service_share_ratio as serviceShareRatio
        from t_partner where del_flag = 0
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java
New file
@@ -0,0 +1,7 @@
package com.ruoyi.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.order.api.model.TChargingOrderShareRecord;
public interface TChargingOrderShareRecordMapper extends BaseMapper<TChargingOrderShareRecord> {
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -50,6 +50,7 @@
import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
import com.ruoyi.order.dto.*;
import com.ruoyi.order.mapper.TChargingOrderMapper;
import com.ruoyi.order.mapper.TChargingOrderShareRecordMapper;
import com.ruoyi.order.mapper.TSettlementConfirmMapper;
import com.ruoyi.order.service.TChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TChargingOrderRefundService;
@@ -70,6 +71,7 @@
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.payment.api.feignClient.AliPaymentClient;
import com.ruoyi.payment.api.feignClient.H5AliPaymentClient;
import com.ruoyi.payment.api.feignClient.IcbcPaymentClient;
import com.ruoyi.payment.api.feignClient.WxPaymentClient;
import com.ruoyi.payment.api.model.*;
import com.ruoyi.payment.api.vo.*;
@@ -149,6 +151,10 @@
    private AliPaymentClient aliPaymentClient;
    @Resource
    private H5AliPaymentClient h5AliPaymentClient;
    @Resource
    private IcbcPaymentClient icbcPaymentClient;
    @Resource
    private TChargingOrderShareRecordMapper chargingOrderShareRecordMapper;
    
    @Resource
    private AppCouponClient appCouponClient;
@@ -552,7 +558,38 @@
            changeUserBalance(chargingOrder.getAppUserId(), balanceRefundAmount);
            recordBalanceRefund(chargingOrder, balanceRefundAmount, refund.getRefundCode());
        }
        triggerChargingOrderShare(chargingOrder);
    }
    private boolean refundByIcbcAndFinish(TChargingOrder chargingOrder, TChargingOrderRefund refund, BigDecimal thirdRefundAmount,
                                          String reason, boolean finishManual, BigDecimal refundAmount, BigDecimal balanceRefundAmount) {
        IcbcRefundReq req = new IcbcRefundReq();
        req.setOutTradeNo(chargingOrder.getCode());
        req.setOrderId(chargingOrder.getRechargeSerialNumber());
        req.setOutRefundNo(refund.getRefundCode());
        req.setRefundAmount(thirdRefundAmount);
        req.setTotalAmount(getRefundableThirdPayAmount(chargingOrder));
        req.setReason(reason);
        R<IcbcRefundResp> result = icbcPaymentClient.refund(req);
        IcbcRefundResp resp = result == null ? null : result.getData();
        if (result == null || result.getCode() != 200 || resp == null || !"SUCCESS".equals(resp.getStatus())) {
            log.error("工行退款失败,订单号:{},退款单号:{},返回:{}", chargingOrder.getCode(), refund.getRefundCode(), result == null ? null : result.getMsg());
            return false;
        }
        if (finishManual) {
            refund.setRefundSerialNumber(firstNotBlank(resp.getRefundId(), resp.getOutRefundNo()));
            refund.setRefundTime(LocalDateTime.now());
            finishManualChargingRefund(chargingOrder, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
        } else {
            chargingOrderRefundService.save(refund);
            chargingOrderStartupFailureWxRefund(refund.getRefundCode(), firstNotBlank(resp.getRefundId(), resp.getOutRefundNo()), "SUCCESS", resp.getRefundTime());
        }
        return true;
    }
    private boolean isIcbcOrder(TChargingOrder chargingOrder) {
        return chargingOrder != null && "ICBC".equals(chargingOrder.getSourceName());
    }
    /**
     * 判断订单是否为白名单订单
@@ -641,7 +678,13 @@
                .eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
        for (TChargingOrder tChargingOrder : list) {
            Integer rechargePaymentType = tChargingOrder.getRechargePaymentType();
            if (1 == rechargePaymentType) {
            if ("ICBC".equals(tChargingOrder.getSourceName())) {
                IcbcPaymentResp data = icbcPaymentClient.query(tChargingOrder.getCode()).getData();
                if (null != data && isIcbcRemovableStatus(data.getStatus(), tChargingOrder)) {
                    this.removeById(tChargingOrder.getId());
                }
            }
            if (!"ICBC".equals(tChargingOrder.getSourceName()) && 1 == rechargePaymentType) {
                NotifyV3PayDecodeRespBody data = wxPaymentClient.queryOrderInfo(tChargingOrder.getCode()).getData();
                if (null != data) {
                    String trade_state = data.getTrade_state();
@@ -651,7 +694,7 @@
                    }
                }
            }
            if (2 == rechargePaymentType) {
            if (!"ICBC".equals(tChargingOrder.getSourceName()) && 2 == rechargePaymentType) {
                AliQueryOrder data = aliPaymentClient.query(tChargingOrder.getCode()).getData();
                if (null != data) {
                    //支付失败,删除无效的订单
@@ -661,7 +704,7 @@
                    }
                }
            }
            if (3 == rechargePaymentType) {
            if (!"ICBC".equals(tChargingOrder.getSourceName()) && 3 == rechargePaymentType) {
                AliQueryOrder data = h5AliPaymentClient.query(tChargingOrder.getCode()).getData();
                if (null != data) {
                    //支付失败,删除无效的订单
@@ -684,7 +727,15 @@
                .eq(TChargingOrder::getAppUserCarId, userId).eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
        if (null != one) {
            //查询三方支付数据,支付中直接结束三方订单
            if (1 == one.getRechargePaymentType()) {
            if ("ICBC".equals(one.getSourceName())) {
                IcbcPaymentResp data = icbcPaymentClient.query(one.getCode()).getData();
                if (null != data && isIcbcRemovableStatus(data.getStatus(), one)) {
                    this.removeById(one.getId());
                } else {
                    return AjaxResult.error("存在支付中订单,请稍后再试");
                }
            }
            if (!"ICBC".equals(one.getSourceName()) && 1 == one.getRechargePaymentType()) {
                NotifyV3PayDecodeRespBody data = wxPaymentClient.queryOrderInfo(one.getCode()).getData();
                if (null != data) {
                    String trade_state = data.getTrade_state();
@@ -698,7 +749,7 @@
                    }
                }
            }
            if (2 == one.getRechargePaymentType()) {
            if (!"ICBC".equals(one.getSourceName()) && 2 == one.getRechargePaymentType()) {
                AliQueryOrder data = aliPaymentClient.query(one.getCode()).getData();
                if (null != data) {
                    String trade_state = data.getTradeStatus();
@@ -713,7 +764,7 @@
                    }
                }
            }
            if (3 == one.getRechargePaymentType()) {
            if (!"ICBC".equals(one.getSourceName()) && 3 == one.getRechargePaymentType()) {
                AliQueryOrder data = h5AliPaymentClient.query(one.getCode()).getData();
                if (null != data) {
                    String trade_state = data.getTradeStatus();
@@ -766,6 +817,9 @@
        chargingOrder.setWhitelistServiceRefundAmount(BigDecimal.ZERO);
        chargingOrder.setVipDiscountAmount(BigDecimal.ZERO);
        chargingOrder.setOrderSource(1);
        if (thirdPayAmount.compareTo(BigDecimal.ZERO) > 0) {
            chargingOrder.setSourceName("ICBC");
        }
        chargingOrder.setTitle("【充电桩充电】" + chargingPile.getNumber() + "号桩/" + tChargingGun.getCode() + "号枪");
        
        Site site = siteClient.getSiteByIds(Arrays.asList(tChargingGun.getSiteId())).getData().get(0);
@@ -826,44 +880,21 @@
        }
        //会员优惠折扣将其计入增加充电时长(增加总充电金额)
        //如果充电总金额未使用完,则需要退回费用=(原金额/总金额)*(总金额-实际充电金额)
        if (1 == addChargingOrder.getPaymentType()) {
            PaymentOrder paymentOrder = new PaymentOrder();
            paymentOrder.setCode(chargingOrder.getCode());
            paymentOrder.setAmount(thirdPayAmount);
            paymentOrder.setOpenId(appUser.getWxOpenid());
            paymentOrder.setDescription("充电充值");
            paymentOrder.setNotifyUrl("/payment/wx/pay/notify");
            Map<String, Object> data = wxPaymentClient.orderPay(paymentOrder).getData();
        IcbcPaymentReq req = new IcbcPaymentReq();
        req.setOutTradeNo(chargingOrder.getCode());
        req.setAmount(thirdPayAmount);
        req.setSubject("充电充值");
        req.setBody("充电充值");
        req.setPaymentType(addChargingOrder.getPaymentType());
        req.setNotifyUrl("/payment/icbc/callBack");
        req.setReturnUrl(addChargingOrder.getReturnUrl());
        req.setOpenId(Integer.valueOf(1).equals(addChargingOrder.getPaymentType()) ? appUser.getWxOpenid() : appUser.getAliOpenid());
        req.setProfitSharing(buildIcbcProfitSharing(site));
        IcbcPaymentResp data = icbcPaymentClient.payment(req).getData();
        if (isIcbcPaymentCreated(data)) {
            return AjaxResult.success(data);
        }
        if (2 == addChargingOrder.getPaymentType()) {
            AliPaymentReq req = new AliPaymentReq();
            req.setOutTradeNo(chargingOrder.getCode());
            req.setTotalAmount(thirdPayAmount.toString());
            req.setSubject("充电充值");
            req.setBuyerOpenId(appUser.getAliOpenid());
            req.setBody("充电充值");
            req.setNotifyUrl("/payment/ali/callBack");
            AliPaymentResp data = aliPaymentClient.payment(req).getData();
            if (null != data) {
                return AjaxResult.success(data);
            }
        }
        if (3 == addChargingOrder.getPaymentType()) {
            AliPaymentReq req = new AliPaymentReq();
            req.setOutTradeNo(chargingOrder.getCode());
            req.setTotalAmount(thirdPayAmount.toString());
            req.setSubject("充电充值");
            req.setBuyerOpenId(appUser.getAliOpenid());
            req.setBody("充电充值");
            req.setNotifyUrl("/payment/aliH5/callBack");
            req.setReturnUrl(addChargingOrder.getReturnUrl());
            String data = h5AliPaymentClient.payment(req).getData();
            if (StringUtils.hasLength(data)) {
                return AjaxResult.success("操作成功", data);
            }
        }
        throw new RuntimeException("无效的支付方式");
        throw new RuntimeException("调起工行支付失败");
    }
    
    
@@ -1337,6 +1368,11 @@
                        return;
                    }
                    this.updateById(chargingOrder);
                    if (isIcbcOrder(chargingOrder)) {
                        refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount, "充电失败,取消充电订单", false,
                                recordedRefundAmount, balanceRefundAmount);
                        return;
                    }
                    if (thirdRefundAmount.compareTo(BigDecimal.ZERO) > 0 && 1 == rechargePaymentType) {
                        WxPaymentRefundModel model = new WxPaymentRefundModel();
                        model.setOut_trade_no(chargingOrder.getCode());
@@ -1488,6 +1524,11 @@
                finishManualChargingRefund(chargingOrder, chargingOrderRefund, money, balanceRefundAmount, thirdRefundAmount);
                return;
            }
            if (isIcbcOrder(chargingOrder)) {
                refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount, "充电完成退款", true,
                        money, balanceRefundAmount);
                return;
            }
            if (1 == rechargePaymentType) {
                WxPaymentRefundModel model = new WxPaymentRefundModel();
                model.setOut_trade_no(chargingOrder.getCode());
@@ -1592,6 +1633,7 @@
                chargingOrder.setRefundSerialNumber(refund_id);
                chargingOrder.setRefundTime(LocalDateTime.now());
                this.updateById(chargingOrder);
                triggerChargingOrderShare(this.getById(order.getId()));
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
@@ -3487,6 +3529,10 @@
                            chargingOrder.setRefundStatus(2);
                            chargingOrder.setRefundTime(LocalDateTime.now());
                            this.updateById(chargingOrder);
                            triggerChargingOrderShare(chargingOrder);
                        } else if (isIcbcOrder(chargingOrder)) {
                            refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount, "充电完成退款", false,
                                    refundAmount, balanceRefundAmount);
                        } else if (1 == rechargePaymentType) {
                            WxPaymentRefundModel model = new WxPaymentRefundModel();
                            model.setOut_trade_no(chargingOrder.getCode());
@@ -3556,6 +3602,7 @@
                        chargingOrder.setRefundStatus(2);
                        chargingOrder.setRefundTime(LocalDateTime.now());
                        this.updateById(chargingOrder);
                        triggerChargingOrderShare(chargingOrder);
                    } else {
                        //构建退款明细
                        TChargingOrderRefund chargingOrderRefund = new TChargingOrderRefund();
@@ -3584,6 +3631,7 @@
                        chargingOrder.setRefundStatus(2);
                        chargingOrder.setRefundTime(LocalDateTime.now());
                        this.updateById(chargingOrder);
                        triggerChargingOrderShare(chargingOrder);
                    }
                }
@@ -3616,6 +3664,9 @@
                summaryData.setElectricPrice(electronic_reduce.setScale(2, BigDecimal.ROUND_HALF_EVEN));
                summaryData.setServicePrice(serviceMoney.setScale(2, BigDecimal.ROUND_HALF_EVEN));
                chargingOrderSummaryDataService.save(summaryData);
                if (channelRefundAmount.compareTo(BigDecimal.ZERO) == 0 && Integer.valueOf(2).equals(chargingOrder.getRefundStatus())) {
                    triggerChargingOrderShare(chargingOrder, site);
                }
                //推送监管平台订单状态和订单详情
                ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
@@ -3767,6 +3818,10 @@
            if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) {
                chargingOrderRefund.setRefundSerialNumber("BALANCE");
                finishManualChargingRefund(tChargingOrder, chargingOrderRefund, manualRefundAmount, balanceRefundAmount, thirdRefundAmount);
            }
            else if (isIcbcOrder(tChargingOrder)) {
                refundByIcbcAndFinish(tChargingOrder, chargingOrderRefund, thirdRefundAmount, "取消订单", true,
                        manualRefundAmount, balanceRefundAmount);
            }
            else if (1 == tChargingOrder.getRechargePaymentType()) {
                WxPaymentRefundModel model = new WxPaymentRefundModel();
@@ -5904,4 +5959,406 @@
            e.printStackTrace();
        }
    }
    private boolean isIcbcRemovableStatus(String status, TChargingOrder order) {
        if (!StringUtils.hasLength(status)) {
            return false;
        }
        if (Arrays.asList("FAIL", "FAILED", "CLOSED", "TRADE_CLOSED", "REVOKED", "PAYERROR").contains(status)) {
            return true;
        }
        // 工行B2C不支持主动关单;NOTPAY超时仅释放本地待支付订单,不代表远端订单已关闭。
        return "NOTPAY".equals(status) && isLocalPendingOrderExpired(order);
    }
    private boolean isLocalPendingOrderExpired(TChargingOrder order) {
        return order != null && order.getCreateTime() != null && order.getCreateTime().plusMinutes(30).isBefore(LocalDateTime.now());
    }
    private boolean isIcbcPaymentCreated(IcbcPaymentResp resp) {
        if (resp == null || !StringUtils.hasLength(resp.getStatus())) {
            return false;
        }
        return "SUCCESS".equals(resp.getStatus());
    }
    private void triggerChargingOrderShare(TChargingOrder chargingOrder, Site site) {
        try {
            if (!isNeedChargingOrderShare(chargingOrder)) {
                return;
            }
            Partner partner = loadPartner(site);
            for (TChargingOrderShareRecord shareRecord : buildChargingOrderShareRecords(chargingOrder, site, partner)) {
                TChargingOrderShareRecord exist = chargingOrderShareRecordMapper.selectOne(new LambdaQueryWrapper<TChargingOrderShareRecord>()
                        .eq(TChargingOrderShareRecord::getChargingOrderCode, shareRecord.getChargingOrderCode())
                        .eq(TChargingOrderShareRecord::getPayChannel, shareRecord.getPayChannel())
                        .eq(TChargingOrderShareRecord::getDelFlag, false)
                        .last("limit 1"));
                if (exist != null && ("SUCCESS".equals(exist.getOnlineShareStatus()) || "PROCESSING".equals(exist.getOnlineShareStatus()))) {
                    continue;
                }
                shareRecord = mergeChargingOrderShareRecord(exist, shareRecord);
                saveOrUpdateChargingOrderShareRecord(shareRecord);
                if (Boolean.TRUE.equals(shareRecord.getNeedOnlineShare())) {
                    processIcbcOnlineShare(shareRecord, partner);
                }
            }
        } catch (Exception e) {
            log.error("充电订单分账记录触发失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), e);
            recordChargingOrderShareFailure(chargingOrder, site, e);
        }
    }
    private void triggerChargingOrderShare(TChargingOrder chargingOrder) {
        try {
            if (chargingOrder == null || chargingOrder.getSiteId() == null) {
                return;
            }
            R<List<Site>> siteResult = siteClient.getSiteByIds(Arrays.asList(chargingOrder.getSiteId()));
            List<Site> sites = siteResult == null ? null : siteResult.getData();
            Site site = sites == null || sites.isEmpty() ? null : sites.get(0);
            triggerChargingOrderShare(chargingOrder, site);
        } catch (Exception e) {
            log.error("充电订单分账站点加载失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), e);
        }
    }
    private boolean isNeedChargingOrderShare(TChargingOrder chargingOrder) {
        return chargingOrder != null
                && Integer.valueOf(5).equals(chargingOrder.getStatus())
                && Integer.valueOf(2).equals(chargingOrder.getRechargePaymentStatus())
                && Integer.valueOf(2).equals(chargingOrder.getRefundStatus())
                && getChargingOrderShareBaseAmount(chargingOrder).compareTo(BigDecimal.ZERO) > 0;
    }
    private Partner loadPartner(Site site) {
        if (site == null || site.getPartnerId() == null) {
            return null;
        }
        R<Partner> partnerResult = partnerClient.getPartnerById(site.getPartnerId());
        return partnerResult == null ? null : partnerResult.getData();
    }
    private List<TChargingOrderShareRecord> buildChargingOrderShareRecords(TChargingOrder chargingOrder, Site site, Partner partner) {
        BigDecimal actualThirdPay = getActualThirdPayAmount(chargingOrder);
        BalanceShareAmount balanceShareAmount = getBalanceShareAmount(chargingOrder);
        BigDecimal actualBalancePay = balanceShareAmount.getPrincipalAmount();
        BigDecimal actualTotalPay = actualThirdPay.add(actualBalancePay).setScale(2, RoundingMode.HALF_UP);
        BigDecimal shareElectricityAmount = getChargingOrderShareElectricityAmount(chargingOrder);
        BigDecimal shareServiceAmount = getChargingOrderShareServiceAmount(chargingOrder);
        BigDecimal shareBaseAmount = shareElectricityAmount.add(shareServiceAmount).setScale(2, RoundingMode.HALF_UP);
        if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0 && isWhitelistOrder(chargingOrder)) {
            actualTotalPay = shareBaseAmount;
        }
        if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0) {
            return Collections.emptyList();
        }
        if (!isWhitelistOrder(chargingOrder) && shareBaseAmount.compareTo(BigDecimal.ZERO) > 0 && actualTotalPay.compareTo(shareBaseAmount) < 0) {
            BigDecimal effectiveRatio = actualTotalPay.divide(shareBaseAmount, 6, RoundingMode.HALF_UP);
            shareElectricityAmount = shareElectricityAmount.multiply(effectiveRatio).setScale(2, RoundingMode.HALF_UP);
            shareServiceAmount = shareServiceAmount.multiply(effectiveRatio).setScale(2, RoundingMode.HALF_UP);
        }
        List<TChargingOrderShareRecord> records = new ArrayList<>();
        if (actualBalancePay.compareTo(BigDecimal.ZERO) > 0) {
            records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, "BALANCE", actualBalancePay,
                    balanceShareAmount.getGiftAmount(), actualTotalPay, shareElectricityAmount, shareServiceAmount));
        }
        if (actualThirdPay.compareTo(BigDecimal.ZERO) > 0) {
            records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, resolveThirdPayChannel(chargingOrder), actualThirdPay,
                    BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
        }
        if (records.isEmpty() && isWhitelistOrder(chargingOrder)) {
            records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, "WHITELIST", actualTotalPay,
                    BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
        }
        return records;
    }
    private TChargingOrderShareRecord buildChargingOrderShareRecord(TChargingOrder chargingOrder, Site site, Partner partner,
                                                                     String payChannel, BigDecimal channelPayAmount, BigDecimal channelGiftAmount,
                                                                     BigDecimal actualTotalPay, BigDecimal shareElectricityAmount,
                                                                     BigDecimal shareServiceAmount) {
        TChargingOrderShareRecord record = new TChargingOrderShareRecord();
        record.setChargingOrderId(chargingOrder.getId());
        record.setChargingOrderCode(chargingOrder.getCode());
        record.setSiteId(chargingOrder.getSiteId());
        record.setPartnerId(site == null ? null : site.getPartnerId());
        record.setPayChannel(payChannel);
        record.setChannelPayAmount(money(channelPayAmount));
        record.setChannelGiftAmount(money(channelGiftAmount));
        BigDecimal channelRatio = channelPayAmount.divide(actualTotalPay, 6, RoundingMode.HALF_UP);
        record.setChannelPayRatio(channelRatio);
        BigDecimal channelElectricityAmount = shareElectricityAmount.multiply(channelRatio).setScale(2, RoundingMode.HALF_UP);
        BigDecimal channelServiceAmount = shareServiceAmount.multiply(channelRatio).setScale(2, RoundingMode.HALF_UP);
        record.setChannelElectricityAmount(channelElectricityAmount);
        record.setChannelServiceAmount(channelServiceAmount);
        record.setElectricityShareRatio(partner == null ? null : partner.getElectricityShareRatio());
        record.setServiceShareRatio(partner == null ? null : partner.getServiceShareRatio());
        BigDecimal electricityShareAmount = calculateShareAmount(channelElectricityAmount, record.getElectricityShareRatio());
        BigDecimal serviceShareAmount = calculateShareAmount(channelServiceAmount, record.getServiceShareRatio());
        record.setElectricityShareAmount(electricityShareAmount);
        record.setServiceShareAmount(serviceShareAmount);
        record.setShareAmount(electricityShareAmount.add(serviceShareAmount).setScale(2, RoundingMode.HALF_UP));
        record.setNeedOnlineShare("ICBC".equals(payChannel) && record.getShareAmount().compareTo(BigDecimal.ZERO) > 0);
        record.setOnlineShareStatus(Boolean.TRUE.equals(record.getNeedOnlineShare()) ? "INIT" : "RECORDED");
        if ("ICBC".equals(payChannel)) {
            record.setSeqNo(chargingOrder.getCode());
            record.setBusiType("1");
            record.setOriTrxDate(chargingOrder.getPayTime() == null ? LocalDate.now() : chargingOrder.getPayTime().toLocalDate());
            record.setSubOrderId("ICBCS" + chargingOrder.getCode());
        }
        if ("ICBC".equals(payChannel) && partner != null) {
            record.setSubMerId(partner.getIcbcSubMerId());
            record.setSubMerPrtclNo(partner.getIcbcSubMerPrtclNo());
            record.setMerAcct(partner.getIcbcMerAcct());
        }
        record.setRetryCount(0);
        record.setDelFlag(false);
        return record;
    }
    private TChargingOrderShareRecord mergeChargingOrderShareRecord(TChargingOrderShareRecord exist, TChargingOrderShareRecord fresh) {
        if (exist == null) {
            return fresh;
        }
        fresh.setId(exist.getId());
        fresh.setRetryCount(exist.getRetryCount() == null ? 0 : exist.getRetryCount());
        fresh.setRequestJson(exist.getRequestJson());
        fresh.setResponseJson(exist.getResponseJson());
        fresh.setIcbcOrderId(exist.getIcbcOrderId());
        fresh.setSubOrderSeq(exist.getSubOrderSeq());
        fresh.setSubOrderTrxid(exist.getSubOrderTrxid());
        fresh.setRecResult(exist.getRecResult());
        fresh.setLastRetryTime(exist.getLastRetryTime());
        return fresh;
    }
    private BigDecimal getActualThirdPayAmount(TChargingOrder chargingOrder) {
        return getRefundableThirdPayAmount(chargingOrder).subtract(money(chargingOrder.getThirdRefundAmount())).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal getActualBalancePayAmount(TChargingOrder chargingOrder) {
        return money(chargingOrder.getBalancePayAmount()).subtract(money(chargingOrder.getBalanceRefundAmount())).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
    }
    private BalanceShareAmount getBalanceShareAmount(TChargingOrder chargingOrder) {
        if (chargingOrder == null || chargingOrder.getId() == null) {
            return new BalanceShareAmount(BigDecimal.ZERO, BigDecimal.ZERO);
        }
        BigDecimal fallbackBalancePay = getActualBalancePayAmount(chargingOrder);
        if (fallbackBalancePay.compareTo(BigDecimal.ZERO) <= 0) {
            return new BalanceShareAmount(BigDecimal.ZERO, BigDecimal.ZERO);
        }
        List<Map<String, Object>> rows = this.baseMapper.exchangeCodeBalanceIncomeByOrderIds(Collections.singletonList(chargingOrder.getId()));
        if (rows == null || rows.isEmpty()) {
            return new BalanceShareAmount(fallbackBalancePay, BigDecimal.ZERO);
        }
        Map<String, Object> row = rows.get(0);
        BigDecimal principalAmount = getSummaryDecimal(row, "balanceIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
        BigDecimal giftAmount = getSummaryDecimal(row, "balanceGiftIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
        return new BalanceShareAmount(principalAmount, giftAmount);
    }
    private BigDecimal getChargingOrderShareElectricityAmount(TChargingOrder chargingOrder) {
        if (chargingOrder == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return money(chargingOrder.getElectrovalence()).add(money(chargingOrder.getWhitelistElectricRefundAmount())).setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal getChargingOrderShareServiceAmount(TChargingOrder chargingOrder) {
        if (chargingOrder == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return money(chargingOrder.getServiceCharge()).add(money(chargingOrder.getWhitelistServiceRefundAmount())).setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal getChargingOrderShareBaseAmount(TChargingOrder chargingOrder) {
        return getChargingOrderShareElectricityAmount(chargingOrder).add(getChargingOrderShareServiceAmount(chargingOrder)).setScale(2, RoundingMode.HALF_UP);
    }
    private String resolveThirdPayChannel(TChargingOrder chargingOrder) {
        if ("ICBC".equals(chargingOrder.getSourceName())) {
            return "ICBC";
        }
        if (Integer.valueOf(1).equals(chargingOrder.getRechargePaymentType())) {
            return "WECHAT";
        }
        if (Integer.valueOf(2).equals(chargingOrder.getRechargePaymentType()) || Integer.valueOf(3).equals(chargingOrder.getRechargePaymentType())) {
            return "ALIPAY";
        }
        return "OTHER";
    }
    private void processIcbcOnlineShare(TChargingOrderShareRecord record, Partner partner) {
        try {
            String invalidReason = validateIcbcShareRecord(record, partner);
            if (StringUtils.hasLength(invalidReason)) {
                markChargingOrderShare(record, "FAIL", invalidReason);
                return;
            }
            IcbcSubOrderSplitReq req = buildIcbcSubOrderSplitReq(record);
            record.setRequestJson(JSON.toJSONString(req));
            record.setOnlineShareStatus("PROCESSING");
            record.setRetryCount(record.getRetryCount() == null ? 1 : record.getRetryCount() + 1);
            record.setLastRetryTime(LocalDateTime.now());
            saveOrUpdateChargingOrderShareRecord(record);
            R<IcbcSubOrderSplitResp> result = icbcPaymentClient.splitSubOrder(req);
            IcbcSubOrderSplitResp resp = result == null ? null : result.getData();
            record.setResponseJson(resp == null ? (result == null ? null : JSON.toJSONString(result)) : resp.getRawResponse());
            if (result == null || result.getCode() != 200 || resp == null) {
                markChargingOrderShare(record, "FAIL", result == null ? "工行拆分接口无响应" : result.getMsg());
                return;
            }
            record.setIcbcOrderId(resp.getOrderId());
            record.setSubOrderSeq(resp.getSubOrderSeq());
            record.setSubOrderTrxid(resp.getSubOrderTrxid());
            record.setRecResult(resp.getRecResult());
            if ("SUCCESS".equals(resp.getStatus()) && ("0".equals(resp.getRecResult()) || !StringUtils.hasLength(resp.getRecResult()))) {
                markChargingOrderShare(record, "SUCCESS", null);
            } else {
                markChargingOrderShare(record, "FAIL", "工行拆分未成功:" + firstNotBlank(resp.getRecResult(), resp.getStatus()));
            }
        } catch (Exception e) {
            log.error("工行线上分账处理失败,订单号:{},渠道:{}", record == null ? null : record.getChargingOrderCode(), record == null ? null : record.getPayChannel(), e);
            if (record != null) {
                markChargingOrderShare(record, "FAIL", firstNotBlank(e.getMessage(), "工行线上分账处理异常"));
            }
        }
    }
    private void recordChargingOrderShareFailure(TChargingOrder chargingOrder, Site site, Exception e) {
        try {
            if (chargingOrder == null || !"ICBC".equals(chargingOrder.getSourceName()) || getActualThirdPayAmount(chargingOrder).compareTo(BigDecimal.ZERO) <= 0) {
                return;
            }
            TChargingOrderShareRecord record = chargingOrderShareRecordMapper.selectOne(new LambdaQueryWrapper<TChargingOrderShareRecord>()
                    .eq(TChargingOrderShareRecord::getChargingOrderCode, chargingOrder.getCode())
                    .eq(TChargingOrderShareRecord::getPayChannel, "ICBC")
                    .eq(TChargingOrderShareRecord::getDelFlag, false)
                    .last("limit 1"));
            if (record == null) {
                BigDecimal actualThirdPay = getActualThirdPayAmount(chargingOrder);
                BigDecimal actualTotalPay = actualThirdPay.add(getBalanceShareAmount(chargingOrder).getPrincipalAmount()).setScale(2, RoundingMode.HALF_UP);
                BigDecimal shareElectricityAmount = getChargingOrderShareElectricityAmount(chargingOrder);
                BigDecimal shareServiceAmount = getChargingOrderShareServiceAmount(chargingOrder);
                record = buildChargingOrderShareRecord(chargingOrder, site, null, "ICBC", actualThirdPay, BigDecimal.ZERO,
                        actualTotalPay.compareTo(BigDecimal.ZERO) > 0 ? actualTotalPay : actualThirdPay, shareElectricityAmount, shareServiceAmount);
            }
            record.setNeedOnlineShare(true);
            markChargingOrderShare(record, "FAIL", firstNotBlank(e.getMessage(), "充电订单分账记录触发异常"));
        } catch (Exception ex) {
            log.error("充电订单分账失败补偿记录落库失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), ex);
        }
    }
    private String validateIcbcShareRecord(TChargingOrderShareRecord record, Partner partner) {
        if (partner == null) {
            return "站点未配置合作商,无法工行拆分";
        }
        if (partner.getElectricityShareRatio() == null && partner.getServiceShareRatio() == null) {
            return "合作商未配置分成比例,跳过工行拆分";
        }
        if (!StringUtils.hasLength(partner.getIcbcSubMerId())) {
            return "合作商未配置工行二级商户编号";
        }
        if (!StringUtils.hasLength(partner.getIcbcSubMerPrtclNo())) {
            return "合作商未配置工行二级商户协议编号";
        }
        if (money(record.getShareAmount()).compareTo(BigDecimal.ZERO) <= 0) {
            return "工行拆分金额为0";
        }
        return null;
    }
    private BigDecimal calculateShareAmount(BigDecimal amount, BigDecimal ratio) {
        if (amount == null || ratio == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return amount.multiply(ratio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
    }
    private IcbcSubOrderSplitReq buildIcbcSubOrderSplitReq(TChargingOrderShareRecord record) {
        IcbcSubOrderSplitReq req = new IcbcSubOrderSplitReq();
        req.setChargingOrderCode(record.getChargingOrderCode());
        req.setSeqNo(record.getSeqNo());
        req.setBusiType(record.getBusiType());
        req.setOriMerId(record.getOriMerId());
        req.setOriTrxDate(record.getOriTrxDate() == null ? null : record.getOriTrxDate().toString());
        req.setSubOrderId(record.getSubOrderId());
        req.setSubMerId(record.getSubMerId());
        req.setSubMerPrtclNo(record.getSubMerPrtclNo());
        req.setClassifyAmt(record.getShareAmount());
        req.setMerAcct(record.getMerAcct());
        req.setMerSpInfor("充电订单分账:" + record.getChargingOrderCode());
        return req;
    }
    private void markChargingOrderShare(TChargingOrderShareRecord record, String status, String reason) {
        record.setOnlineShareStatus(status);
        record.setFailReason(reason);
        saveOrUpdateChargingOrderShareRecord(record);
    }
    private void saveOrUpdateChargingOrderShareRecord(TChargingOrderShareRecord record) {
        if (record.getId() == null) {
            chargingOrderShareRecordMapper.insert(record);
        } else {
            chargingOrderShareRecordMapper.updateById(record);
        }
    }
    private String buildIcbcProfitSharing(Site site) {
        if (site == null || site.getPartnerId() == null) {
            return null;
        }
        R<Partner> partnerResult = partnerClient.getPartnerById(site.getPartnerId());
        Partner partner = partnerResult == null ? null : partnerResult.getData();
        if (partner == null || !StringUtils.hasLength(partner.getIcbcSubMerId()) || !StringUtils.hasLength(partner.getIcbcSubMerPrtclNo())) {
            return null;
        }
        if (partner.getElectricityShareRatio() == null && partner.getServiceShareRatio() == null) {
            return null;
        }
        JSONObject data = new JSONObject();
        data.put("icbcPartnerId", partner.getIcbcPartnerId());
        data.put("icbcSubMerId", partner.getIcbcSubMerId());
        data.put("icbcSubMerPrtclNo", partner.getIcbcSubMerPrtclNo());
        data.put("icbcMerAcct", partner.getIcbcMerAcct());
        if (partner.getElectricityShareRatio() != null) {
            data.put("electricityShareRatio", partner.getElectricityShareRatio());
        }
        if (partner.getServiceShareRatio() != null) {
            data.put("serviceShareRatio", partner.getServiceShareRatio());
        }
        return data.toJSONString();
    }
    private static class BalanceShareAmount {
        private final BigDecimal principalAmount;
        private final BigDecimal giftAmount;
        private BalanceShareAmount(BigDecimal principalAmount, BigDecimal giftAmount) {
            this.principalAmount = principalAmount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : principalAmount.setScale(2, RoundingMode.HALF_UP);
            this.giftAmount = giftAmount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : giftAmount.setScale(2, RoundingMode.HALF_UP);
        }
        private BigDecimal getPrincipalAmount() {
            return principalAmount;
        }
        private BigDecimal getGiftAmount() {
            return giftAmount;
        }
    }
    private String firstNotBlank(String... values) {
        for (String value : values) {
            if (StringUtils.hasLength(value)) {
                return value;
            }
        }
        return "";
    }
}
ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/icbc-ca.jar
Binary files differ
ruoyi-service/ruoyi-payment/lib/readme.md
New file
@@ -0,0 +1,14 @@
### jar包说明
commons-logging-1.2.jar----------------与日志记录相关
icbc-api-sdk-cop.jar-------------------sdk基础jar包
icbc-api-sdk-cop-io.jar----------------sdk各接口输入输出
icbc-ca.jar----------------------------SHA1&SHA256签名工具包
InfosecCrypto_Java1_02_JDK14+.jar------SHA1&SHA256签名工具包依赖
### jar包使用说明
1.支持java版本1.5及以上
2.icbc-api-sdk-cop.jar、icbc-api-sdk-cop-io.jar为必须jar包
3.icbc-ca.jar、InfosecCrypto_Java1_02_JDK14+.jar工具包升级后兼容SHA256和SHA1算法。
### 版本升级
版本升级说明请参考update-info.md
ruoyi-service/ruoyi-payment/lib/update-info.md
New file
@@ -0,0 +1,2 @@
## 20201113
1. 升级SHA256签名工具包icbc-ca.jar、InfosecCrypto_Java1_02_JDK14+.jar
ruoyi-service/ruoyi-payment/pom.xml
@@ -133,6 +133,48 @@
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-api-order</artifactId>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/commons-logging-1.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>hsm-software-share</artifactId>
            <version>1.0.5</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/hsm-software-share-1.0.5.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>icbc-api-sdk-cop-io</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/icbc-api-sdk-cop-io.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>icbc-api-sdk-cop</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/icbc-api-sdk-cop.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>icbc-ca</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/icbc-ca.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.icbc.sdk</groupId>
            <artifactId>infosec-crypto</artifactId>
            <version>1.0.2</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/InfosecCrypto_Java1_02_JDK14+.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
@@ -141,6 +183,9 @@
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
                <executions>
                    <execution>
                        <goals>
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
New file
@@ -0,0 +1,93 @@
package com.ruoyi.payment.controller;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
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.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import com.ruoyi.payment.icbc.service.IcbcPayService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Map;
/**
 * 工行统一支付入口。
 */
@RestController
@RequestMapping("/icbc")
public class IcbcPayController {
    @Resource
    private IcbcPayService icbcPayService;
    @Resource
    private ChargingOrderClient chargingOrderClient;
    @PostMapping("/payment")
    public R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req) {
        return R.ok(icbcPayService.payment(req));
    }
    @PostMapping("/query")
    public R<IcbcPaymentResp> query(@RequestParam("outTradeNo") String outTradeNo) {
        return R.ok(icbcPayService.query(outTradeNo));
    }
    @PostMapping("/close")
    public R<IcbcPaymentResp> close(@RequestParam("outTradeNo") String outTradeNo) {
        return R.ok(icbcPayService.close(outTradeNo));
    }
    @PostMapping("/refund")
    public R<IcbcRefundResp> refund(@RequestBody IcbcRefundReq req) {
        return R.ok(icbcPayService.refund(req));
    }
    @PostMapping("/sub-order/split")
    public R<IcbcSubOrderSplitResp> splitSubOrder(@RequestBody IcbcSubOrderSplitReq req) {
        return R.ok(icbcPayService.splitSubOrder(req));
    }
    @PostMapping("/sub-order/query")
    public R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req) {
        return R.ok(icbcPayService.querySubOrder(req));
    }
    @PostMapping("/callBack")
    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();
    }
    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");
        Integer paymentType = Integer.valueOf(attach);
        if (Integer.valueOf(1).equals(paymentType)) {
            chargingOrderClient.chargingOrderWXCallback(outTradeNo, transactionId, attach);
            return;
        }
        if (Integer.valueOf(2).equals(paymentType)) {
            chargingOrderClient.chargingOrderALICallback(outTradeNo, transactionId, attach);
            return;
        }
        if (Integer.valueOf(3).equals(paymentType)) {
            chargingOrderClient.chargingOrderALIH5Callback(outTradeNo, transactionId, attach);
            return;
        }
        throw new IllegalArgumentException("工行回调支付类型不支持:" + attach);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
New file
@@ -0,0 +1,33 @@
package com.ruoyi.payment.icbc.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
 * 工行支付配置,实际值从 Nacos 的 icbc.pay 前缀读取。
 */
@Data
@Component
@Configuration
@ConfigurationProperties(prefix = "icbc.pay")
public class IcbcPayProperties {
    private String appId;
    private String privateKey;
    private String icbcPublicKey;
    private String signType = "RSA2";
    private String charset = "UTF-8";
    private String format = "json";
    private String gatewayUrl;
    private String refundUrl;
    private String subOrderSplitUrl;
    private String subOrderQueryUrl;
    private String notifyUrl;
    private String merId;
    private String merPrtclNo;
    private String tpAppId;
    private String spbillCreateIp;
    private String profitSharing;
    private Boolean verifyNotifySign = true;
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java
New file
@@ -0,0 +1,31 @@
package com.ruoyi.payment.icbc.service;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
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.IcbcSubOrderSplitReq;
import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
import java.util.Map;
/**
 * 工行统一支付服务。
 */
public interface IcbcPayService {
    IcbcPaymentResp payment(IcbcPaymentReq req);
    IcbcPaymentResp query(String outTradeNo);
    IcbcPaymentResp close(String outTradeNo);
    IcbcRefundResp refund(IcbcRefundReq req);
    IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq 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
New file
@@ -0,0 +1,391 @@
package com.ruoyi.payment.icbc.service.impl;
import com.alibaba.fastjson.JSON;
import com.icbc.api.DefaultIcbcClient;
import com.icbc.api.IcbcResponse;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2;
import com.icbc.api.request.AggregatePaymentRequestV3;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1;
import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1;
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.CardbusinessAggregatepayB2cOnlineMerrefundResponseV1;
import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderResponseV1;
import com.icbc.api.response.CardbusinessOrderSuborderqueryResponseV1;
import com.icbc.api.utils.IcbcSignature;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.payment.api.vo.IcbcPaymentReq;
import com.ruoyi.payment.api.vo.IcbcPaymentResp;
import com.ruoyi.payment.api.vo.IcbcRefundReq;
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.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.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
 * 工行聚合支付骨架实现。
 */
@Slf4j
@Service
public class IcbcPayServiceImpl implements IcbcPayService {
    @Resource
    private IcbcPayProperties properties;
    @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());
        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));
        } catch (Exception e) {
            log.error("工行聚合支付调用失败,订单号:{}", req.getOutTradeNo(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public IcbcPaymentResp query(String outTradeNo) {
        IcbcPaymentResp resp = new IcbcPaymentResp();
        resp.setOrderId(outTradeNo);
        try {
            CardbusinessAggregatepayB2cOnlineOrderqryRequestV2 request = new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2();
            request.setServiceUrl(properties.getGatewayUrl());
            request.setBizContent(buildOrderQueryBiz(outTradeNo));
            CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = buildClient().execute(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()));
            resp.setPayData(JSON.toJSONString(response));
            resp.setRawResponse(JSON.toJSONString(response));
        } catch (Exception e) {
            log.error("工行支付订单查询失败,订单号:{}", outTradeNo, e);
            resp.setStatus("QUERY_FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public IcbcPaymentResp close(String outTradeNo) {
        IcbcPaymentResp resp = new IcbcPaymentResp();
        resp.setOrderId(outTradeNo);
        resp.setStatus("CLOSE_TODO");
        resp.setRawResponse("ICBC close skeleton: SDK close request is not mapped yet");
        log.info("工行关闭订单骨架,订单号:{}", outTradeNo);
        return resp;
    }
    @Override
    public IcbcRefundResp refund(IcbcRefundReq req) {
        IcbcRefundResp resp = new IcbcRefundResp();
        resp.setOutTradeNo(req.getOutTradeNo());
        resp.setOrderId(req.getOrderId());
        resp.setOutRefundNo(req.getOutRefundNo());
        try {
            CardbusinessAggregatepayB2cOnlineMerrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1();
            request.setServiceUrl(properties.getRefundUrl());
            request.setBizContent(buildRefundBiz(req));
            CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = buildClient().execute(request);
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
            resp.setOutTradeNo(firstNotBlank(response.getOutTradeNo(), req.getOutTradeNo()));
            resp.setOrderId(firstNotBlank(response.getOrderId(), req.getOrderId()));
            resp.setOutRefundNo(firstNotBlank(response.getOuttrxSerialNo(), req.getOutRefundNo()));
            resp.setRefundId(firstNotBlank(response.getIntrx_serial_no(), response.getOuttrxSerialNo()));
            resp.setRefundTime(response.getRefund_time());
            resp.setRefundAmount(firstNotBlank(response.getRealRejectAmt(), response.getRejectAmt(), toFen(req.getRefundAmount())));
        } catch (Exception e) {
            log.error("工行退款失败,订单号:{},退款单号:{}", req.getOutTradeNo(), req.getOutRefundNo(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req) {
        IcbcSubOrderSplitResp resp = new IcbcSubOrderSplitResp();
        resp.setChargingOrderCode(req.getChargingOrderCode());
        resp.setSubOrderId(req.getSubOrderId());
        try {
            CardbusinessAggregatepayB2cOnlineSersuborderRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1();
            request.setServiceUrl(properties.getSubOrderSplitUrl());
            request.setBizContent(buildSubOrderSplitBiz(req));
            CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = buildClient().execute(request);
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
            fillSplitOutput(resp, response.getOrderSubmitActOutput());
        } catch (Exception e) {
            log.error("工行小订单拆分失败,订单号:{}", req.getChargingOrderCode(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req) {
        IcbcSubOrderQueryResp resp = new IcbcSubOrderQueryResp();
        try {
            CardbusinessOrderSuborderqueryRequestV1 request = new CardbusinessOrderSuborderqueryRequestV1();
            request.setServiceUrl(properties.getSubOrderQueryUrl());
            request.setBizContent(buildSubOrderQueryBiz(req));
            CardbusinessOrderSuborderqueryResponseV1 response = buildClient().execute(request);
            resp.setOrderStatus(response.getOrderStatus());
            resp.setSubOutMerNo(response.getSubOutMerNo());
            resp.setSubOutAgreeNo(response.getSubOutAgreeNo());
            resp.setOutTradeNo(response.getOutTradeNo());
            resp.setOrderId(response.getOrderId());
            resp.setSubOrderNo(response.getSubOrderNo());
            resp.setSubMerOrderId(response.getSubMerOrderId());
            resp.setOrderAmnt(response.getOrderAmnt());
            resp.setPayDate(response.getPayDate());
            resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
            resp.setRawResponse(JSON.toJSONString(response));
        } catch (Exception e) {
            log.error("工行子订单查询失败,子订单号:{}", req.getSubOrderNo(), e);
            resp.setStatus("FAIL");
            resp.setRawResponse(e.getMessage());
        }
        return resp;
    }
    @Override
    public Map<String, String> parseNotify(Map<String, String[]> requestParams) {
        Map<String, String> params = new HashMap<>();
        for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
            String[] values = entry.getValue();
            params.put(entry.getKey(), values == null || values.length == 0 ? "" : String.join(",", values));
        }
        if (!verifyNotify(params)) {
            throw new IllegalArgumentException("工行回调验签失败");
        }
        String status = firstNotBlank(params.get("orderStatus"), params.get("pay_status"), params.get("status"));
        if (StringUtils.isNotEmpty(status) && !isPaidStatus(status)) {
            throw new IllegalArgumentException("工行回调支付状态非成功:" + status);
        }
        String outTradeNo = firstNotBlank(params.get("outTradeNo"), params.get("out_trade_no"), params.get("order_id"));
        if (StringUtils.isEmpty(outTradeNo)) {
            throw new IllegalArgumentException("工行回调缺少订单号");
        }
        String tradeNo = firstNotBlank(params.get("tradeNo"), params.get("trade_no"), params.get("msg_id"));
        String attach = firstNotBlank(params.get("attach"), params.get("paymentType"), params.get("payType"));
        if (StringUtils.isEmpty(attach)) {
            throw new IllegalArgumentException("工行回调缺少支付类型");
        }
        Map<String, String> result = new HashMap<>();
        result.put("out_trade_no", outTradeNo);
        result.put("transaction_id", tradeNo);
        result.put("attach", attach);
        return result;
    }
    private DefaultIcbcClient buildClient() {
        return new DefaultIcbcClient(
                properties.getAppId(),
                properties.getPrivateKey(),
                properties.getSignType(),
                properties.getCharset(),
                properties.getFormat(),
                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());
        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 按工行分账文档映射到子订单或分账请求。
        return biz;
    }
    private CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz buildOrderQueryBiz(String outTradeNo) {
        CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz biz =
                new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz();
        biz.setOut_trade_no(outTradeNo);
        biz.setIcbc_appid(properties.getAppId());
        biz.setMer_id(properties.getMerId());
        biz.setMer_prtcl_no(properties.getMerPrtclNo());
        return biz;
    }
    private CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz buildRefundBiz(IcbcRefundReq req) {
        CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz biz =
                new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz();
        biz.setOrder_id(req.getOrderId());
        biz.setOut_trade_no(req.getOutTradeNo());
        biz.setOuttrx_serial_no(req.getOutRefundNo());
        biz.setRet_total_amt(toFen(req.getRefundAmount()));
        biz.setTrnsc_ccy("CNY");
        biz.setMer_id(properties.getMerId());
        biz.setMer_prtcl_no(properties.getMerPrtclNo());
        biz.setIcbc_appid(properties.getAppId());
        biz.setMer_acct(req.getMerAcct());
        biz.setOrder_apd_inf(req.getReason());
        return biz;
    }
    private CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz buildSubOrderSplitBiz(IcbcSubOrderSplitReq req) {
        CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz biz =
                new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz();
        JSONArray orderSubmitActInput = new JSONArray();
        JSONObject item = new JSONObject();
        item.put("rec_num", "1");
        item.put("oper_flag", "0");
        item.put("sub_order_id", req.getSubOrderId());
        item.put("busi_type", firstNotBlank(req.getBusiType(), "1"));
        item.put("seq_no", req.getSeqNo());
        item.put("ori_mer_id", firstNotBlank(req.getOriMerId(), properties.getMerId()));
        item.put("ori_trx_date", req.getOriTrxDate());
        item.put("sub_mer_id", req.getSubMerId());
        item.put("sub_mer_prtcl_no", req.getSubMerPrtclNo());
        item.put("classify_amt", toFen(req.getClassifyAmt()));
        item.put("mer_acct", req.getMerAcct());
        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.setIcbcAppid(properties.getAppId());
        biz.setOrderNum("1");
        biz.setOrderSubmitActInput(orderSubmitActInput);
        return biz;
    }
    private CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz buildSubOrderQueryBiz(IcbcSubOrderQueryReq req) {
        CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz biz =
                new CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz();
        biz.setIcbcAppid(properties.getAppId());
        biz.setSerNo(req.getSerNo());
        biz.setOrderNoType(req.getOrderNoType());
        biz.setOrderId(req.getOrderId());
        biz.setMerOrderId(req.getMerOrderId());
        biz.setPrtiId(firstNotBlank(req.getPrtiId(), properties.getMerId()));
        biz.setQueryType(req.getQueryType());
        biz.setSubOrderNo(req.getSubOrderNo());
        biz.setMerTrnscSerno(req.getMerTrnscSerno());
        biz.setSubPrtiId(req.getSubPrtiId());
        return biz;
    }
    private void fillSplitOutput(IcbcSubOrderSplitResp resp, JSONArray output) {
        if (output == null || output.isEmpty()) {
            return;
        }
        JSONObject item = output.getJSONObject(0);
        resp.setRecResult(item.getString("rec_result"));
        resp.setClassifyAmt(item.getString("classify_amt"));
        resp.setSubMerId(item.getString("sub_mer_id"));
        resp.setOrderId(item.getString("order_id"));
        resp.setSubOrderSeq(item.getString("sub_order_seq"));
        resp.setSubOrderTrxid(item.getString("sub_order_trxid"));
        resp.setSubOrderId(firstNotBlank(item.getString("sub_order_id"), resp.getSubOrderId()));
        if (!"0".equals(resp.getRecResult())) {
            resp.setStatus("FAIL");
        }
    }
    private String toFen(BigDecimal amount) {
        return amount == null ? "0" : amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toPlainString();
    }
    private String toIcbcChannel(Integer paymentType) {
        if (Integer.valueOf(1).equals(paymentType)) {
            return "WECHAT";
        }
        if (Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) {
            return "ALIPAY";
        }
        return "ICBC";
    }
    private boolean verifyNotify(Map<String, String> params) {
        if (Boolean.FALSE.equals(properties.getVerifyNotifySign())) {
            return true;
        }
        String sign = params.get("sign");
        if (StringUtils.isEmpty(sign)) {
            return false;
        }
        try {
            String content = buildSignContent(params);
            return IcbcSignature.verify(content, properties.getSignType(), sign, properties.getCharset(), properties.getIcbcPublicKey());
        } catch (Exception e) {
            log.error("工行回调验签异常", e);
            return false;
        }
    }
    private String buildSignContent(Map<String, String> params) {
        Map<String, String> sorted = new TreeMap<>(params);
        sorted.remove("sign");
        sorted.remove("sign_type");
        StringBuilder content = new StringBuilder();
        for (Map.Entry<String, String> entry : sorted.entrySet()) {
            if (StringUtils.isEmpty(entry.getValue())) {
                continue;
            }
            if (content.length() > 0) {
                content.append('&');
            }
            content.append(entry.getKey()).append('=').append(entry.getValue());
        }
        return content.toString();
    }
    private boolean isPaidStatus(String status) {
        return Arrays.asList("1", "2", "SUCCESS", "PAID", "PAY_SUCCESS", "TRADE_SUCCESS").contains(status);
    }
    private String firstNotBlank(String... values) {
        for (String value : values) {
            if (StringUtils.isNotEmpty(value)) {
                return value;
            }
        }
        return "";
    }
}
sql/20260529_icbc_partner_profit_sharing.sql
New file
@@ -0,0 +1,7 @@
ALTER TABLE `t_partner`
  ADD COLUMN `icbc_partner_id` varchar(64) NULL DEFAULT NULL COMMENT '工行合作商ID' AFTER `rate`,
  ADD COLUMN `icbc_sub_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户编号' AFTER `icbc_partner_id`,
  ADD COLUMN `icbc_sub_mer_prtcl_no` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户协议编号' AFTER `icbc_sub_mer_id`,
  ADD COLUMN `icbc_mer_acct` varchar(34) NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号' AFTER `icbc_sub_mer_prtcl_no`,
  ADD COLUMN `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例' AFTER `icbc_mer_acct`,
  ADD COLUMN `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例' AFTER `electricity_share_ratio`;
sql/20260530_charging_order_share_record.sql
New file
@@ -0,0 +1,44 @@
CREATE TABLE IF NOT EXISTS `t_charging_order_share_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `charging_order_id` bigint NULL DEFAULT NULL COMMENT '充电订单ID',
  `charging_order_code` varchar(64) NOT NULL COMMENT '充电订单号',
  `site_id` int NULL DEFAULT NULL COMMENT '站点ID',
  `partner_id` int NULL DEFAULT NULL COMMENT '合作商ID',
  `pay_channel` varchar(16) NOT NULL COMMENT '支付渠道:BALANCE/ICBC/WECHAT/ALIPAY/WHITELIST/OTHER',
  `channel_pay_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道最终实收本金金额,元,不含赠送',
  `channel_gift_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道赠送抵扣金额,元,不参与分账',
  `channel_pay_ratio` decimal(10, 6) NOT NULL DEFAULT 0.000000 COMMENT '该渠道实收占订单最终实收比例',
  `channel_electricity_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道分摊电费金额,元',
  `channel_service_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道分摊服务费金额,元',
  `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例',
  `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例',
  `electricity_share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '电费分账金额,元',
  `service_share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '服务费分账金额,元',
  `share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '分账总金额,元',
  `need_online_share` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否需要线上分账',
  `online_share_status` varchar(16) NOT NULL DEFAULT 'RECORDED' COMMENT '线上分账状态:RECORDED/PROCESSING/SUCCESS/FAIL/SKIP',
  `seq_no` varchar(50) NULL DEFAULT NULL COMMENT '工行原交易消费订单号',
  `busi_type` varchar(1) NULL DEFAULT NULL COMMENT '工行原交易消费订单类型:1外部订单号,2工行订单号',
  `ori_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行原交易商户编号',
  `ori_trx_date` date NULL DEFAULT NULL COMMENT '工行原交易日期',
  `sub_order_id` varchar(50) NULL DEFAULT NULL COMMENT '工行商户自定义子订单号',
  `sub_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户编号',
  `sub_mer_prtcl_no` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户协议编号',
  `mer_acct` varchar(34) NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号',
  `icbc_order_id` varchar(64) NULL DEFAULT NULL COMMENT '工行返回订单号',
  `sub_order_seq` varchar(32) NULL DEFAULT NULL COMMENT '工行子订单序号',
  `sub_order_trxid` varchar(64) NULL DEFAULT NULL COMMENT '工行子订单交易流水',
  `rec_result` varchar(8) NULL DEFAULT NULL COMMENT '工行记录处理结果',
  `request_json` text NULL COMMENT '线上分账请求报文',
  `response_json` text NULL COMMENT '线上分账响应报文',
  `fail_reason` varchar(512) NULL DEFAULT NULL COMMENT '失败或跳过原因',
  `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
  `last_retry_time` datetime NULL DEFAULT NULL COMMENT '最近重试时间',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `del_flag` int NULL DEFAULT 0 COMMENT '删除状态(0=否,1=是)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_channel` (`charging_order_code`, `pay_channel`),
  KEY `idx_share_status` (`online_share_status`),
  KEY `idx_share_sub_order_id` (`sub_order_id`),
  KEY `idx_share_partner` (`partner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='充电订单全渠道分账记录';
sql/charging_pile_service.sql
@@ -1897,6 +1897,12 @@
  `role_id` int NULL DEFAULT NULL COMMENT '角色id',
  `logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'logo地址',
  `rate` double(10, 2) NULL DEFAULT NULL COMMENT '费率',
  `icbc_partner_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行合作商ID',
  `icbc_sub_mer_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行二级商户编号',
  `icbc_sub_mer_prtcl_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行二级商户协议编号',
  `icbc_mer_acct` varchar(34) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号',
  `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例',
  `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例',
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '注册地址',
  `phone_one` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '合作商电话1',
  `phone_two` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '合作商电话2',