跳到主要内容

如何实现微信Native支付?

场景/问题描述

如何实现在轻流中使用微信Native支付进行收款

思路逻辑

通过使用代码块功能进行调用相关微信支付接口,从而生成付款二维码,以及后续去查询订单付款状态,或者客户申请退款,通过直接调用接口去处理退款申请

配置过程

准备工作

image.png

注意:申请商户号成功之后,需要绑定APPID,否则无法进行支付操作

接入准备说明文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml

其余操作根据接入指引完成操作即可

image.png

  • API证书申请完成之后,需要将序列号保存下来,后续参数serial_no需要填写
  • APIV3密钥设置好之后,需要保存下来,后续回调通知内容解密用

image.png

image.png

  • 完成上述操作后:会在本地电脑中生成一个压缩文件,进行解压打开

image.png

  • 打开该文件,并将私钥内容复制保存下来即可

image.png

image.png

表单配置

微信Native支付订单创建

image.png

  • 【1---》生成付款二维码】与【2--确认支付状态】为代码块
  • 由于微信接口文档关于金额需要单位为分,所以这里做了个计算去转换为【总金额,单位为分】:总金额*100
  • image.png
1---》生成付款二维码
代码注释
  • 具体接口文档说明:

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

  • qf_field.{商户订单号$$19466E8E9$$},qf_field.{商品描述$$19466E8EA$$},qf_field.{总金额,单位为分$$19466E8EC$$}需要替换成表单中具体字段

  • serial_no、privateKey需要替换上面所保存下来的序列号和私钥内容

  • notify_url:用于回调通知用,这里放的是轻流应用QS被动的接口。

  • 注:由于接口返回的是二维码链接,并不是二维码图片,所以这里做了处理,调用了生成二维码图片的接口

  • 具体接口文档:

https://www.mxnzp.com/doc/detail?id=2

image.png

  • 需要点击申请appid,去获取对应的app_id和app_srcret,将最后URL中的app_id以及app_sercet进行替换即可
  • image.png
const axios = require('axios');
const crypto = require('crypto');
const data = {
"mchid":"1658838350",
"serial_no":"42******4D6638CEBBB4C48697FB54",
"url": 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
"method": "POST"

};
let reqdata = JSON.stringify({
"mchid": "1658838350",
"out_trade_no": qf_field.{商户订单号$$19466E8E9$$},
"appid": "wx9be66*****4686",
"description": qf_field.{商品描述$$19466E8EA$$},
"notify_url": "https://qingflow.com/api/qsource/13060b3c-8200-d9c000ef0",
"amount": {
"total": qf_field.{总金额,单位为分$$19466E8EC$$},
"currency": "CNY"
}
});

// 生成随机字符串
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
}

const method=data.method;
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateRandomString(32); // 假设generateRandomString函数已经定义
const body =reqdata;
const urlParts = new URL(data.url);
const canonicalUrl = `${urlParts.pathname}${urlParts.search}`;

//私钥
const privateKey = `-----BEGIN PRIVATE KEY-----
Ha8ho0jCtZhh1M6+C02OWXZswqrXH35yRI75T/SpGD87lVLV8iFJ9kHz7db5SLyB
OMGTflLX3QKBgB5aS3pCDSAR4jFBLEuylEpm1LxjJy6J6P7SBzqMRHHLWVV9YZHZ
68x1CDrLmG3vA4YPu15kt6T4iIt+YpAuXmDleilNEJj8UGrdoENA2+Xdh+U8qib0
A+2W7oWcw/YzPbLJtK44DVB3k+eBwl8XqqnvnW3jDpZZpndCTxALX54VAoGBAM/1
hlmRPqX9UrNNZZ4U3o/rLgzdr9/oWaC96Mh+04NeaLzy2DpJYciKOUpd54MzdSTb
5o2dY+15qVM8+jBApWgFlysXCkZ7Yw61s+H74a/oKPPzNdkk0ra7V7ESzkNO1BR4
TAH60fAvchyY4kuURlQyBmKNnfohy5e+D/BFFFFBAoGBAMUS7DGX/66ckdWUvrzw
DSQhtU6gFMMSuUikhg077C4eHtr++7mLy9BEfXroa3aH8Yqn9eyE77aqdQ7zmHV8
kF155dImc0Gfrl8b+bcz+l+Z+ZE7QPliben4fwFCG9BGS/DCKi6vqDtknQHcrida
fTm0lUYFLgYeRcvvVwUcCsK3
-----END PRIVATE KEY-----`;

