Browse Source

新需求

傅行帆 5 years ago
parent
commit
785a85ec1e

+ 9
- 0
src/main/java/com/huiju/estateagents/common/CommConstant.java View File

@@ -632,4 +632,13 @@ public class CommConstant {
632 632
      */
633 633
     public static final String HOUSE_DETAIL_PATH = "onlineSelling/pages/detail/index?id=%s";
634 634
 
635
+    /**
636
+     * 已锁定
637
+     */
638
+    public static final String HOUSE_LOCKING_STATUS_LOCKED = "locked";
639
+
640
+    /**
641
+     * 未锁定
642
+     */
643
+    public static final String HOUSE_LOCKING_STATUS_UNLOCKED = "unlocked";
635 644
 }

+ 42
- 0
src/main/java/com/huiju/estateagents/common/wxpay/IWXPayDomain.java View File

@@ -0,0 +1,42 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+/**
4
+ * 域名管理,实现主备域名自动切换
5
+ */
6
+public abstract interface IWXPayDomain {
7
+    /**
8
+     * 上报域名网络状况
9
+     * @param domain 域名。 比如:api.mch.weixin.qq.com
10
+     * @param elapsedTimeMillis 耗时
11
+     * @param ex 网络请求中出现的异常。
12
+     *           null表示没有异常
13
+     *           ConnectTimeoutException,表示建立网络连接异常
14
+     *           UnknownHostException, 表示dns解析异常
15
+     */
16
+    abstract void report(final String domain, long elapsedTimeMillis, final Exception ex);
17
+
18
+    /**
19
+     * 获取域名
20
+     * @param config 配置
21
+     * @return 域名
22
+     */
23
+    abstract DomainInfo getDomain(final WXPayConfig config);
24
+
25
+    static class DomainInfo{
26
+        public String domain;       //域名
27
+        public boolean primaryDomain;     //该域名是否为主域名。例如:api.mch.weixin.qq.com为主域名
28
+        public DomainInfo(String domain, boolean primaryDomain) {
29
+            this.domain = domain;
30
+            this.primaryDomain = primaryDomain;
31
+        }
32
+
33
+        @Override
34
+        public String toString() {
35
+            return "DomainInfo{" +
36
+                    "domain='" + domain + '\'' +
37
+                    ", primaryDomain=" + primaryDomain +
38
+                    '}';
39
+        }
40
+    }
41
+
42
+}

+ 689
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPay.java View File

