Commit 1e1c5f65d28fb27e4365558a4e29e8a7c321d8d9

Authored by huanggang
1 parent b893160f

rcb payment pipeline supported

Showing 20 changed files with 810 additions and 34 deletions
cashier-boss/src/main/java/com/diligrp/cashier/boss/Constants.java
... ... @@ -13,8 +13,8 @@ public final class Constants {
13 13 public static final String TOKEN_SIGN_ALGORITHM = "HmacSHA256";
14 14 // TOKEN的签名长度
15 15 public static final int TOKEN_SIGN_LENGTH = 8;
16   - // TOKEN过期时长,单位秒 - 两分钟
17   - public static final long TOKEN_TIMEOUT_SECONDS = 120;
  16 + // TOKEN过期时长,单位秒 - 5分钟
  17 + public static final long TOKEN_TIMEOUT_SECONDS = 5 * 60;
18 18  
19 19 public final static String CONTENT_TYPE = "application/json;charset=UTF-8";
20 20  
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/controller/RcbPaymentController.java 0 → 100644
  1 +package com.diligrp.cashier.boss.controller;
  2 +
  3 +import com.diligrp.cashier.boss.exception.BossServiceException;
  4 +import com.diligrp.cashier.pipeline.core.RcbOnlinePipeline;
  5 +import com.diligrp.cashier.pipeline.domain.OnlinePaymentResponse;
  6 +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager;
  7 +import com.diligrp.cashier.pipeline.type.OutPaymentType;
  8 +import com.diligrp.cashier.pipeline.type.PaymentState;
  9 +import com.diligrp.cashier.pipeline.util.RcbSignatureUtils;
  10 +import com.diligrp.cashier.pipeline.util.RcbStateUtils;
  11 +import com.diligrp.cashier.shared.ErrorCode;
  12 +import com.diligrp.cashier.shared.util.JsonUtils;
  13 +import com.diligrp.cashier.shared.util.ObjectUtils;
  14 +import com.diligrp.cashier.trade.model.OnlinePayment;
  15 +import com.diligrp.cashier.trade.service.ICashierPaymentService;
  16 +import com.diligrp.cashier.trade.service.ITradeAssistantService;
  17 +import com.fasterxml.jackson.core.type.TypeReference;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.servlet.http.HttpServletRequest;
  20 +import org.slf4j.Logger;
  21 +import org.slf4j.LoggerFactory;
  22 +import org.springframework.http.HttpStatus;
  23 +import org.springframework.http.ResponseEntity;
  24 +import org.springframework.web.bind.annotation.PathVariable;
  25 +import org.springframework.web.bind.annotation.RequestMapping;
  26 +import org.springframework.web.bind.annotation.RestController;
  27 +
  28 +import java.time.Instant;
  29 +import java.time.LocalDateTime;
  30 +import java.time.ZoneId;
  31 +import java.util.Map;
  32 +
  33 +@RestController
  34 +@RequestMapping(value = "/rcb")
  35 +public class RcbPaymentController {
  36 +
  37 + private static final Logger LOG = LoggerFactory.getLogger(RcbPaymentController.class);
  38 +
  39 + @Resource
  40 + private ITradeAssistantService tradeAssistantService;
  41 +
  42 + @Resource
  43 + private ICashierPaymentService cashierPaymentService;
  44 +
  45 + @Resource
  46 + private IPaymentPipelineManager paymentPipelineManager;
  47 +
  48 + /**
  49 + * 支付结果通知
  50 + */
  51 + @RequestMapping(value = "/payment/{paymentId}/notify.do")
  52 + public ResponseEntity<?> paymentNotify(HttpServletRequest request, @PathVariable("paymentId") String paymentId) {
  53 + String payload = request.getParameter("order");
  54 + LOG.info("Receiving rcb payment pipeline result: {}\n{}", paymentId, payload);
  55 +
  56 + try {
  57 + LocalDateTime when = LocalDateTime.now();
  58 + Map<String, String> params = JsonUtils.fromJsonString(payload, new TypeReference<>(){});
  59 + String sign = params.remove("sign");
  60 + String source = RcbSignatureUtils.map2String(params);
  61 +
  62 + OnlinePayment payment = tradeAssistantService.findByPaymentId(paymentId);
  63 + RcbOnlinePipeline pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), RcbOnlinePipeline.class);
  64 + RcbOnlinePipeline.RcbParams config = pipeline.params();
  65 + if (!RcbSignatureUtils.verify(source, config.getKey(), sign)) {
  66 + LOG.error("Rcb pipeline data sign verify failed");
  67 + throw new BossServiceException(ErrorCode.UNAUTHORIZED_ACCESS_ERROR, "Data sign verify failed");
  68 + }
  69 +
  70 +// String paymentId = params.get("outTradeNo");
  71 + PaymentState paymentState = RcbStateUtils.paymentState(params.get("orderStatus"));
  72 + String outTradeNo = params.get("cposOrderId");
  73 + String paidTime = params.get("paidTime");
  74 + if (ObjectUtils.isNotEmpty(paidTime)) {
  75 + long timestamp = Long.parseLong(paidTime); // ⽀付完成时间戳
  76 + Instant instant = Instant.ofEpochMilli(timestamp);
  77 + when = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
  78 + }
  79 + OutPaymentType outPayType = RcbStateUtils.outPayType(params.get("tradeChannel"));
  80 +
  81 + String payerId = params.get("payUserInfo");
  82 + String errorDesc = params.get("errorDesc");
  83 + // String outOrderId = params.get("chnOrderId"); // 第三方支付通道的订单号
  84 +
  85 + OnlinePaymentResponse paymentResponse = new OnlinePaymentResponse(paymentId, outTradeNo, outPayType,
  86 + payerId, when, paymentState, errorDesc);
  87 + cashierPaymentService.notifyPaymentResponse(paymentResponse);
  88 + return ResponseEntity.ok("SUCCESS");
  89 + } catch (Exception ex) {
  90 + LOG.error("Process rcb payment result exception", ex);
  91 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("FAILED");
  92 + }
  93 + }
  94 +}
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/domain/CashierOrderVO.java
... ... @@ -11,14 +11,14 @@ public class CashierOrderVO {
11 11 private final String userId;
12 12 // 商品描述
13 13 private final String goods;
14   - // 付款金额-元
15   - private final String amount;
  14 + // 付款金额-分
  15 + private final Long amount;
16 16 // 页面回调地址
17 17 private final String redirectUrl;
18 18 // 支付通道
19 19 private final List<PaymentPipeline> pipelines;
20 20  
21   - public CashierOrderVO(String tradeId, String userId, String goods, String amount,
  21 + public CashierOrderVO(String tradeId, String userId, String goods, Long amount,
22 22 String redirectUrl, List<PaymentPipeline> pipelines) {
23 23 this.tradeId = tradeId;
24 24 this.userId = userId;
... ... @@ -40,7 +40,7 @@ public class CashierOrderVO {
40 40 return goods;
41 41 }
42 42  
43   - public String getAmount() {
  43 + public Long getAmount() {
44 44 return amount;
45 45 }
46 46  
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/service/impl/CashierDeskServiceImpl.java
... ... @@ -2,8 +2,8 @@ package com.diligrp.cashier.boss.service.impl;
2 2  
3 3 import com.diligrp.cashier.boss.CashierDeskProperties;
4 4 import com.diligrp.cashier.boss.Constants;
5   -import com.diligrp.cashier.boss.domain.CashierOrderVO;
6 5 import com.diligrp.cashier.boss.domain.CashierOrderToken;
  6 +import com.diligrp.cashier.boss.domain.CashierOrderVO;
7 7 import com.diligrp.cashier.boss.domain.CashierPaymentUrl;
8 8 import com.diligrp.cashier.boss.exception.BossServiceException;
9 9 import com.diligrp.cashier.boss.service.ICashierDeskService;
... ... @@ -12,7 +12,6 @@ import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus;
12 12 import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager;
13 13 import com.diligrp.cashier.pipeline.type.PaymentState;
14 14 import com.diligrp.cashier.shared.ErrorCode;
15   -import com.diligrp.cashier.shared.util.CurrencyUtils;
16 15 import com.diligrp.cashier.shared.util.ObjectUtils;
17 16 import com.diligrp.cashier.trade.domain.*;
18 17 import com.diligrp.cashier.trade.model.TradeOrder;
... ... @@ -87,8 +86,7 @@ public class CashierDeskServiceImpl implements ICashierDeskService {
87 86 List<PaymentPipeline> pipelines = paymentPipelineManager.listPipelines(orderToken.getMchId(), PaymentPipeline.class);
88 87 List<CashierOrderVO.PaymentPipeline> pipelineList = pipelines.stream().map(pipeline ->
89 88 new CashierOrderVO.PaymentPipeline(pipeline.pipelineId(), pipeline.supportedChannel())).toList();
90   - String amount = CurrencyUtils.toNoSymbolCurrency(trade.getMaxAmount());
91   - return new CashierOrderVO(orderToken.getTradeId(), orderToken.getUserId(), trade.getGoods(), amount,
  89 + return new CashierOrderVO(orderToken.getTradeId(), orderToken.getUserId(), trade.getGoods(), trade.getMaxAmount(),
92 90 orderToken.getRedirectUrl(), pipelineList);
93 91 }
94 92  
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/Constants.java
... ... @@ -12,6 +12,8 @@ public final class Constants {
12 12 public static final String WECHAT_PAYMENT_NOTIFY_URI = "%s/wechat/payment/%s/notify.do";
13 13 // 微信退款结果通知URI: 参数1-baseUri 参数2-refundId
14 14 public static final String WECHAT_REFUND_NOTIFY_URI = "%s/wechat/refund/%s/notify.do";
  15 + // 农商行聚合支付结果通知URI
  16 + public static final String RCB_PAYMENT_NOTIFY_URI = "%s/rcb/payment/%s/notify.do";
15 17  
16 18 public static final long ONE_MINUTE = 60 * 1000;
17 19  
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/CardPaymentHttpClient.java
... ... @@ -73,12 +73,12 @@ public class CardPaymentHttpClient extends ServiceEndpointSupport {
73 73 Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
74 74 if ("200".equals(response.get("code"))) {
75 75 Map<String, Object> data = (Map<String, Object>) response.get("data");
76   - String outTradeNo = (String) data.get("outTradeNo");
  76 + String outTradeNo = (String) data.get("tradeId");
77 77 Map<String, Object> payerId = new LinkedHashMap<>();
78 78 payerId.put("customerId", convertLong(data.get("customerId")));
79 79 payerId.put("accountId", convertLong(data.get("accountId")));
80 80 payerId.put("cardNo", data.get("cardNo"));
81   - payerId.put("name", data.get("name"));
  81 + payerId.put("name", data.get("customerName"));
82 82 return new CardPaymentResponse(request.getPaymentId(), outTradeNo, OutPaymentType.DILICARD,
83 83 JsonUtils.toJsonString(payerId), now, PaymentState.SUCCESS, "园区卡支付成功");
84 84 } else {
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/RcbOnlineHttpClient.java 0 → 100644
  1 + package com.diligrp.cashier.pipeline.client;
  2 +
  3 + import com.diligrp.cashier.pipeline.domain.*;
  4 + import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  5 + import com.diligrp.cashier.pipeline.type.OutPaymentType;
  6 + import com.diligrp.cashier.pipeline.type.PaymentState;
  7 + import com.diligrp.cashier.pipeline.util.RcbSignatureUtils;
  8 + import com.diligrp.cashier.pipeline.util.RcbStateUtils;
  9 + import com.diligrp.cashier.shared.ErrorCode;
  10 + import com.diligrp.cashier.shared.exception.ServiceAccessException;
  11 + import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
  12 + import com.diligrp.cashier.shared.util.DateUtils;
  13 + import com.diligrp.cashier.shared.util.JsonUtils;
  14 + import com.diligrp.cashier.shared.util.ObjectUtils;
  15 + import com.diligrp.cashier.shared.util.RandomUtils;
  16 + import com.fasterxml.jackson.core.type.TypeReference;
  17 + import jakarta.annotation.Resource;
  18 + import org.slf4j.Logger;
  19 + import org.slf4j.LoggerFactory;
  20 + import org.springframework.dao.DataAccessException;
  21 + import org.springframework.data.redis.core.RedisOperations;
  22 + import org.springframework.data.redis.core.SessionCallback;
  23 + import org.springframework.data.redis.core.StringRedisTemplate;
  24 + import org.springframework.lang.NonNull;
  25 +
  26 + import javax.net.ssl.SSLContext;
  27 + import javax.net.ssl.TrustManager;
  28 + import javax.net.ssl.X509TrustManager;
  29 + import java.security.cert.X509Certificate;
  30 + import java.time.Instant;
  31 + import java.time.LocalDate;
  32 + import java.time.LocalDateTime;
  33 + import java.time.ZoneId;
  34 + import java.util.*;
  35 + import java.util.concurrent.TimeUnit;
  36 +
  37 + /**
  38 + * 安徽农商银行聚合支付客户端
  39 + */
  40 +public class RcbOnlineHttpClient extends ServiceEndpointSupport {
  41 +
  42 + private static final Logger LOG = LoggerFactory.getLogger(RcbOnlineHttpClient.class);
  43 +
  44 + // 微信API BASE URL
  45 + private static final String WECHAT_BASE_URL = "https://api.weixin.qq.com";
  46 + // code2session接口: 根据登录凭证code获取登录信息
  47 + private static final String CODE_TO_SESSION = "/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
  48 +
  49 + private static final int STATUS_OK = 200;
  50 +
  51 + private final String uri;
  52 +
  53 + private final String merchantNo;
  54 +
  55 + private final String terminalNo;
  56 +
  57 + private final String key;
  58 +
  59 + private final String appId;
  60 +
  61 + @Resource
  62 + private StringRedisTemplate stringRedisTemplate;
  63 +
  64 + public RcbOnlineHttpClient(String uri, String merchantNo, String terminalNo, String key, String appId) {
  65 + this.uri = uri;
  66 + this.merchantNo = merchantNo;
  67 + this.terminalNo = terminalNo;
  68 + this.key = key;
  69 + this.appId = appId;
  70 + }
  71 +
  72 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUrl) {
  73 + Map<String, String> params = new HashMap<>();
  74 + params.put("merchantNo", merchantNo);
  75 + params.put("terminalNo", terminalNo);
  76 + params.put("batchNo", getBatchNo());
  77 + params.put("traceNo", getTraceNo());
  78 + params.put("outTradeNo", request.getPaymentId());
  79 + params.put("transAmount", String.valueOf(request.getAmount()));
  80 + params.put("appid", appId);
  81 + params.put("openId", request.getOpenId());
  82 + params.put("timeExpire", "5"); // 5分钟
  83 + params.put("tradeChannel", "01"); // 微信小程序
  84 + params.put("notifyUrl", notifyUrl);
  85 + params.put("nonceStr", RandomUtils.randomString(32));
  86 + params.put("sign", RcbSignatureUtils.sign(params, key));
  87 +
  88 + String payload = JsonUtils.toJsonString(params);
  89 + LOG.debug("Sending rcb minipro prepay request: {}", payload);
  90 + HttpResult result = send(uri + "/cposp/pay/unifiedorder", payload);
  91 + if (result.statusCode != STATUS_OK) {
  92 + LOG.error("Failed to send rcb minipro prepay, statusCode: {}", result.statusCode);
  93 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "调用小程序预支付接口失败: " + result.statusCode);
  94 + }
  95 + LOG.debug("Received rcb mini pro prepay response: {}", result.responseText);
  96 +
  97 + Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  98 + String resultCode = data.get("resultCode");
  99 + String resultMessage = data.get("resultMessage");
  100 + if ("00".equals(resultCode)) {
  101 + String signature = data.remove("sign");
  102 + if (!RcbSignatureUtils.verify(data, key, signature)) {
  103 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "小程序预支付请求数据验签失败");
  104 + }
  105 + return data.get("payInfo");
  106 + } else {
  107 + LOG.error("Failed to send minipro prepay request, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
  108 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用小程序预支付接口失败: " + resultMessage);
  109 + }
  110 + }
  111 +
  112 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder request) {
  113 + Map<String, String> params = new LinkedHashMap<>();
  114 + params.put("merchantNo", merchantNo);
  115 + params.put("terminalNo", terminalNo);
  116 + params.put("batchNo", getBatchNo());
  117 + params.put("traceNo", getTraceNo());
  118 + params.put("outTradeNo", request.getPaymentId());
  119 + params.put("nonceStr", RandomUtils.randomString(32));
  120 +
  121 + params.put("sign", RcbSignatureUtils.sign(params, key));
  122 + String payload = JsonUtils.toJsonString(params);
  123 + LOG.debug("Sending rcb order query request: {}", payload);
  124 + HttpResult result = send(uri + "/cposp/pay/orderQuery", payload);
  125 + if (result.statusCode != STATUS_OK) {
  126 + LOG.error("Failed to query rcb order, statusCode: {}", result.statusCode);
  127 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询支付状态失败: " + result.statusCode);
  128 + }
  129 + LOG.debug("Received rcb order query response: {}", result.responseText);
  130 +
  131 + Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  132 + String signature = data.remove("sign");
  133 + if (!RcbSignatureUtils.verify(data, key, signature)) {
  134 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "查询支付状态数据验签失败");
  135 + }
  136 +
  137 + String resultCode = data.get("resultCode");
  138 + String resultMessage = data.get("resultMessage");
  139 + if ("00".equals(resultCode)) {
  140 + String orderStatus = data.get("orderStatus");
  141 + String errorDesc = data.get("errorDesc");
  142 + String outTradeNo = data.get("cposOrderId");
  143 + OutPaymentType paymentType = RcbStateUtils.outPayType(data.get("tradeChannel"));
  144 + LocalDateTime when = LocalDateTime.now().withNano(0);
  145 + // 不能使用paidTime或transTime, paidTime只有年月日,transTime是订单发起时间
  146 + // 当前未设计存储支付时间字段(采用记录修改时间作为支付时间),因此采用当前时间作为记录修改时间
  147 + String timeEnd = data.get("timeEnd");
  148 + if (ObjectUtils.isNotEmpty(timeEnd)) {
  149 + long timestamp = Long.parseLong(timeEnd); // ⽀付完成时间戳
  150 + Instant instant = Instant.ofEpochMilli(timestamp);
  151 + when = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
  152 + }
  153 + String payUserInfo = data.get("payUserInfo");
  154 + // 第三方支付通道的订单编号,比如:微信订单
  155 + // String outOrderId = data.get("chnOrderId");
  156 + return new OnlinePaymentResponse(request.getPaymentId(), outTradeNo, paymentType,
  157 + payUserInfo, when, RcbStateUtils.paymentState(orderStatus), errorDesc);
  158 + } else {
  159 + LOG.error("Failed to query rcb order, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
  160 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用支付结果查询接口失败");
  161 + }
  162 + }
  163 +
  164 + public void closePrepayOrder(OnlinePrepayOrder request) {
  165 + Map<String, String> params = new HashMap<>();
  166 + params.put("merchantNo", merchantNo);
  167 + params.put("terminalNo", terminalNo);
  168 + params.put("batchNo", getBatchNo());
  169 + params.put("traceNo", getTraceNo());
  170 + params.put("outTradeNo", request.getPaymentId());
  171 + params.put("nonceStr", RandomUtils.randomString(32));
  172 + params.put("sign", RcbSignatureUtils.sign(params, key));
  173 +
  174 + String payload = JsonUtils.toJsonString(params);
  175 + LOG.info("Sending close rcb order request: {}", payload);
  176 + HttpResult result = send(uri + "/cposp/pay/closeOrder", payload);
  177 + if (result.statusCode != STATUS_OK) {
  178 + LOG.error("Failed to close rcb order, statusCode: {}", result.statusCode);
  179 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "关闭支付订单失败: " + result.statusCode);
  180 + }
  181 + LOG.info("Received close rcb order response: {}", result.responseText);
  182 +
  183 + Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  184 + String resultCode = data.get("resultCode");
  185 + String resultMessage = data.get("resultMessage");
  186 + if (!"00".equals(resultCode)) {
  187 + LOG.error("Failed to close rcb order, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
  188 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "关闭支付订单失败");
  189 + }
  190 +
  191 + String signature = data.remove("sign");
  192 + if (!RcbSignatureUtils.verify(data, key, signature)) {
  193 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "关闭订单数据验签失败");
  194 + }
  195 + }
  196 +
  197 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  198 + Map<String, String> params = new HashMap<>();
  199 + params.put("merchantNo", merchantNo);
  200 + params.put("terminalNo", terminalNo);
  201 + params.put("batchNo", getBatchNo());
  202 + params.put("traceNo", getTraceNo());
  203 + params.put("mchtRefundNo", request.getRefundId());
  204 + params.put("outTradeNo", request.getPaymentId());
  205 + params.put("refundAmount", String.valueOf(request.getAmount()));
  206 + params.put("nonceStr", RandomUtils.randomString(32));
  207 + params.put("sign", RcbSignatureUtils.sign(params, key));
  208 +
  209 + LocalDateTime now = LocalDateTime.now();
  210 + String payload = JsonUtils.toJsonString(params);
  211 + LOG.debug("Sending refund request: {}", payload);
  212 + HttpResult result = send(uri + "/cposp/pay/refund", payload);
  213 + if (result.statusCode != STATUS_OK) {
  214 + LOG.error("Failed to refund rcb order, statusCode: {}", result.statusCode);
  215 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发送退款请求失败: " + result.statusCode);
  216 + }
  217 + LOG.debug("Received rcb order refund response: {}", result.responseText);
  218 +
  219 + Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  220 + String resultCode = data.get("resultCode");
  221 + String resultMessage = data.get("resultMessage");
  222 + if (!"00".equals(resultCode)) {
  223 + LOG.error("Failed to refund, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
  224 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "发送退款请求失败");
  225 + }
  226 +
  227 + String signature = data.remove("sign");
  228 + if (!RcbSignatureUtils.verify(data, key, signature)) {
  229 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "退款接口数据验签失败");
  230 + }
  231 + String refundStatus = data.get("refundStatus");
  232 + String cposRefundOrderId = data.get("cposRefundOrderId");
  233 + PaymentState refundState = RcbStateUtils.refundState(refundStatus);
  234 + return new OnlineRefundResponse(request.getRefundId(), cposRefundOrderId, now, refundState, RcbStateUtils.refundInfo(refundState));
  235 + }
  236 +
  237 + /**
  238 + * 各交易终端,每⽇第⼀笔交易时,需要通过签到,获取批次号等信息。
  239 + *
  240 + * @return 批次号
  241 + */
  242 + protected String getBatchNo() {
  243 + try {
  244 + String key = "rcb:online:batchNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
  245 + String batchNo = stringRedisTemplate.opsForValue().get(key);
  246 + if (batchNo != null) {
  247 + return batchNo;
  248 + }
  249 +
  250 + String payload = String.format("{\"merchantNo\": \"%s\", \"terminalNo\": \"%s\"}", merchantNo, terminalNo);
  251 + LOG.debug("Sending signIn request: {}", payload);
  252 + HttpResult result = send(uri + "/cposp/pay/signIn", payload);
  253 + if (result.statusCode != STATUS_OK) {
  254 + LOG.error("Failed to get rcb batch no, statusCode: {}", result.statusCode);
  255 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "获取签到批次号: " + result.statusCode);
  256 + }
  257 + LOG.debug("Received rcb signIn response: {}", result.responseText);
  258 +
  259 + Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  260 + String resultCode = data.get("resultCode");
  261 + String resultMessage = data.get("resultMessage");
  262 + if ("00".equals(resultCode)) {
  263 + batchNo = data.get("batchNo");
  264 + stringRedisTemplate.opsForValue().set(key, batchNo, 36 * 60 * 60, TimeUnit.SECONDS);
  265 + return batchNo;
  266 + } else {
  267 + LOG.error("Failed to rcb sign in, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
  268 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "签到接口调用失败");
  269 + }
  270 + } catch (ServiceAccessException | PaymentPipelineException rex) {
  271 + throw rex;
  272 + } catch (Exception ex) {
  273 + LOG.error("Failed to get rcb sign in batchNo", ex);
  274 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取签到批次号失败");
  275 + }
  276 + }
  277 +
  278 + protected String getTraceNo() {
  279 + try {
  280 + String key = "rcb:online:traceNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
  281 + StringBuilder traceNo = new StringBuilder();
  282 +
  283 + List<Object> results = stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
  284 + @Override
  285 + public Object execute(@NonNull RedisOperations operations) throws DataAccessException {
  286 + operations.opsForValue().increment(key);
  287 + operations.expire(key, 36 * 60 * 60, TimeUnit.SECONDS);
  288 + return null;
  289 + }
  290 + });
  291 +
  292 + traceNo.append(results.getFirst());
  293 + int length = traceNo.length();
  294 + if (length < 6) {
  295 + for (int i = length; i < 6; i++) {
  296 + traceNo.insert(0, "0");
  297 + }
  298 + }
  299 + return traceNo.toString();
  300 + } catch (Exception ex) {
  301 + LOG.error("Failed to get traceNo", ex);
  302 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取系统跟踪号失败");
  303 + }
  304 + }
  305 +
  306 + protected Optional<javax.net.ssl.SSLContext> buildSSLContext() {
  307 + SSLContext sslContext = null;
  308 + try {
  309 + TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[] {
  310 + new X509TrustManager() {
  311 + @Override
  312 + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
  313 + }
  314 +
  315 + @Override
  316 + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
  317 + }
  318 +
  319 + @Override
  320 + public X509Certificate[] getAcceptedIssuers() {
  321 + return new X509Certificate[0];
  322 + }
  323 + }
  324 + };
  325 + sslContext = SSLContext.getInstance("SSL");
  326 + sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
  327 + } catch (Exception ex) {
  328 + LOG.error("Build SSLContext failed", ex);
  329 + }
  330 + return Optional.ofNullable(sslContext);
  331 + }
  332 +
  333 + private static class AuthorizationSession {
  334 + // 用户唯一标识
  335 + private String openid;
  336 + // 会话密钥
  337 + private String session_key;
  338 + // 用户在开放平台的唯一标识
  339 + private String unionid;
  340 + // 错误码
  341 + private Integer errcode;
  342 + // 错误信息
  343 + private String errmsg;
  344 +
  345 + public String getOpenid() {
  346 + return openid;
  347 + }
  348 +
  349 + public void setOpenid(String openid) {
  350 + this.openid = openid;
  351 + }
  352 +
  353 + public String getSession_key() {
  354 + return session_key;
  355 + }
  356 +
  357 + public void setSession_key(String session_key) {
  358 + this.session_key = session_key;
  359 + }
  360 +
  361 + public String getUnionid() {
  362 + return unionid;
  363 + }
  364 +
  365 + public void setUnionid(String unionid) {
  366 + this.unionid = unionid;
  367 + }
  368 +
  369 + public Integer getErrcode() {
  370 + return errcode;
  371 + }
  372 +
  373 + public void setErrcode(Integer errcode) {
  374 + this.errcode = errcode;
  375 + }
  376 +
  377 + public String getErrmsg() {
  378 + return errmsg;
  379 + }
  380 +
  381 + public void setErrmsg(String errmsg) {
  382 + this.errmsg = errmsg;
  383 + }
  384 + }
  385 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatDirectHttpClient.java
