微信支付的接入,权当笔记。
官方文档:扫码支付 > 模式二。
官方配图:
流程涉及到几个角色:
支付流程如下:
用户
打开PC网站网页的商品页用户
对商品进行下单PC网站网页
发送下单请求到PC网站后台PC网站后台
在自身系统内生成订单(生成内部订单号),并返回给网页PC网站网页
获得内部订单信息,并将用户导向到支付页面用户
确认订单内容,并开始付款流程PC网站网页
发起支付请求到PC网站后台PC网站后台
向微信支付系统发起下单请求,并将返回的信息交付给PC网站网页PC网站网页
收到微信支付返回信息,将其中的QRCode渲染到页面用户
打开微信,使用扫码的方式完成支付微信支付系统
确认支付完成,向notify_url发送回调信息PC网站后台
验证微信支付回调完成,标记订单状态为已支付官方文档:APP支付 > 业务流程。
官方配图:
流程涉及到几个角色:
支付流程如下:
用户
打开PC网站网页的商品页用户
对商品进行下单PC网站网页
发送下单请求到PC网站后台PC网站后台
在自身系统内生成订单(生成内部订单号),并返回给网页PC网站网页
获得内部订单信息,并将用户导向到支付页面用户
确认订单内容,并开始付款流程PC网站网页
发起支付请求到PC网站后台PC网站后台
向微信支付系统发起下单请求,并将返回的信息交付给PC网站网页PC网站网页
收到微信支付返回信息,使用其中的参数唤起微信APP用户
在微信APP中完成支付微信支付系统
确认支付完成,向notify_url发送回调信息PC网站后台
验证微信支付回调完成,标记订单状态为已支付微信的权限系统非常有问题,没有分级的子账号,只有一个申请人所拥有的主账号。这导致了后续的一系列操作全部都必须主账号持有人来完成(对,你没有看错,必须你的老板来操作,别人不能代劳)。
申请人必须操作:
申请完成之后,必须获得以下几个信息,程序才可以对接:
商户秘钥
;微信整个生态中应用非常繁多,因此各种秘钥也多,特别是去生成秘钥的又是老板,一般没有技术经验,所以这步非常非常容易出错,会导致后面验证签名无论如何都不正确,所以一定要当心,必须是商户秘钥微信支付也有证书,但这并不是必须项,大部分支付API是不需要证书的,只有部分才需要。证书也是在商户部分申请,申请完之后可以打包下载,里面含文件:
申请及配置的相关操作可以看这篇:微信公众号支付配置,以及微信APP支付配置。
移动端还会遇到制作应用签名的问题:Android App 签名生成教程。
npm上可用的SDK包基本上只有:tenpay。
SDK的使用:
const tenpay = require('tenpay');
const config = {
appid: '公众号ID',
mchid: '微信商户号',
partnerKey: '微信支付安全密钥',
pfx: require('fs').readFileSync('证书文件路径'),
notify_url: '支付回调网址',
spbill_create_ip: 'IP地址'
};
// 方式一
const api = new tenpay(config);
// 方式二
const api = tenpay.init(config);
// 调试模式(传入第二个参数为true, 可在控制台输出数据)
const api = new tenpay(config, true);
// 沙盒模式(用于微信支付验收)
const sandboxAPI = await tenpay.sandbox(config);
config说明:
证书相关可以查阅:什么是API证书?如何获取API证书?。
如果在接入的时候遇到签名问题,可以使用:微信支付接口签名校验工具或微信公众平台支付接口调试工具进行调试。
API接入手册:扫码支付 > 接口规则 > 协议规则。API列表:扫码支付 > API列表 > 统一下单。
API接入手册:APP支付 > 接口规则 > 协议规则。API列表:APP支付 > API列表 > 统一下单。
范例代码:
const {prepay_id, code_url} = await this.wxpay.unifiedOrder({
out_trade_no: orderId.toString(),
body: name,
// 微信支付的金额是不带小数位的,所有的小数金额都必须*100转为正整数
// 数值先会被小数点后2位截断或补足,然后转为整数字符串
// 8.8 => 8.80 => 880
// 9.24678 => 9.25 => 925
total_fee: (price).toFixed(2).replace(".", ""),
trade_type: type, // NATIVE:扫码支付 | APP:原生支付
product_id: productId,
});
return {
prepayId: prepay_id,
codeUrl: code_url,
};
拿到返回值之后,后续根据支付类型不同,处理方式也不一样:
扫码支付
需要将code_url的值,转换为QRCode,后续前端才可以将其渲染到HTML页面上(当然也可以直接将url传给前端,让前端渲染)。这里比较好用的是:qrcode。
后端代码:
const QRCode = require("qrcode");
// ...
const svg = await (QRCode as any).toString(result.codeUrl, {type: "svg"});
HTML页面:
<html>
<head></head>
<body>
<div id="qrcode" style="width: 200px; height: 200px;"></div>
</body>
</html>
前端代码:
function renderXml(id, xmlString){ // xmlString: svg content
let doc = new DOMParser().parseFromString(xmlString, "application/xml");
let el = document.getElementById(id);
el.appendChild(el.ownerDocument.importNode(doc.documentElement, true));
}
renderXml("qrcode", res.data.extra.codeUrl);
原生支付
在统一下单接口完成之后,还需要根据prepay_id来申请唤起微信的参数:
const params = await this.wxpay.getAppParamsByPrepay({prepay_id: prepayId});
范例代码:
const order = await this.wxpay.orderQuery({out_trade_no: orderId.toString()});
在刚才4.3.1
的例子中,代码里其实已经体现出来了。微信支付的金额和支付宝是不一样的,支付宝的金额就是自然的数额,带小数点后两位。而微信的做法不同,微信要求调用接口下单的时候的支付金额必须不带小数位,所有的金额都是小数点后两位的数字*100转化为整数
的数额。也就是说下单接口金额为1
的情况,实际支付的是0.01
。
// 微信支付的金额是不带小数位的,所有的小数金额都必须*100转为正整数
// 数值先会被小数点后2位截断或补足,然后转为整数字符串
// 8.8 => 8.80 => 880
// 9.24678 => 9.25 => 925
total_fee: (price).toFixed(2).replace(".", "")
微信的所有API都是使用XML作为输入和输出,因此获得的回调信息也是XML。因为之前的API调用中都有SDK处理完了,所以这里就有点恶心,需要自己处理。
回调结构:
export interface IResOrderPayed {
return_code: string; // SUCCESS
return_msg: string; // OK
appid: string; // wx3...
mch_id: number; // 156..
nonce_str: string; // cwGl...
sign: string; // C51A...
result_code: string; // SUCCESS
openid: string; // oqvcFj-Uf...
is_subscribe: string; // N
trade_type: string; // NATIVE
bank_type: string; // OTHERS
total_fee: number; // 1,需要 / 100
fee_type: string; // CNY
transaction_id: string; // 42000004...
out_trade_no: number;
attach: string; // ""
time_end: number; // 2019XXXX144155
trade_state: string; // SUCCESS
cash_fee: number; // 1,需要 / 100
trade_state_desc: string; // 支付成功
cash_fee_type: string; // CNY
}
微信支付的沙箱和支付宝有点不同,支付宝是完全隔离,从环境、访问地址到账号,支付的支付宝APP全部都是单独分离的。微信则不同,账号、测试用APP都还是原来的微信商户号以及用户真实的微信APP,但访问地址是隔离的,在原来的API地址中间插入/sandboxnew/
。
虽然易用性上看起来是微信占优,但实际使用上微信的沙箱有相当多的问题,甚至我研发到最后完成,都完全没使用过沙箱,都是使用0.01的金额进行下单测试。
和支付宝一样,微信支付的沙箱使用不需要额外申请。
使用方面,可以看一篇第三方的教程:浅析微信支付:如何使用沙箱环境测试。
在使用刚才说的SDK的情况下,只需要在SDK初始化的地方做点调整即可:
const config = {
appid: vendorConfig.appId,
mchid: vendorConfig.mchId,
partnerKey: vendorConfig.appSecret,
pfx: LibFs.readFileSync(LibPath.join(__dirname, "../../../pem/wxpay/apiclient_key.pem")).toString(),
notify_url: vendorConfig.callbackUrl,
spbill_create_ip: vendorConfig.allowedIp,
} as Tenpay.IConfig;
// wxpay
if (isSandbox) {
tenpay.init(config, isTest).getSignkey().then((res) => {
const key = res.sandbox_signkey;
this.wxpay = tenpay.init(Object.assign(config, {partnerKey: key, sandbox: true}), isTest);
}).catch((err) => {
throw err;
});
} else {
this.wxpay = tenpay.init(config, isTest);
}
沙箱每次使用的秘钥(商户的API秘钥)和正式环境不一样,正式环境是配置死的,直接拿下来用即可。而沙箱则需要先使用正式环境的商户秘钥去调用getSignkey
获取沙箱使用的临时商户秘钥,才可以后续继续调用API。
理解了沙箱的隔离以及如何初始化沙箱SDK之后,如果直接实际调用的话,还是会遇到问题:
沙箱支付金额(1)无效,请检查需要验收的case
这里还需要额外做一个操作:在微信支付商户接入验收助手
这个公众号申请你的验收case,写入验收金额作为use case。后续在沙箱环境做测试的时候,金额必须完全符合之前申请的验收case,否则报错。
try {
// 业务处理
ctx.type = "application/xml";
ctx.body = "" +
`<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>`;
} catch (err) {
console.log(err);
ctx.type = "application/xml";
ctx.body = "" +
`<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[${err.message}]]></return_msg>
</xml>`;
}
在某些情况下,回调可能没有收到,或者一直报错导致回调没有正常处理。表现在用户视角,就是支付完成后订单状态无法正常改为支付完成(也就不会发货)。这是很糟糕的,因此需要制作修复订单功能。
客户端在检查到订单状态为等待支付之后,可以提供一个入口给用户,用户点击之后,应用后端应该向支付宝查询订单信息,如果在支付宝这里已经是已支付
而在应用这边还是等待支付
的话,就立即将业务订单改为已支付,并向用户发货。
微信支付应该是在应用申请审核通过的时候就直接上线了,不需要额外操作。
EOF