@@ -0,0 +1,689 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+
4
+import java.util.HashMap;
5
+import java.util.Map;
6
+import com.huiju.estateagents.common.wxpay.WXPayConstants.SignType;
7
+
8
+public class WXPay {
9
+
10
+    private WXPayConfig config;
11
+    private SignType signType;
12
+    private boolean autoReport;
13
+    private boolean useSandbox;
14
+    private String notifyUrl;
15
+    private WXPayRequest wxPayRequest;
16
+
17
+    public WXPay(final WXPayConfig config) throws Exception {
18
+        this(config, null, true, false);
19
+    }
20
+
21
+    public WXPay(final WXPayConfig config, final boolean autoReport) throws Exception {
22
+        this(config, null, autoReport, false);
23
+    }
24
+
25
+
26
+    public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception{
27
+        this(config, null, autoReport, useSandbox);
28
+    }
29
+
30
+    public WXPay(final WXPayConfig config, final String notifyUrl) throws Exception {
31
+        this(config, notifyUrl, true, false);
32
+    }
33
+
34
+    public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport) throws Exception {
35
+        this(config, notifyUrl, autoReport, false);
36
+    }
37
+
38
+    public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
39
+        this.config = config;
40
+        this.notifyUrl = notifyUrl;
41
+        this.autoReport = autoReport;
42
+        this.useSandbox = useSandbox;
43
+        //if (useSandbox) {
44
+            this.signType = SignType.MD5;
45
+        //}
46
+        //else {
47
+        //    this.signType = SignType.HMACSHA256;
48
+        //}
49
+        this.wxPayRequest = new WXPayRequest(config);
50
+    }
51
+
52
+    private void checkWXPayConfig() throws Exception {
53
+        if (this.config == null) {
54
+            throw new Exception("config is null");
55
+        }
56
+        if (this.config.getAppID() == null || this.config.getAppID().trim().length() == 0) {
57
+            throw new Exception("appid in config is empty");
58
+        }
59
+        if (this.config.getMchID() == null || this.config.getMchID().trim().length() == 0) {
60
+            throw new Exception("appid in config is empty");
61
+        }
62
+        if (this.config.getCertStream() == null) {
63
+            throw new Exception("cert stream in config is empty");
64
+        }
65
+        if (this.config.getWXPayDomain() == null){
66
+            throw new Exception("config.getWXPayDomain() is null");
67
+        }
68
+
69
+        if (this.config.getHttpConnectTimeoutMs() < 10) {
70
+            throw new Exception("http connect timeout is too small");
71
+        }
72
+        if (this.config.getHttpReadTimeoutMs() < 10) {
73
+            throw new Exception("http read timeout is too small");
74
+        }
75
+
76
+    }
77
+
78
+    /**
79
+     * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br>
80
+     * 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口
81
+     *
82
+     * @param reqData
83
+     * @return
84
+     * @throws Exception
85
+     */
86
+    public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
87
+        reqData.put("appid", config.getAppID());
88
+        reqData.put("mch_id", config.getMchID());
89
+        reqData.put("nonce_str", WXPayUtil.generateNonceStr());
90
+        if (SignType.MD5.equals(this.signType)) {
91
+            reqData.put("sign_type", WXPayConstants.MD5);
92
+        }
93
+        else if (SignType.HMACSHA256.equals(this.signType)) {
94
+            reqData.put("sign_type", WXPayConstants.HMACSHA256);
95
+        }
96
+        reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType));
97
+        return reqData;
98
+    }
99
+
100
+    /**
101
+     * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。
102
+     *
103
+     * @param reqData 向wxpay post的请求数据
104
+     * @return 签名是否有效
105
+     * @throws Exception
106
+     */
107
+    public boolean isResponseSignatureValid(Map<String, String> reqData) throws Exception {
108
+        // 返回数据的签名方式和请求中给定的签名方式是一致的
109
+        return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), this.signType);
110
+    }
111
+
112
+    /**
113
+     * 判断支付结果通知中的sign是否有效
114
+     *
115
+     * @param reqData 向wxpay post的请求数据
116
+     * @return 签名是否有效
117
+     * @throws Exception
118
+     */
119
+    public boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {
120
+        String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);
121
+        SignType signType;
122
+        if (signTypeInData == null) {
123
+            signType = SignType.MD5;
124
+        }
125
+        else {
126
+            signTypeInData = signTypeInData.trim();
127
+            if (signTypeInData.length() == 0) {
128
+                signType = SignType.MD5;
129
+            }
130
+            else if (WXPayConstants.MD5.equals(signTypeInData)) {
131
+                signType = SignType.MD5;
132
+            }
133
+            else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {
134
+                signType = SignType.HMACSHA256;
135
+            }
136
+            else {
137
+                throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
138
+            }
139
+        }
140
+        return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), signType);
141
+    }
142
+
143
+
144
+    /**
145
+     * 不需要证书的请求
146
+     * @param urlSuffix String
147
+     * @param reqData 向wxpay post的请求数据
148
+     * @param connectTimeoutMs 超时时间,单位是毫秒
149
+     * @param readTimeoutMs 超时时间,单位是毫秒
150
+     * @return API返回数据
151
+     * @throws Exception
152
+     */
153
+    public String requestWithoutCert(String urlSuffix, Map<String, String> reqData,
154
+                                     int connectTimeoutMs, int readTimeoutMs) throws Exception {
155
+        String msgUUID = reqData.get("nonce_str");
156
+        String reqBody = WXPayUtil.mapToXml(reqData);
157
+
158
+        String resp = this.wxPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, autoReport);
159
+        return resp;
160
+    }
161
+
162
+
163
+    /**
164
+     * 需要证书的请求
165
+     * @param urlSuffix String
166
+     * @param reqData 向wxpay post的请求数据  Map
167
+     * @param connectTimeoutMs 超时时间,单位是毫秒
168
+     * @param readTimeoutMs 超时时间,单位是毫秒
169
+     * @return API返回数据
170
+     * @throws Exception
171
+     */
172
+    public String requestWithCert(String urlSuffix, Map<String, String> reqData,
173
+                                  int connectTimeoutMs, int readTimeoutMs) throws Exception {
174
+        String msgUUID= reqData.get("nonce_str");
175
+        String reqBody = WXPayUtil.mapToXml(reqData);
176
+
177
+        String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport);
178
+        return resp;
179
+    }
180
+
181
+    /**
182
+     * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。
183
+     * @param xmlStr API返回的XML格式数据
184
+     * @return Map类型数据
185
+     * @throws Exception
186
+     */
187
+    public Map<String, String> processResponseXml(String xmlStr) throws Exception {
188
+        String RETURN_CODE = "return_code";
189
+        String return_code;
190
+        Map<String, String> respData = WXPayUtil.xmlToMap(xmlStr);
191
+        if (respData.containsKey(RETURN_CODE)) {
192
+            return_code = respData.get(RETURN_CODE);
193
+        }
194
+        else {
195
+            throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
196
+        }
197
+
198
+        if (return_code.equals(WXPayConstants.FAIL)) {
199
+            return respData;
200
+        }
201
+        else if (return_code.equals(WXPayConstants.SUCCESS)) {
202
+           if (this.isResponseSignatureValid(respData)) {
203
+               return respData;
204
+           }
205
+           else {
206
+               throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
207
+           }
208
+        }
209
+        else {
210
+            throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
211
+        }
212
+    }
213
+
214
+    /**
215
+     * 作用:提交刷卡支付<br>
216
+     * 场景:刷卡支付
217
+     * @param reqData 向wxpay post的请求数据
218
+     * @return API返回数据
219
+     * @throws Exception
220
+     */
221
+    public Map<String, String> microPay(Map<String, String> reqData) throws Exception {
222
+        return this.microPay(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
223
+    }
224
+
225
+
226
+    /**
227
+     * 作用:提交刷卡支付<br>
228
+     * 场景:刷卡支付
229
+     * @param reqData 向wxpay post的请求数据
230
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
231
+     * @param readTimeoutMs 读超时时间,单位是毫秒
232
+     * @return API返回数据
233
+     * @throws Exception
234
+     */
235
+    public Map<String, String> microPay(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
236
+        String url;
237
+        if (this.useSandbox) {
238
+            url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX;
239
+        }
240
+        else {
241
+            url = WXPayConstants.MICROPAY_URL_SUFFIX;
242
+        }
243
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
244
+        return this.processResponseXml(respXml);
245
+    }
246
+
247
+    /**
248
+     * 提交刷卡支付,针对软POS,尽可能做成功
249
+     * 内置重试机制,最多60s
250
+     * @param reqData
251
+     * @return
252
+     * @throws Exception
253
+     */
254
+    public Map<String, String> microPayWithPos(Map<String, String> reqData) throws Exception {
255
+        return this.microPayWithPos(reqData, this.config.getHttpConnectTimeoutMs());
256
+    }
257
+
258
+    /**
259
+     * 提交刷卡支付,针对软POS,尽可能做成功
260
+     * 内置重试机制,最多60s
261
+     * @param reqData
262
+     * @param connectTimeoutMs
263
+     * @return
264
+     * @throws Exception
265
+     */
266
+    public Map<String, String> microPayWithPos(Map<String, String> reqData, int connectTimeoutMs) throws Exception {
267
+        int remainingTimeMs = 60*1000;
268
+        long startTimestampMs = 0;
269
+        Map<String, String> lastResult = null;
270
+        Exception lastException = null;
271
+
272
+        while (true) {
273
+            startTimestampMs = WXPayUtil.getCurrentTimestampMs();
274
+            int readTimeoutMs = remainingTimeMs - connectTimeoutMs;
275
+            if (readTimeoutMs > 1000) {
276
+                try {
277
+                    lastResult = this.microPay(reqData, connectTimeoutMs, readTimeoutMs);
278
+                    String returnCode = lastResult.get("return_code");
279
+                    if (returnCode.equals("SUCCESS")) {
280
+                        String resultCode = lastResult.get("result_code");
281
+                        String errCode = lastResult.get("err_code");
282
+                        if (resultCode.equals("SUCCESS")) {
283
+                            break;
284
+                        }
285
+                        else {
286
+                            // 看错误码,若支付结果未知,则重试提交刷卡支付
287
+                            if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) {
288
+                                remainingTimeMs = remainingTimeMs - (int)(WXPayUtil.getCurrentTimestampMs() - startTimestampMs);
289
+                                if (remainingTimeMs <= 100) {
290
+                                    break;
291
+                                }
292
+                                else {
293
+                                    WXPayUtil.getLogger().info("microPayWithPos: try micropay again");
294
+                                    if (remainingTimeMs > 5*1000) {
295
+                                        Thread.sleep(5*1000);
296
+                                    }
297
+                                    else {
298
+                                        Thread.sleep(1*1000);
299
+                                    }
300
+                                    continue;
301
+                                }
302
+                            }
303
+                            else {
304
+                                break;
305
+                            }
306
+                        }
307
+                    }
308
+                    else {
309
+                        break;
310
+                    }
311
+                }
312
+                catch (Exception ex) {
313
+                    lastResult = null;
314
+                    lastException = ex;
315
+                }
316
+            }
317
+            else {
318
+                break;
319
+            }
320
+        }
321
+
322
+        if (lastResult == null) {
323
+            throw lastException;
324
+        }
325
+        else {
326
+            return lastResult;
327
+        }
328
+    }
329
+
330
+
331
+
332
+    /**
333
+     * 作用:统一下单<br>
334
+     * 场景:公共号支付、扫码支付、APP支付
335
+     * @param reqData 向wxpay post的请求数据
336
+     * @return API返回数据
337
+     * @throws Exception
338
+     */
339
+    public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
340
+        return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
341
+    }
342
+
343
+
344
+    /**
345
+     * 作用:统一下单<br>
346
+     * 场景:公共号支付、扫码支付、APP支付
347
+     * @param reqData 向wxpay post的请求数据
348
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
349
+     * @param readTimeoutMs 读超时时间,单位是毫秒
350
+     * @return API返回数据
351
+     * @throws Exception
352
+     */
353
+    public Map<String, String> unifiedOrder(Map<String, String> reqData,  int connectTimeoutMs, int readTimeoutMs) throws Exception {
354
+        String url;
355
+        if (this.useSandbox) {
356
+            url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX;
357
+        }
358
+        else {
359
+            url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX;
360
+        }
361
+        if(this.notifyUrl != null) {
362
+            reqData.put("notify_url", this.notifyUrl);
363
+        }
364
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
365
+        return this.processResponseXml(respXml);
366
+    }
367
+
368
+
369
+    /**
370
+     * 作用:查询订单<br>
371
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
372
+     * @param reqData 向wxpay post的请求数据
373
+     * @return API返回数据
374
+     * @throws Exception
375
+     */
376
+    public Map<String, String> orderQuery(Map<String, String> reqData) throws Exception {
377
+        return this.orderQuery(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
378
+    }
379
+
380
+
381
+    /**
382
+     * 作用:查询订单<br>
383
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
384
+     * @param reqData 向wxpay post的请求数据 int
385
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
386
+     * @param readTimeoutMs 读超时时间,单位是毫秒
387
+     * @return API返回数据
388
+     * @throws Exception
389
+     */
390
+    public Map<String, String> orderQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
391
+        String url;
392
+        if (this.useSandbox) {
393
+            url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX;
394
+        }
395
+        else {
396
+            url = WXPayConstants.ORDERQUERY_URL_SUFFIX;
397
+        }
398
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
399
+        return this.processResponseXml(respXml);
400
+    }
401
+
402
+
403
+    /**
404
+     * 作用:撤销订单<br>
405
+     * 场景:刷卡支付
406
+     * @param reqData 向wxpay post的请求数据
407
+     * @return API返回数据
408
+     * @throws Exception
409
+     */
410
+    public Map<String, String> reverse(Map<String, String> reqData) throws Exception {
411
+        return this.reverse(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
412
+    }
413
+
414
+
415
+    /**
416
+     * 作用:撤销订单<br>
417
+     * 场景:刷卡支付<br>
418
+     * 其他:需要证书
419
+     * @param reqData 向wxpay post的请求数据
420
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
421
+     * @param readTimeoutMs 读超时时间,单位是毫秒
422
+     * @return API返回数据
423
+     * @throws Exception
424
+     */
425
+    public Map<String, String> reverse(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
426
+        String url;
427
+        if (this.useSandbox) {
428
+            url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX;
429
+        }
430
+        else {
431
+            url = WXPayConstants.REVERSE_URL_SUFFIX;
432
+        }
433
+        String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
434
+        return this.processResponseXml(respXml);
435
+    }
436
+
437
+
438
+    /**
439
+     * 作用:关闭订单<br>
440
+     * 场景:公共号支付、扫码支付、APP支付
441
+     * @param reqData 向wxpay post的请求数据
442
+     * @return API返回数据
443
+     * @throws Exception
444
+     */
445
+    public Map<String, String> closeOrder(Map<String, String> reqData) throws Exception {
446
+        return this.closeOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
447
+    }
448
+
449
+
450
+    /**
451
+     * 作用:关闭订单<br>
452
+     * 场景:公共号支付、扫码支付、APP支付
453
+     * @param reqData 向wxpay post的请求数据
454
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
455
+     * @param readTimeoutMs 读超时时间,单位是毫秒
456
+     * @return API返回数据
457
+     * @throws Exception
458
+     */
459
+    public Map<String, String> closeOrder(Map<String, String> reqData,  int connectTimeoutMs, int readTimeoutMs) throws Exception {
460
+        String url;
461
+        if (this.useSandbox) {
462
+            url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX;
463
+        }
464
+        else {
465
+            url = WXPayConstants.CLOSEORDER_URL_SUFFIX;
466
+        }
467
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
468
+        return this.processResponseXml(respXml);
469
+    }
470
+
471
+
472
+    /**
473
+     * 作用:申请退款<br>
474
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
475
+     * @param reqData 向wxpay post的请求数据
476
+     * @return API返回数据
477
+     * @throws Exception
478
+     */
479
+    public Map<String, String> refund(Map<String, String> reqData) throws Exception {
480
+        return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
481
+    }
482
+
483
+
484
+    /**
485
+     * 作用:申请退款<br>
486
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付<br>
487
+     * 其他:需要证书
488
+     * @param reqData 向wxpay post的请求数据
489
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
490
+     * @param readTimeoutMs 读超时时间,单位是毫秒
491
+     * @return API返回数据
492
+     * @throws Exception
493
+     */
494
+    public Map<String, String> refund(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
495
+        String url;
496
+        if (this.useSandbox) {
497
+            url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX;
498
+        }
499
+        else {
500
+            url = WXPayConstants.REFUND_URL_SUFFIX;
501
+        }
502
+        String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
503
+        return this.processResponseXml(respXml);
504
+    }
505
+
506
+
507
+    /**
508
+     * 作用:退款查询<br>
509
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
510
+     * @param reqData 向wxpay post的请求数据
511
+     * @return API返回数据
512
+     * @throws Exception
513
+     */
514
+    public Map<String, String> refundQuery(Map<String, String> reqData) throws Exception {
515
+        return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
516
+    }
517
+
518
+
519
+    /**
520
+     * 作用:退款查询<br>
521
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
522
+     * @param reqData 向wxpay post的请求数据
523
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
524
+     * @param readTimeoutMs 读超时时间,单位是毫秒
525
+     * @return API返回数据
526
+     * @throws Exception
527
+     */
528
+    public Map<String, String> refundQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
529
+        String url;
530
+        if (this.useSandbox) {
531
+            url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX;
532
+        }
533
+        else {
534
+            url = WXPayConstants.REFUNDQUERY_URL_SUFFIX;
535
+        }
536
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
537
+        return this.processResponseXml(respXml);
538
+    }
539
+
540
+
541
+    /**
542
+     * 作用:对账单下载(成功时返回对账单数据,失败时返回XML格式数据)<br>
543
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
544
+     * @param reqData 向wxpay post的请求数据
545
+     * @return API返回数据
546
+     * @throws Exception
547
+     */
548
+    public Map<String, String> downloadBill(Map<String, String> reqData) throws Exception {
549
+        return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
550
+    }
551
+
552
+
553
+    /**
554
+     * 作用:对账单下载<br>
555
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付<br>
556
+     * 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data,
557
+     *      其中return_code为`SUCCESS`,data为对账单数据。
558
+     * @param reqData 向wxpay post的请求数据
559
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
560
+     * @param readTimeoutMs 读超时时间,单位是毫秒
561
+     * @return 经过封装的API返回数据
562
+     * @throws Exception
563
+     */
564
+    public Map<String, String> downloadBill(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
565
+        String url;
566
+        if (this.useSandbox) {
567
+            url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX;
568
+        }
569
+        else {
570
+            url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX;
571
+        }
572
+        String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim();
573
+        Map<String, String> ret;
574
+        // 出现错误,返回XML数据
575
+        if (respStr.indexOf("<") == 0) {
576
+            ret = WXPayUtil.xmlToMap(respStr);
577
+        }
578
+        else {
579
+            // 正常返回csv数据
580
+            ret = new HashMap<String, String>();
581
+            ret.put("return_code", WXPayConstants.SUCCESS);
582
+            ret.put("return_msg", "ok");
583
+            ret.put("data", respStr);
584
+        }
585
+        return ret;
586
+    }
587
+
588
+
589
+    /**
590
+     * 作用:交易保障<br>
591
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
592
+     * @param reqData 向wxpay post的请求数据
593
+     * @return API返回数据
594
+     * @throws Exception
595
+     */
596
+    public Map<String, String> report(Map<String, String> reqData) throws Exception {
597
+        return this.report(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
598
+    }
599
+
600
+
601
+    /**
602
+     * 作用:交易保障<br>
603
+     * 场景:刷卡支付、公共号支付、扫码支付、APP支付
604
+     * @param reqData 向wxpay post的请求数据
605
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
606
+     * @param readTimeoutMs 读超时时间,单位是毫秒
607
+     * @return API返回数据
608
+     * @throws Exception
609
+     */
610
+    public Map<String, String> report(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
611
+        String url;
612
+        if (this.useSandbox) {
613
+            url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX;
614
+        }
615
+        else {
616
+            url = WXPayConstants.REPORT_URL_SUFFIX;
617
+        }
618
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
619
+        return WXPayUtil.xmlToMap(respXml);
620
+    }
621
+
622
+
623
+    /**
624
+     * 作用:转换短链接<br>
625
+     * 场景:刷卡支付、扫码支付
626
+     * @param reqData 向wxpay post的请求数据
627
+     * @return API返回数据
628
+     * @throws Exception
629
+     */
630
+    public Map<String, String> shortUrl(Map<String, String> reqData) throws Exception {
631
+        return this.shortUrl(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
632
+    }
633
+
634
+
635
+    /**
636
+     * 作用:转换短链接<br>
637
+     * 场景:刷卡支付、扫码支付
638
+     * @param reqData 向wxpay post的请求数据
639
+     * @return API返回数据
640
+     * @throws Exception
641
+     */
642
+    public Map<String, String> shortUrl(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
643
+        String url;
644
+        if (this.useSandbox) {
645
+            url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX;
646
+        }
647
+        else {
648
+            url = WXPayConstants.SHORTURL_URL_SUFFIX;
649
+        }
650
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
651
+        return this.processResponseXml(respXml);
652
+    }
653
+
654
+
655
+    /**
656
+     * 作用:授权码查询OPENID接口<br>
657
+     * 场景:刷卡支付
658
+     * @param reqData 向wxpay post的请求数据
659
+     * @return API返回数据
660
+     * @throws Exception
661
+     */
662
+    public Map<String, String> authCodeToOpenid(Map<String, String> reqData) throws Exception {
663
+        return this.authCodeToOpenid(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
664
+    }
665
+
666
+
667
+    /**
668
+     * 作用:授权码查询OPENID接口<br>
669
+     * 场景:刷卡支付
670
+     * @param reqData 向wxpay post的请求数据
671
+     * @param connectTimeoutMs 连接超时时间,单位是毫秒
672
+     * @param readTimeoutMs 读超时时间,单位是毫秒
673
+     * @return API返回数据
674
+     * @throws Exception
675
+     */
676
+    public Map<String, String> authCodeToOpenid(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
677
+        String url;
678
+        if (this.useSandbox) {
679
+            url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX;
680
+        }
681
+        else {
682
+            url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX;
683
+        }
684
+        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
685
+        return this.processResponseXml(respXml);
686
+    }
687
+
688
+
689
+} // end class

+ 103
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayConfig.java View File

@@ -0,0 +1,103 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import java.io.InputStream;
4
+
5
+public abstract class WXPayConfig {
6
+
7
+
8
+
9
+    /**
10
+     * 获取 App ID
11
+     *
12
+     * @return App ID
13
+     */
14
+    abstract String getAppID();
15
+
16
+
17
+    /**
18
+     * 获取 Mch ID
19
+     *
20
+     * @return Mch ID
21
+     */
22
+    abstract String getMchID();
23
+
24
+
25
+    /**
26
+     * 获取 API 密钥
27
+     *
28
+     * @return API密钥
29
+     */
30
+    abstract String getKey();
31
+
32
+
33
+    /**
34
+     * 获取商户证书内容
35
+     *
36
+     * @return 商户证书内容
37
+     */
38
+    abstract InputStream getCertStream();
39
+
40
+    /**
41
+     * HTTP(S) 连接超时时间,单位毫秒
42
+     *
43
+     * @return
44
+     */
45
+    public int getHttpConnectTimeoutMs() {
46
+        return 6*1000;
47
+    }
48
+
49
+    /**
50
+     * HTTP(S) 读数据超时时间,单位毫秒
51
+     *
52
+     * @return
53
+     */
54
+    public int getHttpReadTimeoutMs() {
55
+        return 8*1000;
56
+    }
57
+
58
+    /**
59
+     * 获取WXPayDomain, 用于多域名容灾自动切换
60
+     * @return
61
+     */
62
+    abstract IWXPayDomain getWXPayDomain();
63
+
64
+    /**
65
+     * 是否自动上报。
66
+     * 若要关闭自动上报,子类中实现该函数返回 false 即可。
67
+     *
68
+     * @return
69
+     */
70
+    public boolean shouldAutoReport() {
71
+        return true;
72
+    }
73
+
74
+    /**
75
+     * 进行健康上报的线程的数量
76
+     *
77
+     * @return
78
+     */
79
+    public int getReportWorkerNum() {
80
+        return 6;
81
+    }
82
+
83
+
84
+    /**
85
+     * 健康上报缓存消息的最大数量。会有线程去独立上报
86
+     * 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
87
+     *
88
+     * @return
89
+     */
90
+    public int getReportQueueMaxSize() {
91
+        return 10000;
92
+    }
93
+
94
+    /**
95
+     * 批量上报,一次最多上报多个数据
96
+     *
97
+     * @return
98
+     */
99
+    public int getReportBatchSize() {
100
+        return 10;
101
+    }
102
+
103
+}

+ 59
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayConstants.java View File

@@ -0,0 +1,59 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import org.apache.http.client.HttpClient;
4
+
5
+/**
6
+ * 常量
7
+ */
8
+public class WXPayConstants {
9
+
10
+    public enum SignType {
11
+        MD5, HMACSHA256
12
+    }
13
+
14
+    public static final String DOMAIN_API = "api.mch.weixin.qq.com";
15
+    public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com";
16
+    public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com";
17
+    public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com";
18
+
19
+
20
+    public static final String FAIL     = "FAIL";
21
+    public static final String SUCCESS  = "SUCCESS";
22
+    public static final String HMACSHA256 = "HMAC-SHA256";
23
+    public static final String MD5 = "MD5";
24
+
25
+    public static final String FIELD_SIGN = "sign";
26
+    public static final String FIELD_SIGN_TYPE = "sign_type";
27
+
28
+    public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
29
+    public static final String USER_AGENT = WXPAYSDK_VERSION +
30
+            " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
31
+            ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();
32
+
33
+    public static final String MICROPAY_URL_SUFFIX     = "/pay/micropay";
34
+    public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder";
35
+    public static final String ORDERQUERY_URL_SUFFIX   = "/pay/orderquery";
36
+    public static final String REVERSE_URL_SUFFIX      = "/secapi/pay/reverse";
37
+    public static final String CLOSEORDER_URL_SUFFIX   = "/pay/closeorder";
38
+    public static final String REFUND_URL_SUFFIX       = "/secapi/pay/refund";
39
+    public static final String REFUNDQUERY_URL_SUFFIX  = "/pay/refundquery";
40
+    public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill";
41
+    public static final String REPORT_URL_SUFFIX       = "/payitil/report";
42
+    public static final String SHORTURL_URL_SUFFIX     = "/tools/shorturl";
43
+    public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid";
44
+
45
+    // sandbox
46
+    public static final String SANDBOX_MICROPAY_URL_SUFFIX     = "/sandboxnew/pay/micropay";
47
+    public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder";
48
+    public static final String SANDBOX_ORDERQUERY_URL_SUFFIX   = "/sandboxnew/pay/orderquery";
49
+    public static final String SANDBOX_REVERSE_URL_SUFFIX      = "/sandboxnew/secapi/pay/reverse";
50
+    public static final String SANDBOX_CLOSEORDER_URL_SUFFIX   = "/sandboxnew/pay/closeorder";
51
+    public static final String SANDBOX_REFUND_URL_SUFFIX       = "/sandboxnew/secapi/pay/refund";
52
+    public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX  = "/sandboxnew/pay/refundquery";
53
+    public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill";
54
+    public static final String SANDBOX_REPORT_URL_SUFFIX       = "/sandboxnew/payitil/report";
55
+    public static final String SANDBOX_SHORTURL_URL_SUFFIX     = "/sandboxnew/tools/shorturl";
56
+    public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid";
57
+
58
+}
59
+

+ 265
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayReport.java View File

@@ -0,0 +1,265 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import org.apache.http.HttpEntity;
4
+import org.apache.http.HttpResponse;
5
+import org.apache.http.client.HttpClient;
6
+import org.apache.http.client.config.RequestConfig;
7
+import org.apache.http.client.methods.HttpPost;
8
+import org.apache.http.config.RegistryBuilder;
9
+import org.apache.http.conn.socket.ConnectionSocketFactory;
10
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
11
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
12
+import org.apache.http.entity.StringEntity;
13
+import org.apache.http.impl.client.HttpClientBuilder;
14
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
15
+import org.apache.http.util.EntityUtils;
16
+
17
+import java.util.concurrent.ExecutorService;
18
+import java.util.concurrent.Executors;
19
+import java.util.concurrent.LinkedBlockingQueue;
20
+import java.util.concurrent.ThreadFactory;
21
+
22
+/**
23
+ * 交易保障
24
+ */
25
+public class WXPayReport {
26
+
27
+    public static class ReportInfo {
28
+
29
+        /**
30
+         * 布尔变量使用int。0为false, 1为true。
31
+         */
32
+
33
+        // 基本信息
34
+        private String version = "v1";
35
+        private String sdk = WXPayConstants.WXPAYSDK_VERSION;
36
+        private String uuid;  // 交易的标识
37
+        private long timestamp;   // 上报时的时间戳,单位秒
38
+        private long elapsedTimeMillis; // 耗时,单位 毫秒
39
+
40
+        // 针对主域名
41
+        private String firstDomain;  // 第1次请求的域名
42
+        private boolean primaryDomain; //是否主域名
43
+        private int firstConnectTimeoutMillis;  // 第1次请求设置的连接超时时间,单位 毫秒
44
+        private int firstReadTimeoutMillis;  // 第1次请求设置的读写超时时间,单位 毫秒
45
+        private int firstHasDnsError;  // 第1次请求是否出现dns问题
46
+        private int firstHasConnectTimeout; // 第1次请求是否出现连接超时
47
+        private int firstHasReadTimeout; // 第1次请求是否出现连接超时
48
+
49
+        public ReportInfo(String uuid, long timestamp, long elapsedTimeMillis, String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis, boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) {
50
+            this.uuid = uuid;
51
+            this.timestamp = timestamp;
52
+            this.elapsedTimeMillis = elapsedTimeMillis;
53
+            this.firstDomain = firstDomain;
54
+            this.primaryDomain = primaryDomain;
55
+            this.firstConnectTimeoutMillis = firstConnectTimeoutMillis;
56
+            this.firstReadTimeoutMillis = firstReadTimeoutMillis;
57
+            this.firstHasDnsError = firstHasDnsError?1:0;
58
+            this.firstHasConnectTimeout = firstHasConnectTimeout?1:0;
59
+            this.firstHasReadTimeout = firstHasReadTimeout?1:0;
60
+         }
61
+
62
+        @Override
63
+        public String toString() {
64
+            return "ReportInfo{" +
65
+                    "version='" + version + '\'' +
66
+                    ", sdk='" + sdk + '\'' +
67
+                    ", uuid='" + uuid + '\'' +
68
+                    ", timestamp=" + timestamp +
69
+                    ", elapsedTimeMillis=" + elapsedTimeMillis +
70
+                    ", firstDomain='" + firstDomain + '\'' +
71
+                    ", primaryDomain=" + primaryDomain +
72
+                    ", firstConnectTimeoutMillis=" + firstConnectTimeoutMillis +
73
+                    ", firstReadTimeoutMillis=" + firstReadTimeoutMillis +
74
+                    ", firstHasDnsError=" + firstHasDnsError +
75
+                    ", firstHasConnectTimeout=" + firstHasConnectTimeout +
76
+                    ", firstHasReadTimeout=" + firstHasReadTimeout +
77
+                    '}';
78
+        }
79
+
80
+        /**
81
+         * 转换成 csv 格式
82
+         *
83
+         * @return
84
+         */
85
+        public String toLineString(String key) {
86
+            String separator = ",";
87
+            Object[] objects = new Object[] {
88
+                version, sdk, uuid, timestamp, elapsedTimeMillis,
89
+                    firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis,
90
+                    firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout
91
+            };
92
+            StringBuffer sb = new StringBuffer();
93
+            for(Object obj: objects) {
94
+                sb.append(obj).append(separator);
95
+            }
96
+            try {
97
+                String sign = WXPayUtil.HMACSHA256(sb.toString(), key);
98
+                sb.append(sign);
99
+                return sb.toString();
100
+            }
101
+            catch (Exception ex) {
102
+                return null;
103
+            }
104
+
105
+        }
106
+
107
+    }
108
+
109
+    private static final String REPORT_URL = "http://report.mch.weixin.qq.com/wxpay/report/default";
110
+    // private static final String REPORT_URL = "http://127.0.0.1:5000/test";
111
+
112
+
113
+    private static final int DEFAULT_CONNECT_TIMEOUT_MS = 6*1000;
114
+    private static final int DEFAULT_READ_TIMEOUT_MS = 8*1000;
115
+
116
+    private LinkedBlockingQueue<String> reportMsgQueue = null;
117
+    private WXPayConfig config;
118
+    private ExecutorService executorService;
119
+
120
+    private volatile static WXPayReport INSTANCE;
121
+
122
+    private WXPayReport(final WXPayConfig config) {
123
+        this.config = config;
124
+        reportMsgQueue = new LinkedBlockingQueue<String>(config.getReportQueueMaxSize());
125
+
126
+        // 添加处理线程
127
+        executorService = Executors.newFixedThreadPool(config.getReportWorkerNum(), new ThreadFactory() {
128
+            public Thread newThread(Runnable r) {
129
+                Thread t = Executors.defaultThreadFactory().newThread(r);
130
+                t.setDaemon(true);
131
+                return t;
132
+            }
133
+        });
134
+
135
+        if (config.shouldAutoReport()) {
136
+            WXPayUtil.getLogger().info("report worker num: {}", config.getReportWorkerNum());
137
+            for (int i = 0; i < config.getReportWorkerNum(); ++i) {
138
+                executorService.execute(new Runnable() {
139
+                    public void run() {
140
+                        while (true) {
141
+                            // 先用 take 获取数据
142
+                            try {
143
+                                StringBuffer sb = new StringBuffer();
144
+                                String firstMsg = reportMsgQueue.take();
145
+                                WXPayUtil.getLogger().info("get first report msg: {}", firstMsg);
146
+                                String msg = null;
147
+                                sb.append(firstMsg); //会阻塞至有消息
148
+                                int remainNum = config.getReportBatchSize() - 1;
149
+                                for (int j=0; j<remainNum; ++j) {
150
+                                    WXPayUtil.getLogger().info("try get remain report msg");
151
+                                    // msg = reportMsgQueue.poll();  // 不阻塞了
152
+                                    msg = reportMsgQueue.take();
153
+                                    WXPayUtil.getLogger().info("get remain report msg: {}", msg);
154
+                                    if (msg == null) {
155
+                                        break;
156
+                                    }
157
+                                    else {
158
+                                        sb.append("\n");
159
+                                        sb.append(msg);
160
+                                    }
161
+                                }
162
+                                // 上报
163
+                                WXPayReport.httpRequest(sb.toString(), DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
164
+                            }
165
+                            catch (Exception ex) {
166
+                                WXPayUtil.getLogger().warn("report fail. reason: {}", ex.getMessage());
167
+                            }
168
+                        }
169
+                    }
170
+                });
171
+            }
172
+        }
173
+
174
+    }
175
+
176
+    /**
177
+     * 单例,双重校验,请在 JDK 1.5及更高版本中使用
178
+     *
179
+     * @param config
180
+     * @return
181
+     */
182
+    public static WXPayReport getInstance(WXPayConfig config) {
183
+        if (INSTANCE == null) {
184
+            synchronized (WXPayReport.class) {
185
+                if (INSTANCE == null) {
186
+                    INSTANCE = new WXPayReport(config);
187
+                }
188
+            }
189
+        }
190
+        return INSTANCE;
191
+    }
192
+
193
+    public void report(String uuid, long elapsedTimeMillis,
194
+                       String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis,
195
+                       boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) {
196
+        long currentTimestamp = WXPayUtil.getCurrentTimestamp();
197
+        ReportInfo reportInfo = new ReportInfo(uuid, currentTimestamp, elapsedTimeMillis,
198
+                firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis,
199
+                firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout);
200
+        String data = reportInfo.toLineString(config.getKey());
201
+        WXPayUtil.getLogger().info("report {}", data);
202
+        if (data != null) {
203
+            reportMsgQueue.offer(data);
204
+        }
205
+    }
206
+
207
+
208
+    @Deprecated
209
+    private void reportSync(final String data) throws Exception {
210
+        httpRequest(data, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
211
+    }
212
+
213
+    @Deprecated
214
+    private void reportAsync(final String data) throws Exception {
215
+        new Thread(new Runnable() {
216
+            public void run() {
217
+                try {
218
+                    httpRequest(data, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS);
219
+                }
220
+                catch (Exception ex) {
221
+                    WXPayUtil.getLogger().warn("report fail. reason: {}", ex.getMessage());
222
+                }
223
+            }
224
+        }).start();
225
+    }
226
+
227
+    /**
228
+     * http 请求
229
+     * @param data
230
+     * @param connectTimeoutMs
231
+     * @param readTimeoutMs
232
+     * @return
233
+     * @throws Exception
234
+     */
235
+    private static String httpRequest(String data, int connectTimeoutMs, int readTimeoutMs) throws Exception{
236
+        BasicHttpClientConnectionManager connManager;
237
+        connManager = new BasicHttpClientConnectionManager(
238
+                RegistryBuilder.<ConnectionSocketFactory>create()
239
+                        .register("http", PlainConnectionSocketFactory.getSocketFactory())
240
+                        .register("https", SSLConnectionSocketFactory.getSocketFactory())
241
+                        .build(),
242
+                null,
243
+                null,
244
+                null
245
+        );
246
+        HttpClient httpClient = HttpClientBuilder.create()
247
+                .setConnectionManager(connManager)
248
+                .build();
249
+
250
+        HttpPost httpPost = new HttpPost(REPORT_URL);
251
+
252
+        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build();
253
+        httpPost.setConfig(requestConfig);
254
+
255
+        StringEntity postEntity = new StringEntity(data, "UTF-8");
256
+        httpPost.addHeader("Content-Type", "text/xml");
257
+        httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT);
258
+        httpPost.setEntity(postEntity);
259
+
260
+        HttpResponse httpResponse = httpClient.execute(httpPost);
261
+        HttpEntity httpEntity = httpResponse.getEntity();
262
+        return EntityUtils.toString(httpEntity, "UTF-8");
263
+    }
264
+
265
+}

+ 256
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayRequest.java View File

@@ -0,0 +1,256 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import org.apache.http.HttpEntity;
4
+import org.apache.http.HttpResponse;
5
+import org.apache.http.client.HttpClient;
6
+import org.apache.http.client.config.RequestConfig;
7
+import org.apache.http.client.methods.HttpPost;
8
+import org.apache.http.config.RegistryBuilder;
9
+import org.apache.http.conn.ConnectTimeoutException;
10
+import org.apache.http.conn.socket.ConnectionSocketFactory;
11
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
12
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
13
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
14
+import org.apache.http.entity.StringEntity;
15
+import org.apache.http.impl.client.HttpClientBuilder;
16
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
17
+import org.apache.http.util.EntityUtils;
18
+
19
+import javax.net.ssl.KeyManagerFactory;
20
+import javax.net.ssl.SSLContext;
21
+import java.io.InputStream;
22
+import java.net.SocketTimeoutException;
23
+import java.net.UnknownHostException;
24
+import java.security.KeyStore;
25
+import java.security.SecureRandom;
26
+
27
+public class WXPayRequest {
28
+    private WXPayConfig config;
29
+    public WXPayRequest(WXPayConfig config) throws Exception{
30
+
31
+        this.config = config;
32
+    }
33
+
34
+    /**
35
+     * 请求,只请求一次,不做重试
36
+     * @param domain
37
+     * @param urlSuffix
38
+     * @param uuid
39
+     * @param data
40
+     * @param connectTimeoutMs
41
+     * @param readTimeoutMs
42
+     * @param useCert 是否使用证书,针对退款、撤销等操作
43
+     * @return
44
+     * @throws Exception
45
+     */
46
+    private String requestOnce(final String domain, String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert) throws Exception {
47
+        BasicHttpClientConnectionManager connManager;
48
+        if (useCert) {
49
+            // 证书
50
+            char[] password = config.getMchID().toCharArray();
51
+            InputStream certStream = config.getCertStream();
52
+            KeyStore ks = KeyStore.getInstance("PKCS12");
53
+            ks.load(certStream, password);
54
+
55
+            // 实例化密钥库 & 初始化密钥工厂
56
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
57
+            kmf.init(ks, password);
58
+
59
+            // 创建 SSLContext
60
+            SSLContext sslContext = SSLContext.getInstance("TLS");
61
+            sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
62
+
63
+            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
64
+                    sslContext,
65
+                    new String[]{"TLSv1"},
66
+                    null,
67
+                    new DefaultHostnameVerifier());
68
+
69
+            connManager = new BasicHttpClientConnectionManager(
70
+                    RegistryBuilder.<ConnectionSocketFactory>create()
71
+                            .register("http", PlainConnectionSocketFactory.getSocketFactory())
72
+                            .register("https", sslConnectionSocketFactory)
73
+                            .build(),
74
+                    null,
75
+                    null,
76
+                    null
77
+            );
78
+        }
79
+        else {
80
+            connManager = new BasicHttpClientConnectionManager(
81
+                    RegistryBuilder.<ConnectionSocketFactory>create()
82
+                            .register("http", PlainConnectionSocketFactory.getSocketFactory())
83
+                            .register("https", SSLConnectionSocketFactory.getSocketFactory())
84
+                            .build(),
85
+                    null,
86
+                    null,
87
+                    null
88
+            );
89
+        }
90
+
91
+        HttpClient httpClient = HttpClientBuilder.create()
92
+                .setConnectionManager(connManager)
93
+                .build();
94
+
95
+        String url = "https://" + domain + urlSuffix;
96
+        HttpPost httpPost = new HttpPost(url);
97
+
98
+        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build();
99
+        httpPost.setConfig(requestConfig);
100
+
101
+        StringEntity postEntity = new StringEntity(data, "UTF-8");
102
+        httpPost.addHeader("Content-Type", "text/xml");
103
+        httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT + " " + config.getMchID());
104
+        httpPost.setEntity(postEntity);
105
+
106
+        HttpResponse httpResponse = httpClient.execute(httpPost);
107
+        HttpEntity httpEntity = httpResponse.getEntity();
108
+        return EntityUtils.toString(httpEntity, "UTF-8");
109
+
110
+    }
111
+
112
+
113
+    private String request(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert, boolean autoReport) throws Exception {
114
+        Exception exception = null;
115
+        long elapsedTimeMillis = 0;
116
+        long startTimestampMs = WXPayUtil.getCurrentTimestampMs();
117
+        boolean firstHasDnsErr = false;
118
+        boolean firstHasConnectTimeout = false;
119
+        boolean firstHasReadTimeout = false;
120
+        IWXPayDomain.DomainInfo domainInfo = config.getWXPayDomain().getDomain(config);
121
+        if(domainInfo == null){
122
+            throw new Exception("WXPayConfig.getWXPayDomain().getDomain() is empty or null");
123
+        }
124
+        try {
125
+            String result = requestOnce(domainInfo.domain, urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, useCert);
126
+            elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
127
+            config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, null);
128
+            WXPayReport.getInstance(config).report(
129
+                    uuid,
130
+                    elapsedTimeMillis,
131
+                    domainInfo.domain,
132
+                    domainInfo.primaryDomain,
133
+                    connectTimeoutMs,
134
+                    readTimeoutMs,
135
+                    firstHasDnsErr,
136
+                    firstHasConnectTimeout,
137
+                    firstHasReadTimeout);
138
+            return result;
139
+        }
140
+        catch (UnknownHostException ex) {  // dns 解析错误,或域名不存在
141
+            exception = ex;
142
+            firstHasDnsErr = true;
143
+            elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
144
+            WXPayUtil.getLogger().warn("UnknownHostException for domainInfo {}", domainInfo);
145
+            WXPayReport.getInstance(config).report(
146
+                    uuid,
147
+                    elapsedTimeMillis,
148
+                    domainInfo.domain,
149
+                    domainInfo.primaryDomain,
150
+                    connectTimeoutMs,
151
+                    readTimeoutMs,
152
+                    firstHasDnsErr,
153
+                    firstHasConnectTimeout,
154
+                    firstHasReadTimeout
155
+            );
156
+        }
157
+        catch (ConnectTimeoutException ex) {
158
+            exception = ex;
159
+            firstHasConnectTimeout = true;
160
+            elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
161
+            WXPayUtil.getLogger().warn("connect timeout happened for domainInfo {}", domainInfo);
162
+            WXPayReport.getInstance(config).report(
163
+                    uuid,
164
+                    elapsedTimeMillis,
165
+                    domainInfo.domain,
166
+                    domainInfo.primaryDomain,
167
+                    connectTimeoutMs,
168
+                    readTimeoutMs,
169
+                    firstHasDnsErr,
170
+                    firstHasConnectTimeout,
171
+                    firstHasReadTimeout
172
+            );
173
+        }
174
+        catch (SocketTimeoutException ex) {
175
+            exception = ex;
176
+            firstHasReadTimeout = true;
177
+            elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
178
+            WXPayUtil.getLogger().warn("timeout happened for domainInfo {}", domainInfo);
179
+            WXPayReport.getInstance(config).report(
180
+                    uuid,
181
+                    elapsedTimeMillis,
182
+                    domainInfo.domain,
183
+                    domainInfo.primaryDomain,
184
+                    connectTimeoutMs,
185
+                    readTimeoutMs,
186
+                    firstHasDnsErr,
187
+                    firstHasConnectTimeout,
188
+                    firstHasReadTimeout);
189
+        }
190
+        catch (Exception ex) {
191
+            exception = ex;
192
+            elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
193
+            WXPayReport.getInstance(config).report(
194
+                    uuid,
195
+                    elapsedTimeMillis,
196
+                    domainInfo.domain,
197
+                    domainInfo.primaryDomain,
198
+                    connectTimeoutMs,
199
+                    readTimeoutMs,
200
+                    firstHasDnsErr,
201
+                    firstHasConnectTimeout,
202
+                    firstHasReadTimeout);
203
+        }
204
+        config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, exception);
205
+        throw exception;
206
+    }
207
+
208
+
209
+    /**
210
+     * 可重试的,非双向认证的请求
211
+     * @param urlSuffix
212
+     * @param uuid
213
+     * @param data
214
+     * @return
215
+     */
216
+    public String requestWithoutCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception {
217
+        return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), false, autoReport);
218
+    }
219
+
220
+    /**
221
+     * 可重试的,非双向认证的请求
222
+     * @param urlSuffix
223
+     * @param uuid
224
+     * @param data
225
+     * @param connectTimeoutMs
226
+     * @param readTimeoutMs
227
+     * @return
228
+     */
229
+    public String requestWithoutCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs,  boolean autoReport) throws Exception {
230
+        return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, false, autoReport);
231
+    }
232
+
233
+    /**
234
+     * 可重试的,双向认证的请求
235
+     * @param urlSuffix
236
+     * @param uuid
237
+     * @param data
238
+     * @return
239
+     */
240
+    public String requestWithCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception {
241
+        return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), true, autoReport);
242
+    }
243
+
244
+    /**
245
+     * 可重试的,双向认证的请求
246
+     * @param urlSuffix
247
+     * @param uuid
248
+     * @param data
249
+     * @param connectTimeoutMs
250
+     * @param readTimeoutMs
251
+     * @return
252
+     */
253
+    public String requestWithCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception {
254
+        return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, true, autoReport);
255
+    }
256
+}

