微信支付-业务流程图+时序图梳理微信支付链路+封装对接微信API工具类
微信支付-业务流程图+时序图梳理微信支付链路+封装对接微信API工具类
微信支付模块超时关单业务流程图
通过时序图剖析微信支付子系统链路
pay.weixin.qq.com/wiki/doc/ap…
重点步骤说明:
步骤2用户确认支付后,商户调用微信支付Native下单API生成预支付交易以获取支付二维码链接code_url;
商户调用NativeNative下单API后,分正常返回和异常返回情况:
- 正常返回:返回code_url,商户可根据返回的code_url来生成调用OpenSDK的签名以执行下一步。
- 异常返回:返回http code或错误码,商户可根据http code列表 或错误码说明来排查原因并执行下一步操作
步骤4: 商户根据返回的code_url生成二维码供用户扫描,有关二维码的规则请参考3.2.2部分的说明
步骤9-11: 用户支付成功后,商户可通过以下两种方式获取订单状态
方法一: 支付结果通知。用户支付成功后,微信支付会将支付成功的结果以回调通知的形式同步给商户,商户的回调地址需要在调用Native下单API时传入notify_url参数。
方法二: 当因网络抖动或本身notify_url存在问题等原因,导致无法接收到回调通知时,商户也可主动调用查询订单API来获取订单状态
一、支付二维码-请求微信服务器获取支付二维码签名验证封装
签名生成
签名验证封装整体流程:
pay.weixin.qq.com/wiki/doc/ap…
准备工作
插件安装
yarn add jsrsasign@10.6.1 string-random@0.1.3 urllib@3.5.1
jsrsasign:js加密插件
string-random:随机生成字符串插件
urllib:接口请求插件
封装对接微信API工具类
WxPayment.js
1. construtor
获取基础配置数据
class WxPayment {
constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
this.appid = appid; // 公众号appid
this.mchid = mchid; // 商户号mchid
this.private_key = private_key; // 商户私钥 加密传输的数据给微信平台
this.serial_no = serial_no; // 证书序列号,用于声明所使用的证书。 根据序列号微信平台用于选择对应的商户公钥
this.apiv3_private_key = apiv3_private_key; // APIv3密钥,解密平台证书 拿到平台公钥
this.notify_url = notify_url; // 回调地址 支付成功后的回调通知url
this.requestUrls = {
// pc端native下单API
native: () => {
return {
url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
method: 'POST',
pathname: '/v3/pay/transactions/native',
}
},
}
}
}
requestUrls是发起下单的请求链接具体看这里:Native下单API
pay.weixin.qq.com/wiki/doc/ap…
2.构造签名串
构造模板
GET\n
/v3/certificates\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
\n
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n
代码对应:
请求方法:method
请求路径:pathname
时间戳:timestamp
随机数:onece_str
请求报文主体:bodyParamsStr
// RandomTool.js
const randomString = require('string-random')
static randomString(num) {
return randomString(num)
}
const RandomTool = require('./RandomTool')
class WxPayment {
constructor(...) {...}
// 请求微信服务器签名封装
async wxSignRequest({ pathParams, bodyParams, type }) {
1. 构造签名串
let { url, method, pathname } = this.requestUrls[type]({ pathParams })
let timestamp = Math.floor(Date.now() / 1000) // 时间戳
let onece_str = RandomTool.randomString(32); // 随机串
let bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '' // 请求报文主体
let sign = `${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`
}
}
module.exports = WxPayment;
3.计算签名值
绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
const { KJUR, hextob64 } = require('jsrsasign')
const RandomTool = require('./RandomTool')
class WxPayment {
constructor(...) {
...
}
// 请求微信服务器签名封装
async wxSignRequest({ pathParams, bodyParams, type }) {
let { url, method, pathname } = this.requestUrls[type]({ pathParams })
let timestamp = Math.floor(Date.now() / 1000) // 时间戳
let onece_str = RandomTool.randomString(32); // 随机串
let bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '' // 请求报文主体
let signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA')
// 请求头传递签名
let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`
}
/**
* rsa签名
* @param content 签名内容
* @param privateKey 私钥,PKCS#1
* @param hash hash算法,SHA256withRSA
* @returns 返回签名字符串,base64
*/
rsaSign(content, privateKey, hash = 'SHA256withRSA') {
// 创建 Signature 对象
const signature = new KJUR.crypto.Signature({
alg: hash,
// 私钥
prvkeypem: privateKey
})
// 传入待加密字符串
signature.updateString(content)
// 生成密文
const signData = signature.sign()
// 将内容转成base64
return hextob64(signData)
}
}
module.exports = WxPayment;
这里的具体步骤就是
- 首先创建一个
Signature
对象,该对象使用KJUR.crypto.Signature()
方法创建,参数包括要使用的 Hash 算法(默认为 SHA256withRSA)和私钥privateKey
,这里privateKey
参数应该是已经经过 PKCS#1 编码的字符串。创建成功后,可用于对内容进行签名。 signature.updateString(content)
方法用于传入待签名的字符串content
,将其作为签名输入内容。signature.sign()
方法将执行签名操作,并返回签名结果。- 需要将签名结果转换成 Base64 编码格式,使用
hextob64()
方法完成,然后将结果返回即可。
4. 设置HTTP头
// 请求头传递签名
let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`
5.发起请求
// 接口请求
let { status, data } = await urllib.request(url, {
method: method,
dataType: 'text',
data: method == 'GET' ? '' : bodyParams,
timeout: [10000, 15000],
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': Authorization // 自定义生成
},
})
return { status, data }
在封装对外暴露的同一接口
//native统一下单
async native(params) {
let bodyParams = {
...params,
appid: this.appid,
mchid: this.mchid,
notify_url: this.notify_url,
}
return await this.wxSignRequest({ bodyParams, type: 'native' })
}
在其他地方使用下单时都是统一使用navtive接口。
全部代码
const urllib = require('urllib');
const { KJUR, hextob64 } = require('jsrsasign')
const RandomTool = require('./RandomTool')
class WxPayment {
constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
this.appid = appid; // 公众号appid
this.mchid = mchid; // 商户号mchid
this.private_key = private_key; // 商户私钥
this.serial_no = serial_no; // 证书序列号,用于声明所使用的证书
this.apiv3_private_key = apiv3_private_key; // APIv3密钥,解密平台证书
this.notify_url = notify_url; // 回调地址
this.requestUrls = {
// pc端native下单API
native: () => {
return {
url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
method: 'POST',
pathname: '/v3/pay/transactions/native',
}
},
}
}
// 请求微信服务器签名封装
async wxSignRequest({ pathParams, bodyParams, type }) {
let { url, method, pathname } = this.requestUrls[type]({ pathParams })
let timestamp = Math.floor(Date.now() / 1000) // 时间戳
let onece_str = RandomTool.randomString(32); // 随机串
let bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '' // 请求报文主体
let signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA')
// 请求头传递签名
let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`
// 接口请求
let { status, data } = await urllib.request(url, {
method: method,
dataType: 'text',
data: method == 'GET' ? '' : bodyParams,
timeout: [10000, 15000],
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': Authorization
},
})
return { status, data }
}
//native统一下单
async native(params) {
let bodyParams = {
...params,
appid: this.appid,
mchid: this.mchid,
notify_url: this.notify_url,
}
return await this.wxSignRequest({ bodyParams, type: 'native' })
}
/**
* rsa签名
* @param content 签名内容
* @param privateKey 私钥,PKCS#1
* @param hash hash算法,SHA256withRSA
* @returns 返回签名字符串,base64
*/
rsaSign(content, privateKey, hash = 'SHA256withRSA') {
// 创建 Signature 对象
const signature = new KJUR.crypto.Signature({
alg: hash,
// 私钥
prvkeypem: privateKey
})
// 传入待加密字符串
signature.updateString(content)
// 生成密文
const signData = signature.sign()
// 将内容转成base64
return hextob64(signData)
}
}
module.exports = WxPayment;
验证请求微信服务器签名封装
基于上文封装的请求下单的工具进一步的配置支付,将工具对象实例化。
wechatPay.js
const Payment = require('../utils/WxPayment')
const fs = require('fs')
const { resolve } = require('path')
const appid = 'xxxxxxxxx' // 公众号appID
const mchid = '11111111' // 商户号mchID
const serial_no = 'xxxx' // 证书序列号,用于声明所使用的证书
const apiv3_private_key = 'xxxx' // APIv3密钥,用于解密平台证书,拿到平台公钥,解密回调返回的信息。
const notify_url = 'https://740833px45.yicp.fun/api/order/v1/callback' // 回调地址,用户微信通知消息 ,这里使用花生壳内网穿透,处理本地的http://127.0.0.1:8081。
const private_key = fs.readFileSync(resolve(__dirname, '../apiclient_key.pem')).toString() // 秘钥,用于发起微信请求加密
const payment = new Payment({
appid, mchid, private_key, serial_no, apiv3_private_key, notify_url
})
module.exports = { appid, mchid, private_key, serial_no, apiv3_private_key, notify_url, payment }
解读一下上面的逻辑:
- 定义了常量变量,包括公众号 appid、商户号 mchid、证书序列号 serial_no、APIv3 密钥 apiv3_private_key、回调地址 notify_url 和秘钥 private_key。为了进行对象实例化。
- 这里
private_key
的获得是使用fs.readFileSync()
方法读取了一个指定路径下的 PEM 格式私钥文件,该私钥用于发起微信支付请求时进行加密。 - 将这些变量创建
WxPayment
实例对象,并通过module.exports
导出。
实例化对象之后书写对应的接口请求。
router
const express = require('express')
const router = express.Router()
const OrderController = require('../controller/OrderController.js')
// 获取微信支付二维码
router.post('/pay', OrderController.pay)
module.exports = router
controller
/**
* @param query_pay 查询课程是否购买接口
* @param latest 查询课程最近购买动态接口
* @param pay PC微信支付二维码
* @param callback 微信回调
* @param query_state 轮询用户扫码与否
*/
const OrderService = require('../service/OrderService.js')
const OrderController = {
pay: async (req, res) => {
let handleRes = await OrderService.pay(req)
res.send(handleRes);
},
}
module.exports = OrderController
service
const DB = require('../config/sequelize')
const BackCode = require('../utils/BackCode')
const CodeEnum = require('../utils/CodeEnum')
const RandomTool = require('../utils/RandomTool')
const SecretTool = require('../utils/SecretTool')
const GetUserInfoTool = require('../utils/GetUserInfoTool')
const { payment } = require('../config/wechatPay')
const dayjs = require('dayjs')
const redisConfig = require('../config/redisConfig')
const OrderService = {
pay: async (req) => {
let { id, type } = req.body
let token = req.headers.authorization.split(' ').pop()
// 获取用户信息
let userInfo = SecretTool.jwtVerify(token)
// 用户的ip
let ip = GetUserInfoTool.getIp(req)
// 生成32位字符串 商户订单号
let out_trade_no = RandomTool.randomString(32)
// 根据商品的ID查询商品价格
let productInfo = await DB.Product.findOne({ where: { id }, raw: true })
// 拼装用户和商品信息插入数据库
let userPro = {
account_id: userInfo.id,
username: userInfo.username,
user_head_img: userInfo.head_img,
out_trade_no: out_trade_no,
total_amount: productInfo.amount,
pay_amount: productInfo.amount,
product_id: productInfo.id,
product_type: productInfo.product_type,
product_title: productInfo.title,
product_img: productInfo.cover_img,
order_state: 'NEW',
ip: ip
}
// 新订单信息插入数据库
await DB.ProductOrder.create(userPro)
// 微信支付二维码
if (type === 'PC') {
let result = await payment.native({
description: '测试',
out_trade_no, // 正式
amount: {
total: Number(productInfo.amount) * 100, // 正式
}
})
return BackCode.buildSuccessAndData({ data: { code_url: JSON.parse(result.data).code_url, out_trade_no } })
}
},
}
module.exports = OrderService
解读一下Service
的逻辑:
这里使用了很多封装的工具,简单理解调用思路就可以了。
引用模块:
引用了一些自定义模块和第三方模块,包括数据库模块 DB
、返回码模块 BackCode
、状态码枚举模块 CodeEnum
、随机字符串工具模块 RandomTool
、加密工具模块 SecretTool
、获取用户信息工具模块 GetUserInfoTool
、微信支付配置模块 wechatPay
、日期时间工具模块 dayjs
、Redis 配置模块 redisConfig
。
在 pay
的异步函数,从请求体中获取订单号 id
和支付类型 type
。通过请求头中的 Authorization
字段解析出 JWT Token 并获取用户信息 userInfo
。获取客户端的 IP 地址 ip
。利用随机字符串工具生成32位的商户订单号 out_trade_no
。查询商品信息并获取价格 productInfo
。然后将组装用户与商品相关信息,将其插入数据库中。判断支付类型,如果支付类型为 PC,则调用 payment.native()
方法生成微信支付二维码,并返回二维码图片URL code_url
和商户订单号 out_trade_no
,否则直接返回空值。
在生成的url拿到之后使用二维码转换器就可以生成二维码了。
转载自:https://juejin.cn/post/7240333779221872696