Commit 1e1c5f65d28fb27e4365558a4e29e8a7c321d8d9
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<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<T extends PaymentPipeline.PipelineParams> |
| 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<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<T extends WechatPipeline.WechatParams> 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','MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCx/XUbQ4mOr+zwuLn3e621YEgBc/dzbfGuc7mV2ojKN/wUwRRfrgfyln7xHurUeVf8jrPdJZwk/d6mqyZl28i/NS88Ud+jNOSe0bB2DwFEh5zhqbzfKYtBygkkNFzTam12ddSwCpng+59hcgaMdx47e7D6e+3C7Y422gJWMmBadP8gV25J2XP2u/zBl8PXUUsjhlWG474X6p5OGoahVTrfTFUIp6KfST8GvBa0uXjoiD3uS/d+u9VCKd6S2ohBDBEsybKGH8MHHopsF/NRuhlsUWKdR/eTcSItOs2fnE7MIGTeHZiBjA9lDi5qRsq5ryZEf85GU3uJCIlad0JbgsvjAgMBAAECggEBAINjcCDyGAcGgsen9U9lMvOi4USBUHca/78hmiuuqC9uaF0BsoJ2u1MuGQLxKbQy5up+hPOIod0EsmkiCjRCq8vJ/NZwMcAOeX1rmPFtXigyW3KRk+TAjBXCiED7jlJaS/eYP6q8CJ91309VltP10pFiW2BsPzUXm1WOVQ9AHLRoUIrywP+FZlymYBMo8HgMaIhBQdHS8+kxEUD/iJID9V/96sem6v0UOwZw8eVymZ+Yz9LVAxoI2zELyMKM2XrwLkJ1HTaV9VAjoFO09eTLJjZbiRFg0dqNBimSL0H3wDZrNpiOI5ptqs0RSCQ0o10n0DJIITI+ybpak9BtklFotjECgYEA627DKlWPTC4tWX6nt/Ty55+UxAmE5icT5i969eze3qTWcYnaF5R+Xe7ClM+H4cbZ957LwgKQhmmSy8joj9hhlN2opBmRZPuJTKa6hVYY4HUjmTdjiPxQebWK53hQuNLozCCC2Etpb4VBVGxF5d50zf2JD4FR1AspXR8hAag95bcCgYEAwYoLKKXYDqXUUzjmOiDOQFqmC5rfaT/A33Ud27uExHQEMBw7Pxijvdj+Ui7/ykeb7R2+g2eNlr00tohBrwvfiI+rZz2qcglgbZRQpacK1rkhUpW1Vxv5snR9NgvTganII2eRmyKUxQyvAsBUkWhvWXuy5fUma74nO9Y82UvHqzUCgYEAouKJCJsVf2FbYtWr+Cvyeqn/5PmpBwr2S4WCDu+I6oUlEHyNdU75dsefvBExM9W+LAGje2EG2NfmBjPEIvFT4gjRimdeHn2g6nVYCrQclf61WGXn6XiXvP0LU0X8o0LYaZH8tOTH165cGqqmWXllWrcUwrN4B7qJLbJBxcG+wVUCgYBOUGSZixo1OycCkfifNt0er0+XTJDwjsql4Uc2vddIg0WajiHvMzI2xRKMANaibH2M4kdP9twVTfSBk/s4MM6//Jq4CPzqbh7l2GkVztUU9A6m00twtzI/4uEzuG9afXAt21/Q7ZpTbgF3VIoj2KWOCP7oDF4CpQxNKzCuIPrnrQKBgQClomPIXuKw75YCtPN7eq7ul/NPa6GNfzkL0DNl+sxNV0NGjTxSmj7cVkTc7ebduQh1MwbAh1Tlhxt0rRkmzVmDToaH4Hb7ZpHeVNRQLQEBQoaHiiYztH7n6DNWVaICsi5SeeoDEYhcQG08xCgY6K4BKglezodEyPAJ1DJrRt6UHw==', '3C4D1F79159D86544E2B4E04BA3D5F8541818B3A', 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtOrhRXuxkIcKYv4Aqg8OXT2HSUteoCfjnH8z3Ma6vQhgV5nriVR4dGUw/LOqHndKxN/n5RQcUBNQpsfGMATiEcVGkyiJNQZRBKBa6PxD24sCzGTde8wrYPCazibA+wA14Nj9fQIfc9loCJu9IrYrc6p7iJNOHqfYM20PtctrvGs5DgGt1Rav/xqin5f3wQXvungGfRJwbSpoA+ayXzRkFe5UThFEF/NP0PHOc6+pj7xuf5g9HactqdbRJyRIjhhyfAW5BOTAIFGPNVhE6juhyVFyx1uRBdKvZUKj0U76PzT/l8gW0FizeMpSal1oVszCSjo6FdD3II9C3CyJX1A01QIDAQAB', '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()); | ... | ... |