+ 295
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayUtil.java View File

@@ -0,0 +1,295 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import org.slf4j.Logger;
4
+import org.slf4j.LoggerFactory;
5
+import org.w3c.dom.Node;
6
+import org.w3c.dom.NodeList;
7
+
8
+import javax.crypto.Mac;
9
+import javax.crypto.spec.SecretKeySpec;
10
+import javax.xml.parsers.DocumentBuilder;
11
+import javax.xml.transform.OutputKeys;
12
+import javax.xml.transform.Transformer;
13
+import javax.xml.transform.TransformerFactory;
14
+import javax.xml.transform.dom.DOMSource;
15
+import javax.xml.transform.stream.StreamResult;
16
+import java.io.ByteArrayInputStream;
17
+import java.io.InputStream;
18
+import java.io.StringWriter;
19
+import java.security.MessageDigest;
20
+import java.security.SecureRandom;
21
+import java.util.*;
22
+import com.huiju.estateagents.common.wxpay.WXPayConstants.SignType;
23
+
24
+
25
+public class WXPayUtil {
26
+
27
+    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
28
+
29
+    private static final Random RANDOM = new SecureRandom();
30
+
31
+    /**
32
+     * XML格式字符串转换为Map
33
+     *
34
+     * @param strXML XML字符串
35
+     * @return XML数据转换后的Map
36
+     * @throws Exception
37
+     */
38
+    public static Map<String, String> xmlToMap(String strXML) throws Exception {
39
+        try {
40
+            Map<String, String> data = new HashMap<String, String>();
41
+            DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
42
+            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
43
+            org.w3c.dom.Document doc = documentBuilder.parse(stream);
44
+            doc.getDocumentElement().normalize();
45
+            NodeList nodeList = doc.getDocumentElement().getChildNodes();
46
+            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
47
+                Node node = nodeList.item(idx);
48
+                if (node.getNodeType() == Node.ELEMENT_NODE) {
49
+                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
50
+                    data.put(element.getNodeName(), element.getTextContent());
51
+                }
52
+            }
53
+            try {
54
+                stream.close();
55
+            } catch (Exception ex) {
56
+                // do nothing
57
+            }
58
+            return data;
59
+        } catch (Exception ex) {
60
+            WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
61
+            throw ex;
62
+        }
63
+
64
+    }
65
+
66
+    /**
67
+     * 将Map转换为XML格式的字符串
68
+     *
69
+     * @param data Map类型数据
70
+     * @return XML格式的字符串
71
+     * @throws Exception
72
+     */
73
+    public static String mapToXml(Map<String, String> data) throws Exception {
74
+        org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
75
+        org.w3c.dom.Element root = document.createElement("xml");
76
+        document.appendChild(root);
77
+        for (String key: data.keySet()) {
78
+            String value = data.get(key);
79
+            if (value == null) {
80
+                value = "";
81
+            }
82
+            value = value.trim();
83
+            org.w3c.dom.Element filed = document.createElement(key);
84
+            filed.appendChild(document.createTextNode(value));
85
+            root.appendChild(filed);
86
+        }
87
+        TransformerFactory tf = TransformerFactory.newInstance();
88
+        Transformer transformer = tf.newTransformer();
89
+        DOMSource source = new DOMSource(document);
90
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
91
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
92
+        StringWriter writer = new StringWriter();
93
+        StreamResult result = new StreamResult(writer);
94
+        transformer.transform(source, result);
95
+        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
96
+        try {
97
+            writer.close();
98
+        }
99
+        catch (Exception ex) {
100
+        }
101
+        return output;
102
+    }
103
+
104
+
105
+    /**
106
+     * 生成带有 sign 的 XML 格式字符串
107
+     *
108
+     * @param data Map类型数据
109
+     * @param key API密钥
110
+     * @return 含有sign字段的XML
111
+     */
112
+    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
113
+        return generateSignedXml(data, key, SignType.MD5);
114
+    }
115
+
116
+    /**
117
+     * 生成带有 sign 的 XML 格式字符串
118
+     *
119
+     * @param data Map类型数据
120
+     * @param key API密钥
121
+     * @param signType 签名类型
122
+     * @return 含有sign字段的XML
123
+     */
124
+    public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
125
+        String sign = generateSignature(data, key, signType);
126
+        data.put(WXPayConstants.FIELD_SIGN, sign);
127
+        return mapToXml(data);
128
+    }
129
+
130
+
131
+    /**
132
+     * 判断签名是否正确
133
+     *
134
+     * @param xmlStr XML格式数据
135
+     * @param key API密钥
136
+     * @return 签名是否正确
137
+     * @throws Exception
138
+     */
139
+    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
140
+        Map<String, String> data = xmlToMap(xmlStr);
141
+        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
142
+            return false;
143
+        }
144
+        String sign = data.get(WXPayConstants.FIELD_SIGN);
145
+        return generateSignature(data, key).equals(sign);
146
+    }
147
+
148
+    /**
149
+     * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
150
+     *
151
+     * @param data Map类型数据
152
+     * @param key API密钥
153
+     * @return 签名是否正确
154
+     * @throws Exception
155
+     */
156
+    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
157
+        return isSignatureValid(data, key, SignType.MD5);
158
+    }
159
+
160
+    /**
161
+     * 判断签名是否正确,必须包含sign字段,否则返回false。
162
+     *
163
+     * @param data Map类型数据
164
+     * @param key API密钥
165
+     * @param signType 签名方式
166
+     * @return 签名是否正确
167
+     * @throws Exception
168
+     */
169
+    public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
170
+        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
171
+            return false;
172
+        }
173
+        String sign = data.get(WXPayConstants.FIELD_SIGN);
174
+        return generateSignature(data, key, signType).equals(sign);
175
+    }
176
+
177
+    /**
178
+     * 生成签名
179
+     *
180
+     * @param data 待签名数据
181
+     * @param key API密钥
182
+     * @return 签名
183
+     */
184
+    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
185
+        return generateSignature(data, key, SignType.MD5);
186
+    }
187
+
188
+    /**
189
+     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
190
+     *
191
+     * @param data 待签名数据
192
+     * @param key API密钥
193
+     * @param signType 签名方式
194
+     * @return 签名
195
+     */
196
+    public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
197
+        Set<String> keySet = data.keySet();
198
+        String[] keyArray = keySet.toArray(new String[keySet.size()]);
199
+        Arrays.sort(keyArray);
200
+        StringBuilder sb = new StringBuilder();
201
+        for (String k : keyArray) {
202
+            if (k.equals(WXPayConstants.FIELD_SIGN)) {
203
+                continue;
204
+            }
205
+            if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
206
+                sb.append(k).append("=").append(data.get(k).trim()).append("&");
207
+        }
208
+        sb.append("key=").append(key);
209
+        if (SignType.MD5.equals(signType)) {
210
+            return MD5(sb.toString()).toUpperCase();
211
+        }
212
+        else if (SignType.HMACSHA256.equals(signType)) {
213
+            return HMACSHA256(sb.toString(), key);
214
+        }
215
+        else {
216
+            throw new Exception(String.format("Invalid sign_type: %s", signType));
217
+        }
218
+    }
219
+
220
+
221
+    /**
222
+     * 获取随机字符串 Nonce Str
223
+     *
224
+     * @return String 随机字符串
225
+     */
226
+    public static String generateNonceStr() {
227
+        char[] nonceChars = new char[32];
228
+        for (int index = 0; index < nonceChars.length; ++index) {
229
+            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
230
+        }
231
+        return new String(nonceChars);
232
+    }
233
+
234
+
235
+    /**
236
+     * 生成 MD5
237
+     *
238
+     * @param data 待处理数据
239
+     * @return MD5结果
240
+     */
241
+    public static String MD5(String data) throws Exception {
242
+        MessageDigest md = MessageDigest.getInstance("MD5");
243
+        byte[] array = md.digest(data.getBytes("UTF-8"));
244
+        StringBuilder sb = new StringBuilder();
245
+        for (byte item : array) {
246
+            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
247
+        }
248
+        return sb.toString().toUpperCase();
249
+    }
250
+
251
+    /**
252
+     * 生成 HMACSHA256
253
+     * @param data 待处理数据
254
+     * @param key 密钥
255
+     * @return 加密结果
256
+     * @throws Exception
257
+     */
258
+    public static String HMACSHA256(String data, String key) throws Exception {
259
+        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
260
+        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
261
+        sha256_HMAC.init(secret_key);
262
+        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
263
+        StringBuilder sb = new StringBuilder();
264
+        for (byte item : array) {
265
+            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
266
+        }
267
+        return sb.toString().toUpperCase();
268
+    }
269
+
270
+    /**
271
+     * 日志
272
+     * @return
273
+     */
274
+    public static Logger getLogger() {
275
+        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
276
+        return logger;
277
+    }
278
+
279
+    /**
280
+     * 获取当前时间戳,单位秒
281
+     * @return
282
+     */
283
+    public static long getCurrentTimestamp() {
284
+        return System.currentTimeMillis()/1000;
285
+    }
286
+
287
+    /**
288
+     * 获取当前时间戳,单位毫秒
289
+     * @return
290
+     */
291
+    public static long getCurrentTimestampMs() {
292
+        return System.currentTimeMillis();
293
+    }
294
+
295
+}

