.gitignore
@@ -5,6 +5,7 @@ # Package Files # *.jar !ruoyi-service/ruoyi-payment/lib/*.jar *.war *.ear 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
@@ -2,3 +2,4 @@ com.ruoyi.payment.api.factory.AliPaymentFallbackFactory com.ruoyi.payment.api.factory.AppAliPaymentFallbackFactory 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,9 +154,33 @@ 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,6 +558,37 @@ 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(); return AjaxResult.success(data); } if (2 == addChargingOrder.getPaymentType()) { AliPaymentReq req = new AliPaymentReq(); IcbcPaymentReq req = new IcbcPaymentReq(); req.setOutTradeNo(chargingOrder.getCode()); req.setTotalAmount(thirdPayAmount.toString()); req.setAmount(thirdPayAmount); 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.setPaymentType(addChargingOrder.getPaymentType()); req.setNotifyUrl("/payment/icbc/callBack"); req.setReturnUrl(addChargingOrder.getReturnUrl()); String data = h5AliPaymentClient.payment(req).getData(); if (StringUtils.hasLength(data)) { return AjaxResult.success("操作成功", data); 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); } } 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+.jarBinary files differ
ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jarBinary files differ
ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jarBinary files differ
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jarBinary files differ
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jarBinary files differ
ruoyi-service/ruoyi-payment/lib/icbc-ca.jarBinary 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',