• 【微信公众号】微信 jsapi 支付大概流程


    一、个人对微信 jsapi 支付的介绍

    • jsapi 支付可运用于微信小程序和 h5
    • jsapi 支付不会弹出二维码(这点与 native 支付不同)
    • jsapi 支付运用于移动端
    • native 支付运用于 PC 端

    🔥 微信支付需要商家申请商户号,商务号需要和小程序或公众号(服务号)进行绑定
    💦 在微信商务平台可开通 native 支付、jsapi 支付或其他支付方式
    🔥 在微信商务平台能够获取到:mch_id(商户号)、
    💦 在微信商务平台可配置 API v3 密钥(主要用于平台证书解密、回调信息解密)
    🔥 下载并配置商户证书(获取证书序列号和商户私钥 ➡️ apiclient_key.pem)
    💦 设置支付授权目录
    🔥 设置授权域名

    上述操作的详细步骤都可在下面的链接中找到:
    https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml

    二、微信支付需要的数据库表(例子)

    1. 订单表

    订单表应与一个【用户表】关联,进而得知是那个用户创建的订单?进而得知某某用户创建的订单的支付情况。

    CREATE TABLE `pay_order` (
      `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单 id',
      `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
      `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
      `user_id` bigint(20) DEFAULT NULL COMMENT '用户 id', 
      `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)', 
      `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
      `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2. 支付信息表

    该表中的字段大多来自微信商户平台返回的支付通知。该表记录的是微信商户平台对该笔账单的支付情况。

    CREATE TABLE `payment_info` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录 id',
      `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
      `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
      `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
      `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
      `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
      `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
      `content` text COMMENT '通知参数',
      `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3. 退款信息表

    当用户支付的时候,需要创建一个【退款订单】,用于记录退款情况。

    CREATE TABLE `refund_info` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单 id',
      `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
      `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
      `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
      `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
      `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
      `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
      `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
      `content_return` text COMMENT '申请退款返回参数',
      `content_notify` text COMMENT '退款结果通知参数',
      `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    三、微信支付必须的参数(配置文件)

    wechat:
      mch-id: 1736608159
      mch-serial-no: 99A92506FC7B6BE2DA2F3F87898FFFF1C1D3811A
      private-key-path: apiclient_key.pem
      api-v3-key: 312f0ebadc062813ebc6812325fc2f23
      appid: wx60aa253aa13aaeb6
      domain: https://api.mch.weixin.qq.com
      notify-domain: https://helloboy.roroam.com
      app-secret: a712223142566a78a7beced6eff3ge59
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ✏️ 【mch-id】: 商户号(商家申请商户号后,可让管理员登录微信商户平台查看)

    ✏️ 【mch-serial-no】: 证书序列号(某些情况下,将需要更新密钥对和证书。为了保证更换过程中不影响 API 的使用,请求和应答的 HTTP 头部中包括证书序列号 ,以声明签名或者加密所用的密钥对和证书。)

    ✏️ 【private-key-path】: 商户 API 私钥文件路径(商户申请商户 API 证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。)
    私钥和证书:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_1.shtml

    ✏️ 【api-v3-key】: APIV3 秘钥(主要用于平台证书解密、回调信息解密)具体使用:证书和回调报文解密https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml

    ✏️ 【appid】: 小程序或服务号的唯一标识(调用 jsapi 的下单接口时需要、前端 JS 方法调起支付的时候需要)

    ✏️ 【domain】: domain 是域名的意思(在此表示向微信商户平台发起请求的路径的基本路径

    jsapi 下单接口:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
    查询订单接口: https://api.mch.weixin.qq.com/v3/pay/transactions/id/{transaction_id}
    关闭订单接口:https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close
    申请退款接口:https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
    …(很多微信商务平台的接口的基本地址都是一样的,将它抽取为 domain)

    ✏️ 【notify_domain】: 通知域名(用户下单之后、退款之后,微信商户平台会向我们的商户系统返回通知 ➖ 告诉我们用户的支付情况或退款单的退款情况)【这个通知域名必须是外网能够访问得到的,可以是 ngrok 地址】

    ✏️ 【app_secret】: app 密码(在小程序或公众号中能够找到,可通过它向微信系统获取用户的 openid)【openid 用户的唯一标识】

    四、WxPayConfig.java

    第三节的配置文件中的内容大多都是为 WxPayConfig 而服务,下面把 WxPayConfig.java 中的代码拷贝如下,并做简单介绍(深层次的东西我不懂
    在使用 Java 代码前,先引入一些 MAVEN 依赖,免得报错

    1. MAVEN 依赖

    🎈 微信支付 apiv3 开发工具包

      
      <dependency>
          <groupId>com.github.wechatpay-apiv3groupId>
          <artifactId>wechatpay-apache-httpclientartifactId>
          <version>0.3.0version>
      dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    🎈 谷歌的 JSON 处理工具(可进行 JSON 字符串和 Java 对象的转换)

      
      <dependency>
          <groupId>com.google.code.gsongroupId>
          <artifactId>gsonartifactId>
      dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    🎈 简单易用的缓存框架

      
      <dependency>
          <groupId>org.ehcachegroupId>
          <artifactId>ehcacheartifactId>
      dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2. Java 代码(商户私钥、签名验证…)

    @Slf4j
    @Component
    @Data
    @ConfigurationProperties("wechat")
    public class WxPayConfig implements ApplicationContextAware {
       private static WxPayConfig wxPayConfig;
    
       // 商户号
       private String mchId;
    
       // 商户API证书序列号
       private String mchSerialNo;
    
       // 商户私钥文件
       private String privateKeyPath;
    
       // APIv3密钥
       private String apiV3Key;
    
       // 应用的唯一标识
       private String appid;
    
       // 微信服务器地址
       private String domain;
    
       // 接收结果通知地址
       private String notifyDomain;
    
       private String appSecret;
    
       public static WxPayConfig getInstance() {
           return wxPayConfig;
       }
    
       @Override
       public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
           wxPayConfig = this;
       }
    
       /**
        * 获取商户私钥
        *
        * @param privateKeyPath 私钥文件所在路径
        * @return 私钥
        */
       public PrivateKey getPrivateKey(String privateKeyPath) {
           try {
               return PemUtil.loadPrivateKey(
                       new FileInputStream(privateKeyPath));
           } catch (Exception e) {
               e.printStackTrace();
               throw new IllegalArgumentException("私钥文件不存在(可能是私钥文件路径错误)");
           }
       }
    
       /**
        * 获取签名验证器
        * 签名验证器: 用于验签的对象
        */
       @Bean
       public ScheduledUpdateCertificatesVerifier getVerifier() {
           // 获取商户私钥
           PrivateKey privateKey = getPrivateKey(privateKeyPath);
    
           // 私钥签名对象
           PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
    
           //身份认证对象
           WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
    
           // 使用定时更新的签名验证器,不需要传入证书
           ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                   wechatPay2Credentials,
                   apiV3Key.getBytes(StandardCharsets.UTF_8));
    
           return verifier;
       }
    
       /**
        * 获取微信远程 http 请求对象
        *
        * @param verifier 签名验证器
        */
       @Bean(name = "wxPayClient")
       public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
           WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                   .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                   .withValidator(new WechatPay2Validator(verifier));
           return builder.build();
       }
    
       /**
        * 【签名】使用字段 appId、timeStamp、nonceStr、package 计算得出的签名值
        * appId: 应用的唯一标识
        * timeStamp: 时间戳
        * nonceStr: 随机字符串
        * package: 订单详情扩展字符串 (形如:"prepay_id=wx32j43k1kj5li1o")
        * 

    * 官方文档链接:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml#menu1 * * @return 签名值 */ public static String createSignVal(String prepayId, String timeStampStr, String nonceStr) { // 构建签名串 String signString = buildSignString(prepayId, timeStampStr, nonceStr); // 计算签名值 String signVal = calculateSignVal(signString); return signVal; } /** * 计算签名值【下面是官方说明】 * 绝大多数编程语言提供的签名函数支持对签名数据进行签名 *

    * 强烈建议商户调用该类函数, 使用【商户私钥】对【待签名串】进行【SHA256 with RSA】签名 * 并对签名结果进行 Base64 编码得到签名值 * * @param signStr 待签名字符串 * @return 签名值 */ private static String calculateSignVal(String signStr) { try { // Signature: java 提供的用来为应用程序提供数字签名算法功能的类(数字签名用于确保数字数据的验证和完整性) // Signature: java.security Signature signature = Signature.getInstance("SHA256withRSA"); // 初始化签名(官方要求使用商户私钥对待签名串进行 SHA256 with RSA 签名 PrivateKey privateKey = WxPayConfig.getInstance().getPrivateKey(WxPayConfig.getInstance().getPrivateKeyPath()); signature.initSign(privateKey); // 将数据添加到签名 signature.update(signStr.getBytes(StandardCharsets.UTF_8)); // 计算签名 byte[] signRes = signature.sign(); // 对签名结果(signRes)进行 Base64 编码 // Base64 是 java.util 里面的类 return Base64.getEncoder().encodeToString(signRes); } catch (Exception e) { e.printStackTrace(); log.error("计算签名值出错:WxUtil - calculateSignVal"); return null; } } /** * 【该方法服务于 createSignVal 方法】 * 构造签名串【下面是官方说明】 * 创建签名串:签名串一共有四行, 每一行为一个参数。行尾以【\n (换行符, ASCII 编码值为 0x0A) 结束】, 包括最后一行 * 如果参数本身以【\n】结尾, 也需要附加一个【\n】 *

    * 签名格式内容:appid、timeStamp、nonceStr、package * * @param prepayId 预支付 id (调用 JSAPI 下单后可获得它的值) * @return 签名字符串 */ private static String buildSignString(String prepayId, String timeStampStr, String nonceStr) { return WxPayConfig.getInstance().getAppid() + "\n" + timeStampStr + "\n" + nonceStr + "\n" + "prepay_id=" + prepayId + "\n"; // 一定要拼接一个【prepay_id】字符串, 之前一直无法支付成功就是这里有问题 } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163

    上面代码中核心且复杂的方法有三个:

    (1)getPrivateKey

    📗 getPrivateKey:获取商户私钥(个人理解:对数据进行解密操作,公钥加密,私钥解密)【我不是很懂,大概知道一点点】

    privateKeyPath 是商户私钥文件所在路径(一般都是放在类路径),使用 PemUtil 工具(来自 com.wechat.pay.contrib.apache.httpclient.util 包)加载 privateKeyPath 路径的商户私钥文件,并返回 PrivateKey 对象

        /**
         * 获取商户私钥
         *
         * @param privateKeyPath 私钥文件所在路径
         * @return 私钥
         */
        public PrivateKey getPrivateKey(String privateKeyPath) {
            try {
                return PemUtil.loadPrivateKey(
                        new FileInputStream(privateKeyPath));
            } catch (Exception e) {
                e.printStackTrace();
                throw new IllegalArgumentException("私钥文件不存在(可能是私钥文件路径错误)");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    (2)getVerifier

    📗 getVerifier:获取签名验证器(个人理解:就是一个被人封装好的,用于验证签名的对象)。该方法中会使用到很多熟悉的东西,如:privateKeyPath(商户私钥文件路径)、mchSerialNo(证书序列号)、privateKey(私钥对象)、mchId(商户号)。签名验证器(Verifier)是一个帮助开发者对网络请求进行签名验证的东西,是被人封装好的,开发者直接使用它进行验签即可。

    验证签名的作用:① 增加完整性校验(保证数据的来源和完整性);② 增加保密性

       /**
        * 获取签名验证器
        * 签名验证器: 用于验签的对象
        */
       @Bean
       public ScheduledUpdateCertificatesVerifier getVerifier() {
           // 获取商户私钥
           PrivateKey privateKey = getPrivateKey(privateKeyPath);
    
           // 私钥签名对象
           PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
    
           // 身份认证对象
           WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
    
           // 使用定时更新的签名验证器,不需要传入证书
           ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                   wechatPay2Credentials,
                   apiV3Key.getBytes(StandardCharsets.UTF_8));
    
           return verifier;
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    (3)getWxPayClient

    📗 getWxPayClient:CloseableHttpClient 对象是微信支付平台推荐的实现远程 HTTP 请求的对象。SpringBoot 项目启动后,会创建 CloseableHttpClient 对象的实例(把它的实例放入 IoC 容器)。后期我们需要向微信商务平台发送网络请求(如:下单、退款、关单…)的时候就使用 IoC 容器里面的 CloseableHttpClient 对象。

    创建它的实例需要签名验证器作为参数。代码内部也要使用到商户号、证书序列号和商户秘钥。

    官方地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay
    /wechatpay6_0.shtml

    在这里插入图片描述

       /**
        * 获取微信远程 http 请求对象
        *
        * @param verifier 签名验证器
        */
       @Bean(name = "wxPayClient")
       public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
           WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                   .withMerchant(mchId, mchSerialNo, getPrivateKey(privateKeyPath))
                   .withValidator(new WechatPay2Validator(verifier));
           return builder.build();
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    五、工具类相关

    1. WxPayUtil

    该类中有五个方法,我简单说明一下,后期都会使用到。
    ⛄️ ① notifyCiphertext2plainText:把通知密文转换为明文。(用户支付之后成功和商户系统退款成功之后)

    /**
     * 用于创建订单(支付订单、退款订单)编号
     */
    public class WxPayUtil {
        /**
         * 通知密文(ciphertext)转明文
         *
         * @param notifyMap 包含进行解密所需的参数
         * @return 明文
         */
        public static String notifyCiphertext2plainText(Map notifyMap) {
            try {
                // resource: 通知数据
                Map notifyData = (Map) notifyMap.get("resource");
                // 数据密文
                String ciphertext = (String) notifyData.get("ciphertext");
                // 附加数据
                String associatedData = (String) notifyData.get("associated_data");
                // 随机串
                String nonce = (String) notifyData.get("nonce");
    
                // 创建解密对象
                AesUtil aesUtil = new AesUtil(WxPayConfig.getInstance().getApiV3Key().getBytes());
    
                return aesUtil.decryptToString(associatedData.getBytes(),
                        nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
            } catch (Exception e) {
                e.printStackTrace();
                throw new IllegalArgumentException("微信支付通知密文转密文出现错误");
            }
        }
    
        /**
         * 生成支付订单编号
         */
        public static String generatePayOrderNumber() {
            return "ORDER_" + generateNumber();
        }
    
        /**
         * 生成退款订单编号
         */
        public static String generateRefundNumber() {
            return "REFUND_" + generateNumber();
        }
    
        /**
         * 生成订单编号
         */
        private static String generateNumber() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            String newDate = sdf.format(new Date());
            StringBuilder result = new StringBuilder();
            Random random = new Random();
            for (int i = 0; i < 3; i++) {
                result.append(random.nextInt(10));
            }
            return newDate + result + UUID.randomUUID().toString().substring(0, 5);
        }
    
        /**
         * 获取当前时间字符串
         *
         * @return 当前时间字符串
         */
        public static String getCurTimeStr() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return sdf.format(new Date()).toString();
        }
    
    }
    
    • 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
  • 相关阅读:
    js适配文件
    [附源码]java毕业设计药品销售管理系统
    phpstudy安装imagick扩展
    日常随笔——linux 更换cmake 版本
    【UTMB】如何查看 UTMB 个人积分 | 个人表现分 | 对比ITRA与UTMB表现分
    Visual Studio中的四款代码格式化工具
    美国洛杉矶站群服务器如何提高网站排名?
    科普达人丨一文看懂阿里云的秘密武器“神龙架构”
    rk dp 原理分析和调试
    Schema_CN28_XNN0付款/扣除和转账净额
  • 原文地址:https://blog.csdn.net/m0_54189068/article/details/126354958