微信支付流程(V2)
# 微信支付流程
# 准备
再使用前阅读微信支付文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
因为我们使用的是“公众号”支付,所以我们采用的是**JSAPI
**支付类型。
在开始使用之前,我们需要准备以下内容
# 商户号
商户号的申请:由公司作为一个商户进行申请(个人无法申请),因需要提供营业执照等相关材料。
# 公众号
目前项目中存在多个“公众号平台”,以生产与测试环境为例。
生产环境使用的是由公司认证的“公众号”,测试环境使用的是未认证的“测试公众号”。
# 配置
# 商户号管理界面配置
生成商户密钥
绑定公众号:公众号需要是通过认证的,否则无法绑定;
配置
JSAPI
支付授权目录:配置支付支付调起页面,eg: https://address/openPay/
,(只需要配置到html
页面的上级目录即可),支付页面调起代码微信支付提供:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6获取支付证书,因为微信支付一些业务需要支付证书进行双向认证,所以需要通过商户号平台进行支付证书的申请,操作地址:
微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全
,参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
我们如何使用“测试公众号”进行支付验证?
# 编码准备
在编码之前,需要下载微信提供的SDK:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3 (如果觉得个人很厉害,可以忽略)
下载之后需要根据需求,更改部分代码,然后重新打包上传maven,也可以放在项目中。
目前这边根据后续的业务需求,更改了SDK
中的WXPayConfig.class
,因为SDK
中提供的个别属性是default
类型,在后续的使用继承中无法使用,所以将它更改为public
,如:
public abstract class WXPayConfig {
/**
* 获取 App ID
*
* @return App ID
*/
public abstract String getAppID();
/**
* 获取 Mch ID
*
* @return Mch ID
*/
public abstract String getMchID();
/**
* 获取 API 密钥
*
* @return API密钥
*/
public abstract String getKey();
/**
* 获取商户证书内容
*
* @return 商户证书内容
*/
public abstract InputStream getCertStream();
/**
* HTTP(S) 连接超时时间,单位毫秒
*
* @return
*/
public int getHttpConnectTimeoutMs() {
return 6*1000;
}
/**
* HTTP(S) 读数据超时时间,单位毫秒
*
* @return
*/
public int getHttpReadTimeoutMs() {
return 8*1000;
}
/**
* 获取WXPayDomain, 用于多域名容灾自动切换
* @return
*/
public abstract IWXPayDomain getWXPayDomain();
/**
* 是否自动上报。
* 若要关闭自动上报,子类中实现该函数返回 false 即可。
*
* @return
*/
public boolean shouldAutoReport() {
return true;
}
/**
* 进行健康上报的线程的数量
*
* @return
*/
public int getReportWorkerNum() {
return 6;
}
/**
* 健康上报缓存消息的最大数量。会有线程去独立上报
* 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
*
* @return
*/
public int getReportQueueMaxSize() {
return 10000;
}
/**
* 批量上报,一次最多上报多个数据
*
* @return
*/
public int getReportBatchSize() {
return 10;
}
}
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
# 如何正确的使用SDK
?
# 继承WXPayConfig.class
public class WeChatPayConfig extends WXPayConfig {
private String appId;
private String mchId;
private String key;
private byte[] certData;
public WeChatPayConfig() throws IOException {
// 读取支付证书
InputStream certInputStream =
Objects.requireNonNull(WeChatPayConfig.class.getClassLoader().getResourceAsStream("static/weChat/apiclient_cert.p12"));
this.certData = IOUtils.toByteArray(certInputStream);
}
@Override
public String getAppID() {
return this.appId;
}
@Override
public String getMchID() {
return this.mchId;
}
@Override
public String getKey() {
return this.key;
}
@Override
public InputStream getCertStream() {
return new ByteArrayInputStream(this.certData);
}
@Override
public IWXPayDomain getWXPayDomain() {
IWXPayDomain iwxPayDomain = new IWXPayDomain() {
@Override
public void report(String s, long l, Exception e) {
}
@Override
public DomainInfo getDomain(WXPayConfig wxPayConfig) {
return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
return iwxPayDomain;
}
}
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
# 构建微信支付客户端
/**
* 获取微信支付客户端
*
* @param payBaseDTO
* @return 微信支付客户端
* @throws Exception 异常
*/
private WXPay getWeChatPay(PayBaseDTO payBaseDTO) throws Exception {
WXPayConfig wxPayConfig = buildWeChatPayConfig(payBaseDTO);
// 构建微信客户端时,注意“useSandbox”这个值的使用
return new WXPay(wxPayConfig, true, useSandbox);
}
/**
* 构建微信支付配置信息
*
* @param payBaseDTO 支付配置基础信息
* @return 微信支付配置
*/
private WXPayConfig buildWeChatPayConfig(PayBaseDTO payBaseDTO) throws IOException {
WeChatPayConfig weChatPayConfig = new WeChatPayConfig();
weChatPayConfig.setAppId(payBaseDTO.getWxMpAppId());
weChatPayConfig.setKey(payBaseDTO.getWxMpMerchantKey());
weChatPayConfig.setMchId(payBaseDTO.getWxMpMerchantNo());
return weChatPayConfig;
}
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
useSandbox
是否使用沙箱环境,微信支付提供沙箱,但沙箱只能用于验证接口调用是否正常,不能将业务串联验证,而这个属性还影响到了微信客户端使用的加密类型,微信支付提供了 MD5
、HMACSHA256
加密,如果不使用“沙箱”环境,一般推荐使用HMACSHA256
加密。
而加密类型用于加密签名,加密类型必须要与签名生成的加密类型一致,否则无法通过签名验证(见下一个代码块)。
如:
public WXPay(WXPayConfig config, String notifyUrl, boolean autoReport, boolean useSandbox) throws Exception {
this.config = config;
this.notifyUrl = notifyUrl;
this.autoReport = autoReport;
this.useSandbox = useSandbox;
if (useSandbox) {
this.signType = SignType.MD5;
} else {
this.signType = SignType.HMACSHA256;
}
this.wxPayRequest = new WXPayRequest(config);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 构建请求微信支付参数(以下单为例)
/**
* 构建微信支付请求参数
*
* @param reqDTO 接口入参
* @param payInf 构建的支付订单
* @param request servlet请求数据
* @return 具有顺序的结果集
*/
@Override
protected Map<String, String> buildPayParam(OrderDTO reqDTO, HttpServletRequest request) {
int totalFee = reqDTO.getAmount().multiply(BigDecimal.valueOf(100)).intValue();
Map<String, String> innerMap = Maps.newHashMapWithExpectedSize(8);
innerMap.put("body", "aqara订单支付");
innerMap.put("out_trade_no", reqDTO.getOutTradeNo());
innerMap.put("total_fee", totalFee + "");
innerMap.put("spbill_create_ip", request.getRemoteAddr());
innerMap.put("notify_url", payCallbackNotify);
innerMap.put("trade_type", "JSAPI");
innerMap.put("openid", reqDTO.getOpenId());
return innerMap;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 构建参数中不需要设置的参数
因为构建微信客户端 WXPay.class
时已经设置了 appID
、mchID
、key
、certStream
,所以在构建参数时不需要设置,而微信支付SDK
中,在调用接口前也封装了一部分操作,如:
/**
* @param reqData 构建传递的参数
*/
public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
reqData.put("appid", this.config.getAppID());
reqData.put("mch_id", this.config.getMchID());
reqData.put("nonce_str", WXPayUtil.generateNonceStr());
if (SignType.MD5.equals(this.signType)) {
reqData.put("sign_type", "MD5");
} else if (SignType.HMACSHA256.equals(this.signType)) {
reqData.put("sign_type", "HMAC-SHA256");
}
reqData.put("sign", WXPayUtil.generateSignature(reqData, this.config.getKey(), this.signType));
return reqData;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
部分字段解释:
appID
:公众号AppID
mchID
:商户号
key
:商户密钥
certStream
:支付证书文件流
不需要构建的参数:
appID
:公众号AppID
mchID
:商户号
nonce_str
:随机值
sign_type
:签名类型
sign
:签名
# 退款回调的注意事项
退款通知API
中,涉及了一个加密内容:
如何解密?
解密代码:
/**
* 解密步骤如下:
* (1)对加密串A做base64解码,得到加密串B
* (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
* <p>
* (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
*
* @param req_info 加密信息
* @return 解密之后的映射关系
*/
private Map<String, String> decoderNotifyRequestInfo(String req_info) {
try {
SecretKeySpec key = new SecretKeySpec(DigestUtils.md5Hex(merchantKey.getBytes(CharsetNames.UTF_8)).toLowerCase().getBytes(), "AES");
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, key);
String result = new String(cipher.doFinal(Base64.decode(req_info)));
return WXPayUtil.xmlToMap(result);
} catch (Exception e) {
log.error("解密退款通知信息失败", e);
throw new BizException("解密退款通知信息失败");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 如何使用“测试公众号”进行支付验证?
在项目中:
- 在支付下单需要获取与线上公众号绑定的账户的openid(需要是当前微信账号)
- 先使用正常的“测试公众号”的appId登录到支付页面,但不进行支付
- 更改“测试公众号”的appid等信息为线上公众号的appid等信息
- 重启微信公众号服务
- 在“测试公众号”中进行支付,支付页面可调起
注:整个过程中,“测试公众号”页面不可退出,且不能退出订单中心页面,如果退出了,需要重新执行2,3,4,5步骤
# 参考文献:
- [1] 微信支付 (opens new window)