Commit e8bc3fd84c96c3567d52ad687a611819f8aabaef
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 | package com.diligrp.cashier.boss; | 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 | import org.springframework.context.annotation.ComponentScan; | 9 | import org.springframework.context.annotation.ComponentScan; |
| 4 | import org.springframework.context.annotation.Configuration; | 10 | import org.springframework.context.annotation.Configuration; |
| 5 | 11 | ||
| 12 | +import java.util.Map; | ||
| 13 | + | ||
| 6 | @Configuration | 14 | @Configuration |
| 7 | @ComponentScan("com.diligrp.cashier.boss") | 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 | package com.diligrp.cashier.boss; | 1 | package com.diligrp.cashier.boss; |
| 2 | 2 | ||
| 3 | import com.diligrp.cashier.assistant.AssistantConfiguration; | 3 | import com.diligrp.cashier.assistant.AssistantConfiguration; |
| 4 | +import com.diligrp.cashier.pipeline.PipelineConfiguration; | ||
| 5 | +import com.diligrp.cashier.shared.SharedConfiguration; | ||
| 4 | import org.springframework.boot.SpringApplication; | 6 | import org.springframework.boot.SpringApplication; |
| 5 | import org.springframework.boot.SpringBootConfiguration; | 7 | import org.springframework.boot.SpringBootConfiguration; |
| 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; | 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
| @@ -10,7 +12,7 @@ import org.springframework.context.annotation.Import; | @@ -10,7 +12,7 @@ import org.springframework.context.annotation.Import; | ||
| 10 | 12 | ||
| 11 | @SpringBootConfiguration | 13 | @SpringBootConfiguration |
| 12 | @EnableAutoConfiguration | 14 | @EnableAutoConfiguration |
| 13 | -@Import({BossConfiguration.class, AssistantConfiguration.class}) | 15 | +@Import({BossConfiguration.class, PipelineConfiguration.class, AssistantConfiguration.class, SharedConfiguration.class}) |
| 14 | @EnableDiscoveryClient | 16 | @EnableDiscoveryClient |
| 15 | public class CashierServiceBootstrap { | 17 | public class CashierServiceBootstrap { |
| 16 | public static void main(String[] args) { | 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
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,12 +2,12 @@ dependencies { | ||
| 2 | //api 'org.springframework.boot:spring-boot-starter-data-mongodb' | 2 | //api 'org.springframework.boot:spring-boot-starter-data-mongodb' |
| 3 | api libs.mybatis.starter | 3 | api libs.mybatis.starter |
| 4 | api 'org.springframework.boot:spring-boot-starter-data-redis' | 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 | api 'org.springframework.boot:spring-boot-starter-amqp' | 6 | api 'org.springframework.boot:spring-boot-starter-amqp' |
| 7 | api 'org.springframework.cloud:spring-cloud-starter-loadbalancer' | 7 | api 'org.springframework.cloud:spring-cloud-starter-loadbalancer' |
| 8 | api 'org.springframework.cloud:spring-cloud-starter-openfeign' | 8 | api 'org.springframework.cloud:spring-cloud-starter-openfeign' |
| 9 | api libs.cache.caffeine | 9 | api libs.cache.caffeine |
| 10 | - api libs.collections4 | 10 | + api libs.common.collections4 |
| 11 | 11 | ||
| 12 | runtimeOnly libs.mysql.driver | 12 | runtimeOnly libs.mysql.driver |
| 13 | } | 13 | } |
cashier-trade/src/main/java/com/diligrp/cashier/trade/TradeConfiguration.java
0 → 100644
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
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 = 'com.alibaba.cloud:spring-cloud-starter | @@ -17,8 +17,9 @@ spring-cloud-nacos-discovery = {module = 'com.alibaba.cloud:spring-cloud-starter | ||
| 17 | spring-cloud-nacos-config = {module = 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config', version.ref = 'springCloudAlibabaVersion'} | 17 | spring-cloud-nacos-config = {module = 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config', version.ref = 'springCloudAlibabaVersion'} |
| 18 | mybatis-starter = {module = 'org.mybatis.spring.boot:mybatis-spring-boot-starter', version.ref = 'mybatisVersion'} | 18 | mybatis-starter = {module = 'org.mybatis.spring.boot:mybatis-spring-boot-starter', version.ref = 'mybatisVersion'} |
| 19 | mysql-driver = {module = 'mysql:mysql-connector-java', version.ref = 'mysqlDriverVersion'} | 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 | cache-caffeine = {module = 'com.github.ben-manes.caffeine:caffeine', version.ref = 'caffeineVersion'} | 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 | [plugins] | 24 | [plugins] |
| 24 | spring-boot = {id = 'org.springframework.boot', version.ref = 'springBootVersion'} | 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 | \ No newline at end of file | 124 | \ No newline at end of file |