+ 30
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WXPayXmlUtil.java View File

@@ -0,0 +1,30 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import org.w3c.dom.Document;
4
+
5
+import javax.xml.XMLConstants;
6
+import javax.xml.parsers.DocumentBuilder;
7
+import javax.xml.parsers.DocumentBuilderFactory;
8
+import javax.xml.parsers.ParserConfigurationException;
9
+
10
+/**
11
+ * 2018/7/3
12
+ */
13
+public final class WXPayXmlUtil {
14
+    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
15
+        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
16
+        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
17
+        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
18
+        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
19
+        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
20
+        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
21
+        documentBuilderFactory.setXIncludeAware(false);
22
+        documentBuilderFactory.setExpandEntityReferences(false);
23
+
24
+        return documentBuilderFactory.newDocumentBuilder();
25
+    }
26
+
27
+    public static Document newDocument() throws ParserConfigurationException {
28
+        return newDocumentBuilder().newDocument();
29
+    }
30
+}

+ 126
- 0
src/main/java/com/huiju/estateagents/common/wxpay/WxConfig.java View File

@@ -0,0 +1,126 @@
1
+package com.huiju.estateagents.common.wxpay;
2
+
3
+import java.io.*;
4
+import java.net.HttpURLConnection;
5
+import java.net.URL;
6
+
7
+/**
8
+ * 监控 控制器
9
+ * @author fxf
10
+ */
11
+public class WxConfig extends WXPayConfig {
12
+	private String appid;
13
+
14
+	private String machId;
15
+
16
+	private String key;
17
+
18
+	private byte[] certData;
19
+
20
+	//api文件放在oss里所以是个http地址
21
+	private String apiPath;
22
+
23
+	public WxConfig() throws Exception {
24
+		URL urlConet = new URL(apiPath);
25
+		HttpURLConnection con = (HttpURLConnection)urlConet.openConnection();
26
+		con.setRequestMethod("GET");
27
+		con.setConnectTimeout(4 * 1000);
28
+		InputStream inStream = con .getInputStream();
29
+		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
30
+		byte[] buffer = new byte[2048];
31
+		int len = 0;
32
+		while( (len=inStream.read(buffer)) != -1 ){
33
+			outStream.write(buffer, 0, len);
34
+		}
35
+		inStream.close();
36
+		this.certData =  outStream.toByteArray();
37
+	}
38
+
39
+	/**
40
+	 * 获取 App ID
41
+	 *
42
+	 * @return App ID
43
+	 */
44
+	@Override
45
+	public String getAppID() {
46
+		//银城慧家
47
+		return appid;
48
+	}
49
+	
50
+	/**
51
+	 * 获取 Mch ID
52
+	 *
53
+	 * @return Mch ID
54
+	 */
55
+	@Override
56
+	public String getMchID() {
57
+		return machId;
58
+	}
59
+	
60
+	/**
61
+	 * 获取 API 密钥
62
+	 *
63
+	 * @return API密钥
64
+	 */
65
+	@Override
66
+	public String getKey() {
67
+		return key;
68
+	}
69
+	
70
+	/**
71
+	 * 获取商户证书内容
72
+	 *
73
+	 * @return 商户证书内容
74
+	 */
75
+	@Override
76
+	public InputStream getCertStream() {
77
+		ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
78
+		return certBis;
79
+
80
+	}
81
+
82
+	public int getHttpConnectTimeoutMs() {
83
+		return 8000;
84
+	}
85
+
86
+	public int getHttpReadTimeoutMs() {
87
+		return 10000;
88
+	}
89
+	
90
+	/**
91
+	 * 获取WXPayDomain, 用于多域名容灾自动切换
92
+	 *
93
+	 * @return
94
+	 */
95
+	@Override
96
+	IWXPayDomain getWXPayDomain() {
97
+		IWXPayDomain iwxPayDomain = new IWXPayDomain() {
98
+			@Override
99
+			public void report(String domain, long elapsedTimeMillis, Exception ex) {
100
+			
101
+			}
102
+			@Override
103
+			public DomainInfo getDomain(WXPayConfig config) {
104
+				return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
105
+			}
106
+		};
107
+		return iwxPayDomain;
108
+	}
109
+
110
+	public void setAppid(String appid) {
111
+		this.appid = appid;
112
+	}
113
+
114
+	public void setMachId(String machId) {
115
+		this.machId = machId;
116
+	}
117
+
118
+	public void setKey(String key) {
119
+		this.key = key;
120
+	}
121
+
122
+	public void setApiPath(String apiPath) {
123
+		this.apiPath = apiPath;
124
+	}
125
+}
126
+