const message =
`${method}\n` +
`${canonicalUrl}\n` +
`${timestamp}\n` +
`${nonce}\n` +
`${body}\n`;

// 使用私钥创建签名对象
const sign = crypto.createSign('RSA-SHA256');
sign.write(message);
sign.end();

// 使用私钥对签名字符串进行签名
const signature = sign.sign(privateKey, 'base64');

const schema = 'WECHATPAY2-SHA256-RSA2048';
const token =`mchid="${data.mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${data.serial_no}",signature="${signature}"`;

let config = {
method: data.method,
maxBodyLength: Infinity,
url: data.url,
headers: {
'Authorization': `${schema} ${token}`,
'Content-Type': 'application/json'
},
data: reqdata
};

axios(config).then(function (res) {
var resdata=res.data
var code_url=resdata.code_url
let config1 = {
method: 'get',
maxBodyLength: Infinity,
url: `https://www.mxnzp.com/api/qrcode/create/single?content=${encodeURIComponent(code_url)}&type=0&app_id=0gggsysvv*****&app_secret=NkG46aFodgjDrC4jQ******`,
headers: {}
};
axios(config1)
.then(function (res1) {
qf_output={'data':res1.data}
});
});
代码测试与配置

image.png

image.png

image.png

2--确认支付状态
代码注释
  • 具体接口文档(根据商户订单号查询):

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml

  • qf_field.{商户订单号$$19466E8E9$$}替换成表单中对应字段

  • data中的url需要替换成接口文档中的URL,具体拿什么内容参考图片红线部分

image.png

  • serial_no、privateKey需要替换上面所保存下来的序列号和私钥内容

const axios = require('axios');
const crypto = require('crypto');
const data = {
"mchid":"1658838350",
"out_trade_no":qf_field.{商户订单号$$19466E8E9$$},
"serial_no":"42363********8CEBBB4C48697FB54",
"url": "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/",
"method": "GET"
};
data.url = `${data.url}${data.out_trade_no}?mchid=${data.mchid}`;

let reqbody = "";

// 生成随机字符串
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
}

const method=data.method;
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateRandomString(32); // 假设generateRandomString函数已经定义
const body =reqbody;
const urlParts = new URL(data.url);
const canonicalUrl = `${urlParts.pathname}${urlParts.search}`;

//私钥
const privateKey = `-----BEGIN PRIVATE KEY-----
68x1CDrLmG3vA4YPu15kt6T4iIt+YpAuXmDleilNEJj8UGrdoENA2+Xdh+U8qib0
A+2W7oWcw/YzPbLJtK44DVB3k+eBwl8XqqnvnW3jDpZZpndCTxALX54VAoGBAM/1
hlmRPqX9UrNNZZ4U3o/rLgzdr9/oWaC96Mh+04NeaLzy2DpJYciKOUpd54MzdSTb
5o2dY+15qVM8+jBApWgFlysXCkZ7Yw61s+H74a/oKPPzNdkk0ra7V7ESzkNO1BR4
TAH60fAvchyY4kuURlQyBmKNnfohy5e+D/BFFFFBAoGBAMUS7DGX/66ckdWUvrzw
DSQhtU6gFMMSuUikhg077C4eHtr++7mLy9BEfXroa3aH8Yqn9eyE77aqdQ7zmHV8
kF155dImc0Gfrl8b+bcz+l+Z+ZE7QPliben4fwFCG9BGS/DCKi6vqDtknQHcrida
fTm0lUYFLgYeRcvvVwUcCsK3
-----END PRIVATE KEY-----`;

