RcbOnlineHttpClient.java
15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package com.diligrp.cashier.pipeline.client;
import com.diligrp.cashier.pipeline.domain.*;
import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
import com.diligrp.cashier.pipeline.type.OutPaymentType;
import com.diligrp.cashier.pipeline.type.PaymentState;
import com.diligrp.cashier.pipeline.util.RcbSignatureUtils;
import com.diligrp.cashier.pipeline.util.RcbStateUtils;
import com.diligrp.cashier.shared.ErrorCode;
import com.diligrp.cashier.shared.exception.ServiceAccessException;
import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
import com.diligrp.cashier.shared.util.*;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.lang.NonNull;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 安徽农商银行聚合支付客户端
*/
public class RcbOnlineHttpClient extends ServiceEndpointSupport {
private static final Logger LOG = LoggerFactory.getLogger(RcbOnlineHttpClient.class);
private static final int STATUS_OK = 200;
private final String uri;
private final String merchantNo;
private final String terminalNo;
private final String key;
private final String appId;
private final StringRedisTemplate stringRedisTemplate;
public RcbOnlineHttpClient(String uri, String merchantNo, String terminalNo, String key, String appId) {
this.uri = uri;
this.merchantNo = merchantNo;
this.terminalNo = terminalNo;
this.key = key;
this.appId = appId;
this.stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class);
}
public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUrl) {
Map<String, String> params = new HashMap<>();
params.put("merchantNo", merchantNo);
params.put("terminalNo", terminalNo);
params.put("batchNo", getBatchNo());
params.put("traceNo", getTraceNo());
params.put("outTradeNo", request.getPaymentId());
params.put("transAmount", String.valueOf(request.getAmount()));
params.put("appid", appId);
params.put("openId", request.getOpenId());
params.put("timeExpire", "5"); // 5分钟
params.put("tradeChannel", "01"); // 微信小程序
params.put("notifyUrl", notifyUrl);
params.put("nonceStr", RandomUtils.randomString(32));
params.put("sign", RcbSignatureUtils.sign(params, key));
String payload = JsonUtils.toJsonString(params);
LOG.info("Sending rcb MiniPro prepay request: {}\n{}", request.getPaymentId(), payload);
HttpResult result = send(uri + "/cposp/pay/unifiedorder", payload);
if (result.statusCode != STATUS_OK) {
LOG.error("Failed to send rcb MiniPro prepay request: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "调用小程序预支付接口失败: " + result.statusCode);
}
LOG.debug("Rcb MiniPro prepay received: {}\n{}", request.getPaymentId(), result.responseText);
Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
String resultCode = data.get("resultCode");
String resultMessage = data.get("resultMessage");
if ("00".equals(resultCode)) {
String signature = data.remove("sign");
if (!RcbSignatureUtils.verify(data, key, signature)) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "小程序预支付请求数据验签失败");
}
return data.get("payInfo");
} else {
LOG.error("Failed to send MiniPro prepay request: {}\n{}", request.getPaymentId(), result.responseText);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用小程序预支付接口失败: " + resultMessage);
}
}
public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder request) {
Map<String, String> params = new LinkedHashMap<>();
params.put("merchantNo", merchantNo);
params.put("terminalNo", terminalNo);
params.put("batchNo", getBatchNo());
params.put("traceNo", getTraceNo());
params.put("outTradeNo", request.getPaymentId());
params.put("nonceStr", RandomUtils.randomString(32));
params.put("sign", RcbSignatureUtils.sign(params, key));
String payload = JsonUtils.toJsonString(params);
LOG.info("Sending query rcb prepay order request: {}", request.getPaymentId());
HttpResult result = send(uri + "/cposp/pay/orderQuery", payload);
if (result.statusCode != STATUS_OK) {
LOG.error("Failed to query rcb prepay order: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询支付状态失败: " + result.statusCode);
}
LOG.debug("Query rcb prepay order response received: {}\n{}", request.getPaymentId(), result.responseText);
Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
String signature = data.remove("sign");
if (!RcbSignatureUtils.verify(data, key, signature)) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "查询支付状态数据验签失败");
}
String resultCode = data.get("resultCode");
String resultMessage = data.get("resultMessage");
if ("00".equals(resultCode)) {
String orderStatus = data.get("orderStatus");
String errorDesc = data.get("errorDesc");
String outTradeNo = data.get("cposOrderId");
OutPaymentType paymentType = RcbStateUtils.outPayType(data.get("tradeChannel"));
LocalDateTime when = LocalDateTime.now().withNano(0);
// 不能使用paidTime或transTime, paidTime只有年月日,transTime是订单发起时间
// 当前未设计存储支付时间字段(采用记录修改时间作为支付时间),因此采用当前时间作为记录修改时间
String timeEnd = data.get("timeEnd");
if (ObjectUtils.isNotEmpty(timeEnd)) {
long timestamp = Long.parseLong(timeEnd); // ⽀付完成时间戳
Instant instant = Instant.ofEpochMilli(timestamp);
when = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}
String payUserInfo = data.get("payUserInfo");
// 第三方支付通道的订单编号,比如:微信订单
// String outOrderId = data.get("chnOrderId");
return new OnlinePaymentResponse(request.getPaymentId(), outTradeNo, paymentType,
payUserInfo, when, RcbStateUtils.paymentState(orderStatus), errorDesc);
} else {
LOG.error("Failed to query rcb prepay order state: {}\n{}", request.getPaymentId(), result.responseText);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用支付结果查询接口失败: " + resultMessage);
}
}
public void closePrepayOrder(OnlinePrepayOrder request) {
Map<String, String> params = new HashMap<>();
params.put("merchantNo", merchantNo);
params.put("terminalNo", terminalNo);
params.put("batchNo", getBatchNo());
params.put("traceNo", getTraceNo());
params.put("outTradeNo", request.getPaymentId());
params.put("nonceStr", RandomUtils.randomString(32));
params.put("sign", RcbSignatureUtils.sign(params, key));
String payload = JsonUtils.toJsonString(params);
LOG.info("Sending close rcb prepay order request: {}", request.getPaymentId());
HttpResult result = send(uri + "/cposp/pay/closeOrder", payload);
if (result.statusCode != STATUS_OK) {
LOG.error("Failed to close rcb prepay order: {}", request.getPaymentId());
throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "关闭支付订单失败: " + result.statusCode);
}
LOG.debug("Close rcb prepay order response received: {}\n{}", request.getPaymentId(), result.responseText);
Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
String resultCode = data.get("resultCode");
String resultMessage = data.get("resultMessage");
if (!"00".equals(resultCode)) {
LOG.error("Failed to close rcb prepay order: {}\n{}", request.getPaymentId(), result.responseText);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "关闭支付订单失败: " + resultMessage);
}
String signature = data.remove("sign");
if (!RcbSignatureUtils.verify(data, key, signature)) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "关闭订单数据验签失败");
}
}
public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
Map<String, String> params = new HashMap<>();
params.put("merchantNo", merchantNo);
params.put("terminalNo", terminalNo);
params.put("batchNo", getBatchNo());
params.put("traceNo", getTraceNo());
params.put("mchtRefundNo", request.getRefundId());
params.put("outTradeNo", request.getPaymentId());
params.put("refundAmount", String.valueOf(request.getAmount()));
params.put("nonceStr", RandomUtils.randomString(32));
params.put("sign", RcbSignatureUtils.sign(params, key));
LocalDateTime now = LocalDateTime.now();
String payload = JsonUtils.toJsonString(params);
LOG.info("Sending rcb payment refund request: {}", request.getRefundId());
HttpResult result = send(uri + "/cposp/pay/refund", payload);
if (result.statusCode != STATUS_OK) {
LOG.error("Failed to send rcb refund request: {}", request.getRefundId());
throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发送退款请求失败: " + result.statusCode);
}
LOG.debug("Rcb payment refund response received: {}\n{}", request.getRefundId(), result.responseText);
Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
String resultCode = data.get("resultCode");
String resultMessage = data.get("resultMessage");
if (!"00".equals(resultCode)) {
LOG.error("Failed to request rcb payment refund: {}\n{}", request.getRefundId(), result.responseText);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "发送退款请求失败: " + resultMessage);
}
String signature = data.remove("sign");
if (!RcbSignatureUtils.verify(data, key, signature)) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "退款接口数据验签失败");
}
String refundStatus = data.get("refundStatus");
String cposRefundOrderId = data.get("cposRefundOrderId");
PaymentState refundState = RcbStateUtils.refundState(refundStatus);
return new OnlineRefundResponse(request.getRefundId(), cposRefundOrderId, now, refundState, RcbStateUtils.refundInfo(refundState));
}
/**
* 各交易终端,每⽇第⼀笔交易时,需要通过签到,获取批次号等信息。
*
* @return 批次号
*/
protected String getBatchNo() {
try {
String key = "cashier:rcb:batchNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
String batchNo = stringRedisTemplate.opsForValue().get(key);
if (batchNo != null) {
return batchNo;
}
String payload = String.format("{\"merchantNo\": \"%s\", \"terminalNo\": \"%s\"}", merchantNo, terminalNo);
LOG.debug("Sending rcb signIn request: {}", payload);
HttpResult result = send(uri + "/cposp/pay/signIn", payload);
if (result.statusCode != STATUS_OK) {
LOG.error("Failed to get rcb batch no, statusCode: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "获取签到批次号: " + result.statusCode);
}
LOG.debug("Rcb signIn response received: {}", result.responseText);
Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
String resultCode = data.get("resultCode");
String resultMessage = data.get("resultMessage");
if ("00".equals(resultCode)) {
batchNo = data.get("batchNo");
stringRedisTemplate.opsForValue().set(key, batchNo, 36 * 60 * 60, TimeUnit.SECONDS);
return batchNo;
} else {
LOG.error("Failed to rcb signIn: {}", result.responseText);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "签到接口调用失败: " + resultMessage);
}
} catch (ServiceAccessException | PaymentPipelineException rex) {
throw rex;
} catch (Exception ex) {
LOG.error("Failed to rcb signIn", ex);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取签到批次号失败");
}
}
protected String getTraceNo() {
try {
String key = "cashier:rcb:traceNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
StringBuilder traceNo = new StringBuilder();
List<Object> results = stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(@NonNull RedisOperations operations) throws DataAccessException {
operations.opsForValue().increment(key);
operations.expire(key, 36 * 60 * 60, TimeUnit.SECONDS);
return null;
}
});
traceNo.append(results.getFirst());
int length = traceNo.length();
if (length < 6) {
for (int i = length; i < 6; i++) {
traceNo.insert(0, "0");
}
}
return traceNo.toString();
} catch (Exception ex) {
LOG.error("Failed to get rcb traceNo", ex);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取系统跟踪号失败");
}
}
protected Optional<javax.net.ssl.SSLContext> buildSSLContext() {
SSLContext sslContext = null;
try {
TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[] {
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
} catch (Exception ex) {
LOG.error("Build SSLContext failed", ex);
}
return Optional.ofNullable(sslContext);
}
}