+ 75
- 0
src/main/java/com/huiju/estateagents/controller/WxPayController.java View File

@@ -0,0 +1,75 @@
1
+package com.huiju.estateagents.controller;
2
+
3
+import com.google.common.collect.Maps;
4
+import com.huiju.estateagents.base.BaseController;
5
+import com.huiju.estateagents.base.ResponseBean;
6
+import com.huiju.estateagents.common.wxpay.WXPayUtil;
7
+import com.huiju.estateagents.entity.TaOrder;
8
+import com.huiju.estateagents.service.IWxPayService;
9
+import lombok.extern.slf4j.Slf4j;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import javax.servlet.http.HttpServletRequest;
14
+import javax.servlet.http.HttpServletResponse;
15
+import javax.servlet.http.HttpSession;
16
+import java.io.ByteArrayOutputStream;
17
+import java.io.IOException;
18
+import java.io.InputStream;
19
+import java.util.HashMap;
20
+import java.util.Map;
21
+
22
+@Slf4j
23
+@RestController
24
+@RequestMapping("/api")
25
+public class WxPayController extends BaseController {
26
+
27
+    @Autowired
28
+    private IWxPayService wxPayService;
29
+
30
+    @RequestMapping(value = "/pay/unifiedOrder",method = RequestMethod.POST)
31
+    public ResponseBean wxUnifiedOrder(@RequestBody TaOrder taOrder,
32
+                                       HttpServletRequest request){
33
+        ResponseBean responseBean = new ResponseBean();
34
+        try {
35
+            taOrder.setOrgId(getOrgId(request));
36
+            taOrder.setPersonId(getPersonId(request));
37
+            responseBean = wxPayService.payUnifiedOrder(taOrder,request.getRemoteAddr());
38
+        } catch (Exception e) {
39
+            e.printStackTrace();
40
+            responseBean.addError(e.getMessage());
41
+            return responseBean;
42
+        }
43
+        return responseBean;
44
+    }
45
+
46
+    @RequestMapping(value = "/notify/{type}",method = RequestMethod.POST)
47
+    public String wxBillNotify(@PathVariable String type, HttpServletRequest request){
48
+
49
+        Map<String, String> resultMap = new HashMap<>();
50
+        try {
51
+            InputStream inStream = request.getInputStream();
52
+            ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
53
+            byte[] buffer = new byte[1024];
54
+            int len = 0;
55
+            while ((len = inStream.read(buffer)) != -1) {
56
+                outSteam.write(buffer, 0, len);
57
+            }
58
+
59
+            // 获取微信调用我们notify_url的返回信息
60
+            String result = new String(outSteam.toByteArray(), "utf-8");
61
+            log.info("wxnotify:微信支付----result----=" + result);
62
+
63
+            // 关闭流
64
+            outSteam.close();
65
+            inStream.close();
66
+            // xml转换为map
67
+            resultMap = WXPayUtil.xmlToMap(result);
68
+        } catch (Exception e) {
69
+            e.printStackTrace();
70
+        }
71
+
72
+        return wxPayService.wxNotify(resultMap, type);
73
+    }
74
+
75
+}