... ... @@ -53,8 +53,8 @@ public class WechatDirectHttpClient extends WechatHttpClient {
53 53 * Native支付下单, 返回二维码链接
54 54 */
55 55 @Override
56   - public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
57   - String payload = nativePrepayRequest(request, notifyUri);
  56 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUrl) throws Exception {
  57 + String payload = nativePrepayRequest(request, notifyUrl);
58 58 // 获取认证信息和签名信息
59 59 String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST,
60 60 NATIVE_PREPAY, payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
... ... @@ -81,8 +81,8 @@ public class WechatDirectHttpClient extends WechatHttpClient {
81 81 * 小程序支付下单
82 82 */
83 83 @Override
84   - public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
85   - String payload = miniProPrepayRequest(request, notifyUri);
  84 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUrl) throws Exception {
  85 + String payload = miniProPrepayRequest(request, notifyUrl);
86 86 // 获取认证信息和签名信息
87 87 String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, JSAPI_PREPAY,
88 88 payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatPartnerHttpClient.java
... ... @@ -54,8 +54,8 @@ public class WechatPartnerHttpClient extends WechatHttpClient {
54 54 * Native支付下单, 返回二维码链接
55 55 */
56 56 @Override
57   - public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
58   - String payload = nativePrepayRequest(request, notifyUri);
  57 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUrl) throws Exception {
  58 + String payload = nativePrepayRequest(request, notifyUrl);
59 59 // 获取认证信息和签名信息
60 60 String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, NATIVE_PREPAY,
61 61 payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
... ... @@ -82,8 +82,8 @@ public class WechatPartnerHttpClient extends WechatHttpClient {
82 82 * 小程序支付下单, 返回prepay_id
83 83 */
84 84 @Override
85   - public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
86   - String payload = miniProPrepayRequest(request, notifyUri);
  85 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUrl) throws Exception {
  86 + String payload = miniProPrepayRequest(request, notifyUrl);
87 87 // 获取认证信息和签名信息
88 88 String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, JSAPI_PREPAY,
89 89 payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/DiliCardPipeline.java
... ... @@ -27,8 +27,8 @@ public class DiliCardPipeline extends PaymentPipeline&lt;DiliCardPipeline.CardParam
27 27 public DiliCardPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
28 28 super(mchId, pipelineId, name, uri, params);
29 29 CardParams config = params();
30   - AssertUtils.notNull(config.getOutMchId(), "园区卡支付缺少参数配置: outMchId");
31   - AssertUtils.notNull(config.getAccountId(), "园区卡支付缺少参数配置: accountId");
  30 + checkParam(config.getOutMchId(), "outMchId");
  31 + checkParam(config.getAccountId(), "accountId");
32 32  
33 33 this.strategy = new DefaultTimeStrategy();
34 34 this.client = new CardPaymentHttpClient(uri, config.getOutMchId(), config.getAccountId());
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/PaymentPipeline.java
... ... @@ -7,6 +7,7 @@ import com.diligrp.cashier.shared.util.ObjectUtils;
7 7  
8 8 import java.lang.reflect.Constructor;
9 9 import java.lang.reflect.InvocationTargetException;
  10 +import java.util.Objects;
10 11  
11 12 /**
12 13 * 所有支付通道的基类, 目前分两类支付通道: 在线支付通道、园区卡支付通道
... ... @@ -65,12 +66,18 @@ public abstract class PaymentPipeline&lt;T extends PaymentPipeline.PipelineParams&gt;
65 66 return params;
66 67 }
67 68  
68   - protected void checkParam(String label, String value) {
  69 + protected void checkParam(String value, String label) {
69 70 if (ObjectUtils.isEmpty(value)) {
70 71 throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, String.format("支付通道缺少参数配置: %s", label));
71 72 }
72 73 }
73 74  
  75 + protected void checkParam(Object value, String label) {
  76 + if (Objects.isNull(value)) {
  77 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, String.format("支付通道缺少参数配置: %s", label));
  78 + }
  79 + }
  80 +
74 81 protected static abstract class PipelineParams {
75 82 public PipelineParams(String params) {
76 83 parseParams(params);
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/RcbOnlinePipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.Constants;
  4 +import com.diligrp.cashier.pipeline.client.RcbOnlineHttpClient;
  5 +import com.diligrp.cashier.pipeline.domain.*;
  6 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  7 +import com.diligrp.cashier.pipeline.type.ChannelType;
  8 +import com.diligrp.cashier.pipeline.type.PaymentState;
  9 +import com.diligrp.cashier.shared.ErrorCode;
  10 +import com.diligrp.cashier.shared.util.ObjectUtils;
  11 +
  12 +import java.time.LocalDateTime;
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +import java.util.StringTokenizer;
  16 +
  17 +/**
  18 + * 农商行聚合支付通道
  19 + */
  20 +public class RcbOnlinePipeline extends OnlinePipeline<RcbOnlinePipeline.RcbParams> {
  21 +
  22 + private final ScanTimeStrategy strategy;
  23 +
  24 + private final RcbOnlineHttpClient client;
  25 +
  26 + public RcbOnlinePipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  27 + super(mchId, pipelineId, name, uri, params);
  28 + if (ObjectUtils.isEmpty(params)) {
  29 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "支付通道未进行参数配置");
  30 + }
  31 + RcbParams config = params();
  32 +
  33 + checkParam(config.getMerchantNo(), "merchantNo");
  34 + checkParam(config.getTerminalNo(), "terminalNo");
  35 + checkParam(config.getAppId(), "appId");
  36 + checkParam(config.getAppSecret(), "appSecret");
  37 + checkParam(config.getKey(), "key");
  38 + checkParam(config.getNotifyUrl(), "notifyUrl");
  39 + this.client = new RcbOnlineHttpClient(uri, config.getMerchantNo(), config.getTerminalNo(), config.getKey(), config.getAppId());
  40 + this.strategy = new DefaultTimeStrategy();
  41 + }
  42 +
  43 + /**
  44 + * 小程序支付
  45 + */
  46 + public MiniProPrepayResponse sendMiniProPrepayRequest(MiniProPrepayRequest request) {
  47 + String notifyUri = String.format(Constants.RCB_PAYMENT_NOTIFY_URI, params().getNotifyUrl(), request.getPaymentId());
  48 + request.put(Constants.PARAM_MCH_ID, params().getMerchantNo());
  49 + String payInfo = client.sendMiniProPrepayRequest(request, notifyUri);
  50 + // 解析农商行小程序支付信息
  51 + StringTokenizer tokenizer = new StringTokenizer(payInfo, ";", false);
  52 + Map<String, String> result = new HashMap<>(8);
  53 + while (tokenizer.hasMoreTokens()) {
  54 + String token = tokenizer.nextToken();
  55 + int index = token.indexOf("=");
  56 + if (index > 0) {
  57 + String key = token.substring (0, index).trim ();
  58 + String value = token.substring (index + 1).trim ();
  59 + result.put(key, value);
  60 + }
  61 + }
  62 + String prepayId = result.get("tradeNO");
  63 + String timeStamp = result.get("timeStamp");
  64 + String nonceStr = result.get("nonceStr");
  65 +// String packet = result.get("body"); // prepay_id=<prepayId>
  66 + String signType = result.get("signType");
  67 + String paySign = result.get("paySign");
  68 + return MiniProPrepayResponse.of(request.getPaymentId(), prepayId, prepayId, timeStamp, nonceStr, signType, paySign);
  69 + }
  70 +
  71 + @Override
  72 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder request) {
  73 + return client.queryPrepayResponse(request);
  74 + }
  75 +
  76 + @Override
  77 + public void closePrepayOrder(OnlinePrepayOrder request) {
  78 + client.closePrepayOrder(request);
  79 + }
  80 +
  81 + @Override
  82 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  83 + return client.sendRefundRequest(request);
  84 + }
  85 +
  86 + @Override
  87 + public OnlineRefundResponse queryRefundResponse(OnlineRefundOrder request) {
  88 + return new OnlineRefundResponse(request.getRefundId(), request.getOutTradeNo(), LocalDateTime.now(),
  89 + PaymentState.PROCESSING, "支付通道不支持退款状态查询");
  90 + }
  91 +
  92 + @Override
  93 + public ScanTimeStrategy getTimeStrategy() {
  94 + return strategy;
  95 + }
  96 +
  97 + @Override
  98 + public ChannelType supportedChannel() {
  99 + return ChannelType.RCB;
  100 + }
  101 +
  102 + @Override
  103 + public Class<RcbParams> paramClass() {
  104 + return RcbParams.class;
  105 + }
  106 +
  107 + public static class RcbParams extends PaymentPipeline.PipelineParams {
  108 + public RcbParams(String params) {
  109 + super(params);
  110 + }
  111 +
  112 + private String merchantNo;
  113 +
  114 + private String terminalNo;
  115 +
  116 + private String appId;
  117 +
  118 + private String appSecret;
  119 +
  120 + private String key;
  121 +
  122 + private String notifyUrl;
  123 +
  124 + public String getMerchantNo() {
  125 + return merchantNo;
  126 + }
  127 +
  128 + public void setMerchantNo(String merchantNo) {
  129 + this.merchantNo = merchantNo;
  130 + }
  131 +
  132 + public String getTerminalNo() {
  133 + return terminalNo;
  134 + }
  135 +
  136 + public void setTerminalNo(String terminalNo) {
  137 + this.terminalNo = terminalNo;
  138 + }
  139 +
  140 + public String getAppId() {
  141 + return appId;
  142 + }
  143 +
  144 + public void setAppId(String appId) {
  145 + this.appId = appId;
  146 + }
  147 +
  148 + public String getAppSecret() {
  149 + return appSecret;
  150 + }
  151 +
  152 + public void setAppSecret(String appSecret) {
  153 + this.appSecret = appSecret;
  154 + }
  155 +
  156 + public String getKey() {
  157 + return key;
  158 + }
  159 +
  160 + public void setKey(String key) {
  161 + this.key = key;
  162 + }
  163 +
  164 + public String getNotifyUrl() {
  165 + return notifyUrl;
  166 + }
  167 +
  168 + public void setNotifyUrl(String notifyUrl) {
  169 + this.notifyUrl = notifyUrl;
  170 + }
  171 + }
  172 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/WechatPartnerPipeline.java
... ... @@ -32,9 +32,9 @@ public class WechatPartnerPipeline extends WechatPipeline&lt;WechatPartnerPipeline.
32 32 public WechatPartnerPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
33 33 super(mchId, pipelineId, name, uri, params);
34 34 PartnerWechatParams config = params();
35   - AssertUtils.notEmpty(config.getSubMchId(), "微信支付缺少参数配置: subMchId");
36   - AssertUtils.notEmpty(config.getSubAppId(), "微信支付缺少参数配置: subAppId");
37   - AssertUtils.notEmpty(config.getAppSecret(), "微信支付缺少参数配置: appSecret");
  35 + checkParam(config.getSubMchId(), "subMchId");
  36 + checkParam(config.getSubAppId(), "subAppId");
  37 + checkParam(config.getAppSecret(), "appSecret");
38 38 this.strategy = new DefaultTimeStrategy();
39 39 }
40 40  
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/WechatPipeline.java
... ... @@ -10,7 +10,6 @@ import com.diligrp.cashier.pipeline.type.ChannelType;
10 10 import com.diligrp.cashier.pipeline.util.WechatConstants;
11 11 import com.diligrp.cashier.shared.ErrorCode;
12 12 import com.diligrp.cashier.shared.security.RsaCipher;
13   -import com.diligrp.cashier.shared.util.AssertUtils;
14 13 import org.slf4j.Logger;
15 14 import org.slf4j.LoggerFactory;
16 15  
... ... @@ -31,7 +30,7 @@ public abstract class WechatPipeline&lt;T extends WechatPipeline.WechatParams&gt; exte
31 30 public WechatPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
32 31 super(mchId, pipelineId, name, uri, params);
33 32 WechatParams config = params();
34   - AssertUtils.notEmpty(config.getNotifyUrl(), "微信支付缺少参数配置: notifyUrl");
  33 + checkParam(config.getNotifyUrl(), "notifyUrl");