const message =
`${method}\n` +
`${canonicalUrl}\n` +
`${timestamp}\n` +
`${nonce}\n` +
`${body}\n`;

// 使用私钥创建签名对象
const sign = crypto.createSign('RSA-SHA256');
sign.write(message);
sign.end();

// 使用私钥对签名字符串进行签名
const signature = sign.sign(privateKey, 'base64');

const schema = 'WECHATPAY2-SHA256-RSA2048';
const token =`mchid="${data.mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${data.serial_no}",signature="${signature}"`;

let config = {
method: data.method,
maxBodyLength: Infinity,
url:data.url,
headers: {
'Authorization': `${schema} ${token}`,
'Content-Type': 'application/json'
},
data: reqbody
};

axios(config).then(function (res) {
qf_output={'data':res.data}
});
代码测试与配置

image.png

image.png

image.png

流程设计

image.png

  • 申请人节点:企业员工发起支付订单
  • 支付人扫码支付订单节点:具体支付人进行扫码订单支付,如果支付人对于订单信息有问题,可以通过并在反馈中说明
  • 员工确认支付状态节点:企业员工进行确认支付订单状态,如果订单信息有误,可以修改订单,并重新生成二维码,回退给支付人进行扫码付款。
申请人

image.png

支付人扫码支付订单
  • 节点处理人选择成员字段为表单【订单支付人】
  • 其余字段设置为仅可见,红色框中设置为隐藏

image.png

员工确认支付状态
  • 节点负责人设置为表单【订单创建人】
  • 字段全部设置为可编辑

image.png

微信Native支付申请退款

image.png

  • 【---》退款】为代码块

  • 【商户订单号】为数据关联字段,关联【微信Native支付订单创建】应用的商户订单号

  • image.png

  • 【原订单金额】设置为关联已有数据

  • image.png

  • 【订单总金额,单位为分】和【退款金额,单位为分】计算逻辑为对应金额*100

  • image.png

---》退款
代码注释
  • 具体接口文档:

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

  • qf_field.{商户订单号$$1946966C9$$}、qf_field.{商户订单号$$1946966C9$$}、qf_field.{退款原因$$1946966D2$$}、qf_field.{退款金额,单位为分$$19469798C$$}替换成表单中对应字段

  • serial_no、privateKey需要替换上面所保存下来的序列号和私钥内容

  • notify_url:用于回调通知用,这里放的是轻流应用QS被动的接口。

const axios = require('axios');
const crypto = require('crypto');
const data = {
"mchid":"1658838350",
"serial_no":"4236******CEBBB4C48697FB54",
"url": 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
"method": "POST"

};
let reqbody = JSON.stringify({
"out_trade_no": qf_field.{商户订单号$$1946966C9$$},
"out_refund_no":qf_field.{商户订单号$$1946966C9$$},
"reason":qf_field.{退款原因$$1946966D2$$},
"notify_url": "https://qingflow.com/api/qsource/13060b3c-8200-00ef0",
"amount": {
"refund": qf_field.{退款金额,单位为分$$19469798C$$},
"total":qf_field.{订单总金额,单位为分$$1946C5010$$},
"currency": "CNY"
}
});

// 生成随机字符串
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
}

const method=data.method;
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateRandomString(32); // 假设generateRandomString函数已经定义
const body =reqbody;
const urlParts = new URL(data.url);
const canonicalUrl = `${urlParts.pathname}${urlParts.search}`;