+ 1
- 1
src/main/java/com/huiju/estateagents/entity/TaOrder.java View File

@@ -48,7 +48,7 @@ public class TaOrder implements Serializable {
48 48
     /**
49 49
      * targetId
50 50
      */
51
-    private Integer targetId;
51
+    private String targetId;
52 52
 
53 53
     /**
54 54
      * targetType

+ 2
- 1
src/main/java/com/huiju/estateagents/interceptor/AccessInterceptor.java View File

@@ -69,7 +69,8 @@ public class AccessInterceptor implements HandlerInterceptor {
69 69
             "/webjars/springfox-swagger-ui/css/typography.css",
70 70
             "/clean/menurole",
71 71
             "/clean/buttonrole",
72
-            "/api/admin/taPersonFromRecord"
72
+            "/api/admin/taPersonFromRecord",
73
+            "/api/notify",//微信支付所有回掉放过
73 74
     };
74 75
 
75 76
     /*

+ 19
- 0
src/main/java/com/huiju/estateagents/service/IWxPayService.java View File

@@ -0,0 +1,19 @@
1
+package com.huiju.estateagents.service;
2
+
3
+import com.huiju.estateagents.base.ResponseBean;
4
+import com.huiju.estateagents.entity.TaOrder;
5
+
6
+import java.util.Map;
7
+
8
+public interface IWxPayService {
9
+
10
+    /**
11
+     * 微信支付-统一下单
12
+     * @param taOrder
13
+     * @param clientIp
14
+     * @return
15
+     */
16
+    ResponseBean payUnifiedOrder(TaOrder taOrder, String clientIp) throws Exception;
17
+
18
+    String wxNotify(Map<String, String> resultMap, String type);
19
+}

