Commit e8bc3fd84c96c3567d52ad687a611819f8aabaef

Authored by huanggang
1 parent b2e74712

payment pipeline supported

Showing 66 changed files with 4211 additions and 5 deletions
cashier-boss/src/main/java/com/diligrp/cashier/boss/BossConfiguration.java
1 1 package com.diligrp.cashier.boss;
2 2  
  3 +import com.diligrp.cashier.shared.service.LifeCycle;
  4 +import org.springframework.beans.BeansException;
  5 +import org.springframework.boot.ApplicationArguments;
  6 +import org.springframework.boot.ApplicationRunner;
  7 +import org.springframework.context.ApplicationContext;
  8 +import org.springframework.context.ApplicationContextAware;
3 9 import org.springframework.context.annotation.ComponentScan;
4 10 import org.springframework.context.annotation.Configuration;
5 11  
  12 +import java.util.Map;
  13 +
6 14 @Configuration
7 15 @ComponentScan("com.diligrp.cashier.boss")
8   -public class BossConfiguration {
  16 +public class BossConfiguration implements ApplicationContextAware, ApplicationRunner {
  17 +
  18 + private ApplicationContext applicationContext;
  19 +
  20 + @Override
  21 + public void run(ApplicationArguments args) throws Exception {
  22 + Map<String, LifeCycle> beans = applicationContext.getBeansOfType(LifeCycle.class);
  23 + for (LifeCycle lifeCycle : beans.values()) {
  24 + lifeCycle.start();
  25 + }
  26 + }
  27 +
  28 + @Override
  29 + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  30 + this.applicationContext = applicationContext;
  31 + }
9 32 }
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/CashierServiceBootstrap.java
1 1 package com.diligrp.cashier.boss;
2 2  
3 3 import com.diligrp.cashier.assistant.AssistantConfiguration;
  4 +import com.diligrp.cashier.pipeline.PipelineConfiguration;
  5 +import com.diligrp.cashier.shared.SharedConfiguration;
4 6 import org.springframework.boot.SpringApplication;
5 7 import org.springframework.boot.SpringBootConfiguration;
6 8 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
... ... @@ -10,7 +12,7 @@ import org.springframework.context.annotation.Import;
10 12  
11 13 @SpringBootConfiguration
12 14 @EnableAutoConfiguration
13   -@Import({BossConfiguration.class, AssistantConfiguration.class})
  15 +@Import({BossConfiguration.class, PipelineConfiguration.class, AssistantConfiguration.class, SharedConfiguration.class})