35 34 }
36 35  
37 36 /**
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/impl/PaymentPipelineManagerImpl.java
... ... @@ -60,6 +60,10 @@ public class PaymentPipelineManagerImpl extends LifeCycle implements IPaymentPip
60 60 DiliCardPipeline paymentPipeline = new DiliCardPipeline(pipeline.getMchId(), pipeline.getPipelineId(),
61 61 pipeline.getName(), pipeline.getUri(), pipeline.getParam());
62 62 registerPaymentPipeline(paymentPipeline);
  63 + } else if (ChannelType.RCB.equalTo(pipeline.getChannelId())) {
  64 + RcbOnlinePipeline paymentPipeline = new RcbOnlinePipeline(pipeline.getMchId(), pipeline.getPipelineId(),
  65 + pipeline.getName(), pipeline.getUri(), pipeline.getParam());
  66 + registerPaymentPipeline(paymentPipeline);
63 67 } else {
64 68 LOG.warn("Ignore payment pipeline configuration: {}", pipeline.getName());
65 69 }
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/impl/WechatPaymentServiceImpl.java
1 1 package com.diligrp.cashier.pipeline.service.impl;
2 2  
3 3 import com.diligrp.cashier.pipeline.client.WechatHttpClient;
  4 +import com.diligrp.cashier.pipeline.core.OnlinePipeline;
  5 +import com.diligrp.cashier.pipeline.core.RcbOnlinePipeline;
  6 +import com.diligrp.cashier.pipeline.core.WechatDirectPipeline;
4 7 import com.diligrp.cashier.pipeline.core.WechatPartnerPipeline;
5   -import com.diligrp.cashier.pipeline.core.WechatPipeline;
6 8 import com.diligrp.cashier.pipeline.domain.wechat.UploadShippingRequest;
7 9 import com.diligrp.cashier.pipeline.domain.wechat.WechatAccessToken;
  10 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
8 11 import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager;
9 12 import com.diligrp.cashier.pipeline.service.IWechatPaymentService;
  13 +import com.diligrp.cashier.shared.ErrorCode;
10 14 import jakarta.annotation.Resource;
11 15 import org.springframework.stereotype.Service;
12 16  
... ... @@ -16,19 +20,26 @@ public class WechatPaymentServiceImpl implements IWechatPaymentService {
16 20 @Resource
17 21 private IPaymentPipelineManager paymentPipelineManager;
18 22  
  23 + private WechatHttpClient wechatHttpClient = new WechatHttpClient(null, null);
  24 +
19 25 /**
20 26 * 根据登陆凭证code获取openId
21 27 */
22 28 @Override
23 29 public String openIdByCode(Long pipelineId, String code) {
24   - WechatPipeline<?> pipeline = paymentPipelineManager.findPipelineById(pipelineId, WechatPipeline.class);
  30 + OnlinePipeline<?> pipeline = paymentPipelineManager.findPipelineById(pipelineId, OnlinePipeline.class);
25 31 if (pipeline instanceof WechatPartnerPipeline partnerPipeline) {
26 32 // 服务商模式下使用WechatParams配置的信息获取openId
27 33 WechatPartnerPipeline.PartnerWechatParams params = partnerPipeline.params();
28   - return pipeline.getClient().loginAuthorization(params.getSubAppId(), params.getAppSecret(), code);
29   - } else {
  34 + return partnerPipeline.getClient().loginAuthorization(params.getSubAppId(), params.getAppSecret(), code);
  35 + } else if (pipeline instanceof WechatDirectPipeline directPipeline) {
30 36 // 直连模式直接使用WebConfig配置的信息获取openId
31   - return pipeline.getClient().loginAuthorization(code);
  37 + return directPipeline.getClient().loginAuthorization(code);
  38 + } else if (pipeline instanceof RcbOnlinePipeline rcbOnlinePipeline) {
  39 + RcbOnlinePipeline.RcbParams params = rcbOnlinePipeline.params();
  40 + return wechatHttpClient.loginAuthorization(params.getAppId(), params.getAppSecret(), code);
  41 + } else {
  42 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "该支付通道不支持此操作");
32 43 }
33 44 }
34 45  
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/util/RcbSignatureUtils.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.util;
  2 +
  3 +import com.diligrp.cashier.shared.security.HexUtils;
  4 +import com.diligrp.cashier.shared.util.ObjectUtils;
  5 +
  6 +import java.nio.charset.StandardCharsets;
  7 +import java.security.MessageDigest;
  8 +import java.security.NoSuchAlgorithmException;
  9 +import java.util.Comparator;
  10 +import java.util.Map;
  11 +import java.util.TreeMap;
  12 +import java.util.stream.Collectors;
  13 +
  14 +/**
  15 + * 安徽农商行的聚合支付文档中并未提到验签功能
  16 + */
  17 +public final class RcbSignatureUtils {
  18 + public static String sign(Map<String, String> params, String key) {
  19 + String data = map2String(params).concat("&key=").concat(key);
  20 + MessageDigest md5;
  21 + try {
  22 + md5 = MessageDigest.getInstance("MD5");
  23 + md5.update(data.getBytes(StandardCharsets.UTF_8));
  24 + return HexUtils.encodeHexStr(md5.digest(), false);
  25 + } catch (NoSuchAlgorithmException ex) {
  26 + // Never happened
  27 + throw new RuntimeException(ex);
  28 + }
  29 + }
  30 +
  31 + public static boolean verify(Map<String, String> params, String key, String signature) {
  32 + String data = sign(params, key);
  33 + return data.equals(signature);
  34 + }
  35 +
  36 + public static boolean verify(String params, String key, String signature) {
  37 + String data = params.concat("&key=").concat(key);
  38 + MessageDigest md5 = null;
  39 + try {
  40 + md5 = MessageDigest.getInstance("MD5");
  41 + md5.update(data.getBytes(StandardCharsets.UTF_8));
  42 + String toBeSigned = HexUtils.encodeHexStr(md5.digest(), false);
  43 + return toBeSigned.equals(signature);
  44 + } catch (NoSuchAlgorithmException ex) {
  45 + // Never happened
  46 + throw new RuntimeException(ex);
  47 + }
  48 +
  49 + }
  50 +
  51 + public static String map2String(Map<String, String> params) {
  52 + TreeMap<String, String> data = new TreeMap<>(Comparator.naturalOrder());
  53 + data.putAll(params);
  54 + return data.entrySet().stream().filter(e -> ObjectUtils.isNotEmpty(e.getValue()))
  55 + .map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&", "", ""));
  56 + }
  57 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/util/RcbStateUtils.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.util;
  2 +
  3 +import com.diligrp.cashier.pipeline.type.OutPaymentType;
  4 +import com.diligrp.cashier.pipeline.type.PaymentState;
  5 +
  6 +public final class RcbStateUtils {
  7 + public static PaymentState paymentState(String orderStatus) {
  8 + return switch (orderStatus) {
  9 + case "3", "6" -> PaymentState.SUCCESS; // 3-交易成功; 6-交易成功但有退款
  10 + case "1", "2" -> PaymentState.PROCESSING;
  11 + default -> PaymentState.FAILED;
  12 + };
  13 + }
  14 +
  15 + public static PaymentState refundState(String orderStatus) {
  16 + return switch (orderStatus) {
  17 + case "01" -> PaymentState.SUCCESS;
  18 + case "02" -> PaymentState.FAILED;
  19 + default -> PaymentState.PROCESSING;
  20 + };
  21 + }
  22 +
  23 + public static String refundInfo(PaymentState refundState) {
  24 + return switch (refundState) {
  25 + case SUCCESS -> "退款成功";
  26 + case FAILED -> "退款失败";
  27 + default -> "退款处理中";
  28 + };
  29 + }
  30 +
  31 + public static OutPaymentType outPayType(String tradeChannel) {
  32 + return switch (tradeChannel) {
  33 + case "01" -> OutPaymentType.WXPAY; // 微信
  34 + case "02" -> OutPaymentType.ALIPAY; // 支付宝
  35 + case "04" -> OutPaymentType.UPAY; // 银联
  36 + default -> OutPaymentType.NOP; // 未知支付方式
  37 + };
  38 + }
  39 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/DateUtils.java
... ... @@ -16,6 +16,12 @@ import java.util.Date;
16 16 */
17 17 public class DateUtils {
18 18  
  19 + public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
  20 +
  21 + public static final String YYYY_MM_DD = "yyyy-MM-dd";
  22 +
  23 + public static final String YYYYMMDD = "yyyyMMdd";
  24 +
19 25 public static String formatDateTime(LocalDateTime when, String format) {
20 26 if (ObjectUtils.isNull(when)) {
21 27 return null;
... ...
scripts/cashier-data.sql
... ... @@ -6,6 +6,8 @@ INSERT INTO `dili_cashier`.upay_payment_pipeline(mch_id, pipeline_id, channel_id
6 6 VALUES (1001, 10011, 10, 10, '中瑞微信服务商支付通道', 'https://api.mch.weixin.qq.com', '{"subMchId":"1679224186", "subAppId":"wxad27b69b888b6dc9", "appSecret":"9c254c0ab932b3c30292a05679a688f7", "notifyUrl": "https://cashier.test.gszdtop.com"}', 1, now(), now());
7 7 INSERT INTO `dili_cashier`.upay_wechat_param(pipeline_id, mch_id, app_id, app_secret, serial_no, private_key, wechat_serial_no, wechat_public_key, api_v3_key, type, created_time)
8 8 VALUES(10011, '1679223106', 'wxca99d56a6ab15f29', '9c254c0ab932b3c30292a05679a688f7', '60C2877836D1D618D2E40186995BB00299D92F44','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', 'RSfFvEBBQiHz8GZyDcP2eSUlZJgKjdxk', 2, now());
9   --- 根据环境配置不同的uri
  9 +
  10 +INSERT INTO `dili_cashier`.upay_payment_pipeline(mch_id, pipeline_id, channel_id, type, name, uri, param, state, created_time, modified_time)
  11 +VALUES (1001, 10014, 29, 2, '中瑞农商行聚合支付通道', 'https://epos.ahrcu.com:3443', '{"merchantNo": "94734065411016B", "terminalNo": "19A03301", "appId": "wx6f15ee0bd788c744", "appSecret": "4ab4152adf21da99c629fe7f3ce6b571", "key": "45913E42F86C18F512BEF54C2F0FE5BF", "notifyUrl": "https://cashier.test.gszdtop.com"}', 1, now(), now());
10 12 INSERT INTO `dili_cashier`.upay_payment_pipeline(mch_id, pipeline_id, channel_id, type, name, uri, param, state, created_time, modified_time)
11 13 VALUES (1001, 10012, 19, 2, '中瑞园区卡支付通道', 'http://gateway.dev.nong12.com/pay-service', '{"outMchId": 9, "accountId": 118924}', 1, now(), now());
... ...