+ 176
- 0
src/main/java/com/huiju/estateagents/service/impl/WxPayServiceImpl.java View File

@@ -0,0 +1,176 @@
1
+package com.huiju.estateagents.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.huiju.estateagents.base.ResponseBean;
5
+import com.huiju.estateagents.common.CommConstant;
6
+import com.huiju.estateagents.common.wxpay.WXPay;
7
+import com.huiju.estateagents.common.wxpay.WXPayConstants;
8
+import com.huiju.estateagents.common.wxpay.WXPayUtil;
9
+import com.huiju.estateagents.common.wxpay.WxConfig;
10
+import com.huiju.estateagents.entity.*;
11
+import com.huiju.estateagents.mapper.TaRaiseMapper;
12
+import com.huiju.estateagents.mapper.TaRaiseRecordMapper;
13
+import com.huiju.estateagents.mapper.TaWxPayConfigMapper;
14
+import com.huiju.estateagents.service.IWxPayService;
15
+import lombok.extern.slf4j.Slf4j;
16
+import org.springframework.beans.factory.annotation.Autowired;
17
+import org.springframework.beans.factory.annotation.Value;
18
+import org.springframework.stereotype.Service;
19
+
20
+import java.time.Instant;
21
+import java.util.HashMap;
22
+import java.util.Map;
23
+
24
+@Slf4j
25
+@Service
26
+public class WxPayServiceImpl implements IWxPayService {
27
+
28
+    @Value("${pay-notify}")
29
+    private String payNotify;
30
+
31
+    @Autowired
32
+    private TaWxPayConfigMapper taWxPayConfigMapper;
33
+
34
+    @Autowired
35
+    private TaRaiseRecordMapper taRaiseRecordMapper;
36
+
37
+    @Autowired
38
+    private TaRaiseMapper taRaiseMapper;
39
+
40
+    /**
41
+     * 微信支付-统一下单
42
+     *
43
+     * @param taOrder
44
+     * @param clientIp
45
+     * @return
46
+     */
47
+    @Override
48
+    public ResponseBean payUnifiedOrder(TaOrder taOrder, String clientIp) throws Exception {
49
+        //构建微信配置
50
+        WxConfig config = getWxConfig(taOrder.getOrgId());
51
+        WXPay wxpay = new WXPay(config);
52
+
53
+        String payPrice = getPayPrice(taOrder);
54
+        if (null == payPrice){
55
+            throw new Exception("此认筹单异常,请联系置业顾问!");
56
+        }
57
+
58
+        //生成商户订单
59
+        String tradeNo = "这边的规则要重新定义";
60
+        taOrder.setTradeNo(tradeNo);
61
+
62
+        //下单
63
+        Map<String, String> data = new HashMap<String, String>();
64
+        data.put("body", taOrder.getTargetType().equals("house") ? "房源认筹" : "微信支付");
65
+        //商品号唯一
66
+        data.put("out_trade_no", tradeNo);
67
+        data.put("device_info", "");
68
+        data.put("fee_type", "CNY");
69
+        //金额 后台计算
70
+        data.put("total_fee", payPrice);
71
+        //终端IP
72
+        data.put("spbill_create_ip", clientIp);
73
+        //回调地址
74
+        data.put("notify_url", payNotify+taOrder.getTargetType());
75
+        // 此处指定为APP支付
76
+        data.put("trade_type", "JSAPI");
77
+        Map<String, String> resp = wxpay.unifiedOrder(data);
78
+        log.info("下单成功:{}", resp);
79
+        if (resp.get("result_code").equalsIgnoreCase(WXPayConstants.FAIL)){
80
+            throw new Exception(resp.get("err_code_des"));
81
+        }
82
+
83
+        //第二次重新生成签名
84
+        Map<String, String> secondSignData = new HashMap<String, String>();
85
+        long timestamp = Instant.now().getEpochSecond();
86
+        secondSignData.put("appid", config.getAppID());
87
+        secondSignData.put("partnerid", config.getMchID());
88
+        secondSignData.put("prepayid", resp.get("prepay_id"));
89
+        secondSignData.put("noncestr", resp.get("nonce_str"));
90
+        secondSignData.put("timestamp", String.valueOf(timestamp));
91
+        secondSignData.put("package", "Sign=WXPay");
92
+        secondSignData.put("sign", WXPayUtil.generateSignature(secondSignData, config.getKey(), WXPayConstants.SignType.MD5));
93
+
94
+        log.info("订单: {},订单类型: {}, 状态变更为正在支付", taOrder.getTradeNo(), taOrder.getTargetType());
95
+
96
+        // 更改认筹单单状态 为正在支付,并插入订单表
97
+        updateOrderStatus(taOrder);
98
+        return ResponseBean.success(secondSignData);
99
+    }
100
+
101
+    @Override
102
+    public String wxNotify(Map<String, String> resultMap, String type) {
103
+        String failResult = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[失败]]></return_msg>" + "</xml> ";
104
+        String successResult = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
105
+        if (WXPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("result_code"))) {
106
+            //处理业务逻辑 修改订单状态等
107
+            resultMap.put("pay_type","0");
108
+            String state = "";
109
+            if (type.equals("house")) {
110
+                //state = wxPayService.wxCarNotify(resultMap);
111
+            }
112
+
113
+            if (state.equals("success")){
114
+                log.info("微信支付成功。。");
115
+                return successResult;
116
+            }
117
+        }else{
118
+            return failResult;
119
+        }
120
+        return null;
121
+    }
122
+
123
+    /**
124
+     * 获取支付的钱数
125
+     * @param taOrder
126
+     * @return
127
+     */
128
+    private String getPayPrice(TaOrder taOrder) {
129
+        //根据targetType取不同的表的数据
130
+        if (taOrder.getTargetType().equals("house")){
131
+            TaRaiseRecord taRaiseRecord = taRaiseRecordMapper.selectById(taOrder.getTargetId());
132
+            if (null == taRaiseRecord){
133
+                return null;
134
+            }
135
+            if (taRaiseRecord.getHouseLockingStatus().equals(CommConstant.HOUSE_LOCKING_STATUS_UNLOCKED)){
136
+                return null;
137
+            }
138
+            TaRaise taRaise = taRaiseMapper.selectById(taRaiseRecord.getRaiseId());
139
+            if (null == taRaise){
140
+                return null;
141
+            }
142
+            return String.valueOf(taRaise.getRaisePrice());
143
+        }
144
+        return null;
145
+    }
146
+
147
+    /**
148
+     * 获取每个小程序独立的微信账户
149
+     * @param orgId
150
+     * @return
151
+     */
152
+    private WxConfig getWxConfig(Integer orgId) throws Exception {
153
+        //根据orgId获取支付配置
154
+        QueryWrapper<TaWxPayConfig> taWxPayConfigQueryWrapper = new QueryWrapper<>();
155
+        taWxPayConfigQueryWrapper.eq("org_id",orgId);
156
+        TaWxPayConfig taWxPayConfig = taWxPayConfigMapper.selectOne(taWxPayConfigQueryWrapper);
157
+        if (null == taWxPayConfig){
158
+            throw new Exception("请先完善支付信息");
159
+        }
160
+        //把库里的值赋值给微信
161
+        WxConfig config = new WxConfig();
162
+        config.setAppid(taWxPayConfig.getAppid());
163
+        config.setMachId(taWxPayConfig.getMchId());
164
+        config.setApiPath(taWxPayConfig.getApiPath());
165
+        config.setKey(taWxPayConfig.getMchKey());
166
+        return config;
167
+    }
168
+
169
+    /**
170
+     * 更改订单状态为正在支付
171
+     * @param taOrder
172
+     */
173
+    private void updateOrderStatus(TaOrder taOrder) {
174
+
175
+    }
176
+}

+ 4
- 1
src/main/resources/application-blue.yml View File

@@ -71,4 +71,7 @@ sms:
71 71
     sign: 营销云
72 72
 
73 73
   visitor:
74
-    code: "0501"
74
+    code: "0501"
75
+
76
+# 支付回调
77
+pay-notify: https://dev.pawoma.cn/api/notify/

+ 18
- 0
src/test/java/com/huiju/estateagents/WxPayTest.java View File

@@ -0,0 +1,18 @@
1
+package com.huiju.estateagents;
2
+
3
+import org.junit.Test;
4
+import org.junit.runner.RunWith;
5
+import org.springframework.boot.test.context.SpringBootTest;
6
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
7
+import org.springframework.transaction.annotation.Transactional;
8
+
9
+@RunWith(SpringJUnit4ClassRunner.class)
10
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
11
+@Transactional
12
+public class WxPayTest {
13
+
14
+    @Test
15
+    public void getLocationCity() {
16
+
17
+    }
18
+}