微信支付流程(V2)

原创💡 Abner Mai 2021-08-30 Pay
  • 微信支付
  • 支付
  • Java
约 1963 字 大约 7 分钟

# 微信支付流程

# 准备

再使用前阅读微信支付文档:https://pay.weixin.qq.com/wiki/doc/api/index.html

因为我们使用的是“公众号”支付,所以我们采用的是**JSAPI**支付类型。

在开始使用之前,我们需要准备以下内容

# 商户号

商户号的申请:由公司作为一个商户进行申请(个人无法申请),因需要提供营业执照等相关材料。

# 公众号

目前项目中存在多个“公众号平台”,以生产与测试环境为例。

生产环境使用的是由公司认证的“公众号”,测试环境使用的是未认证的“测试公众号”。

# 配置

# 商户号管理界面配置

  1. 生成商户密钥

  2. 绑定公众号:公众号需要是通过认证的,否则无法绑定;

  3. 配置JSAPI支付授权目录:配置支付支付调起页面,eg: https://address/openPay/,(只需要配置到html页面的上级目录即可),支付页面调起代码微信支付提供:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6

  4. 获取支付证书,因为微信支付一些业务需要支付证书进行双向认证,所以需要通过商户号平台进行支付证书的申请,操作地址:微信商户平台(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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 如何正确的使用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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 构建微信支付客户端

/**
     * 获取微信支付客户端
     *
     * @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;
    }


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

useSandbox是否使用沙箱环境,微信支付提供沙箱,但沙箱只能用于验证接口调用是否正常,不能将业务串联验证,而这个属性还影响到了微信客户端使用的加密类型,微信支付提供了 MD5HMACSHA256加密,如果不使用“沙箱”环境,一般推荐使用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);
    }
1
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;
    }
1
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时已经设置了 appIDmchIDkeycertStream,所以在构建参数时不需要设置,而微信支付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;
    }
1
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中,涉及了一个加密内容:

202108261211430

如何解密?

解密代码:

/**
     * 解密步骤如下:
     * (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("解密退款通知信息失败");
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 如何使用“测试公众号”进行支付验证?

在项目中:

  1. 在支付下单需要获取与线上公众号绑定的账户的openid(需要是当前微信账号)
  2. 先使用正常的“测试公众号”的appId登录到支付页面,但不进行支付
  3. 更改“测试公众号”的appid等信息为线上公众号的appid等信息
  4. 重启微信公众号服务
  5. 在“测试公众号”中进行支付,支付页面可调起

注:整个过程中,“测试公众号”页面不可退出,且不能退出订单中心页面,如果退出了,需要重新执行2,3,4,5步骤

# 参考文献:

- [1] 微信支付 (opens new window)

- [2] 测试公众号验证支付 (opens new window)

上次编辑于: 2021年9月29日 17:05