//私钥
const privateKey = `-----BEGIN PRIVATE KEY-----
OMGTflLX3QKBgB5aS3pCDSAR4jFBLEuylEpm1LxjJy6J6P7SBzqMRHHLWVV9YZHZ
68x1CDrLmG3vA4YPu15kt6T4iIt+YpAuXmDleilNEJj8UGrdoENA2+Xdh+U8qib0
A+2W7oWcw/YzPbLJtK44DVB3k+eBwl8XqqnvnW3jDpZZpndCTxALX54VAoGBAM/1
hlmRPqX9UrNNZZ4U3o/rLgzdr9/oWaC96Mh+04NeaLzy2DpJYciKOUpd54MzdSTb
5o2dY+15qVM8+jBApWgFlysXCkZ7Yw61s+H74a/oKPPzNdkk0ra7V7ESzkNO1BR4
TAH60fAvchyY4kuURlQyBmKNnfohy5e+D/BFFFFBAoGBAMUS7DGX/66ckdWUvrzw
DSQhtU6gFMMSuUikhg077C4eHtr++7mLy9BEfXroa3aH8Yqn9eyE77aqdQ7zmHV8
kF155dImc0Gfrl8b+bcz+l+Z+ZE7QPliben4fwFCG9BGS/DCKi6vqDtknQHcrida
fTm0lUYFLgYeRcvvVwUcCsK3
-----END PRIVATE KEY-----`;

const message =
`${method}\n` +
`${canonicalUrl}\n` +
`${timestamp}\n` +
`${nonce}\n` +
`${body}\n`;

// 使用私钥创建签名对象
const sign = crypto.createSign('RSA-SHA256');
sign.write(message);
sign.end();

// 使用私钥对签名字符串进行签名
const signature = sign.sign(privateKey, 'base64');

const schema = 'WECHATPAY2-SHA256-RSA2048';
const token =`mchid="${data.mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${data.serial_no}",signature="${signature}"`;

let config = {
method: data.method,
maxBodyLength: Infinity,
url: data.url,
headers: {
'Authorization': `${schema} ${token}`,
'Content-Type': 'application/json'
},
data: reqbody
};

axios(config).then(function (res) {
qf_output={'data':res.data}
});
代码测试与配置

image.png

image.png

注意:这里【退款状态】为单项选择字段,并且这里的内容不可改动,需要为图中的内容,单项选择配置了逻辑表单

image.png

image.png

流程设计

image.png

  • 申请人节点:需要具体订单支付人来进行申请
  • 处理退款节点:需要企业对应的员工来进行处理退款申请
申请人节点
  • 退款段落中所有字段设置为隐藏,因为申请人不需要看到

image.png

处理退款

节点负责人设置为固定的一个人即可,代码块字段设置为可编辑,其余字段设置为仅可见。

image.png

回调通知内容解析

这里仅仅做演示解析,后续代码块节点功能上线后,可以作为自动接收回调用

image.png

这里的URL就是上面notify_url的内容

image.png

表单设计

这里只做解析演示,具体解析的字段内容,需要根据具体需求去做改动

image.png

解密
代码注释
  • qf_field.{具体回调内容$$19458800A$$}替换成具体对应表单字段

  • key:对应上面保存下来的APIV3密钥内容

const crypto = require('crypto');

const objString = qf_field.{具体回调内容$$19458800A$$}
const obj = JSON.parse(objString);
function decrypt(nonce, ciphertext, associatedData) {
const key = 'q888i777n66******22w111';
const keyBytes = Buffer.from(key, 'utf-8');
const nonceBytes = Buffer.from(nonce, 'utf-8');
const adBytes = Buffer.from(associatedData, 'utf-8');
const data = Buffer.from(ciphertext, 'base64');

const decipher = crypto.createDecipheriv('aes-256-gcm', keyBytes, nonceBytes);
decipher.setAuthTag(data.slice(-16));
decipher.setAAD(adBytes);

const decrypted = Buffer.concat([decipher.update(data.slice(0, -16)), decipher.final()]);
return decrypted.toString('utf-8');
}

const data = decrypt(obj.resource.nonce, obj.resource.ciphertext, obj.resource.associated_data);
const objdata = JSON.parse(data);
qf_output={objdata}

代码测试与配置

image.png

image.png

image.png

具体效果

image.png

实际效果

演示视频

视频:微信支付DEMO.mov。请在原文中查看。

效果图

image.png

image.png