14 16 @EnableDiscoveryClient
15 17 public class CashierServiceBootstrap {
16 18 public static void main(String[] args) {
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/Constants.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline;
  2 +
  3 +public final class Constants {
  4 +
  5 + // 附加参数列表
  6 + // 外部商户号
  7 + public static final String PARAM_MCH_ID = "outMchId";
  8 + // 用户标识
  9 + public static final String PARAM_OPEN_ID = "openId";
  10 +
  11 + // 微信支付结果通知URI: 参数1-baseUri 参数2-paymentId
  12 + public static final String WECHAT_PAYMENT_NOTIFY_URI = "%s/wechat/payment/%s/notify.do";
  13 + // 微信退款结果通知URI: 参数1-baseUri 参数2-refundId
  14 + public static final String WECHAT_REFUND_NOTIFY_URI = "%s/wechat/refund/%s/notify.do";
  15 +
  16 + public static final long ONE_MINUTE = 60 * 1000;
  17 +
  18 + public static final long TEN_MINUTES = 10 * ONE_MINUTE;
  19 +
  20 + public static final long ONE_SECOND = 1000;
  21 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/PipelineConfiguration.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline;
  2 +
  3 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  4 +import org.mybatis.spring.annotation.MapperScan;
  5 +import org.springframework.context.annotation.ComponentScan;
  6 +import org.springframework.context.annotation.Configuration;
  7 +
  8 +@Configuration
  9 +@ComponentScan("com.diligrp.cashier.pipeline")
  10 +@MapperScan(basePackages = {"com.diligrp.cashier.pipeline.dao"}, markerInterface = MybatisMapperSupport.class)
  11 +public class PipelineConfiguration {
  12 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/CardPaymentHttpClient.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.client;
  2 +
  3 +import com.diligrp.cashier.pipeline.domain.*;
  4 +import com.diligrp.cashier.pipeline.type.PaymentState;
  5 +import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
  6 +import org.slf4j.Logger;
  7 +import org.slf4j.LoggerFactory;
  8 +
  9 +public class CardPaymentHttpClient extends ServiceEndpointSupport {
  10 +
  11 + private static final Logger LOG = LoggerFactory.getLogger(CardPaymentHttpClient.class);
  12 +
  13 + private static final String QUERY_PAYMENT_URI = "/api/cardPayment/queryResult";
  14 +
  15 + private static final String LIST_CARD_URI = "/api/query/list";
  16 +
  17 + private final String baseUri;
  18 +
  19 + public CardPaymentHttpClient(String baseUri) {
  20 + this.baseUri = baseUri;
  21 + }
  22 +
  23 + public OnlinePaymentResponse sendPaymentRequest(OnlinePaymentRequest request) {
  24 + return null;
  25 + }
  26 +
  27 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) {
  28 + return null;
  29 + }
  30 +
  31 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  32 + return null;
  33 + }
  34 +
  35 + private PaymentState paymentState(int stateCode) {
  36 + return switch (stateCode) {
  37 + case 4 -> PaymentState.SUCCESS;
  38 + case 5 -> PaymentState.FAILED;
  39 + default -> PaymentState.PENDING;
  40 + };
  41 + }
  42 +
  43 + private Long convertLong(Object value) {
  44 + return value != null ? ((Number) value).longValue() : null;
  45 + }
  46 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatDirectHttpClient.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.client;
  2 +
  3 +import com.diligrp.cashier.pipeline.domain.*;
  4 +import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage;
  5 +import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig;
  6 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  7 +import com.diligrp.cashier.pipeline.type.PaymentState;
  8 +import com.diligrp.cashier.pipeline.util.WechatConstants;
  9 +import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
  10 +import com.diligrp.cashier.pipeline.util.WechatStateUtils;
  11 +import com.diligrp.cashier.shared.ErrorCode;
  12 +import com.diligrp.cashier.shared.util.AssertUtils;
  13 +import com.diligrp.cashier.shared.util.DateUtils;
  14 +import com.diligrp.cashier.shared.util.JsonUtils;
  15 +import com.fasterxml.jackson.core.type.TypeReference;
  16 +import org.slf4j.Logger;
  17 +import org.slf4j.LoggerFactory;
  18 +
  19 +import java.net.URI;
  20 +import java.net.http.HttpClient;
  21 +import java.net.http.HttpRequest;
  22 +import java.time.Duration;
  23 +import java.time.LocalDateTime;
  24 +import java.util.LinkedHashMap;
  25 +import java.util.Map;
  26 +
  27 +/**
  28 + * 微信支付HTTP客户端-直联模式
  29 + */
  30 +public class WechatDirectHttpClient extends WechatHttpClient {
  31 +
  32 + private static final Logger LOG = LoggerFactory.getLogger(WechatDirectHttpClient.class);
  33 +
  34 + private static final String NATIVE_PREPAY = "/v3/pay/transactions/native";
  35 +
  36 + private static final String JSAPI_PREPAY = "/v3/pay/transactions/jsapi";
  37 + // 查询订单状态
  38 + private static final String TRANSACTION_QUERY = "/v3/pay/transactions/out-trade-no/%s?mchid=%s";
  39 + // 关闭订单
  40 + private static final String TRANSACTION_CLOSE = "/v3/pay/transactions/out-trade-no/%s/close";
  41 + // 退款
  42 + private static final String TRANSACTION_REFUND = "/v3/refund/domestic/refunds";
  43 + // 退款查询
  44 + private static final String REFUND_QUERY = "/v3/refund/domestic/refunds/%s";
  45 +
  46 + // 退款
  47 + public WechatDirectHttpClient(String wechatBaseUri, WechatConfig wechatConfig) {
  48 + super(wechatBaseUri, wechatConfig);
  49 + }
  50 +
  51 + /**
  52 + * Native支付下单, 返回二维码链接
  53 + */
  54 + @Override
  55 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
  56 + String payload = nativePrepayRequest(request, notifyUri);
  57 + // 获取认证信息和签名信息
  58 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST,
  59 + NATIVE_PREPAY, payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  60 + HttpHeader[] headers = new HttpHeader[3];
  61 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  62 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  63 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  64 +
  65 + LOG.info("Sending wechat native prepay request...");
  66 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  67 + HttpResult result = send(wechatBaseUri + NATIVE_PREPAY, headers, payload);
  68 + verifyHttpResult(result);
  69 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  70 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  71 + return NativePrepayResponse.of(request.getPaymentId(), null, (String) response.get("code_url"));
  72 + } else {
  73 + LOG.error("Wechat native prepay failed: {}\n{}", result.statusCode, result.responseText);
  74 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  75 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败: " + message.getMessage());
  76 + }
  77 + }
  78 +
  79 + /**
  80 + * 小程序支付下单
  81 + */
  82 + @Override
  83 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
  84 + String payload = miniProPrepayRequest(request, notifyUri);
  85 + // 获取认证信息和签名信息
  86 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, JSAPI_PREPAY,
  87 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  88 + HttpHeader[] headers = new HttpHeader[3];
  89 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  90 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  91 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  92 +
  93 + LOG.info("Sending wechat jsapi prepay request...");
  94 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  95 + HttpResult result = send(wechatBaseUri + JSAPI_PREPAY, headers, payload);
  96 + verifyHttpResult(result);
  97 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  98 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  99 + return (String) response.get("prepay_id");
  100 + } else {
  101 + LOG.error("Wechat jsapi prepay failed: {}\n{}", result.statusCode, result.responseText);
  102 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  103 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败: " + message.getMessage());
  104 + }
  105 + }
  106 +
  107 + /**
  108 + * 查询微信预支付订单状态
  109 + */
  110 + @Override
  111 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) throws Exception {
  112 + // 获取认证信息和签名信息
  113 + String uri = String.format(TRANSACTION_QUERY, order.getPaymentId(), wechatConfig.getMchId());
  114 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET, uri,
  115 + wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  116 +
  117 + HttpRequest.Builder httpRequest = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + uri))
  118 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  119 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
  120 + .header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  121 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  122 + LOG.info("Sending wechat query merchant order request...");
  123 + LOG.debug("Authorization: {}\n", authorization);
  124 + HttpResult result = execute(httpRequest.GET().build());
  125 + verifyHttpResult(result);
  126 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回(关闭的订单trade_state = CLOSED)
  127 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  128 + LocalDateTime when = DateUtils.parseDateTime((String)response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  129 + @SuppressWarnings("unchecked")
  130 + Map<String, Object> payer = (Map<String, Object>) response.get("payer");
  131 + String openId = payer == null ? null : (String) payer.get("openid");
  132 + PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state"));
  133 + return OnlinePaymentResponse.of((String) response.get("out_trade_no"), (String) response.get("transaction_id"),
  134 + openId, when, state.getCode(), (String) response.get("trade_state_desc"));
  135 + } else {
  136 + LOG.info("Wechat query transaction status failed: {}", result.statusCode);
  137 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  138 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信订单查询失败: " + message.getMessage());
  139 + }
  140 + }
  141 +
  142 + /**
  143 + * 关闭微信订单 - 关闭订单不会回调通知
  144 + * 返回码204标识关闭成功, 其他错误码表明关闭异常失败;
  145 + * 已支付完成的订单调用关闭返回400, 订单不存在返回404
  146 + */
  147 + @Override
  148 + public void closePrepayOrder(OnlinePrepayOrder request) throws Exception {
  149 + // 获取认证信息和签名信息
  150 + String uri = String.format(TRANSACTION_CLOSE, request.getPaymentId());
  151 + Map<String, Object> params = new LinkedHashMap<>();
  152 + params.put("mchid", wechatConfig.getMchId());
  153 + String payload = JsonUtils.toJsonString(params);
  154 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, uri,
  155 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  156 +
  157 + HttpHeader[] headers = new HttpHeader[3];
  158 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  159 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  160 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  161 + LOG.info("Sending close wechat prepay order request...");
  162 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  163 + HttpResult result = send(wechatBaseUri + uri, headers, payload);
  164 + verifyHttpResult(result);
  165 + LOG.info("Close wechat prepay order statusCode: {}", result.statusCode);
  166 + if (result.statusCode != 200 && result.statusCode != 204) {
  167 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  168 + throw new PaymentPipelineException(ErrorCode.INVALID_OBJECT_STATE, "关闭微信订单失败: " + message.getMessage());
  169 + }
  170 + }
  171 +
  172 + /**
  173 + * 申请微信支付退款
  174 + */
  175 + @Override
  176 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request, String notifyUri) throws Exception {
  177 + String payload = refundRequest(request, notifyUri);
  178 + // 获取认证信息和签名信息
  179 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST,
  180 + TRANSACTION_REFUND, payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  181 + HttpHeader[] headers = new HttpHeader[3];
  182 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  183 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  184 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  185 +
  186 + LOG.info("Sending wechat payment refund request...");
  187 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  188 + HttpResult result = send(wechatBaseUri + TRANSACTION_REFUND, headers, payload);
  189 + verifyHttpResult(result);
  190 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  191 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  192 + LocalDateTime when = DateUtils.parseDateTime((String) response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  193 + PaymentState state = WechatStateUtils.getRefundState((String) response.get("status"));
  194 + return OnlineRefundResponse.of((String) response.get("out_refund_no"), (String) response.get("refund_id"),
  195 + when, state.getCode(), (String) response.get("status"));
  196 + } else {
  197 + LOG.info("send wechat refund failed: {}", result.statusCode);
  198 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  199 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信退款失败: " + message.getMessage());
  200 + }
  201 + }
  202 +
  203 + /**
  204 + * 微信支付退款查询
  205 + */
  206 + @Override
  207 + public OnlineRefundResponse queryRefundOrder(OnlineRefundOrder request) throws Exception {
  208 + String uri = String.format(REFUND_QUERY, request.getRefundId());
  209 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET,
  210 + uri, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  211 +
  212 + HttpRequest.Builder httpRequest = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + uri))
  213 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  214 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
  215 + .header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  216 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  217 + LOG.info("Sending wechat refund query request...");
  218 + LOG.debug("Authorization: {}\n", authorization);
  219 + HttpResult result = execute(httpRequest.GET().build());
  220 + verifyHttpResult(result);
  221 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  222 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  223 + LocalDateTime when = DateUtils.parseDateTime((String) response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  224 + PaymentState state = WechatStateUtils.getRefundState((String) response.get("status"));
  225 + return OnlineRefundResponse.of((String) response.get("out_refund_no"), (String) response.get("refund_id"),
  226 + when, state.getCode(), (String) response.get("user_received_account"));
  227 + } else if (result.statusCode == 404) {
  228 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "发起微信退款查询失败: 退款单不存在");
  229 + } else {
  230 + LOG.info("Wechat refund query failed: {}", result.statusCode);
  231 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  232 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "微信退款查询失败: " + message.getMessage());
  233 + }
  234 + }
  235 +
  236 + private String nativePrepayRequest(NativePrepayRequest request, String notifyUri) {
  237 + Map<String, Object> params = new LinkedHashMap<>();
  238 + params.put("appid", wechatConfig.getAppId());
  239 + params.put("mchid", wechatConfig.getMchId());
  240 + params.put("description", request.getGoods());
  241 + params.put("out_trade_no", request.getPaymentId());
  242 + params.put("time_expire", DateUtils.formatDateTime(request.getWhen().plusMinutes(WechatConstants.PAY_DURATION_MINS),
  243 + WechatConstants.RFC3339_FORMAT));
  244 + params.put("notify_url", notifyUri);
  245 + Map<String, Object> amount = new LinkedHashMap<>();
  246 + amount.put("total", request.getAmount());
  247 + amount.put("currency", "CNY");
  248 + params.put("amount", amount);
  249 +
  250 + return JsonUtils.toJsonString(params);
  251 + }
  252 +
  253 + private String miniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) {
  254 + AssertUtils.notEmpty(request.getOpenId(), "参数错误: 缺失用户标识openId");
  255 + Map<String, Object> params = new LinkedHashMap<>();
  256 + params.put("appid", wechatConfig.getAppId());
  257 + params.put("mchid", wechatConfig.getMchId());
  258 + params.put("description", request.getGoods());
  259 + params.put("out_trade_no", request.getPaymentId());
  260 + params.put("time_expire", DateUtils.formatDateTime(request.getWhen().plusMinutes(WechatConstants.PAY_DURATION_MINS),
  261 + WechatConstants.RFC3339_FORMAT));
  262 + params.put("notify_url", notifyUri);
  263 + Map<String, Object> amount = new LinkedHashMap<>();
  264 + amount.put("total", request.getAmount());
  265 + amount.put("currency", "CNY");
  266 + params.put("amount", amount);
  267 + Map<String, Object> payer = new LinkedHashMap<>();
  268 + payer.put("openid", request.getOpenId());
  269 + params.put("payer", payer);
  270 + return JsonUtils.toJsonString(params);
  271 + }
  272 +
  273 + private String refundRequest(OnlineRefundRequest request, String notifyUri) {
  274 + Map<String, Object> params = new LinkedHashMap<>();
  275 + params.put("out_trade_no", request.getPaymentId());
  276 + params.put("out_refund_no", request.getRefundId());
  277 + params.put("reason", request.getDescription());
  278 + params.put("notify_url", notifyUri);
  279 + Map<String, Object> amount = new LinkedHashMap<>();
  280 + amount.put("refund", request.getAmount());
  281 + amount.put("total", request.getMaxAmount());
  282 + amount.put("currency", "CNY");
  283 + params.put("amount", amount);
  284 + return JsonUtils.toJsonString(params);
  285 + }
  286 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatHttpClient.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.client;
  2 +
  3 +import com.diligrp.cashier.pipeline.domain.*;
  4 +import com.diligrp.cashier.pipeline.domain.wechat.*;
  5 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  6 +import com.diligrp.cashier.pipeline.util.WechatConstants;
  7 +import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
  8 +import com.diligrp.cashier.shared.ErrorCode;
  9 +import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
  10 +import com.diligrp.cashier.shared.util.DateUtils;
  11 +import com.diligrp.cashier.shared.util.JsonUtils;
  12 +import com.diligrp.cashier.shared.util.ObjectUtils;
  13 +import com.fasterxml.jackson.core.type.TypeReference;
  14 +import org.slf4j.Logger;
  15 +import org.slf4j.LoggerFactory;
  16 +
  17 +import java.io.ByteArrayInputStream;
  18 +import java.net.URI;
  19 +import java.net.http.HttpClient;
  20 +import java.net.http.HttpRequest;
  21 +import java.nio.charset.StandardCharsets;
  22 +import java.security.cert.Certificate;
  23 +import java.security.cert.CertificateFactory;
  24 +import java.time.Duration;
  25 +import java.util.*;
  26 +
  27 +/**
  28 + * 微信支付基础功能HTTP客户端
  29 + */
  30 +public class WechatHttpClient extends ServiceEndpointSupport {
  31 + private static final Logger LOG = LoggerFactory.getLogger(WechatHttpClient.class);
  32 +
  33 + // 获取微信平台证书列表
  34 + private static final String LIST_CERTIFICATE = "/v3/certificates";
  35 +
  36 + private static final String WECHAT_BASE_URL = "https://api.weixin.qq.com";
  37 +
  38 + private static final String CODE_TO_SESSION = "/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
  39 +
  40 + private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";
  41 +
  42 + private static final String UPLOAD_SHIPPING_URL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
  43 +
  44 + protected String wechatBaseUri;
  45 +
  46 + protected WechatConfig wechatConfig;
  47 +
  48 + public WechatHttpClient(String wechatBaseUri, WechatConfig wechatConfig) {
  49 + this.wechatBaseUri = wechatBaseUri;
  50 + this.wechatConfig = wechatConfig;
  51 + }
  52 +
  53 + /**
  54 + * Native预支付下单, 返回二维码链接
  55 + */
  56 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
  57 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持Native支付");
  58 + }
  59 +
  60 + /**
  61 + * 小程序支付预支付下单
  62 + */
  63 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
  64 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持JsApi支付");
  65 + }
  66 +
  67 + /**
  68 + * 查询微信预支付订单状态
  69 + */
  70 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) throws Exception {
  71 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
  72 + }
  73 +
  74 + /**
  75 + * 关闭预支付订单
  76 + */
  77 + public void closePrepayOrder(OnlinePrepayOrder request) throws Exception {
  78 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
  79 + }
  80 +
  81 + /**
  82 + * 支付退款申请
  83 + */
  84 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request, String notifyUri) throws Exception {
  85 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
  86 + }
  87 +
  88 + /**
  89 + * 查询退款状态
  90 + */
  91 + public OnlineRefundResponse queryRefundOrder(OnlineRefundOrder request) throws Exception {
  92 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
  93 + }
  94 +
  95 + /**
  96 + * 刷新微信支付平台数字证书(公钥)
  97 + * 微信支付平台证书 - 当旧证书即将过期时,微信将新老证书将并行使用
  98 + * <a href="https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/wechatpay5_1.shtml">...</a>
  99 + * <a href="https://pay.weixin.qq.com/docs/merchant/apis/platform-certificate/api-v3-get-certificates/get.html">...</a>
  100 + */
  101 + public void refreshCertificates() throws Exception {
  102 + // 获取认证信息和签名信息
  103 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET,
  104 + LIST_CERTIFICATE, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  105 +
  106 + HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + LIST_CERTIFICATE))
  107 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  108 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
  109 + .header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  110 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  111 + LOG.info("Sending wechat list certificate request...");
  112 + LOG.debug("Authorization: {}\n", authorization);
  113 + ServiceEndpointSupport.HttpResult result = execute(request.GET().build());
  114 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回; 返回成功时再进行数据验签,对数据无安全隐患
  115 + // 获取验签使用的微信平台证书序列号, 本地获取平台证书到则进行验签, 如获取不到则说明旧证书即将过期,新老证书正并行使用
  116 + String serialNo = result.header(WechatConstants.HEADER_SERIAL_NO);
  117 + String timestamp = result.header(WechatConstants.HEADER_TIMESTAMP);
  118 + String nonce = result.header(WechatConstants.HEADER_NONCE);
  119 + String sign = result.header(WechatConstants.HEADER_SIGNATURE);
  120 + LOG.debug("\n------Wechat Platform Data Verify------\nWechatpay-Serial={}\nWechatpay-Timestamp={}\n" +
  121 + "Wechatpay-Nonce={}\nWechatpay-Signature={}\n--------------------------------------", serialNo, timestamp, nonce, sign);
  122 + LOG.debug(result.responseText);
  123 + Optional<WechatCertificate> certificate = wechatConfig.getCertificate(serialNo);
  124 + if (certificate.isPresent()) {
  125 + boolean success = WechatSignatureUtils.verify(result.responseText, timestamp, nonce, sign,
  126 + certificate.get().getPublicKey());
  127 + if (!success) {
  128 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "获取微信支付平台证书验签失败");
  129 + }
  130 + } else {
  131 + LOG.warn("Old certificate is about to expire, new one is in use");
  132 + }
  133 +
  134 + CertificateResponse response = JsonUtils.fromJsonString(result.responseText, CertificateResponse.class);
  135 + List<CertificateResponse.Certificate> certificates = response.getData();
  136 + if (ObjectUtils.isNotEmpty(certificates)) {
  137 + for (CertificateResponse.Certificate cert : certificates) {
  138 + // 利用ApiV3Key解密平台公钥
  139 + String certStr = WechatSignatureUtils.decrypt(cert.getEncrypt_certificate().getCiphertext(),
  140 + cert.getEncrypt_certificate().getNonce(), cert.getEncrypt_certificate().getAssociated_data(),
  141 + wechatConfig.getApiV3Key());
  142 + ByteArrayInputStream is = new ByteArrayInputStream(certStr.getBytes(StandardCharsets.UTF_8));
  143 + Certificate x509Cert = CertificateFactory.getInstance("X509").generateCertificate(is);
  144 + wechatConfig.putCertificate(WechatCertificate.of(cert.getSerial_no(), x509Cert.getPublicKey()));
  145 + LOG.info("{} certificate added", cert.getSerial_no());
  146 + }
  147 + }
  148 + LOG.info("Refresh certificate repository success");
  149 + } else {
  150 + LOG.info("Refresh certificate repository failed: {}", result.statusCode);
  151 + }
  152 + }
  153 +
  154 + public String loginAuthorization(String code) {
  155 + return loginAuthorization(wechatConfig.getAppId(), wechatConfig.getAppSecret(), code);
  156 + }
  157 +
  158 + /**
  159 + * 小程序登录授权,根据wx.login获得的临时登录凭证code,获取登录信息openId等
  160 + */
  161 + public String loginAuthorization(String appId, String appSecret, String code) {
  162 + String uri = String.format(CODE_TO_SESSION, appId, appSecret, code);
  163 + HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(WECHAT_BASE_URL + uri))
  164 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  165 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  166 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  167 + LOG.info("Requesting MiniPro login authorization info: {}\n{}", code, uri);
  168 + HttpResult result = execute(request.GET().build());
  169 + if (result.statusCode == 200) {
  170 + LOG.debug("MiniPro login authorization info response\n{}", result.responseText);
  171 + AuthorizationSession session = JsonUtils.fromJsonString(result.responseText, AuthorizationSession.class);
  172 + if (session.getErrcode() != null && session.getErrcode() != 0) {
  173 + LOG.error("Request MiniPro login authorization info failed: {}", session.getErrmsg());
  174 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取小程序登录授权信息失败: " + session.getErrmsg());
  175 + }
  176 + return session.getOpenid();
  177 + } else {
  178 + LOG.error("Request MiniPro login authorization info failed: {}", result.statusCode);
  179 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取小程序登录授权信息失败");
  180 + }
  181 + }
  182 +
  183 + public WechatAccessToken getAccessToken() {
  184 + return getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
  185 + }
  186 +
  187 + /**
  188 + * 获取小程序接口调用凭证:Token有效期内重复调用该接口不会更新Token,有效期5分钟前更新Token,新旧Token并行5分钟;该接口调用频率限制为 1万次每分钟,每天限制调用 50万次;
  189 + * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html">...</a>
  190 + */
  191 + public WechatAccessToken getAccessToken(String appId, String appSecret) {
  192 + Map<String, Object> params = new HashMap<>();
  193 + params.put("grant_type", "client_credential");
  194 + params.put("appid", appId);
  195 + params.put("secret", appSecret);
  196 + params.put("force_refresh", Boolean.FALSE);
  197 + String payload = JsonUtils.toJsonString(params);
  198 + LOG.debug("Request MiniPro Api access token: {}", payload);
  199 + HttpResult result = send(ACCESS_TOKEN_URL, payload);
  200 + if (result.statusCode == 200) {
  201 + LOG.debug("MiniPro Api access token response: {}", result.responseText);
  202 + Map<String, Object> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  203 + return WechatAccessToken.of((String)data.get("access_token"), (Integer) data.get("expires_in"));
  204 + } else {
  205 + LOG.error("Request MiniPro Api access token failed: {}", result.statusCode);
  206 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取微信接口调用凭证失败");
  207 + }
  208 + }
  209 +
  210 + /**
  211 + * 微信发货信息录入接口
  212 + * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html">...</a>
  213 + */
  214 + public void sendUploadShippingRequest(UploadShippingRequest request, String accessToken) {
  215 + String uri = String.format("%s?access_token=%s", UPLOAD_SHIPPING_URL, accessToken);
  216 + Map<String, Object> orderKey = new HashMap<>();
  217 + orderKey.put("order_number_type", 2);
  218 + orderKey.put("transaction_id", request.getTransactionId());
  219 +
  220 + Map<String, Object> shipping = new HashMap<>();
  221 + shipping.put("item_desc", request.getGoods());
  222 + List<Map<String, Object>> shippingList = new ArrayList<>();
  223 + shippingList.add(shipping);
  224 +
  225 + Map<String, Object> payer = new HashMap<>();
  226 + payer.put("openid", request.getOpenId());
  227 +
  228 + Map<String, Object> params = new HashMap<>();
  229 + params.put("order_key", orderKey);
  230 + params.put("logistics_type", request.getLogisticsType());
  231 + params.put("delivery_mode", 1);
  232 + params.put("shipping_list", shippingList);
  233 + params.put("upload_time", DateUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ssZ"));
  234 + params.put("payer", payer);
  235 +
  236 + String payload = JsonUtils.toJsonString(params);
  237 + LOG.debug("Request wechat upload shipping: {}", payload);
  238 + HttpResult result = send(uri, payload);
  239 + if (result.statusCode == 200) {
  240 + LOG.debug("Wechat upload shipping response: {}", result.responseText);
  241 + Map<String, Object> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  242 + int errorCode = (Integer) data.get("errcode");
  243 + if (errorCode != 0) {
  244 + String errorMessage = (String)data.get("errmsg");
  245 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "微信发货信息录入失败: " + errorMessage);
  246 + }
  247 + } else {
  248 + LOG.error("Request wechat upload shipping failed: {}", result.statusCode);
  249 + throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用微信发货信息录入接口失败");
  250 + }
  251 + }
  252 +
  253 + /**
  254 + * 用于微信支付结果通知数据验签
  255 + */
  256 + public boolean dataVerify(String serialNo, String timestamp, String nonce, String sign, String payload) throws Exception {
  257 + LOG.debug("\n------Wechat Platform Data Verify------\nWechatpay-Serial={}\nWechatpay-Timestamp={}\n"
  258 + + "Wechatpay-Nonce={}\nWechatpay-Signature={}\n{}\n--------------------------------------",
  259 + serialNo, timestamp, nonce, sign, payload == null ? "" : payload);
  260 + Optional<WechatCertificate> certOpt = wechatConfig.getCertificate(serialNo);
  261 + if (certOpt.isEmpty()) { // 找不到证书则重新向微信平台请求新证书(旧证书即将过期时出现)
  262 + refreshCertificates();
  263 + }
  264 +
  265 + WechatCertificate certificate = wechatConfig.getCertificate(serialNo).orElseThrow(() ->
  266 + new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "找不到微信平台数字证书"));
  267 + return WechatSignatureUtils.verify(payload, timestamp, nonce, sign, certificate.getPublicKey());
  268 + }
  269 +
  270 + protected void verifyHttpResult(HttpResult result) throws Exception {
  271 + if (result.statusCode == 401) {
  272 + return; // 微信签名失败,则不进行验签操作
  273 + }
  274 + // 获取验签使用的微信平台证书序列号, 本地获取平台证书到则进行验签, 如获取不到则说明旧证书即将过期,新老证书正并行使用
  275 + String serialNo = result.header(WechatConstants.HEADER_SERIAL_NO);
  276 + String timestamp = result.header(WechatConstants.HEADER_TIMESTAMP);
  277 + String nonce = result.header(WechatConstants.HEADER_NONCE);
  278 + String sign = result.header(WechatConstants.HEADER_SIGNATURE);
  279 + if (!dataVerify(serialNo, timestamp, nonce, sign, result.responseText)) {
  280 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "微信数据验签失败");
  281 + }
  282 + }
  283 +
  284 + public WechatConfig getWechatConfig() {
  285 + return wechatConfig;
  286 + }
  287 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatPartnerHttpClient.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.client;
  2 +
  3 +import com.diligrp.cashier.pipeline.Constants;
  4 +import com.diligrp.cashier.pipeline.domain.*;
  5 +import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage;
  6 +import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig;
  7 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  8 +import com.diligrp.cashier.pipeline.type.PaymentState;
  9 +import com.diligrp.cashier.pipeline.util.WechatConstants;
  10 +import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
  11 +import com.diligrp.cashier.pipeline.util.WechatStateUtils;
  12 +import com.diligrp.cashier.shared.ErrorCode;
  13 +import com.diligrp.cashier.shared.util.AssertUtils;
  14 +import com.diligrp.cashier.shared.util.DateUtils;
  15 +import com.diligrp.cashier.shared.util.JsonUtils;
  16 +import com.fasterxml.jackson.core.type.TypeReference;
  17 +import org.slf4j.Logger;
  18 +import org.slf4j.LoggerFactory;
  19 +
  20 +import java.net.URI;
  21 +import java.net.http.HttpClient;
  22 +import java.net.http.HttpRequest;
  23 +import java.time.Duration;
  24 +import java.time.LocalDateTime;
  25 +import java.util.LinkedHashMap;
  26 +import java.util.Map;
  27 +
  28 +/**
  29 + * 微信支付HTTP客户端-服务商模式
  30 + */
  31 +public class WechatPartnerHttpClient extends WechatHttpClient {
  32 +
  33 + private static final Logger LOG = LoggerFactory.getLogger(WechatPartnerHttpClient.class);
  34 +
  35 + private static final String NATIVE_PREPAY = "/v3/pay/partner/transactions/native";
  36 +
  37 + private static final String JSAPI_PREPAY = "/v3/pay/partner/transactions/jsapi";
  38 +
  39 + // 查询订单状态
  40 + private static final String TRANSACTION_QUERY = "/v3/pay/partner/transactions/out-trade-no/%s?sp_mchid=%s&sub_mchid=%s";
  41 + // 关闭订单
  42 + private static final String TRANSACTION_CLOSE = "/v3/pay/partner/transactions/out-trade-no/%s/close";
  43 + // 退款
  44 + private static final String NATIVE_REFUND = "/v3/refund/domestic/refunds";
  45 + // 退款查询
  46 + private static final String NATIVE_REFUND_QUERY = "/v3/refund/domestic/refunds/%s?sub_mchid=%s";
  47 +
  48 + public WechatPartnerHttpClient(String wechatBaseUri, WechatConfig wechatConfig) {
  49 + super(wechatBaseUri, wechatConfig);
  50 + }
  51 +
  52 + /**
  53 + * Native支付下单, 返回二维码链接
  54 + */
  55 + @Override
  56 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
  57 + String payload = nativePrepayRequest(request, notifyUri);
  58 + // 获取认证信息和签名信息
  59 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, NATIVE_PREPAY,
  60 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  61 + HttpHeader[] headers = new HttpHeader[3];
  62 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  63 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  64 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  65 +
  66 + LOG.info("Sending wechat native prepay request...");
  67 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  68 + HttpResult result = send(wechatBaseUri + NATIVE_PREPAY, headers, payload);
  69 + verifyHttpResult(result);
  70 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  71 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  72 + return NativePrepayResponse.of(request.getPaymentId(), null, (String) response.get("code_url"));
  73 + } else {
  74 + LOG.error("Wechat native prepay failed: {}\n{}", result.statusCode, result.responseText);
  75 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  76 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败: " + message.getMessage());
  77 + }
  78 + }
  79 +
  80 + /**
  81 + * 小程序支付下单, 返回prepay_id
  82 + */
  83 + @Override
  84 + public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
  85 + String payload = miniProPrepayRequest(request, notifyUri);
  86 + // 获取认证信息和签名信息
  87 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, JSAPI_PREPAY,
  88 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  89 + HttpHeader[] headers = new HttpHeader[3];
  90 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  91 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  92 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  93 +
  94 + LOG.info("Sending wechat JsApI prepay request...");
  95 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  96 + HttpResult result = send(wechatBaseUri + JSAPI_PREPAY, headers, payload);
  97 + verifyHttpResult(result);
  98 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  99 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  100 + return (String) response.get("prepay_id");
  101 + } else {
  102 + LOG.error("Wechat JsApI prepay failed: {}\n{}", result.statusCode, result.responseText);
  103 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  104 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败: " + message.getMessage());
  105 + }
  106 + }
  107 +
  108 + /**
  109 + * 查询微信预支付订单状态
  110 + */
  111 + @Override
  112 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) throws Exception {
  113 + String subMchId = order.getString(Constants.PARAM_MCH_ID);
  114 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  115 +
  116 + // 获取认证信息和签名信息
  117 + String uri = String.format(TRANSACTION_QUERY, order.getPaymentId(), wechatConfig.getMchId(), subMchId);
  118 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET, uri,
  119 + wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  120 +
  121 + HttpRequest.Builder httpRequest = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + uri))
  122 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  123 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
  124 + .header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  125 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  126 + LOG.info("Sending wechat query merchant order request...");
  127 + LOG.debug("Authorization: {}\n", authorization);
  128 + HttpResult result = execute(httpRequest.GET().build());
  129 + verifyHttpResult(result);
  130 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回(关闭的订单trade_state = CLOSED)
  131 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  132 + LocalDateTime when = DateUtils.parseDateTime((String) response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  133 + @SuppressWarnings("unchecked")
  134 + Map<String, Object> payer = (Map<String, Object>) response.get("payer");
  135 + String openId = payer == null ? null : (String) payer.get("sp_openid"); // 获取服务商APPID下的openId,而非子商户APPID下的openId
  136 + PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state"));
  137 + return OnlinePaymentResponse.of((String) response.get("out_trade_no"), (String) response.get("transaction_id"),
  138 + openId, when, state.getCode(), (String) response.get("trade_state_desc"));
  139 + } else {
  140 + LOG.info("Wechat query transaction status failed: {}", result.statusCode);
  141 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  142 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信订单查询失败: " + message.getMessage());
  143 + }
  144 + }
  145 +
  146 + /**
  147 + * 关闭微信订单 - 关闭订单不会回调通知
  148 + * 返回码204标识关闭成功, 其他错误码表明关闭异常失败;
  149 + * 已支付完成的订单调用关闭返回400, 订单不存在返回404
  150 + */
  151 + @Override
  152 + public void closePrepayOrder(OnlinePrepayOrder request) throws Exception {
  153 + String subMchId = request.getString(Constants.PARAM_MCH_ID);
  154 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  155 +
  156 + // 获取认证信息和签名信息
  157 + String uri = String.format(TRANSACTION_CLOSE, request.getPaymentId());
  158 + Map<String, Object> params = new LinkedHashMap<>();
  159 + params.put("sp_mchid", wechatConfig.getMchId());
  160 + params.put("sub_mchid", subMchId);
  161 + String payload = JsonUtils.toJsonString(params);
  162 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, uri,
  163 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  164 +
  165 + HttpHeader[] headers = new HttpHeader[3];
  166 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  167 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  168 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  169 + LOG.info("Sending close wechat prepay order request...");
  170 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  171 + HttpResult result = send(wechatBaseUri + uri, headers, payload);
  172 + verifyHttpResult(result);
  173 + LOG.info("Close wechat prepay order statusCode: {}", result.statusCode);
  174 + if (result.statusCode != 200 && result.statusCode != 204) {
  175 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  176 + throw new PaymentPipelineException(ErrorCode.INVALID_OBJECT_STATE, "关闭微信订单失败: " + message.getMessage());
  177 + }
  178 + }
  179 +
  180 + /**
  181 + * 申请微信支付退款
  182 + */
  183 + @Override
  184 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request, String notifyUri) throws Exception {
  185 + String payload = refundRequest(request, notifyUri);
  186 + // 获取认证信息和签名信息
  187 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_POST, NATIVE_REFUND,
  188 + payload, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  189 + HttpHeader[] headers = new HttpHeader[3];
  190 + headers[0] = HttpHeader.create(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  191 + headers[1] = HttpHeader.create(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON);
  192 + headers[2] = HttpHeader.create(WechatConstants.HEADER_AUTHORIZATION, authorization);
  193 +
  194 + LOG.info("Sending wechat payment refund request...");
  195 + LOG.debug("Authorization: {}\n{}", authorization, payload);
  196 + HttpResult result = send(wechatBaseUri + NATIVE_REFUND, headers, payload);
  197 + verifyHttpResult(result);
  198 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  199 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  200 + LocalDateTime when = DateUtils.parseDateTime((String) response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  201 + PaymentState state = WechatStateUtils.getRefundState((String) response.get("status"));
  202 + return OnlineRefundResponse.of((String) response.get("out_refund_no"), (String) response.get("refund_id"),
  203 + when, state.getCode(), (String) response.get("status"));
  204 + } else {
  205 + LOG.info("send wechat payment refund failed: {}", result.statusCode);
  206 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  207 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信退款失败: " + message.getMessage());
  208 + }
  209 + }
  210 +
  211 + /**
  212 + * 微信支付退款查询
  213 + */
  214 + @Override
  215 + public OnlineRefundResponse queryRefundOrder(OnlineRefundOrder request) throws Exception {
  216 + String subMchId = request.getString(Constants.PARAM_MCH_ID);
  217 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  218 +
  219 + String uri = String.format(NATIVE_REFUND_QUERY, request.getRefundId(), subMchId);
  220 + String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET, uri,
  221 + wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
  222 +
  223 + HttpRequest.Builder httpRequest = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + uri))
  224 + .version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  225 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
  226 + .header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
  227 + .header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
  228 + LOG.info("Sending wechat refund query request...");
  229 + LOG.debug("Authorization: {}\n", authorization);
  230 + HttpResult result = execute(httpRequest.GET().build());
  231 + verifyHttpResult(result);
  232 + if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回
  233 + Map<String, Object> response = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
  234 + LocalDateTime when = DateUtils.parseDateTime((String) response.get("success_time"), WechatConstants.RFC3339_FORMAT);
  235 + PaymentState state = WechatStateUtils.getRefundState((String) response.get("status"));
  236 + return OnlineRefundResponse.of((String) response.get("out_refund_no"), (String) response.get("refund_id"),
  237 + when, state.getCode(), (String) response.get("user_received_account"));
  238 + } else if (result.statusCode == 404) {
  239 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "发起微信退款查询失败: 退款单不存在");
  240 + } else {
  241 + LOG.info("Wechat query mch refund failed: {}", result.statusCode);
  242 + ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class);
  243 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "微信退款查询失败: " + message.getMessage());
  244 + }
  245 + }
  246 +
  247 + private String nativePrepayRequest(NativePrepayRequest request, String notifyUri) {
  248 + String subMchId = request.getString(Constants.PARAM_MCH_ID);
  249 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  250 +
  251 + Map<String, Object> params = new LinkedHashMap<>();
  252 + params.put("sp_appid", wechatConfig.getAppId());
  253 + params.put("sp_mchid", wechatConfig.getMchId());
  254 +// params.put("sub_appid", subMerchant.getAppId());
  255 + params.put("sub_mchid", subMchId);
  256 + params.put("description", request.getGoods());
  257 + params.put("out_trade_no", request.getPaymentId());
  258 + params.put("time_expire", DateUtils.formatDateTime(request.getWhen().plusMinutes(WechatConstants.PAY_DURATION_MINS),
  259 + WechatConstants.RFC3339_FORMAT));
  260 + params.put("notify_url", notifyUri);
  261 + Map<String, Object> amount = new LinkedHashMap<>();
  262 + amount.put("total", request.getAmount());
  263 + amount.put("currency", "CNY");
  264 + params.put("amount", amount);
  265 + return JsonUtils.toJsonString(params);
  266 + }
  267 +
  268 + private String miniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) {
  269 + String subMchId = request.getString(Constants.PARAM_MCH_ID);
  270 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  271 + AssertUtils.notEmpty(request.getOpenId(), "参数错误: 缺失用户标识openId");
  272 +
  273 + Map<String, Object> params = new LinkedHashMap<>();
  274 + params.put("sp_appid", wechatConfig.getAppId());
  275 + params.put("sp_mchid", wechatConfig.getMchId());
  276 +// params.put("sub_appid", subMerchant.getAppId());
  277 + params.put("sub_mchid", subMchId);
  278 + params.put("description", request.getGoods());
  279 + params.put("out_trade_no", request.getPaymentId());
  280 + params.put("time_expire", DateUtils.formatDateTime(request.getWhen().plusMinutes(WechatConstants.PAY_DURATION_MINS),
  281 + WechatConstants.RFC3339_FORMAT));
  282 + params.put("notify_url", notifyUri);
  283 + Map<String, Object> amount = new LinkedHashMap<>();
  284 + amount.put("total", request.getAmount());
  285 + amount.put("currency", "CNY");
  286 + params.put("amount", amount);
  287 + Map<String, Object> payer = new LinkedHashMap<>();
  288 + // 传入微信用户在服务商APPID下的openid,如果是子商户APPID下的openid,则应该使用sub_appid
  289 + payer.put("sp_openid", request.getOpenId());
  290 + params.put("payer", payer);
  291 + return JsonUtils.toJsonString(params);
  292 + }
  293 +
  294 + private String refundRequest(OnlineRefundRequest request, String notifyUri) {
  295 + String subMchId = request.getString(Constants.PARAM_MCH_ID);
  296 + AssertUtils.notEmpty(subMchId, "参数错误: 服务商模式下缺失子商户号");
  297 +
  298 + Map<String, Object> params = new LinkedHashMap<>();
  299 + params.put("sub_mchid", subMchId);
  300 + params.put("out_trade_no", request.getPaymentId());
  301 + params.put("out_refund_no", request.getRefundId());
  302 + params.put("reason", request.getDescription());
  303 + params.put("notify_url", notifyUri);
  304 + Map<String, Object> amount = new LinkedHashMap<>();
  305 + amount.put("refund", request.getAmount());
  306 + amount.put("total", request.getMaxAmount());
  307 + amount.put("currency", "CNY");
  308 + params.put("amount", amount);
  309 +
  310 + return JsonUtils.toJsonString(params);
  311 + }
  312 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/CardPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.client.CardPaymentHttpClient;
  4 +import com.diligrp.cashier.pipeline.domain.*;
  5 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  6 +import com.diligrp.cashier.shared.ErrorCode;
  7 +import org.slf4j.Logger;
  8 +import org.slf4j.LoggerFactory;
  9 +
  10 +/**
  11 + * 园区卡支付通道抽象模型
  12 + */
  13 +public abstract class CardPipeline extends PaymentPipeline<CardPipeline.CardParams> {
  14 +
  15 + private static final Logger LOG = LoggerFactory.getLogger(CardPipeline.class);
  16 +
  17 + private final ScanTimeStrategy strategy;
  18 +
  19 + private final CardPaymentHttpClient client;
  20 +
  21 + public CardPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  22 + super(mchId, pipelineId, name, uri, params);
  23 + this.strategy = new DefaultTimeStrategy();
  24 + this.client = new CardPaymentHttpClient(uri);
  25 + }
  26 +
  27 + /**
  28 + * 发送预支付订单的支付申请 - 目前针对园区卡扫码支付
  29 + */
  30 + public OnlinePaymentResponse sendPaymentRequest(OnlinePaymentRequest request) {
  31 + try {
  32 + return client.sendPaymentRequest(request);
  33 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  34 + throw pse;
  35 + } catch (Exception ex) {
  36 + LOG.error("Send card payment request exception", ex);
  37 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起地利支付申请失败");
  38 + }
  39 + }
  40 +
  41 + /**
  42 + * 查询支付订单状态
  43 + */
  44 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) {
  45 + try {
  46 + return client.queryPrepayResponse(order);
  47 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  48 + throw pse;
  49 + } catch (Exception ex) {
  50 + LOG.error("Query card payment state request exception", ex);
  51 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询地利支付状态失败");
  52 + }
  53 + }
  54 +
  55 + /**
  56 + * 关闭预支付订单
  57 + */
  58 + public void closePrepayOrder(OnlinePrepayOrder request) {
  59 + // 不调用远程关单接口
  60 + }
  61 +
  62 + /**
  63 + * 退款申请
  64 + */
  65 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  66 + try {
  67 + return client.sendRefundRequest(request);
  68 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  69 + throw pse;
  70 + } catch (Exception ex) {
  71 + LOG.error("Send card refund request exception", ex);
  72 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信退款失败");
  73 + }
  74 + }
  75 +
  76 + public CardPaymentHttpClient getClient() {
  77 + return this.client;
  78 + }
  79 +
  80 + public ScanTimeStrategy getTimeStrategy() {
  81 + return strategy;
  82 + }
  83 +
  84 + @Override
  85 + public Class<CardParams> paramClass() {
  86 + return CardParams.class;
  87 + }
  88 +
  89 + public static class CardParams extends PipelineParams {
  90 + public CardParams(String params) {
  91 + super(params);
  92 + }
  93 + }
  94 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/DefaultTimeStrategy.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import static com.diligrp.cashier.pipeline.Constants.*;
  4 +
  5 +public class DefaultTimeStrategy implements ScanTimeStrategy {
  6 + // 预支付订单扫描时间策略
  7 + private long[] prepayTimeArray = { 10 * ONE_MINUTE };
  8 +
  9 + // 直接支付订单扫描时间策略
  10 + private long[] paymentTimeArray = {5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 5 * ONE_SECOND, 10 * ONE_SECOND, 15 * ONE_SECOND};
  11 +
  12 + // 退款订单扫描时间策略
  13 + private long[] refundTimeArray = { 10 * ONE_MINUTE };
  14 +
  15 + @Override
  16 + public long nextPrepayScanTime(int times) {
  17 + if (times - 1 < prepayTimeArray.length) {
  18 + return prepayTimeArray[times - 1];
  19 + }
  20 + return -1;
  21 + }
  22 +
  23 + @Override
  24 + public long nextPaymentScanTime(int times) {
  25 + if (times - 1 < paymentTimeArray.length) {
  26 + return paymentTimeArray[times - 1];
  27 + }
  28 + return -1;
  29 + }
  30 +
  31 + @Override
  32 + public long nextRefundScanTime(int times) {
  33 + if (times - 1 < refundTimeArray.length) {
  34 + return refundTimeArray[times - 1];
  35 + }
  36 + return -1;
  37 + }
  38 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/DiliCardPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.type.ChannelType;
  4 +
  5 +/**
  6 + * 地利园区支付通道模型
  7 + */
  8 +public class DiliCardPipeline extends CardPipeline {
  9 +
  10 + public DiliCardPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  11 + super(mchId, pipelineId, name, uri, params);
  12 + }
  13 +
  14 + @Override
  15 + public ChannelType supportedChannel() {
  16 + return ChannelType.DILIPAY;
  17 + }
  18 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/IPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.type.ChannelType;
  4 +
  5 +/**
  6 + * 支付通道领域模型接口
  7 + */
  8 +public interface IPipeline<T> {
  9 + /**
  10 + * 通道所属商户
  11 + */
  12 + long mchId();
  13 +
  14 + /**
  15 + * 通道ID
  16 + */
  17 + long pipelineId();
  18 +
  19 + /**
  20 + * 通道名称
  21 + */
  22 + String name();
  23 +
  24 + /**
  25 + * 通道服务URI
  26 + */
  27 + String uri();
  28 +
  29 + /**
  30 + * 通道所属渠道
  31 + */
  32 + ChannelType supportedChannel();
  33 +
  34 + /**
  35 + * 配置参数
  36 + */
  37 + T params();
  38 +
  39 + /**
  40 + * 配置参数类
  41 + */
  42 + Class<T> paramClass();
  43 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/OnlinePipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.domain.*;
  4 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +/**
  8 + * 在线支付通道抽象模型
  9 + *
  10 + * TODO: 此支付通道类是否所有支付通道的基类(微信、支付宝、银行聚合支付),还是仅仅作为银行聚合支付的基类,需要仔细斟酌
  11 + */
  12 +public abstract class OnlinePipeline<T extends PaymentPipeline.PipelineParams> extends PaymentPipeline<T> {
  13 +
  14 + public OnlinePipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  15 + super(mchId, pipelineId, name, uri, params);
  16 + }
  17 +
  18 + /**
  19 + * 扫码支付 - 客户扫商户收款码
  20 + */
  21 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request) {
  22 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持扫码支付");
  23 + }
  24 +
  25 + /**
  26 + * 付款码支付 - 商户扫客户付款码
  27 + */
  28 + public OnlinePaymentResponse sendQrCodePaymentRequest(OnlinePaymentRequest request) {
  29 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持付款码支付");
  30 + }
  31 +
  32 + /**
  33 + * 小程序支付
  34 + */
  35 + public MiniProPrepayResponse sendMiniProPrepayRequest(MiniProPrepayRequest request) {
  36 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持小程序支付");
  37 + }
  38 +
  39 + /**
  40 + * 查询预支付订单状态
  41 + */
  42 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) {
  43 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持查询支付状态");
  44 + }
  45 +
  46 + /**
  47 + * 关闭预支付订单
  48 + */
  49 + public void closePrepayOrder(OnlinePrepayOrder order) {
  50 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持关闭订单");
  51 + }
  52 +
  53 + /**
  54 + * 退款申请
  55 + */
  56 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  57 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持退款");
  58 + }
  59 +
  60 + /**
  61 + * 查询退款状态
  62 + */
  63 + public OnlineRefundResponse queryRefundResponse(OnlineRefundOrder request) {
  64 + throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道暂不支持查询退款状态");
  65 + }
  66 +
  67 + public abstract ScanTimeStrategy getTimeStrategy();
  68 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/PaymentPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  4 +import com.diligrp.cashier.shared.ErrorCode;
  5 +import com.diligrp.cashier.shared.util.JsonUtils;
  6 +import com.diligrp.cashier.shared.util.ObjectUtils;
  7 +
  8 +import java.lang.reflect.Constructor;
  9 +import java.lang.reflect.InvocationTargetException;
  10 +
  11 +public abstract class PaymentPipeline<T extends PaymentPipeline.PipelineParams> implements IPipeline<T> {
  12 + // 通道所属商户
  13 + private final long mchId;
  14 + // 通道ID
  15 + private final long pipelineId;
  16 + // 通道名称
  17 + private final String name;
  18 + // 通道服务URI
  19 + private final String uri;
  20 + // 通道参数配置
  21 + private final T params;
  22 +
  23 + public PaymentPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  24 + this.mchId = mchId;
  25 + this.pipelineId = pipelineId;
  26 + this.name = name;
  27 + this.uri = uri;
  28 +
  29 + Constructor<T> constructor = paramClass().getConstructor(String.class);
  30 + try {
  31 + this.params = constructor.newInstance(params);
  32 + } catch (InvocationTargetException tex) {
  33 + throw (Exception) tex.getCause();
  34 + }
  35 + }
  36 +
  37 + @Override
  38 + public long mchId() {
  39 + return this.mchId;
  40 + }
  41 +
  42 + @Override
  43 + public long pipelineId() {
  44 + return this.pipelineId;
  45 + }
  46 +
  47 + @Override
  48 + public String name() {
  49 + return this.name;
  50 + }
  51 +
  52 + @Override
  53 + public String uri() {
  54 + return this.uri;
  55 + }
  56 +
  57 + @Override
  58 + public T params() {
  59 + return params;
  60 + }
  61 +
  62 + protected void checkParam(String label, String value) {
  63 + if (ObjectUtils.isEmpty(value)) {
  64 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, String.format("支付通道缺少参数配置: %s", label));
  65 + }
  66 + }
  67 +
  68 + protected static abstract class PipelineParams {
  69 + public PipelineParams(String params) {
  70 + parseParams(params);
  71 + }
  72 +
  73 + protected void parseParams(String params) {
  74 + if (ObjectUtils.isNotEmpty(params)) {
  75 + JsonUtils.fromJsonString(this, params);
  76 + }
  77 + }
  78 + }
  79 +
  80 + public static class NoneParams extends PipelineParams {
  81 + public NoneParams(String params) {
  82 + super(params);
  83 + }
  84 +
  85 + @Override
  86 + protected void parseParams(String params) {
  87 + }
  88 + }
  89 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/ScanTimeStrategy.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +public interface ScanTimeStrategy {
  4 + // 预支付订单延迟扫描时间
  5 + long nextPrepayScanTime(int times);
  6 +
  7 + // 支付订单延迟扫描时间
  8 + long nextPaymentScanTime(int times);
  9 +
  10 + // 退款订单延迟扫描时间
  11 + long nextRefundScanTime(int times);
  12 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/WechatDirectPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.client.WechatDirectHttpClient;
  4 +import com.diligrp.cashier.pipeline.client.WechatHttpClient;
  5 +import com.diligrp.cashier.shared.util.AssertUtils;
  6 +
  7 +/**
  8 + * 微信支付通道抽象模型-直联模式
  9 + */
  10 +public class WechatDirectPipeline extends WechatPipeline {
  11 + private final ScanTimeStrategy strategy;
  12 +
  13 + // 直联模式下的微信客户端
  14 + private WechatDirectHttpClient client;
  15 +
  16 + public WechatDirectPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  17 + super(mchId, pipelineId, name, uri, params);
  18 + this.strategy = new DefaultTimeStrategy();
  19 + }
  20 +
  21 + @Override
  22 + public ScanTimeStrategy getTimeStrategy() {
  23 + return this.strategy;
  24 + }
  25 +
  26 + public void configure(String mchId, String appId, String appSecret, String serialNo, String privateKeyStr,
  27 + String wechatSerialNo, String wechatPublicKey, String apiV3KeyStr) {
  28 + super.configure(mchId, appId, appSecret, serialNo, privateKeyStr, wechatSerialNo, wechatPublicKey, apiV3KeyStr);
  29 + client = new WechatDirectHttpClient(uri(), wechatConfig);
  30 + }
  31 +
  32 + @Override
  33 + public WechatHttpClient getClient() {
  34 + AssertUtils.notNull(client, "微信支付通道客户端未配置");
  35 + return client;
  36 + }
  37 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/WechatPartnerPipeline.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.core;
  2 +
  3 +import com.diligrp.cashier.pipeline.client.WechatHttpClient;
  4 +import com.diligrp.cashier.pipeline.client.WechatPartnerHttpClient;
  5 +import com.diligrp.cashier.shared.util.AssertUtils;
  6 +
  7 +/**
  8 + * 微信支付通道抽象模型-服务商模式
  9 + */
  10 +public class WechatPartnerPipeline extends WechatPipeline {
  11 +
  12 + private final ScanTimeStrategy strategy;
  13 +
  14 + // 服务商模式下的微信客户端
  15 + private WechatPartnerHttpClient client;
  16 +
  17 + public WechatPartnerPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  18 + super(mchId, pipelineId, name, uri, params);
  19 + this.strategy = new DefaultTimeStrategy();
  20 + }
  21 +
  22 + @Override
  23 + public ScanTimeStrategy getTimeStrategy() {
  24 + return this.strategy;
  25 + }
  26 +
  27 + public void configure(String mchId, String appId, String appSecret, String serialNo, String privateKeyStr,
  28 + String publicKeyNo, String publicKeyStr, String apiV3KeyStr) {
  29 + super.configure(mchId, appId, appSecret, serialNo, privateKeyStr, publicKeyNo, publicKeyStr, apiV3KeyStr);
  30 + client = new WechatPartnerHttpClient(uri(), wechatConfig);
  31 + }
  32 +
  33 + @Override
  34 + public WechatHttpClient getClient() {
  35 + AssertUtils.notNull(client, "微信支付通道客户端未配置");
  36 + return client;
  37 + }
  38 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/WechatPipeline.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.WechatHttpClient;
  5 +import com.diligrp.cashier.pipeline.domain.*;
  6 +import com.diligrp.cashier.pipeline.domain.wechat.WechatCertificate;
  7 +import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig;
  8 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  9 +import com.diligrp.cashier.pipeline.type.ChannelType;
  10 +import com.diligrp.cashier.pipeline.util.WechatConstants;
  11 +import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
  12 +import com.diligrp.cashier.shared.ErrorCode;
  13 +import com.diligrp.cashier.shared.security.RsaCipher;
  14 +import com.diligrp.cashier.shared.util.AssertUtils;
  15 +import com.diligrp.cashier.shared.util.RandomUtils;
  16 +import org.slf4j.Logger;
  17 +import org.slf4j.LoggerFactory;
  18 +
  19 +import javax.crypto.spec.SecretKeySpec;
  20 +import java.nio.charset.StandardCharsets;
  21 +import java.security.PrivateKey;
  22 +
  23 +/**
  24 + * 微信支付通道抽象模型
  25 + */
  26 +public abstract class WechatPipeline extends OnlinePipeline<WechatPipeline.WechatParams> {
  27 +
  28 + private static final Logger LOG = LoggerFactory.getLogger(WechatPipeline.class);
  29 +
  30 + // 微信支付通道配置
  31 + protected WechatConfig wechatConfig;
  32 +
  33 + public WechatPipeline(long mchId, long pipelineId, String name, String uri, String params) throws Exception {
  34 + super(mchId, pipelineId, name, uri, params);
  35 + WechatParams config = params();
  36 + AssertUtils.notEmpty(config.notifyUri, String.format("微信支付缺少参数配置: notifyBaseUri"));
  37 + }
  38 +
  39 + /**
  40 + * 微信支付通道配置
  41 + *
  42 + * @param mchId - 微信商户号
  43 + * @param appId - 微信小程序ID
  44 + * @param appSecret - 小程序密钥
  45 + * @param serialNo - 商户公钥序列号
  46 + * @param privateKeyStr - BASE64编码的商户私钥
  47 + * @param wechatSerialNo - 微信平台公钥序列号
  48 + * @param wechatPublicKey - BASE64编码的微信平台公钥
  49 + * @param apiV3KeyStr - BASE64编码的apiV3Key
  50 + */
  51 + public void configure(String mchId, String appId, String appSecret, String serialNo, String privateKeyStr,
  52 + String wechatSerialNo, String wechatPublicKey, String apiV3KeyStr) {
  53 + checkParam("微信商户号", mchId);
  54 + checkParam("微信小程序ID", appId);
  55 + checkParam("小程序密钥", appSecret);
  56 + checkParam("商户公钥序列号", serialNo);
  57 + checkParam("商户私钥", privateKeyStr);
  58 + checkParam("微信平台公钥序列号", wechatSerialNo);
  59 + checkParam("微信平台公钥", wechatPublicKey);
  60 + checkParam("微信平台apiV3Key", apiV3KeyStr);
  61 +
  62 + PrivateKey privateKey;
  63 + try {
  64 + privateKey = RsaCipher.getPrivateKey(privateKeyStr);
  65 + } catch (Exception e) {
  66 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "商户私钥配置错误");
  67 + }
  68 +
  69 + byte[] keyBytes = apiV3KeyStr.getBytes(StandardCharsets.UTF_8); // 需32位
  70 + if (keyBytes.length != WechatConstants.KEY_LENGTH_BYTE) {
  71 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "微信支付平台apiV3Key配置错误");
  72 + }
  73 + SecretKeySpec apiV3Key = new SecretKeySpec(keyBytes, WechatConstants.AES_ALGORITHM);
  74 +
  75 + wechatConfig = new WechatConfig(mchId, appId, appSecret, serialNo, privateKey, apiV3Key);
  76 + try {
  77 + wechatConfig.putCertificate(WechatCertificate.of(wechatSerialNo, RsaCipher.getPublicKey(wechatPublicKey)));
  78 + } catch (Exception ex) {
  79 + throw new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "微信支付平台公钥配置错误");
  80 + }
  81 + }
  82 +
  83 + /**
  84 + * 扫码预支付下单 - 客户扫商户收款码
  85 + */
  86 + @Override
  87 + public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request) {
  88 + try {
  89 + String notifyUri = String.format(Constants.WECHAT_PAYMENT_NOTIFY_URI, params().notifyUri, request.getPaymentId());
  90 + return getClient().sendNativePrepayRequest(request, notifyUri);
  91 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  92 + throw pse;
  93 + } catch (Exception ex) {
  94 + LOG.error("Send wechat native prepay request exception", ex);
  95 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败");
  96 + }
  97 + }
  98 +
  99 + /**
  100 + * 付款码支付 - 商户扫客户付款码
  101 + */
  102 + @Override
  103 + public OnlinePaymentResponse sendQrCodePaymentRequest(OnlinePaymentRequest request) {
  104 + return super.sendQrCodePaymentRequest(request);
  105 + }
  106 +
  107 + /**
  108 + * 小程序预支付下单
  109 + */
  110 + public MiniProPrepayResponse sendMiniProPrepayRequest(MiniProPrepayRequest request) {
  111 + try {
  112 + String notifyUri = String.format(Constants.WECHAT_PAYMENT_NOTIFY_URI, params().notifyUri, request.getPaymentId());
  113 + String prepayId = getClient().sendMiniProPrepayRequest(request, notifyUri);
  114 +
  115 + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
  116 + String nonceStr = RandomUtils.randomString(32);
  117 + String packet = "prepay_id=" + prepayId;
  118 + String appId = getClient().getWechatConfig().getAppId();
  119 + String message = String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packet);
  120 + String paySign = WechatSignatureUtils.signature(message, getClient().getWechatConfig().getPrivateKey());
  121 + String signType = WechatConstants.RSA_ALGORITHM;
  122 + return MiniProPrepayResponse.of(request.getPaymentId(), prepayId, prepayId, timeStamp, nonceStr, signType, paySign);
  123 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  124 + throw pse;
  125 + } catch (Exception ex) {
  126 + LOG.error("Send wechat mini pro prepay request exception", ex);
  127 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信预支付失败");
  128 + }
  129 + }
  130 +
  131 + /**
  132 + * 查询预支付订单状态
  133 + */
  134 + public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder order) {
  135 + try {
  136 + return getClient().queryPrepayResponse(order);
  137 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  138 + throw pse;
  139 + } catch (Exception ex) {
  140 + LOG.error("Query wechat prepay order request exception", ex);
  141 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询微信支付状态失败");
  142 + }
  143 + }
  144 +
  145 + /**
  146 + * 关闭预支付订单
  147 + */
  148 + public void closePrepayOrder(OnlinePrepayOrder request) {
  149 + try {
  150 + getClient().closePrepayOrder(request);
  151 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  152 + throw pse;
  153 + } catch (Exception ex) {
  154 + LOG.error("Close wechat prepay order exception", ex);
  155 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "关闭微信预支付订单失败");
  156 + }
  157 + }
  158 +
  159 + /**
  160 + * 退款申请
  161 + */
  162 + public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
  163 + try {
  164 + String notifyUri = String.format(Constants.WECHAT_REFUND_NOTIFY_URI, params().notifyUri, request.getRefundId());
  165 + return getClient().sendRefundRequest(request, notifyUri);
  166 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  167 + throw pse;
  168 + } catch (Exception ex) {
  169 + LOG.error("Send wechat refund request exception", ex);
  170 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发起微信退款失败");
  171 + }
  172 + }
  173 +
  174 + /**
  175 + * 查询退款状态
  176 + */
  177 + public OnlineRefundResponse queryRefundResponse(OnlineRefundOrder request) {
  178 + try {
  179 + return getClient().queryRefundOrder(request);
  180 + } catch (PaymentPipelineException | IllegalArgumentException pse) {
  181 + throw pse;
  182 + } catch (Exception ex) {
  183 + LOG.error("Query wechat refund order exception", ex);
  184 + throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询微信退款失败");
  185 + }
  186 + }
  187 +
  188 + public abstract WechatHttpClient getClient();
  189 +
  190 + @Override
  191 + public ChannelType supportedChannel() {
  192 + return ChannelType.WXPAY;
  193 + }
  194 +
  195 + @Override
  196 + public Class<WechatParams> paramClass() {
  197 + return WechatParams.class;
  198 + }
  199 +
  200 + public static class WechatParams extends PipelineParams {
  201 + // 微信支付通知base地址,如: https://gateway.diligrp.com/pay-service
  202 + private String notifyUri;
  203 +
  204 + public WechatParams(String params) {
  205 + super(params);
  206 + }
  207 +
  208 + public String getNotifyUri() {
  209 + return notifyUri;
  210 + }
  211 +
  212 + public void setNotifyUri(String notifyUri) {
  213 + this.notifyUri = notifyUri;
  214 + }
  215 + }
  216 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/dao/IPaymentPipelineDao.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.dao;
  2 +
  3 +import com.diligrp.cashier.pipeline.model.PaymentPipelineDO;
  4 +import com.diligrp.cashier.pipeline.model.WechatParam;
  5 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  6 +import org.springframework.stereotype.Repository;
  7 +
  8 +import java.util.List;
  9 +import java.util.Optional;
  10 +
  11 +/**
  12 + * 支付通道数据访问层
  13 + */
  14 +@Repository("paymentPipelineDao")
  15 +public interface IPaymentPipelineDao extends MybatisMapperSupport {
  16 +
  17 + List<PaymentPipelineDO> listPaymentPipelines();
  18 +
  19 + Optional<WechatParam> findWechatParam(Long pipelineId);
  20 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/MiniProPrepayRequest.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import java.time.LocalDateTime;
  4 +
  5 +public class MiniProPrepayRequest extends OnlinePaymentRequest {
  6 + // 小程序openId
  7 + private final String openId;
  8 +
  9 + public MiniProPrepayRequest(String paymentId, long amount, String goods, String description, LocalDateTime when, String openId) {
  10 + super(paymentId, amount, goods, description, when);
  11 + this.openId = openId;
  12 + }
  13 +
  14 + public String getOpenId() {
  15 + return openId;
  16 + }
  17 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/MiniProPrepayResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +/**
  4 + * 小程序预支付响应
  5 + */
  6 +public class MiniProPrepayResponse extends OnlinePrepayResponse {
  7 + // 微信预支付ID
  8 + protected String prepayId;
  9 + // 时间戳
  10 + protected String timeStamp;
  11 + // 随机字符串
  12 + protected String nonceStr;
  13 + // 签名类型
  14 + protected String signType;
  15 + // 签名
  16 + protected String paySign;
  17 +
  18 + public static MiniProPrepayResponse of(String paymentId, String outTradeNo, String prepayId, String timeStamp,
  19 + String nonceStr, String signType, String paySign) {
  20 + MiniProPrepayResponse response = new MiniProPrepayResponse();
  21 + response.paymentId = paymentId;
  22 + response.outTradeNo = outTradeNo;
  23 + response.prepayId = prepayId;
  24 + response.timeStamp = timeStamp;
  25 + response.nonceStr = nonceStr;
  26 + response.signType = signType;
  27 + response.paySign = paySign;
  28 +
  29 + return response;
  30 + }
  31 +
  32 + public String getPrepayId() {
  33 + return prepayId;
  34 + }
  35 +
  36 + public String getTimeStamp() {
  37 + return timeStamp;
  38 + }
  39 +
  40 + public String getNonceStr() {
  41 + return nonceStr;
  42 + }
  43 +
  44 + public String getPacket() {
  45 + return "prepay_id=" + prepayId;
  46 + }
  47 +
  48 + public String getSignType() {
  49 + return signType;
  50 + }
  51 +
  52 + public String getPaySign() {
  53 + return paySign;
  54 + }
  55 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/NativePrepayRequest.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import java.time.LocalDateTime;
  4 +
  5 +public class NativePrepayRequest extends OnlinePaymentRequest {
  6 + public NativePrepayRequest(String paymentId, long amount, String goods, String description, LocalDateTime when) {
  7 + super(paymentId, amount, goods, description, when);
  8 + }
  9 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/NativePrepayResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +/**
  4 + * 扫码预支付响应
  5 + */
  6 +public class NativePrepayResponse extends OnlinePrepayResponse {
  7 + // 二维码链接
  8 + protected String codeUri;
  9 +
  10 + public static NativePrepayResponse of(String paymentId, String outTradeNo, String codeUri) {
  11 + NativePrepayResponse response = new NativePrepayResponse();
  12 + response.paymentId = paymentId;
  13 + response.outTradeNo = outTradeNo;
  14 + response.codeUri = codeUri;
  15 + return response;
  16 + }
  17 +
  18 + public String getCodeUri() {
  19 + return codeUri;
  20 + }
  21 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlinePaymentRequest.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +import java.time.LocalDateTime;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * 预支付申请模型
  10 + */
  11 +public class OnlinePaymentRequest extends ContainerSupport {
  12 + // 支付ID
  13 + protected String paymentId;
  14 + // 交易金额 - 分
  15 + protected long amount;
  16 + // 商品描述
  17 + protected String goods;
  18 + // 交易备注
  19 + protected String description;
  20 + // 交易时间
  21 + protected LocalDateTime when;
  22 +
  23 + public OnlinePaymentRequest(String paymentId, long amount, String goods, String description, LocalDateTime when) {
  24 + this.paymentId = paymentId;
  25 + this.amount = amount;
  26 + this.goods = goods;
  27 + this.description = description;
  28 + this.when = when;
  29 + }
  30 +
  31 + public String getPaymentId() {
  32 + return paymentId;
  33 + }
  34 +
  35 + public long getAmount() {
  36 + return amount;
  37 + }
  38 +
  39 + public String getGoods() {
  40 + return goods;
  41 + }
  42 +
  43 + public String getDescription() {
  44 + return description;
  45 + }
  46 +
  47 + public LocalDateTime getWhen() {
  48 + return when;
  49 + }
  50 +
  51 + public void putParams(Map<String, Object> params) {
  52 + if (params != null) {
  53 + putAll(params);
  54 + }
  55 + }
  56 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlinePaymentResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +/**
  8 + * 在线支付结果领域模型
  9 + */
  10 +public class OnlinePaymentResponse extends ContainerSupport {
  11 + // 支付ID
  12 + private String paymentId;
  13 + // 支付通道订单号
  14 + private String outTradeNo;
  15 + // 支付方Id - 比如微信OpenId
  16 + private String payerId;
  17 + // 支付时间
  18 + private LocalDateTime when;
  19 + // 支付状态
  20 + private Integer state;
  21 + // 交易备注
  22 + private String message;
  23 +
  24 + public static OnlinePaymentResponse of(String paymentId, String outTradeNo, String payerId,
  25 + LocalDateTime when, Integer state, String message) {
  26 + OnlinePaymentResponse response = new OnlinePaymentResponse();
  27 + response.paymentId = paymentId;
  28 + response.outTradeNo = outTradeNo;
  29 + response.payerId = payerId;
  30 + response.when = when;
  31 + response.state = state;
  32 + response.message = message;
  33 + return response;
  34 + }
  35 +
  36 + public String getPaymentId() {
  37 + return paymentId;
  38 + }
  39 +
  40 + public String getOutTradeNo() {
  41 + return outTradeNo;
  42 + }
  43 +
  44 + public String getPayerId() {
  45 + return payerId;
  46 + }
  47 +
  48 + public LocalDateTime getWhen() {
  49 + return when;
  50 + }
  51 +
  52 + public Integer getState() {
  53 + return state;
  54 + }
  55 +
  56 + public String getMessage() {
  57 + return message;
  58 + }
  59 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlinePrepayOrder.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +public class OnlinePrepayOrder extends ContainerSupport {
  6 + // 支付ID
  7 + private final String paymentId;
  8 + // 通道订单号
  9 + private final String outTradeNo;
  10 +
  11 + public OnlinePrepayOrder(String paymentId, String outTradeNo) {
  12 + this.paymentId = paymentId;
  13 + this.outTradeNo = outTradeNo;
  14 + }
  15 +
  16 + public String getPaymentId() {
  17 + return paymentId;
  18 + }
  19 +
  20 + public String getOutTradeNo() {
  21 + return outTradeNo;
  22 + }
  23 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlinePrepayResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +/**
  4 + * 预支付响应
  5 + */
  6 +public class OnlinePrepayResponse {
  7 + // 支付订单号
  8 + protected String paymentId;
  9 + // 通道订单号
  10 + protected String outTradeNo;
  11 +
  12 + public String getPaymentId() {
  13 + return paymentId;
  14 + }
  15 +
  16 + public String getOutTradeNo() {
  17 + return outTradeNo;
  18 + }
  19 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlineRefundOrder.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +public class OnlineRefundOrder extends ContainerSupport {
  6 + // 退款ID
  7 + private final String refundId;
  8 + // 通道订单号
  9 + private final String outTradeNo;
  10 +
  11 + public OnlineRefundOrder(String refundId, String outTradeNo) {
  12 + this.refundId = refundId;
  13 + this.outTradeNo = outTradeNo;
  14 + }
  15 +
  16 + public String getRefundId() {
  17 + return refundId;
  18 + }
  19 +
  20 + public String getOutTradeNo() {
  21 + return outTradeNo;
  22 + }
  23 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlineRefundRequest.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +/**
  8 + * 微信退款领域模型
  9 + */
  10 +public class OnlineRefundRequest extends ContainerSupport {
  11 + // 退款单号
  12 + private String refundId;
  13 + // 原订单号
  14 + private String paymentId;
  15 + // 原通道订单号
  16 + private String outTradeNo;
  17 + // 原订单总金额
  18 + private Long maxAmount;
  19 + // 退款金额
  20 + private Long amount;
  21 + // 交易备注或退款理由
  22 + private String description;
  23 + // 交易时间
  24 + private LocalDateTime when;
  25 +
  26 + public static OnlineRefundRequest of(String refundId, String paymentId, String outTradeNo, Long maxAmount,
  27 + Long amount, String description, LocalDateTime when) {
  28 + OnlineRefundRequest request = new OnlineRefundRequest();
  29 + request.refundId = refundId;
  30 + request.paymentId = paymentId;
  31 + request.outTradeNo = outTradeNo;
  32 + request.maxAmount = maxAmount;
  33 + request.amount = amount;
  34 + request.description = description;
  35 + request.when = when;
  36 + return request;
  37 + }
  38 +
  39 + public String getRefundId() {
  40 + return refundId;
  41 + }
  42 +
  43 + public String getPaymentId() {
  44 + return paymentId;
  45 + }
  46 +
  47 + public String getOutTradeNo() {
  48 + return outTradeNo;
  49 + }
  50 +
  51 + public Long getMaxAmount() {
  52 + return maxAmount;
  53 + }
  54 +
  55 + public Long getAmount() {
  56 + return amount;
  57 + }
  58 +
  59 + public String getDescription() {
  60 + return description;
  61 + }
  62 +
  63 + public LocalDateTime getWhen() {
  64 + return when;
  65 + }
  66 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlineRefundResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain;
  2 +
  3 +import com.diligrp.cashier.shared.domain.ContainerSupport;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +/**
  8 + * 退款结果领域模型
  9 + */
  10 +public class OnlineRefundResponse extends ContainerSupport {
  11 + // 商户退款单号
  12 + private String refundId;
  13 + // 通道退款订单号
  14 + private String outTradeNo;
  15 + // 退款完成时间
  16 + private LocalDateTime when;
  17 + // 退款状态
  18 + private Integer state;
  19 + // 交易备注
  20 + private String message;
  21 +
  22 + public static OnlineRefundResponse of(String refundId, String outTradeNo, LocalDateTime when,
  23 + Integer state, String message) {
  24 + OnlineRefundResponse response = new OnlineRefundResponse();
  25 + response.refundId = refundId;
  26 + response.outTradeNo = outTradeNo;
  27 + response.when = when;
  28 + response.state = state;
  29 + response.message = message;
  30 + return response;
  31 + }
  32 +
  33 + public String getRefundId() {
  34 + return refundId;
  35 + }
  36 +
  37 + public String getOutTradeNo() {
  38 + return outTradeNo;
  39 + }
  40 +
  41 + public LocalDateTime getWhen() {
  42 + return when;
  43 + }
  44 +
  45 + public Integer getState() {
  46 + return state;
  47 + }
  48 +
  49 + public String getMessage() {
  50 + return message;
  51 + }
  52 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/card/UserCardDTO.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.card;
  2 +
  3 +/**
  4 + * 园区卡信息
  5 + */
  6 +public class UserCardDTO {
  7 + // 客户ID
  8 + private Long customerId;
  9 + // 账号ID
  10 + private Long accountId;
  11 + // 卡号
  12 + private String cardNo;
  13 + // 持卡人名称
  14 + private String name;
  15 +
  16 + public UserCardDTO() {
  17 + }
  18 +
  19 + public UserCardDTO(Long accountId, String cardNo) {
  20 + this.accountId = accountId;
  21 + this.cardNo = cardNo;
  22 + }
  23 +
  24 + public UserCardDTO(Long customerId, Long accountId, String cardNo, String name) {
  25 + this.customerId = customerId;
  26 + this.accountId = accountId;
  27 + this.cardNo = cardNo;
  28 + this.name = name;
  29 + }
  30 +
  31 + public Long getCustomerId() {
  32 + return customerId;
  33 + }
  34 +
  35 + public void setCustomerId(Long customerId) {
  36 + this.customerId = customerId;
  37 + }
  38 +
  39 + public Long getAccountId() {
  40 + return accountId;
  41 + }
  42 +
  43 + public void setAccountId(Long accountId) {
  44 + this.accountId = accountId;
  45 + }
  46 +
  47 + public String getCardNo() {
  48 + return cardNo;
  49 + }
  50 +
  51 + public void setCardNo(String cardNo) {
  52 + this.cardNo = cardNo;
  53 + }
  54 +
  55 + public String getName() {
  56 + return name;
  57 + }
  58 +
  59 + public void setName(String name) {
  60 + this.name = name;
  61 + }
  62 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/AuthorizationSession.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +public class AuthorizationSession {
  4 + // 用户唯一标识
  5 + private String openid;
  6 + // 会话密钥
  7 + private String session_key;
  8 + // 用户在开放平台的唯一标识
  9 + private String unionid;
  10 + // 错误码
  11 + private Integer errcode;
  12 + // 错误信息
  13 + private String errmsg;
  14 +
  15 + public String getOpenid() {
  16 + return openid;
  17 + }
  18 +
  19 + public void setOpenid(String openid) {
  20 + this.openid = openid;
  21 + }
  22 +
  23 + public String getSession_key() {
  24 + return session_key;
  25 + }
  26 +
  27 + public void setSession_key(String session_key) {
  28 + this.session_key = session_key;
  29 + }
  30 +
  31 + public String getUnionid() {
  32 + return unionid;
  33 + }
  34 +
  35 + public void setUnionid(String unionid) {
  36 + this.unionid = unionid;
  37 + }
  38 +
  39 + public Integer getErrcode() {
  40 + return errcode;
  41 + }
  42 +
  43 + public void setErrcode(Integer errcode) {
  44 + this.errcode = errcode;
  45 + }
  46 +
  47 + public String getErrmsg() {
  48 + return errmsg;
  49 + }
  50 +
  51 + public void setErrmsg(String errmsg) {
  52 + this.errmsg = errmsg;
  53 + }
  54 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/CertificateResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +import java.util.List;
  4 +
  5 +/**
  6 + * 微信支付平台证书响应接口模型
  7 + */
  8 +public class CertificateResponse {
  9 + // 证书列表
  10 + private List<Certificate> data;
  11 +
  12 + public List<Certificate> getData() {
  13 + return data;
  14 + }
  15 +
  16 + public void setData(List<Certificate> data) {
  17 + this.data = data;
  18 + }
  19 +
  20 + public static class Certificate {
  21 + // 证书序列号
  22 + private String serial_no;
  23 + // 证书有效时间
  24 + private String effective_time;
  25 + // 证书过期时间
  26 + private String expire_time;
  27 + // 证书内容
  28 + private EncryptCertificate encrypt_certificate;
  29 +
  30 + public String getSerial_no() {
  31 + return serial_no;
  32 + }
  33 +
  34 + public void setSerial_no(String serial_no) {
  35 + this.serial_no = serial_no;
  36 + }
  37 +
  38 + public String getEffective_time() {
  39 + return effective_time;
  40 + }
  41 +
  42 + public void setEffective_time(String effective_time) {
  43 + this.effective_time = effective_time;
  44 + }
  45 +
  46 + public String getExpire_time() {
  47 + return expire_time;
  48 + }
  49 +
  50 + public void setExpire_time(String expire_time) {
  51 + this.expire_time = expire_time;
  52 + }
  53 +
  54 + public EncryptCertificate getEncrypt_certificate() {
  55 + return encrypt_certificate;
  56 + }
  57 +
  58 + public void setEncrypt_certificate(EncryptCertificate encrypt_certificate) {
  59 + this.encrypt_certificate = encrypt_certificate;
  60 + }
  61 +
  62 + public static class EncryptCertificate {
  63 + // 证书加密算法
  64 + private String algorithm;
  65 + // 随机字符串
  66 + private String nonce;
  67 + // 附加数据
  68 + private String associated_data;
  69 + // 证书密文
  70 + private String ciphertext;
  71 +
  72 + public String getAlgorithm() {
  73 + return algorithm;
  74 + }
  75 +
  76 + public void setAlgorithm(String algorithm) {
  77 + this.algorithm = algorithm;
  78 + }
  79 +
  80 + public String getNonce() {
  81 + return nonce;
  82 + }
  83 +
  84 + public void setNonce(String nonce) {
  85 + this.nonce = nonce;
  86 + }
  87 +
  88 + public String getAssociated_data() {
  89 + return associated_data;
  90 + }
  91 +
  92 + public void setAssociated_data(String associated_data) {
  93 + this.associated_data = associated_data;
  94 + }
  95 +
  96 + public String getCiphertext() {
  97 + return ciphertext;
  98 + }
  99 +
  100 + public void setCiphertext(String ciphertext) {
  101 + this.ciphertext = ciphertext;
  102 + }
  103 + }
  104 + }
  105 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/DirectPaymentResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 交易结果模型 - 直联模式
  5 + */
  6 +public class DirectPaymentResponse {
  7 + // 微信订单号
  8 + private String transaction_id;
  9 + // 商户订单号
  10 + private String out_trade_no;
  11 + // 交易状态
  12 + private String trade_state;
  13 + // 交易状态描述
  14 + private String trade_state_desc;
  15 + // 支付完成时间
  16 + private String success_time;
  17 + // 支付方
  18 + private Payer payer;
  19 +
  20 + public String getTransaction_id() {
  21 + return transaction_id;
  22 + }
  23 +
  24 + public void setTransaction_id(String transaction_id) {
  25 + this.transaction_id = transaction_id;
  26 + }
  27 +
  28 + public String getOut_trade_no() {
  29 + return out_trade_no;
  30 + }
  31 +
  32 + public void setOut_trade_no(String out_trade_no) {
  33 + this.out_trade_no = out_trade_no;
  34 + }
  35 +
  36 + public String getTrade_state() {
  37 + return trade_state;
  38 + }
  39 +
  40 + public void setTrade_state(String trade_state) {
  41 + this.trade_state = trade_state;
  42 + }
  43 +
  44 + public String getTrade_state_desc() {
  45 + return trade_state_desc;
  46 + }
  47 +
  48 + public void setTrade_state_desc(String trade_state_desc) {
  49 + this.trade_state_desc = trade_state_desc;
  50 + }
  51 +
  52 + public String getSuccess_time() {
  53 + return success_time;
  54 + }
  55 +
  56 + public void setSuccess_time(String success_time) {
  57 + this.success_time = success_time;
  58 + }
  59 +
  60 + public Payer getPayer() {
  61 + return payer;
  62 + }
  63 +
  64 + public void setPayer(Payer payer) {
  65 + this.payer = payer;
  66 + }
  67 +
  68 + public static class Payer {
  69 + // 用户标识OpenId
  70 + private String openid;
  71 +
  72 + public String getOpenid() {
  73 + return openid;
  74 + }
  75 +
  76 + public void setOpenid(String openid) {
  77 + this.openid = openid;
  78 + }
  79 + }
  80 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/ErrorMessage.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +public class ErrorMessage {
  4 + private String code;
  5 +
  6 + private String message;
  7 +
  8 + public String getCode() {
  9 + return code;
  10 + }
  11 +
  12 + public void setCode(String code) {
  13 + this.code = code;
  14 + }
  15 +
  16 + public String getMessage() {
  17 + return message;
  18 + }
  19 +
  20 + public void setMessage(String message) {
  21 + this.message = message;
  22 + }
  23 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/PartnerPaymentResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 交易结果模型 - 服务商模式
  5 + */
  6 +public class PartnerPaymentResponse {
  7 + // 微信订单号
  8 + private String transaction_id;
  9 + // 商户订单号
  10 + private String out_trade_no;
  11 + // 交易状态
  12 + private String trade_state;
  13 + // 交易状态描述
  14 + private String trade_state_desc;
  15 + // 支付完成时间
  16 + private String success_time;
  17 + // 支付方
  18 + private Payer payer;
  19 +
  20 + public String getTransaction_id() {
  21 + return transaction_id;
  22 + }
  23 +
  24 + public void setTransaction_id(String transaction_id) {
  25 + this.transaction_id = transaction_id;
  26 + }
  27 +
  28 + public String getOut_trade_no() {
  29 + return out_trade_no;
  30 + }
  31 +
  32 + public void setOut_trade_no(String out_trade_no) {
  33 + this.out_trade_no = out_trade_no;
  34 + }
  35 +
  36 + public String getTrade_state() {
  37 + return trade_state;
  38 + }
  39 +
  40 + public void setTrade_state(String trade_state) {
  41 + this.trade_state = trade_state;
  42 + }
  43 +
  44 + public String getTrade_state_desc() {
  45 + return trade_state_desc;
  46 + }
  47 +
  48 + public void setTrade_state_desc(String trade_state_desc) {
  49 + this.trade_state_desc = trade_state_desc;
  50 + }
  51 +
  52 + public String getSuccess_time() {
  53 + return success_time;
  54 + }
  55 +
  56 + public void setSuccess_time(String success_time) {
  57 + this.success_time = success_time;
  58 + }
  59 +
  60 + public Payer getPayer() {
  61 + return payer;
  62 + }
  63 +
  64 + public void setPayer(Payer payer) {
  65 + this.payer = payer;
  66 + }
  67 +
  68 + public static class Payer {
  69 + // 服务商下用户OpenId
  70 + private String sp_openid;
  71 + // 子商户下用户OpenId
  72 + private String sub_openid;
  73 +
  74 + public String getSp_openid() {
  75 + return sp_openid;
  76 + }
  77 +
  78 + public void setSp_openid(String sp_openid) {
  79 + this.sp_openid = sp_openid;
  80 + }
  81 +
  82 + public String getSub_openid() {
  83 + return sub_openid;
  84 + }
  85 +
  86 + public void setSub_openid(String sub_openid) {
  87 + this.sub_openid = sub_openid;
  88 + }
  89 + }
  90 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/RefundNotifyResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 退款结果模型
  5 + */
  6 +public class RefundNotifyResponse {
  7 + // 微信退款单号
  8 + private String refund_id;
  9 + // 商户退款单号
  10 + private String out_refund_no;
  11 + // 原支付交易对应的商户订单号
  12 + private String out_trade_no;
  13 + // 退款状态 - 退款结果通知返回
  14 + private String refund_status;
  15 + // 退款完成时间
  16 + private String success_time;
  17 +
  18 + public String getRefund_id() {
  19 + return refund_id;
  20 + }
  21 +
  22 + public void setRefund_id(String refund_id) {
  23 + this.refund_id = refund_id;
  24 + }
  25 +
  26 + public String getOut_refund_no() {
  27 + return out_refund_no;
  28 + }
  29 +
  30 + public void setOut_refund_no(String out_refund_no) {
  31 + this.out_refund_no = out_refund_no;
  32 + }
  33 +
  34 + public String getOut_trade_no() {
  35 + return out_trade_no;
  36 + }
  37 +
  38 + public void setOut_trade_no(String out_trade_no) {
  39 + this.out_trade_no = out_trade_no;
  40 + }
  41 +
  42 + public String getRefund_status() {
  43 + return refund_status;
  44 + }
  45 +
  46 + public void setRefund_status(String refund_status) {
  47 + this.refund_status = refund_status;
  48 + }
  49 +
  50 + public String getSuccess_time() {
  51 + return success_time;
  52 + }
  53 +
  54 + public void setSuccess_time(String success_time) {
  55 + this.success_time = success_time;
  56 + }
  57 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/UploadShippingRequest.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 微信发货申请
  5 + *
  6 + * 解决买家微信付款成功,卖家微信商户号无法收到钱,需要去自己的微信后台操作一下【发货】
  7 + */
  8 +public class UploadShippingRequest {
  9 + // 微信订单号
  10 + private String transactionId;
  11 + // 物流模式
  12 + private Integer logisticsType;
  13 + // 商品信息
  14 + private String goods;
  15 + // 支付人标识
  16 + private String openId;
  17 +
  18 + public static UploadShippingRequest of(String transactionId, Integer logisticsType, String goods, String openId) {
  19 + UploadShippingRequest request = new UploadShippingRequest();
  20 + request.transactionId = transactionId;
  21 + request.logisticsType = logisticsType;
  22 + request.goods = goods;
  23 + request.openId = openId;
  24 + return request;
  25 + }
  26 +
  27 + public String getTransactionId() {
  28 + return transactionId;
  29 + }
  30 +
  31 + public Integer getLogisticsType() {
  32 + return logisticsType;
  33 + }
  34 +
  35 + public String getGoods() {
  36 + return goods;
  37 + }
  38 +
  39 + public String getOpenId() {
  40 + return openId;
  41 + }
  42 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/WechatAccessToken.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 微信小程序接口调用凭证
  5 + */
  6 +public class WechatAccessToken {
  7 + // 接口调用凭证
  8 + private String token;
  9 + // 凭证有效时间,单位:秒
  10 + private Integer expiresIn;
  11 +
  12 + public static WechatAccessToken of(String token, Integer expiresIn) {
  13 + WechatAccessToken accessToken = new WechatAccessToken();
  14 + accessToken.token = token;
  15 + accessToken.expiresIn = expiresIn;
  16 + return accessToken;
  17 + }
  18 +
  19 + public String getToken() {
  20 + return token;
  21 + }
  22 +
  23 + public Integer getExpiresIn() {
  24 + return expiresIn;
  25 + }
  26 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/WechatCertificate.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +import java.security.PublicKey;
  4 +
  5 +/**
  6 + * 微信支付平台证书
  7 + */
  8 +public class WechatCertificate {
  9 + // 公钥序列号
  10 + private String serialNo;
  11 + // 公钥
  12 + private PublicKey publicKey;
  13 +
  14 + public static WechatCertificate of(String serialNo, PublicKey publicKey) {
  15 + WechatCertificate certificate = new WechatCertificate();
  16 + certificate.serialNo = serialNo;
  17 + certificate.publicKey = publicKey;
  18 + return certificate;
  19 + }
  20 +
  21 + public String getSerialNo() {
  22 + return serialNo;
  23 + }
  24 +
  25 + public PublicKey getPublicKey() {
  26 + return publicKey;
  27 + }
  28 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/WechatConfig.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +import javax.crypto.spec.SecretKeySpec;
  4 +import java.security.PrivateKey;
  5 +import java.util.Map;
  6 +import java.util.Optional;
  7 +import java.util.concurrent.ConcurrentHashMap;
  8 +
  9 +/**
  10 + * 微信支付直连模式配置
  11 + */
  12 +public class WechatConfig {
  13 + // 微信商户号
  14 + private final String mchId;
  15 + // 微信AppId
  16 + private final String appId;
  17 + // appSecret,用于小程序支付
  18 + private final String appSecret;
  19 + // 商户公钥序列号
  20 + private final String serialNo;
  21 + // 商户私钥
  22 + private final PrivateKey privateKey;
  23 + // 商户ApiV3Key - Base64编码
  24 + private final SecretKeySpec apiV3Key;
  25 +
  26 + // 微信支付平台证书 - 当旧证书即将过期时,新老证书将并行使用
  27 + // https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/wechatpay5_1.shtml
  28 + // https://pay.weixin.qq.com/docs/merchant/apis/platform-certificate/api-v3-get-certificates/get.html
  29 + private final Map<String, WechatCertificate> certificates = new ConcurrentHashMap<>();
  30 +
  31 + /**
  32 + * 微信支付通道配置
  33 + *
  34 + * @param mchId - 微信商户号
  35 + * @param appId - 微信小程序ID
  36 + * @param appSecret - 小程序密钥
  37 + * @param serialNo - 商户公钥号
  38 + * @param privateKey - 商户私钥
  39 + * @param apiV3Key - 微信apiV3Key
  40 + */
  41 + public WechatConfig(String mchId, String appId, String appSecret, String serialNo, PrivateKey privateKey, SecretKeySpec apiV3Key) {
  42 + this.mchId = mchId;
  43 + this.appId = appId;
  44 + this.appSecret = appSecret;
  45 + this.serialNo = serialNo;
  46 + this.privateKey = privateKey;
  47 + this.apiV3Key = apiV3Key;
  48 + }
  49 +
  50 + public String getMchId() {
  51 + return mchId;
  52 + }
  53 +
  54 + public String getAppId() {
  55 + return appId;
  56 + }
  57 +
  58 + public String getAppSecret() {
  59 + return appSecret;
  60 + }
  61 +
  62 + public String getSerialNo() {
  63 + return serialNo;
  64 + }
  65 +
  66 + public PrivateKey getPrivateKey() {
  67 + return privateKey;
  68 + }
  69 +
  70 + public SecretKeySpec getApiV3Key() {
  71 + return apiV3Key;
  72 + }
  73 +
  74 + public void putCertificate(WechatCertificate certificate) {
  75 + if (certificate != null) {
  76 + certificates.put(certificate.getSerialNo(), certificate);
  77 + }
  78 + }
  79 +
  80 + public Optional<WechatCertificate> getCertificate(String serialNo) {
  81 + return Optional.ofNullable(certificates.get(serialNo));
  82 + }
  83 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/wechat/WechatNotifyResponse.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.domain.wechat;
  2 +
  3 +/**
  4 + * 微信支付结果通知模型
  5 + */
  6 +public class WechatNotifyResponse {
  7 + // 通知ID
  8 + private String id;
  9 + // 通知类型
  10 + private String event_type;
  11 + // 通知数据类型
  12 + private String resource_type;
  13 + // 通知数据
  14 + private Resource resource;
  15 +
  16 + public String getId() {
  17 + return id;
  18 + }
  19 +
  20 + public void setId(String id) {
  21 + this.id = id;
  22 + }
  23 +
  24 + public String getEvent_type() {
  25 + return event_type;
  26 + }
  27 +
  28 + public void setEvent_type(String event_type) {
  29 + this.event_type = event_type;
  30 + }
  31 +
  32 + public String getResource_type() {
  33 + return resource_type;
  34 + }
  35 +
  36 + public void setResource_type(String resource_type) {
  37 + this.resource_type = resource_type;
  38 + }
  39 +
  40 + public Resource getResource() {
  41 + return resource;
  42 + }
  43 +
  44 + public void setResource(Resource resource) {
  45 + this.resource = resource;
  46 + }
  47 +
  48 + public static class Resource {
  49 + // 加密算法类型 - AEAD_AES_256_GCM
  50 + private String algorithm;
  51 + // 数据密文
  52 + private String ciphertext;
  53 + // 附加数据
  54 + private String associated_data;
  55 + // 原始类型
  56 + private String original_type;
  57 + // 随机串
  58 + private String nonce;
  59 +
  60 + public String getAlgorithm() {
  61 + return algorithm;
  62 + }
  63 +
  64 + public void setAlgorithm(String algorithm) {
  65 + this.algorithm = algorithm;
  66 + }
  67 +
  68 + public String getCiphertext() {
  69 + return ciphertext;
  70 + }
  71 +
  72 + public void setCiphertext(String ciphertext) {
  73 + this.ciphertext = ciphertext;
  74 + }
  75 +
  76 + public String getAssociated_data() {
  77 + return associated_data;
  78 + }
  79 +
  80 + public void setAssociated_data(String associated_data) {
  81 + this.associated_data = associated_data;
  82 + }
  83 +
  84 + public String getOriginal_type() {
  85 + return original_type;
  86 + }
  87 +
  88 + public void setOriginal_type(String original_type) {
  89 + this.original_type = original_type;
  90 + }
  91 +
  92 + public String getNonce() {
  93 + return nonce;
  94 + }
  95 +
  96 + public void setNonce(String nonce) {
  97 + this.nonce = nonce;
  98 + }
  99 + }
  100 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/exception/PaymentPipelineException.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.exception;
  2 +
  3 +import com.diligrp.cashier.shared.exception.PlatformServiceException;
  4 +
  5 +public class PaymentPipelineException extends PlatformServiceException {
  6 +
  7 + public PaymentPipelineException(String message) {
  8 + super(message);
  9 + }
  10 +
  11 + public PaymentPipelineException(String code, String message) {
  12 + super(code, message);
  13 + }
  14 +
  15 + public PaymentPipelineException(String message, Throwable ex) {
  16 + super(message, ex);
  17 + }
  18 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/model/PaymentPipelineDO.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.model;
  2 +
  3 +import com.diligrp.cashier.shared.domain.BaseDO;
  4 +
  5 +public class PaymentPipelineDO extends BaseDO {
  6 + // 商户ID
  7 + private Long mchId;
  8 + // 通道ID
  9 + private Long pipelineId;
  10 + // 支付渠道
  11 + private Integer channelId;
  12 + // 通道类型
  13 + private Integer type;
  14 + // 通道名称
  15 + private String name;
  16 + // 通道uri
  17 + private String uri;
  18 + // 通道参数
  19 + private String param;
  20 + // 通道状态
  21 + private Integer state;
  22 +
  23 + public Long getMchId() {
  24 + return mchId;
  25 + }
  26 +
  27 + public void setMchId(Long mchId) {
  28 + this.mchId = mchId;
  29 + }
  30 +
  31 + public Long getPipelineId() {
  32 + return pipelineId;
  33 + }
  34 +
  35 + public void setPipelineId(Long pipelineId) {
  36 + this.pipelineId = pipelineId;
  37 + }
  38 +
  39 + public Integer getChannelId() {
  40 + return channelId;
  41 + }
  42 +
  43 + public void setChannelId(Integer channelId) {
  44 + this.channelId = channelId;
  45 + }
  46 +
  47 + public Integer getType() {
  48 + return type;
  49 + }
  50 +
  51 + public void setType(Integer type) {
  52 + this.type = type;
  53 + }
  54 +
  55 + public String getName() {
  56 + return name;
  57 + }
  58 +
  59 + public void setName(String name) {
  60 + this.name = name;
  61 + }
  62 +
  63 + public String getUri() {
  64 + return uri;
  65 + }
  66 +
  67 + public void setUri(String uri) {
  68 + this.uri = uri;
  69 + }
  70 +
  71 + public String getParam() {
  72 + return param;
  73 + }
  74 +
  75 + public void setParam(String param) {
  76 + this.param = param;
  77 + }
  78 +
  79 + public Integer getState() {
  80 + return state;
  81 + }
  82 +
  83 + public void setState(Integer state) {
  84 + this.state = state;
  85 + }
  86 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/model/WechatParam.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.model;
  2 +
  3 +import com.diligrp.cashier.shared.domain.BaseDO;
  4 +
  5 +public class WechatParam extends BaseDO {
  6 + // 通道ID
  7 + private Long pipelineId;
  8 + // 商户号, 服务商模式下为服务商商户号
  9 + private String mchId;
  10 + // 小程序ID
  11 + private String appId;
  12 + // 小程序密钥
  13 + private String appSecret;
  14 + // 商户公钥序列号
  15 + private String serialNo;
  16 + // 商户私钥
  17 + private String privateKey;
  18 + // 微信公钥序列号
  19 + private String wechatSerialNo;
  20 + // 微信公钥
  21 + private String wechatPublicKey;
  22 + // 微信apiV3Key
  23 + private String apiV3Key;
  24 + // 通道类型: 直连通道或服务商通道
  25 + private Integer type;
  26 +
  27 + public Long getPipelineId() {
  28 + return pipelineId;
  29 + }
  30 +
  31 + public void setPipelineId(Long pipelineId) {
  32 + this.pipelineId = pipelineId;
  33 + }
  34 +
  35 + public String getMchId() {
  36 + return mchId;
  37 + }
  38 +
  39 + public void setMchId(String mchId) {
  40 + this.mchId = mchId;
  41 + }
  42 +
  43 + public String getAppId() {
  44 + return appId;
  45 + }
  46 +
  47 + public void setAppId(String appId) {
  48 + this.appId = appId;
  49 + }
  50 +
  51 + public String getAppSecret() {
  52 + return appSecret;
  53 + }
  54 +
  55 + public void setAppSecret(String appSecret) {
  56 + this.appSecret = appSecret;
  57 + }
  58 +
  59 + public String getSerialNo() {
  60 + return serialNo;
  61 + }
  62 +
  63 + public void setSerialNo(String serialNo) {
  64 + this.serialNo = serialNo;
  65 + }
  66 +
  67 + public String getPrivateKey() {
  68 + return privateKey;
  69 + }
  70 +
  71 + public void setPrivateKey(String privateKey) {
  72 + this.privateKey = privateKey;
  73 + }
  74 +
  75 + public String getWechatSerialNo() {
  76 + return wechatSerialNo;
  77 + }
  78 +
  79 + public void setWechatSerialNo(String wechatSerialNo) {
  80 + this.wechatSerialNo = wechatSerialNo;
  81 + }
  82 +
  83 + public String getWechatPublicKey() {
  84 + return wechatPublicKey;
  85 + }
  86 +
  87 + public void setWechatPublicKey(String wechatPublicKey) {
  88 + this.wechatPublicKey = wechatPublicKey;
  89 + }
  90 +
  91 + public String getApiV3Key() {
  92 + return apiV3Key;
  93 + }
  94 +
  95 + public void setApiV3Key(String apiV3Key) {
  96 + this.apiV3Key = apiV3Key;
  97 + }
  98 +
  99 + public Integer getType() {
  100 + return type;
  101 + }
  102 +
  103 + public void setType(Integer type) {
  104 + this.type = type;
  105 + }
  106 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/IPaymentPipelineManager.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.service;
  2 +
  3 +import com.diligrp.cashier.pipeline.core.PaymentPipeline;
  4 +import com.diligrp.cashier.pipeline.type.ChannelType;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * 支付通道管理器接口
  10 + */
  11 +public interface IPaymentPipelineManager {
  12 + /**
  13 + * 注册支付通道
  14 + */
  15 + <T extends PaymentPipeline<?>> void registerPaymentPipeline(T pipeline);
  16 +
  17 + /**
  18 + * 获取商户配置的指定类型的支付通道
  19 + */
  20 + <T extends PaymentPipeline<?>> T findPipelineByMchId(long mchId, Class<T> type);
  21 +
  22 + /**
  23 + * 获取商户配置的指定类型的支付通道
  24 + */
  25 + <T extends PaymentPipeline<?>> T findPipelineByMchId(long mchId, ChannelType channel, Class<T> type);
  26 +
  27 + /**
  28 + * 获取商户配置的指定类型的支付通道
  29 + */
  30 + <T extends PaymentPipeline<?>> List<T> listPipelines(long mchId, Class<T> type);
  31 +
  32 + /**
  33 + * 根据ID获取支付通道
  34 + */
  35 + <T extends PaymentPipeline<?>> T findPipelineById(long pipelineId, Class<T> type);
  36 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/impl/PaymentPipelineManagerImpl.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.service.impl;
  2 +
  3 +import com.diligrp.cashier.pipeline.core.*;
  4 +import com.diligrp.cashier.pipeline.dao.IPaymentPipelineDao;
  5 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  6 +import com.diligrp.cashier.pipeline.model.PaymentPipelineDO;
  7 +import com.diligrp.cashier.pipeline.model.WechatParam;
  8 +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager;
  9 +import com.diligrp.cashier.pipeline.type.ChannelType;
  10 +import com.diligrp.cashier.pipeline.type.WechatPipelineType;
  11 +import com.diligrp.cashier.shared.ErrorCode;
  12 +import com.diligrp.cashier.shared.service.LifeCycle;
  13 +import jakarta.annotation.Resource;
  14 +import org.slf4j.Logger;
  15 +import org.slf4j.LoggerFactory;
  16 +import org.springframework.stereotype.Component;
  17 +
  18 +import java.util.HashMap;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +import java.util.Optional;
  22 +import java.util.stream.Collectors;
  23 +
  24 +@Component("paymentPipelineManager")
  25 +public class PaymentPipelineManagerImpl extends LifeCycle implements IPaymentPipelineManager {
  26 +
  27 + private static final Logger LOG = LoggerFactory.getLogger(PaymentPipelineManagerImpl.class);
  28 +
  29 + private final Map<Long, IPipeline<?>> pipelines = new HashMap<>();
  30 +
  31 + @Resource
  32 + private IPaymentPipelineDao paymentPipelineDao;
  33 +
  34 + protected void doStart() throws Exception {
  35 + List<PaymentPipelineDO> pipelines = paymentPipelineDao.listPaymentPipelines();
  36 + for (PaymentPipelineDO pipeline : pipelines) {
  37 + if (ChannelType.WXPAY.equalTo(pipeline.getChannelId())) {
  38 + Optional<WechatParam> paramOpt = paymentPipelineDao.findWechatParam(pipeline.getPipelineId());
  39 + if (paramOpt.isPresent()) {
  40 + WechatParam param = paramOpt.get();
  41 + if (WechatPipelineType.DIRECT.equalTo(param.getType())) {
  42 + WechatPipeline paymentPipeline = new WechatDirectPipeline(pipeline.getMchId(), pipeline.getPipelineId(),
  43 + pipeline.getName(), pipeline.getUri(), pipeline.getParam());
  44 + paymentPipeline.configure(param.getMchId(), param.getAppId(), param.getAppSecret(), param.getSerialNo(),
  45 + param.getPrivateKey(), param.getWechatSerialNo(), param.getWechatPublicKey(), param.getApiV3Key());
  46 + registerPaymentPipeline(paymentPipeline);
  47 + } else if (WechatPipelineType.PARTNER.equalTo(param.getType())) {
  48 + WechatPipeline paymentPipeline = new WechatPartnerPipeline(pipeline.getMchId(), pipeline.getPipelineId(),
  49 + pipeline.getName(), pipeline.getUri(), pipeline.getParam());
  50 + paymentPipeline.configure(param.getMchId(), param.getAppId(), param.getAppSecret(), param.getSerialNo(),
  51 + param.getPrivateKey(), param.getWechatSerialNo(), param.getWechatPublicKey(), param.getApiV3Key());
  52 + registerPaymentPipeline(paymentPipeline);
  53 + } else {
  54 + LOG.warn("Ignore wechat payment pipeline: {}, because of invalid wechat pipeline type", pipeline.getName());
  55 + }
  56 + } else {
  57 + LOG.warn("Ignore wechat payment pipeline: {}, since wechat params not found", pipeline.getName());
  58 + }
  59 + } else if (ChannelType.DILIPAY.equalTo(pipeline.getChannelId())) { // 地利园区卡支付通道
  60 + DiliCardPipeline paymentPipeline = new DiliCardPipeline(pipeline.getMchId(), pipeline.getPipelineId(),
  61 + pipeline.getName(), pipeline.getUri(), pipeline.getParam());
  62 + registerPaymentPipeline(paymentPipeline);
  63 + } else {
  64 + LOG.warn("Ignore payment pipeline configuration: {}", pipeline.getName());
  65 + }
  66 + }
  67 + }
  68 +
  69 + @Override
  70 + public <T extends PaymentPipeline<?>> void registerPaymentPipeline(T pipeline) {
  71 + pipelines.put(pipeline.pipelineId(), pipeline);
  72 + LOG.info("{} payment pipeline registered", pipeline.name());
  73 + }
  74 +
  75 + @Override
  76 + public <T extends PaymentPipeline<?>> T findPipelineByMchId(long mchId, Class<T> type) {
  77 + List<T> allPipelines = pipelines.values().stream().filter(p -> p.mchId() == mchId)
  78 + .filter(p -> type.isAssignableFrom(p.getClass())).map(type::cast).toList();
  79 + if (allPipelines.isEmpty()) {
  80 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "该商户未配置此支付通道");
  81 + }
  82 + if (allPipelines.size() > 1) {
  83 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "该商户未配置支付路由");
  84 + }
  85 + return allPipelines.get(0);
  86 + }
  87 +
  88 + @Override
  89 + public <T extends PaymentPipeline<?>> T findPipelineByMchId(long mchId, ChannelType channel, Class<T> type) {
  90 + List<T> allPipelines = pipelines.values().stream().filter(p -> p.mchId() == mchId && p.supportedChannel() == channel)
  91 + .filter(p -> type.isAssignableFrom(p.getClass())).map(type::cast).toList();
  92 + if (allPipelines.isEmpty()) {
  93 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "该商户未配置此支付通道");
  94 + }
  95 + if (allPipelines.size() > 1) {
  96 + throw new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "该商户未配置支付路由");
  97 + }
  98 + return allPipelines.get(0);
  99 + }
  100 +
  101 + @Override
  102 + public <T extends PaymentPipeline<?>> List<T> listPipelines(long mchId, Class<T> type) {
  103 + return pipelines.values().stream().filter(p -> p.mchId() == mchId)
  104 + .filter(p -> type.isAssignableFrom(p.getClass())).map(type::cast).collect(Collectors.toList());
  105 + }
  106 +
  107 + @Override
  108 + public <T extends PaymentPipeline<?>> T findPipelineById(long pipelineId, Class<T> type) {
  109 + return pipelines.values().stream().filter(p -> p.pipelineId() == pipelineId && type.isAssignableFrom(p.getClass()))
  110 + .map(type::cast).findAny().orElseThrow(() -> new PaymentPipelineException(ErrorCode.OBJECT_NOT_FOUND, "系统不支持该支付通道"));
  111 + }
  112 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/type/ChannelType.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.type;
  2 +
  3 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  4 +import com.diligrp.cashier.shared.ErrorCode;
  5 +import com.diligrp.cashier.shared.type.IEnumType;
  6 +
  7 +import java.util.Arrays;
  8 +import java.util.Map;
  9 +import java.util.Optional;
  10 +import java.util.stream.Collectors;
  11 +import java.util.stream.Stream;
  12 +
  13 +/**
  14 + * 支持的渠道
  15 + */
  16 +public enum ChannelType implements IEnumType {
  17 +
  18 + NOP("未知渠道", 0),
  19 +
  20 + WXPAY("微信渠道", 10),
  21 +
  22 + ALIPAY("支付宝渠道", 11),
  23 +
  24 + DILIPAY("地利渠道", 18), // 地利园区卡支付
  25 +
  26 + ICBC("工商银行", 20),
  27 +
  28 + ABC("农业银行", 21),
  29 +
  30 + BOC("中国银行", 22),
  31 +
  32 + CCB("建设银行", 23),
  33 +
  34 + BCM("交通银行", 24),
  35 +
  36 + CITIC("中信银行", 25),
  37 +
  38 + CMB("招商银行", 27),
  39 +
  40 + SJBANK("盛京银行", 28),
  41 +
  42 + RCB("农商银行", 29),
  43 +
  44 + HZBANK("杭州银行", 30);
  45 +
  46 + private final String name;
  47 + private final int code;
  48 +
  49 + ChannelType(String name, int code) {
  50 + this.name = name;
  51 + this.code = code;
  52 + }
  53 +
  54 + public boolean equalTo(int code) {
  55 + return this.code == code;
  56 + }
  57 +
  58 + public static Optional<ChannelType> getType(int code) {
  59 + Stream<ChannelType> TYPES = Arrays.stream(ChannelType.values());
  60 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  61 + }
  62 +
  63 + public static ChannelType getByCode(int code) {
  64 + return getType(code).orElseThrow(() -> new PaymentPipelineException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "系统不支持此支付渠道"));
  65 + }
  66 +
  67 + public static String getName(int code) {
  68 + return getType(code).map(ChannelType::getName).orElse(null);
  69 + }
  70 +
  71 + public static Optional<ChannelType> getBankChannel(String bankCode) {
  72 + Stream<ChannelType> TYPES = Arrays.stream(ChannelType.values());
  73 + return TYPES.filter(type -> type.name().equalsIgnoreCase(bankCode)).findFirst();
  74 + }
  75 +
  76 + public static Map<Integer, String> getTypeNameMap() {
  77 + return Arrays.stream(ChannelType.values()).collect(Collectors.toMap(ChannelType::getCode, ChannelType::getName));
  78 + }
  79 +
  80 + @Override
  81 + public String getName() {
  82 + return name;
  83 + }
  84 +
  85 + @Override
  86 + public int getCode() {
  87 + return code;
  88 + }
  89 +
  90 + @Override
  91 + public String toString() {
  92 + return name;
  93 + }
  94 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/type/PaymentState.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +/**
  11 + * 支付状态列表
  12 + */
  13 +public enum PaymentState implements IEnumType {
  14 +
  15 + PENDING("待支付", 1),
  16 +
  17 + PROCESSING("支付中", 2),
  18 +
  19 + SUCCESS("支付成功", 4),
  20 +
  21 + FAILED("支付失败", 6);
  22 +
  23 + private final String name;
  24 + private final int code;
  25 +
  26 + PaymentState(String name, int code) {
  27 + this.name = name;
  28 + this.code = code;
  29 + }
  30 +
  31 + public boolean equalTo(int code) {
  32 + return this.code == code;
  33 + }
  34 +
  35 + public static Optional<PaymentState> getState(int code) {
  36 + Stream<PaymentState> STATES = Arrays.stream(PaymentState.values());
  37 + return STATES.filter(state -> state.getCode() == code).findFirst();
  38 + }
  39 +
  40 + public static String getName(int code) {
  41 + return getState(code).map(PaymentState::getName).orElse(null);
  42 + }
  43 +
  44 + public static List<PaymentState> getStates() {
  45 + return Arrays.asList(PaymentState.values());
  46 + }
  47 +
  48 + @Override
  49 + public String getName() {
  50 + return name;
  51 + }
  52 +
  53 + @Override
  54 + public int getCode() {
  55 + return code;
  56 + }
  57 +
  58 + @Override
  59 + public String toString() {
  60 + return name;
  61 + }
  62 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/type/PipelineType.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.Map;
  7 +import java.util.Optional;
  8 +import java.util.stream.Collectors;
  9 +import java.util.stream.Stream;
  10 +
  11 +/**
  12 + * 支持的支付通道
  13 + */
  14 +public enum PipelineType implements IEnumType {
  15 + DIRECT_BANK("银企直连通道", 1),
  16 +
  17 + ONLINE_BANK("聚合支付通道", 2),
  18 +
  19 + WECHAT_PAY("微信支付通道", 10),
  20 +
  21 + ALI_PAY("支付宝通道", 11);
  22 +
  23 + private final String name;
  24 + private final int code;
  25 +
  26 + PipelineType(String name, int code) {
  27 + this.name = name;
  28 + this.code = code;
  29 + }
  30 +
  31 + public boolean equalTo(int code) {
  32 + return this.code == code;
  33 + }
  34 +
  35 + public static Optional<PipelineType> getType(int code) {
  36 + Stream<PipelineType> TYPES = Arrays.stream(PipelineType.values());
  37 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  38 + }
  39 +
  40 + public static String getName(int code) {
  41 + return getType(code).map(PipelineType::getName).orElse(null);
  42 + }
  43 +
  44 + public static Map<Integer, String> getTypeNameMap() {
  45 + return Arrays.stream(PipelineType.values()).collect(Collectors.toMap(PipelineType::getCode, PipelineType::getName));
  46 + }
  47 +
  48 + @Override
  49 + public String getName() {
  50 + return name;
  51 + }
  52 +
  53 + @Override
  54 + public int getCode() {
  55 + return code;
  56 + }
  57 +
  58 + @Override
  59 + public String toString() {
  60 + return name;
  61 + }
  62 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/type/WechatPipelineType.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.Optional;
  7 +import java.util.stream.Stream;
  8 +
  9 +/**
  10 + * 微信支付通道类型
  11 + */
  12 +public enum WechatPipelineType implements IEnumType {
  13 + DIRECT("直连通道", 1),
  14 +
  15 + PARTNER("服务商通道", 2);
  16 +
  17 + private final String name;
  18 + private final int code;
  19 +
  20 + WechatPipelineType(String name, int code) {
  21 + this.name = name;
  22 + this.code = code;
  23 + }
  24 +
  25 + public boolean equalTo(int code) {
  26 + return this.code == code;
  27 + }
  28 +
  29 + public static Optional<WechatPipelineType> getType(int code) {
  30 + Stream<WechatPipelineType> TYPES = Arrays.stream(WechatPipelineType.values());
  31 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  32 + }
  33 +
  34 + public static String getName(int code) {
  35 + return getType(code).map(WechatPipelineType::getName).orElse(null);
  36 + }
  37 +
  38 + @Override
  39 + public String getName() {
  40 + return name;
  41 + }
  42 +
  43 + @Override
  44 + public int getCode() {
  45 + return code;
  46 + }
  47 +
  48 + @Override
  49 + public String toString() {
  50 + return name;
  51 + }
  52 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/util/WechatConstants.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.util;
  2 +
  3 +/**
  4 + * 微信支付平台常量列表
  5 + */
  6 +public class WechatConstants {
  7 + // 签名算法 - 请求时签名使用
  8 + public static final String SIGN_ALGORITHM = "SHA256WithRSA";
  9 + // 密钥算法 - 请求时签名使用
  10 + public static final String RSA_ALGORITHM = "RSA";
  11 + // 加密算法 - 证书和回调报文解密使用
  12 + public static final String AES_ALGORITHM = "AES";
  13 + // 加密算法 - 证书和回调报文解密使用
  14 + public static final String AESGCM_ALGORITHM = "AES/GCM/NoPadding";
  15 + // 密钥长度 - 证书和回调报文解密使用
  16 + public static final int KEY_LENGTH_BYTE = 32;
  17 + public static final int TAG_LENGTH_BIT = 128;
  18 +
  19 + // HTTP常量
  20 + public static final String HTTP_GET = "GET";
  21 + public static final String HTTP_POST = "POST";
  22 +
  23 + // 微信平台HTTP请求常量
  24 + public static final String HEADER_USER_AGENT = "User-Agent";
  25 + public static final String USER_AGENT = "DiliPay-HttpClient/2.4.0 Java/11.0.6";
  26 + public static final String HEADER_AUTHORIZATION = "Authorization";
  27 + public static final String HEADER_ACCEPT = "Accept";
  28 + public static final String ACCEPT_JSON = "application/json";
  29 +
  30 + // 微信平台HTTP响应常量
  31 + public static final String HEADER_TIMESTAMP = "Wechatpay-Timestamp";
  32 + public static final String HEADER_NONCE = "Wechatpay-Nonce";
  33 + public static final String HEADER_SERIAL_NO = "Wechatpay-Serial";
  34 + public static final String HEADER_SIGNATURE = "Wechatpay-Signature";
  35 +
  36 + public static String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss+08:00";
  37 + public static String NOTIFY_EVENT_TYPE = "TRANSACTION.SUCCESS";
  38 + public static String REFUND_EVENT_TYPE = "REFUND.SUCCESS";
  39 +
  40 + // 支付状态常量列表
  41 + public static final String STATE_SUCCESS = "SUCCESS"; // 支付成功
  42 + public static final String STATE_REFUND = "REFUND"; // 转入退款
  43 + public static final String STATE_NOTPAY = "NOTPAY"; // 未支付
  44 + public static final String STATE_CLOSED = "CLOSED"; // 已关闭
  45 + public static final String STATE_REVOKED = "REVOKED"; // 已撤销(付款码支付)
  46 + public static final String STATE_USERPAYING = "USERPAYING"; // 用户支付中(付款码支付)
  47 + public static final String STATE_PAYERROR = "PAYERROR"; // 支付失败(其他原因,如银行返回失败)
  48 +
  49 + // 退款状态常量列表
  50 + public static final String REFUND_SUCCESS = "SUCCESS"; // 退款成功
  51 + public static final String REFUND_CLOSED = "CLOSED"; // 退款关闭
  52 + public static final String REFUND_PROCESSING = "PROCESSING"; // 退款处理中
  53 + public static final String REFUND_ABNORMAL = "ABNORMAL"; // 退款异常
  54 +
  55 + // 预支付订单有效期持续时间,超过这个时间将不能支付
  56 + public static final int PAY_DURATION_MINS = 8;
  57 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/util/WechatSignatureUtils.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.util;
  2 +
  3 +import com.diligrp.cashier.shared.util.ObjectUtils;
  4 +import com.diligrp.cashier.shared.util.RandomUtils;
  5 +import org.slf4j.Logger;
  6 +import org.slf4j.LoggerFactory;
  7 +
  8 +import javax.crypto.Cipher;
  9 +import javax.crypto.spec.GCMParameterSpec;
  10 +import javax.crypto.spec.SecretKeySpec;
  11 +import java.nio.charset.StandardCharsets;
  12 +import java.security.PrivateKey;
  13 +import java.security.PublicKey;
  14 +import java.security.Signature;
  15 +import java.util.Base64;
  16 +import java.util.stream.Collectors;
  17 +import java.util.stream.Stream;
  18 +
  19 +/**
  20 + * 微信签名验签工具类
  21 + */
  22 +public class WechatSignatureUtils {
  23 +
  24 + private static final Logger LOG = LoggerFactory.getLogger(WechatSignatureUtils.class);
  25 +
  26 + // 常用字符常量
  27 + private static String EMPTY_STR = "";
  28 + private static String ENTER_STR = "\n";
  29 + // 微信认证类型 - 请求时使用
  30 + private static final String TOKEN_FORMAT = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%s\",serial_no=\"%s\",signature=\"%s\"";
  31 +
  32 + /**
  33 + * 获取微信支付的认证信息和签名信息
  34 + *
  35 + * @param mchId - 服务商商户号
  36 + * @param method - 请求方法: GET/POST/PUT/**
  37 + * @param uri - 请求URL
  38 + * @param payload - POST请求消息体
  39 + * @param privateKey - Base64编码的私钥
  40 + * @param keySerialNo - 商户API证书(公钥)序列号
  41 + * @return 认证信息和签名信息,通过Http Header传输,格式 Authorization: 认证类型 签名信息
  42 + * @throws Exception 签名异常
  43 + */
  44 + public static String authorization(String mchId, String method, String uri, String payload,
  45 + PrivateKey privateKey, String keySerialNo) throws Exception {
  46 + String nonce = RandomUtils.randomString(32);
  47 + String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
  48 + // No need handle uri here, the caller ensure the normal and valid URL
  49 + // URL url = new URL("https://www.diligrp.com/ddd?aaa=x"); url.getPath() = /ddd url.getQuery() = "aa=x"
  50 + String message = Stream.of(method, uri, timestamp, nonce, payload).map(o -> o == null ? EMPTY_STR : o)
  51 + .collect(Collectors.joining(ENTER_STR, EMPTY_STR, ENTER_STR));
  52 + LOG.debug("\n---Wechat Platform Sign Param---\n{}\n--------------------------------", message);
  53 + String signature = signature(message, privateKey);
  54 + return String.format(TOKEN_FORMAT, mchId, nonce, timestamp, keySerialNo, signature);
  55 + }
  56 +
  57 + /**
  58 + * @see WechatSignatureUtils#authorization(String, String, String, String, PrivateKey, String)
  59 + */
  60 + public static String authorization(String mchId, String method, String uri, PrivateKey privateKey,
  61 + String keySerialNo) throws Exception {
  62 + return authorization(mchId, method, uri, null, privateKey, keySerialNo);
  63 + }
  64 +
  65 + /**
  66 + * 微信支付签名
  67 + *
  68 + * @param message - 被签名字符串
  69 + * @param privateKey - 服务商私钥
  70 + * @return 签名数据
  71 + * @throws Exception 签名异常
  72 + */
  73 + public static String signature(String message, PrivateKey privateKey) throws Exception {
  74 + byte[] packet = message.getBytes(StandardCharsets.UTF_8);
  75 + Signature signature = Signature.getInstance(WechatConstants.SIGN_ALGORITHM);
  76 + signature.initSign(privateKey);
  77 + signature.update(packet);
  78 + return Base64.getEncoder().encodeToString(signature.sign());
  79 + }
  80 +
  81 + /**
  82 + * 微信支付数据验签
  83 + *
  84 + * @param payload - POST请求消息体
  85 + * @param timestamp - 时间戳
  86 + * @param nonce - 随机字符串
  87 + * @param sign - 签名数据
  88 + * @param publicKey - 微信支付平台公钥
  89 + * @return 是否验签成功
  90 + * @throws Exception 验签异常
  91 + */
  92 + public static boolean verify(String payload, String timestamp, String nonce, String sign, PublicKey publicKey) throws Exception {
  93 + byte[] signBytes = Base64.getDecoder().decode(sign);
  94 + StringBuilder message = new StringBuilder();
  95 + message.append(timestamp).append("\n").append(nonce).append("\n").append(payload).append("\n");
  96 + byte[] packet = message.toString().getBytes(StandardCharsets.UTF_8);
  97 +
  98 + Signature signature = Signature.getInstance(WechatConstants.SIGN_ALGORITHM);
  99 + signature.initVerify(publicKey);
  100 + signature.update(packet);
  101 + return signature.verify(signBytes);
  102 + }
  103 +
  104 + /**
  105 + * 报文解密, 证书和微信回调时使用
  106 + *
  107 + * @param payload - Base64编码的待解密的消息
  108 + * @param nonce - 解密使用随机字符串
  109 + * @param extraData - 解密使用的附加数据包, 可为空
  110 + * @param apiV3Key - 微信平台apiV3Key
  111 + * @return 消息明文
  112 + * @throws Exception 解密异常
  113 + */
  114 + public static String decrypt(String payload, String nonce, String extraData, SecretKeySpec apiV3Key) throws Exception {
  115 + byte[] packet = Base64.getDecoder().decode(payload);
  116 + byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
  117 +
  118 + Cipher cipher = Cipher.getInstance(WechatConstants.AESGCM_ALGORITHM);
  119 + GCMParameterSpec spec = new GCMParameterSpec(WechatConstants.TAG_LENGTH_BIT, nonceBytes);
  120 + cipher.init(Cipher.DECRYPT_MODE, apiV3Key, spec);
  121 + if (ObjectUtils.isNotEmpty(extraData)) {
  122 + cipher.updateAAD(extraData.getBytes(StandardCharsets.UTF_8));
  123 + }
  124 +
  125 + return new String(cipher.doFinal(packet), StandardCharsets.UTF_8);
  126 + }
  127 +}
... ...
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/util/WechatStateUtils.java 0 → 100644
  1 +package com.diligrp.cashier.pipeline.util;
  2 +
  3 +import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  4 +import com.diligrp.cashier.pipeline.type.PaymentState;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +public final class WechatStateUtils {
  8 + public static PaymentState getPaymentState(String wechatState) {
  9 + PaymentState state = switch (wechatState) {
  10 + // 转入退款WechatConstants.STATE_REFUND也认为是支付成功,只有支付成功的才会转入STATE_REFUND状态
  11 + // 目前支付系统发生退款时,不会修改原来支付记录upay_trade_payment,只会修改upay_trade_order
  12 + case WechatConstants.STATE_SUCCESS, WechatConstants.STATE_REFUND -> PaymentState.SUCCESS;
  13 + case WechatConstants.STATE_NOTPAY -> PaymentState.PENDING;
  14 + case WechatConstants.STATE_USERPAYING -> PaymentState.PROCESSING;
  15 + case WechatConstants.STATE_CLOSED, WechatConstants.STATE_REVOKED, WechatConstants.STATE_PAYERROR -> PaymentState.FAILED;
  16 + default ->
  17 + throw new PaymentPipelineException(ErrorCode.INVALID_OBJECT_STATE, "未知的微信支付状态: " + wechatState);
  18 + };
  19 + return state;
  20 + }
  21 +
  22 + public static boolean isPendingState(String wechatState) {
  23 + return switch (wechatState) {
  24 + case WechatConstants.STATE_NOTPAY, WechatConstants.STATE_USERPAYING -> true;
  25 + default -> false;
  26 + };
  27 + }
  28 +
  29 + public static PaymentState getRefundState(String wechatState) {
  30 + return switch (wechatState) {
  31 + // 目前支付系统发生退款时,不会修改原来支付记录upay_trade_payment,只会修改upay_trade_order
  32 + case WechatConstants.REFUND_SUCCESS -> PaymentState.SUCCESS;
  33 + case WechatConstants.REFUND_PROCESSING -> PaymentState.PROCESSING;
  34 + case WechatConstants.REFUND_CLOSED, WechatConstants.REFUND_ABNORMAL -> PaymentState.FAILED;
  35 + default ->
  36 + throw new PaymentPipelineException(ErrorCode.INVALID_OBJECT_STATE, "未知的微信退款状态: " + wechatState);
  37 + };
  38 + }
  39 +}
... ...
cashier-pipeline/src/main/resources/com/diligrp/cashier/dao/mapper/IPaymentPipelineDao.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8" ?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4 +
  5 +<mapper namespace="com.diligrp.cashier.pipeline.dao.IPaymentPipelineDao">
  6 + <resultMap id="PaymentPipelineMap" type="com.diligrp.cashier.pipeline.model.PaymentPipelineDO">
  7 + <id column="id" property="id"/>
  8 + <result column="mch_id" property="mchId"/>
  9 + <result column="pipeline_id" property="pipelineId"/>
  10 + <result column="channel_id" property="channelId"/>
  11 + <result column="type" property="type"/>
  12 + <result column="name" property="name"/>
  13 + <result column="uri" property="uri"/>
  14 + <result column="param" property="param"/>
  15 + <result column="state" property="state"/>
  16 + <result column="created_time" property="createdTime"/>
  17 + <result column="modified_time" property="modifiedTime"/>
  18 + </resultMap>
  19 +
  20 + <resultMap id="WechatParamMap" type="com.diligrp.cashier.pipeline.model.WechatParam">
  21 + <id column="id" property="id"/>
  22 + <result column="pipeline_id" property="pipelineId"/>
  23 + <result column="mch_id" property="mchId"/>
  24 + <result column="app_id" property="appId"/>
  25 + <result column="app_secret" property="appSecret"/>
  26 + <result column="serial_no" property="serialNo"/>
  27 + <result column="private_key" property="privateKey"/>
  28 + <result column="wechat_serial_no" property="wechatSerialNo"/>
  29 + <result column="wechat_public_key" property="wechatPublicKey"/>
  30 + <result column="api_v3_key" property="apiV3Key"/>
  31 + <result column="type" property="type"/>
  32 + <result column="created_time" property="createdTime"/>
  33 + </resultMap>
  34 +
  35 + <select id="listPaymentPipelines" resultMap="PaymentPipelineMap">
  36 + SELECT * FROM upay_payment_pipeline
  37 + </select>
  38 +
  39 + <select id="findWechatParam" parameterType="long" resultMap="WechatParamMap">
  40 + SELECT * FROM upay_wechat_param WHERE pipeline_id = #{pipelineId}
  41 + </select>
  42 +</mapper>
... ...
cashier-shared/build.gradle
... ... @@ -2,12 +2,12 @@ dependencies {
2 2 //api 'org.springframework.boot:spring-boot-starter-data-mongodb'
3 3 api libs.mybatis.starter
4 4 api 'org.springframework.boot:spring-boot-starter-data-redis'
5   - api "org.redisson:redisson-spring-boot-starter:${libs.versions.redissonVersion.get()}"
  5 + api libs.redisson.starter
6 6 api 'org.springframework.boot:spring-boot-starter-amqp'
7 7 api 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
8 8 api 'org.springframework.cloud:spring-cloud-starter-openfeign'
9 9 api libs.cache.caffeine
10   - api libs.collections4
  10 + api libs.common.collections4
11 11  
12 12 runtimeOnly libs.mysql.driver
13 13 }
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/TradeConfiguration.java 0 → 100644
  1 +package com.diligrp.cashier.trade;
  2 +
  3 +import org.springframework.context.annotation.ComponentScan;
  4 +import org.springframework.context.annotation.Configuration;
  5 +
  6 +@Configuration
  7 +@ComponentScan("com.diligrp.cashier.trade")
  8 +public class TradeConfiguration {
  9 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/exception/TradePaymentException.java 0 → 100644
  1 +package com.diligrp.cashier.trade.exception;
  2 +
  3 +import com.diligrp.cashier.shared.exception.PlatformServiceException;
  4 +
  5 +public class TradePaymentException extends PlatformServiceException {
  6 +
  7 + public TradePaymentException(String message) {
  8 + super(message);
  9 + }
  10 +
  11 + public TradePaymentException(String code, String message) {
  12 + super(code, message);
  13 + }
  14 +
  15 + public TradePaymentException(String message, Throwable ex) {
  16 + super(message, ex);
  17 + }
  18 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/CashierDesk.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +import com.diligrp.cashier.shared.type.IEnumType;
  5 +import com.diligrp.cashier.trade.exception.TradePaymentException;
  6 +
  7 +import java.util.Arrays;
  8 +import java.util.Objects;
  9 +import java.util.Optional;
  10 +import java.util.stream.Stream;
  11 +
  12 +/**
  13 + * 终端来源
  14 + */
  15 +public enum CashierDesk implements IEnumType {
  16 +
  17 + NOP("无收银台", 0),
  18 +
  19 + PC("PC收银台", 1),
  20 +
  21 + MINIPRO("小程序收银台", 2),
  22 +
  23 + H5("H5收银台", 2),
  24 +
  25 + APP("APP收银台", 3);
  26 +
  27 + private final String name;
  28 + private final int code;
  29 +
  30 + CashierDesk(String name, int code) {
  31 + this.name = name;
  32 + this.code = code;
  33 + }
  34 +
  35 + public boolean equalTo(int code) {
  36 + return this.code == code;
  37 + }
  38 +
  39 + public static Optional<CashierDesk> getType(int code) {
  40 + Stream<CashierDesk> TYPES = Arrays.stream(CashierDesk.values());
  41 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  42 + }
  43 +
  44 + public static CashierDesk getByCode(int code) {
  45 + return getType(code).orElseThrow(() -> new TradePaymentException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "系统暂不支持此收银台类型"));
  46 + }
  47 +
  48 + public static void validateIfNonNull(Integer code) {
  49 + if (Objects.nonNull(code)) {
  50 + Stream<CashierDesk> TYPES = Arrays.stream(CashierDesk.values());
  51 + TYPES.filter(type -> type.getCode() == code).findFirst().orElseThrow(() -> new IllegalArgumentException("Invalid cashier desk"));
  52 + }
  53 + }
  54 +
  55 + public static String getName(int code) {
  56 + return getType(code).map(CashierDesk::getName).orElse(null);
  57 + }
  58 +
  59 + @Override
  60 + public String getName() {
  61 + return name;
  62 + }
  63 +
  64 + @Override
  65 + public int getCode() {
  66 + return code;
  67 + }
  68 +
  69 + @Override
  70 + public String toString() {
  71 + return name;
  72 + }
  73 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/OutPaymentType.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +import com.diligrp.cashier.shared.type.IEnumType;
  5 +import com.diligrp.cashier.trade.exception.TradePaymentException;
  6 +
  7 +import java.util.Arrays;
  8 +import java.util.Optional;
  9 +import java.util.stream.Stream;
  10 +
  11 +/**
  12 + * 聚合支付时实际使用的支付方式
  13 + */
  14 +public enum OutPaymentType implements IEnumType {
  15 + NOP("其他方式", 0),
  16 +
  17 + WXPAY("微信支付", 10),
  18 +
  19 + ALIPAY("支付宝支付", 11),
  20 +
  21 + UPAY("银联支付", 12),
  22 +
  23 + APP("掌银支付", 13),
  24 +
  25 + BANKCARD("银行卡支付", 20);
  26 +
  27 + private final String name;
  28 + private final int code;
  29 +
  30 + OutPaymentType(String name, int code) {
  31 + this.name = name;
  32 + this.code = code;
  33 + }
  34 +
  35 + public boolean equalTo(int code) {
  36 + return this.code == code;
  37 + }
  38 +
  39 + public static Optional<OutPaymentType> getType(int code) {
  40 + Stream<OutPaymentType> TYPES = Arrays.stream(OutPaymentType.values());
  41 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  42 + }
  43 +
  44 + public static OutPaymentType getByCode(int code) {
  45 + return getType(code).orElseThrow(() -> new TradePaymentException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "系统暂不支持该支付方式"));
  46 + }
  47 +
  48 + public static String getName(int code) {
  49 + return getType(code).map(OutPaymentType::getName).orElse(null);
  50 + }
  51 +
  52 + @Override
  53 + public String getName() {
  54 + return name;
  55 + }
  56 +
  57 + @Override
  58 + public int getCode() {
  59 + return code;
  60 + }
  61 +
  62 + @Override
  63 + public String toString() {
  64 + return name;
  65 + }
  66 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/PaymentType.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +import com.diligrp.cashier.shared.type.IEnumType;
  5 +import com.diligrp.cashier.trade.exception.TradePaymentException;
  6 +
  7 +import java.util.Arrays;
  8 +import java.util.Optional;
  9 +import java.util.stream.Stream;
  10 +
  11 +/**
  12 + * 支付方式
  13 + */
  14 +public enum PaymentType implements IEnumType {
  15 + NATIVE("扫码支付", 1), // 客户扫商户收款码
  16 +
  17 + QRCODE("付款码支付", 2), // 商户扫客户付款码
  18 +
  19 + MINI_PRO("小程序支付", 3); // 目前特指微信小程序支付
  20 +
  21 + private final String name;
  22 + private final int code;
  23 +
  24 + PaymentType(String name, int code) {
  25 + this.name = name;
  26 + this.code = code;
  27 + }
  28 +
  29 + public boolean equalTo(int code) {
  30 + return this.code == code;
  31 + }
  32 +
  33 + public static Optional<PaymentType> getType(int code) {
  34 + Stream<PaymentType> TYPES = Arrays.stream(PaymentType.values());
  35 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  36 + }
  37 +
  38 + public static PaymentType getByCode(int code) {
  39 + return getType(code).orElseThrow(() -> new TradePaymentException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "系统不支持此类支付方式"));
  40 + }
  41 +
  42 + public static String getName(int code) {
  43 + return getType(code).map(PaymentType::getName).orElse(null);
  44 + }
  45 +
  46 + @Override
  47 + public String getName() {
  48 + return name;
  49 + }
  50 +
  51 + @Override
  52 + public int getCode() {
  53 + return code;
  54 + }
  55 +
  56 + @Override
  57 + public String toString() {
  58 + return name;
  59 + }
  60 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/SnowflakeKey.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.assistant.service.impl.SnowflakeKeyManager;
  4 +
  5 +public enum SnowflakeKey implements SnowflakeKeyManager.SnowflakeKey {
  6 + TRADE_ID,
  7 +
  8 + PAYMENT_ID
  9 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/TradeState.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +/**
  11 + * 交易状态列表
  12 + */
  13 +public enum TradeState implements IEnumType {
  14 +
  15 + PENDING("待处理", 1),
  16 +
  17 + SUCCESS("交易成功", 4),
  18 +
  19 + REFUND("交易退款", 5),
  20 +
  21 + FAILED("交易失败", 6),
  22 +
  23 + CLOSED("交易关闭", 7);
  24 +
  25 + private final String name;
  26 + private final int code;
  27 +
  28 + TradeState(String name, int code) {
  29 + this.name = name;
  30 + this.code = code;
  31 + }
  32 +
  33 + public boolean equalTo(int code) {
  34 + return this.code == code;
  35 + }
  36 +
  37 + public static Optional<TradeState> getState(int code) {
  38 + Stream<TradeState> STATES = Arrays.stream(TradeState.values());
  39 + return STATES.filter(state -> state.getCode() == code).findFirst();
  40 + }
  41 +
  42 + public static String getName(int code) {
  43 + return getState(code).map(TradeState::getName).orElse(null);
  44 + }
  45 +
  46 + public static List<TradeState> getStates() {
  47 + return Arrays.asList(TradeState.values());
  48 + }
  49 +
  50 + /**
  51 + * 交易订单是否允许退款; 允许多次交易退款
  52 + *
  53 + * @param state - 交易订单状态
  54 + * @return 是否允许交易退款
  55 + */
  56 + public static boolean forRefund(int state) {
  57 + return state == TradeState.SUCCESS.getCode() || state == TradeState.REFUND.getCode();
  58 + }
  59 +
  60 + /**
  61 + * 交易订单是否允许撤销
  62 + *
  63 + * @param state - 交易订单状态
  64 + * @return 是否允许撤销
  65 + */
  66 + public static boolean forCancel(int state) {
  67 + return state == TradeState.SUCCESS.getCode();
  68 + }
  69 +
  70 + @Override
  71 + public String getName() {
  72 + return name;
  73 + }
  74 +
  75 + @Override
  76 + public int getCode() {
  77 + return code;
  78 + }
  79 +
  80 + @Override
  81 + public String toString() {
  82 + return name;
  83 + }
  84 +}
... ...
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/TradeType.java 0 → 100644
  1 +package com.diligrp.cashier.trade.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +/**
  11 + * 交易类型列表
  12 + */
  13 +public enum TradeType implements IEnumType {
  14 +
  15 + TRADE("交易", 10),
  16 +
  17 + REFUND("退款", 20);
  18 +
  19 + private final String name;
  20 + private final int code;
  21 +
  22 + TradeType(String name, int code) {
  23 + this.name = name;
  24 + this.code = code;
  25 + }
  26 +
  27 + public boolean equalTo(int code) {
  28 + return this.code == code;
  29 + }
  30 +
  31 + public static Optional<TradeType> getType(int code) {
  32 + Stream<TradeType> TYPES = Arrays.stream(TradeType.values());
  33 + return TYPES.filter(type -> type.getCode() == code).findFirst();
  34 + }
  35 +
  36 + public static String getName(int code) {
  37 + return getType(code).map(TradeType::getName).orElse(null);
  38 + }
  39 +
  40 + public static List<TradeType> getTypes() {
  41 + return Arrays.asList(TradeType.values());
  42 + }
  43 +
  44 + @Override
  45 + public String getName() {
  46 + return name;
  47 + }
  48 +
  49 + @Override
  50 + public int getCode() {
  51 + return code;
  52 + }
  53 +
  54 + @Override
  55 + public String toString() {
  56 + return name;
  57 + }
  58 +}
... ...
gradle/libs.versions.toml
... ... @@ -17,8 +17,9 @@ spring-cloud-nacos-discovery = {module = &#39;com.alibaba.cloud:spring-cloud-starter
17 17 spring-cloud-nacos-config = {module = 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config', version.ref = 'springCloudAlibabaVersion'}
18 18 mybatis-starter = {module = 'org.mybatis.spring.boot:mybatis-spring-boot-starter', version.ref = 'mybatisVersion'}
19 19 mysql-driver = {module = 'mysql:mysql-connector-java', version.ref = 'mysqlDriverVersion'}
  20 +redisson-starter = {module = 'org.redisson:redisson-spring-boot-starter', version.ref = 'redissonVersion'}
20 21 cache-caffeine = {module = 'com.github.ben-manes.caffeine:caffeine', version.ref = 'caffeineVersion'}
21   -collections4 = {module = 'org.apache.commons:commons-collections4', version.ref = 'collections4Version'}
  22 +common-collections4 = {module = 'org.apache.commons:commons-collections4', version.ref = 'collections4Version'}
22 23  
23 24 [plugins]
24 25 spring-boot = {id = 'org.springframework.boot', version.ref = 'springBootVersion'}
... ...
scripts/dili-cashier.sql
  1 +-- --------------------------------------------------------------------
  2 +-- 商户表
  3 +-- 说明:商户表用于维护接入支付的商户
  4 +-- --------------------------------------------------------------------
  5 +DROP TABLE IF EXISTS `upay_merchant`;
  6 +CREATE TABLE `upay_merchant` (
  7 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  8 + `mch_id` BIGINT NOT NULL COMMENT '商户ID',
  9 + `name` VARCHAR(60) NOT NULL COMMENT '商户名称',
  10 + `access_token` VARCHAR(40) NOT NULL COMMENT '访问令牌',
  11 + `param` JSON COMMENT '参数配置',
  12 + `address` VARCHAR(128) COMMENT '商户地址',
  13 + `linkman` VARCHAR(40) COMMENT '联系人',
  14 + `telephone` VARCHAR(20) COMMENT '电话号码',
  15 + `state` TINYINT UNSIGNED NOT NULL COMMENT '商户状态',
  16 + `created_time` DATETIME COMMENT '创建时间',
  17 + `modified_time` DATETIME COMMENT '修改时间',
  18 + PRIMARY KEY (`id`),
  19 + UNIQUE KEY `uk_merchant_mchId` (`mch_id`) USING BTREE
  20 +) ENGINE=InnoDB COMMENT '支付商户';
  21 +
  22 +-- --------------------------------------------------------------------
  23 +-- 支付订单数据模型
  24 +-- --------------------------------------------------------------------
  25 +DROP TABLE IF EXISTS `upay_trade_order`;
  26 +CREATE TABLE `upay_trade_order` (
  27 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  28 + `mch_id` BIGINT NOT NULL COMMENT '商户ID',
  29 + `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID',
  30 + `type` TINYINT UNSIGNED NOT NULL COMMENT '交易类型', -- 购买会员
  31 +-- `channel_id` TINYINT UNSIGNED NOT NULL COMMENT '支付渠道',
  32 + `out_trade_no` VARCHAR(40) COMMENT '外部流水号', -- 商户流水号
  33 + `amount` BIGINT NOT NULL COMMENT '金额-分',
  34 + `max_amount` BIGINT NOT NULL COMMENT '初始金额-分',
  35 + `goods` VARCHAR(128) COMMENT '商品描述',
  36 + `order_timeout` INTEGER UNSIGNED NOT NULL COMMENT '超时间隔时间-秒',
  37 + `state` TINYINT UNSIGNED NOT NULL COMMENT '交易状态',
  38 + `description` VARCHAR(128) COMMENT '交易备注',
  39 + `attach` VARCHAR(255) COMMENT '附加数据',
  40 + `cashier_desk` TINYINT UNSIGNED NOT NULL COMMENT '收银台类型',
  41 + `version` INTEGER UNSIGNED NOT NULL COMMENT '数据版本号',
  42 + `created_time` DATETIME COMMENT '创建时间',
  43 + `modified_time` DATETIME COMMENT '修改时间',
  44 + PRIMARY KEY (`id`),
  45 + UNIQUE KEY `uk_trade_order_tradeId` (`trade_id`) USING BTREE,
  46 + KEY `idx_trade_order_outTradeNo` (`out_trade_no`, `mch_id`) USING BTREE,
  47 + KEY `idx_trade_order_createdTime` (`created_time`) USING BTREE
  48 +) ENGINE=InnoDB COMMENT '支付订单';
  49 +
  50 +-- --------------------------------------------------------------------
  51 +-- 支付申请数据模型
  52 +-- --------------------------------------------------------------------
  53 +DROP TABLE IF EXISTS `upay_online_payment`;
  54 +CREATE TABLE `upay_online_payment` (
  55 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  56 + `out_mch_id` VARCHAR(40) COMMENT '外部商户号',
  57 + `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID',
  58 + `type` TINYINT UNSIGNED NOT NULL COMMENT '交易类型',
  59 + `payment_id` VARCHAR(40) NOT NULL COMMENT '支付ID',
  60 + `channel_id` TINYINT UNSIGNED NOT NULL COMMENT '支付通道',
  61 + `pay_type` TINYINT UNSIGNED NOT NULL COMMENT '支付方式', -- NATIVE MINIPRO
  62 + `pipeline_id` BIGINT NOT NULL COMMENT '通道ID',
  63 + `goods` VARCHAR(128) NOT NULL COMMENT '商品描述',
  64 + `amount` BIGINT NOT NULL COMMENT '申请金额-分',
  65 + `object_id` VARCHAR(60) COMMENT '操作对象', -- prepareId,二维码,或退款时的原单号
  66 + `payer_id` VARCHAR(128) COMMENT '支付方', -- 比如微信openId,或json格式数据
  67 + `finish_time` DATETIME COMMENT '支付时间',
  68 + `out_trade_no` VARCHAR(40) COMMENT '通道流水号',
  69 + `out_pay_type` TINYINT UNSIGNED NOT NULL COMMENT '实际支付方式', -- 银行聚合支付时使用
  70 + `state` TINYINT UNSIGNED NOT NULL COMMENT '申请状态',
  71 + `notify_uri` VARCHAR(128) COMMENT '业务回调链接',
  72 + `description` VARCHAR(256) COMMENT '交易备注',
  73 + `version` INTEGER UNSIGNED NOT NULL COMMENT '数据版本号',
  74 + `created_time` DATETIME COMMENT '创建时间',
  75 + `modified_time` DATETIME COMMENT '修改时间',
  76 + PRIMARY KEY (`id`),
  77 + UNIQUE KEY `uk_online_payment_paymentId` (`payment_id`) USING BTREE,
  78 + KEY `idx_online_payment_tradeId` (`trade_id`) USING BTREE,
  79 + KEY `idx_online_payment_finishTime` (`finish_time`, `state`) USING BTREE,
  80 + KEY `idx_online_payment_outTradeNo` (`out_trade_no`) USING BTREE,
  81 + KEY `idx_online_payment_createdTime` (`created_time`) USING BTREE
  82 +) ENGINE=InnoDB COMMENT '支付申请';
  83 +
  84 +-- --------------------------------------------------------------------
  85 +-- 通道管理配置数据模型
  86 +-- --------------------------------------------------------------------
  87 +DROP TABLE IF EXISTS `upay_payment_pipeline`;
  88 +CREATE TABLE `upay_payment_pipeline` (
  89 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  90 + `mch_id` BIGINT NOT NULL COMMENT '商户ID',
  91 + `pipeline_id` BIGINT NOT NULL COMMENT '通道ID',
  92 + `channel_id` TINYINT UNSIGNED NOT NULL COMMENT '渠道', -- 建设银行、农业银行、微信、支付宝
  93 + `type` TINYINT UNSIGNED NOT NULL COMMENT '通道类型', -- 银企直连通道、聚合支付通道、微信支付通道、支付宝通道
  94 + `name` VARCHAR(40) NOT NULL COMMENT '通道名称',
  95 + `uri` VARCHAR(60) NOT NULL COMMENT '通道URI',
  96 + `param` JSON COMMENT '通道参数',
  97 + `state` TINYINT UNSIGNED NOT NULL COMMENT '通道状态',
  98 + `created_time` DATETIME COMMENT '创建时间',
  99 + `modified_time` DATETIME COMMENT '修改时间',
  100 + PRIMARY KEY (`id`),
  101 + UNIQUE KEY `uk_payment_pipeline_pipelineId` (`pipeline_id`) USING BTREE
  102 +) ENGINE=InnoDB COMMENT '通道管理配置';
  103 +
  104 +-- --------------------------------------------------------------------
  105 +-- 微信支付通道参数配置数据模型
  106 +-- --------------------------------------------------------------------
  107 +DROP TABLE IF EXISTS `upay_wechat_param`;
  108 +CREATE TABLE `upay_wechat_param` (
  109 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  110 + `pipeline_id` BIGINT NOT NULL COMMENT '通道ID',
  111 + `mch_id` VARCHAR(20) NOT NULL COMMENT '商户号', -- 服务商模式下为服务商商户号
  112 + `app_id` VARCHAR(30) NOT NULL COMMENT '小程序ID',
  113 + `app_secret` VARCHAR(50) NOT NULL COMMENT '小程序密钥',
  114 + `serial_no` VARCHAR(50) NOT NULL COMMENT '商户公钥序列号',
  115 + `private_key` TEXT NOT NULL COMMENT '商户私钥',
  116 + `wechat_serial_no` VARCHAR(50) NOT NULL COMMENT '微信公钥序列号',
  117 + `wechat_public_key` TEXT NOT NULL COMMENT '微信公钥',
  118 + `api_v3_key` VARCHAR(50) NOT NULL COMMENT '微信apiV3Key',
  119 + `type` TINYINT UNSIGNED NOT NULL COMMENT '通道类型', -- 直连通道或服务商通道
  120 + `created_time` DATETIME COMMENT '创建时间',
  121 + PRIMARY KEY (`id`),
  122 + UNIQUE KEY `uk_wechat_param_pipelineId` (`pipeline_id`) USING BTREE
  123 +) ENGINE=InnoDB COMMENT '微信支付通道参数配置';
0 124 \ No newline at end of file
... ...