All Articles

微信支付接入

1. 前言

微信支付的接入,权当笔记。

2. 支付流程

2.1 网页端

官方文档:扫码支付 > 模式二

官方配图:

流程涉及到几个角色:

  • PC网站网页
  • PC网站后台
  • 微信支付系统

支付流程如下:

  • 用户打开PC网站网页的商品页
  • 用户对商品进行下单
  • PC网站网页发送下单请求到PC网站后台
  • PC网站后台在自身系统内生成订单(生成内部订单号),并返回给网页
  • PC网站网页获得内部订单信息,并将用户导向到支付页面
  • 用户确认订单内容,并开始付款流程
  • PC网站网页发起支付请求到PC网站后台
  • PC网站后台向微信支付系统发起下单请求,并将返回的信息交付给PC网站网页
  • PC网站网页收到微信支付返回信息,将其中的QRCode渲染到页面
  • 用户打开微信,使用扫码的方式完成支付
  • 微信支付系统确认支付完成,向notify_url发送回调信息
  • PC网站后台验证微信支付回调完成,标记订单状态为已支付

2.2 移动原生

官方文档:APP支付 > 业务流程

官方配图:

流程涉及到几个角色:

  • PC网站网页
  • PC网站后台
  • 微信支付系统

支付流程如下:

  • 用户打开PC网站网页的商品页
  • 用户对商品进行下单
  • PC网站网页发送下单请求到PC网站后台
  • PC网站后台在自身系统内生成订单(生成内部订单号),并返回给网页
  • PC网站网页获得内部订单信息,并将用户导向到支付页面
  • 用户确认订单内容,并开始付款流程
  • PC网站网页发起支付请求到PC网站后台
  • PC网站后台向微信支付系统发起下单请求,并将返回的信息交付给PC网站网页
  • PC网站网页收到微信支付返回信息,使用其中的参数唤起微信APP
  • 用户在微信APP中完成支付
  • 微信支付系统确认支付完成,向notify_url发送回调信息
  • PC网站后台验证微信支付回调完成,标记订单状态为已支付

3. 准备工作

微信的权限系统非常有问题,没有分级的子账号,只有一个申请人所拥有的主账号。这导致了后续的一系列操作全部都必须主账号持有人来完成(对,你没有看错,必须你的老板来操作,别人不能代劳)。

申请人必须操作:

  • 申请公司账号
  • 申请支付应用

申请完成之后,必须获得以下几个信息,程序才可以对接:

  • APPID:支付应用的ID
  • 商户号:商户的ID
  • 商户的API秘钥:这里切记是商户秘钥;微信整个生态中应用非常繁多,因此各种秘钥也多,特别是去生成秘钥的又是老板,一般没有技术经验,所以这步非常非常容易出错,会导致后面验证签名无论如何都不正确,所以一定要当心,必须是商户秘钥

微信支付也有证书,但这并不是必须项,大部分支付API是不需要证书的,只有部分才需要。证书也是在商户部分申请,申请完之后可以打包下载,里面含文件:

  • apiclient_cert.p12:证书pkcs12格式
  • apiclient_cert.pem:证书
  • apiclient_key.pem:私钥

申请及配置的相关操作可以看这篇:微信公众号支付配置,以及微信APP支付配置

移动端还会遇到制作应用签名的问题:Android App 签名生成教程

4. API & SDK

4.1 SDK选择

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说明:

  • appid - 公众号ID(必填)
  • mchid - 微信商户号(必填)
  • partnerKey - 微信支付安全密钥(必填, 在微信商户管理界面获取)
  • pfx - 证书文件(选填, 在微信商户管理界面获取)
    • 当不需要调用依赖证书的API时可不填此参数
    • 若业务流程中使用了依赖证书的API则需要在初始化时传入此参数
  • notify_url - 支付结果通知回调地址(选填)
    • 可以在初始化的时候传入设为默认值, 不传则需在调用相关API时传入
    • 调用相关API时传入新值则使用新值
  • refund_url - 退款结果通知回调地址(选填)
    • 可以在初始化的时候传入设为默认值, 不传则使用微信商户后台配置
    • 调用相关API时传入新值则使用新值
  • spbill_create_ip - IP地址(选填)
    • 可以在初始化的时候传入设为默认值, 不传则默认值为127.0.0.1
    • 调用相关API时传入新值则使用新值

证书相关可以查阅:什么是API证书?如何获取API证书?

如果在接入的时候遇到签名问题,可以使用:微信支付接口签名校验工具微信公众平台支付接口调试工具进行调试。

4.2 API使用基础

4.2.1 网页端

API接入手册:扫码支付 > 接口规则 > 协议规则。API列表:扫码支付 > API列表 > 统一下单

4.2.2 移动原生

API接入手册:APP支付 > 接口规则 > 协议规则。API列表:APP支付 > API列表 > 统一下单

4.3 常用接口

4.3.1 统一下单接口

范例代码:

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});

4.3.2 订单查询接口

范例代码:

const order = await this.wxpay.orderQuery({out_trade_no: orderId.toString()});

4.4 金额问题

在刚才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(".", "")

4.5 回调处理

微信的所有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
}

5. 沙箱使用

微信支付的沙箱和支付宝有点不同,支付宝是完全隔离,从环境、访问地址到账号,支付的支付宝APP全部都是单独分离的。微信则不同,账号、测试用APP都还是原来的微信商户号以及用户真实的微信APP,但访问地址是隔离的,在原来的API地址中间插入/sandboxnew/

虽然易用性上看起来是微信占优,但实际使用上微信的沙箱有相当多的问题,甚至我研发到最后完成,都完全没使用过沙箱,都是使用0.01的金额进行下单测试。

5.1 沙箱申请

和支付宝一样,微信支付的沙箱使用不需要额外申请。

5.2 沙箱使用

使用方面,可以看一篇第三方的教程:浅析微信支付:如何使用沙箱环境测试

在使用刚才说的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。

5.3 沙箱问题

理解了沙箱的隔离以及如何初始化沙箱SDK之后,如果直接实际调用的话,还是会遇到问题:

沙箱支付金额(1)无效,请检查需要验收的case

参见:微信支付的沙箱环境能否使用?#668

这里还需要额外做一个操作:在微信支付商户接入验收助手这个公众号申请你的验收case,写入验收金额作为use case。后续在沙箱环境做测试的时候,金额必须完全符合之前申请的验收case,否则报错。

6. 错误处理

6.1 回调处理

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>`;
}

6.2 修复订单

在某些情况下,回调可能没有收到,或者一直报错导致回调没有正常处理。表现在用户视角,就是支付完成后订单状态无法正常改为支付完成(也就不会发货)。这是很糟糕的,因此需要制作修复订单功能。

客户端在检查到订单状态为等待支付之后,可以提供一个入口给用户,用户点击之后,应用后端应该向支付宝查询订单信息,如果在支付宝这里已经是已支付而在应用这边还是等待支付的话,就立即将业务订单改为已支付,并向用户发货。

7. 上线生效

微信支付应该是在应用申请审核通过的时候就直接上线了,不需要额外操作。

Appendix

链接

EOF

Published 2019/12/27

Some tech & personal blog posts