赞
踩
微信的支付方式有以下几种,不同的支付方式适用于不同的支付场景,而今天要给大家讲的就是 小程序支付 方式
说到支付功能就要涉及到金钱交易,必定是有比较严格的规范及流程,如要求小程序必须具备企业性质,必须拥有
微信支付商户平台的账号
PS:申请微信支付商户平台需要一个微信小程序或公众号等,建议按照以下流程进行操作
1、申请微信小程序账号
申请成功可拿到 AppID(小程序 id)和 AppSecret(小程序密钥)
申请类型为企业性质,否则无法接入微信支付
2、微信小程序认证
通过认证的小程序才能接入微信支付和绑定商户平台
3、申请商户平台账号
需要第一步申请的 AppID
申请成功可拿到 MchID(商户 id)和 MchKey(商户密钥)
4、信小程序关联商户号
微信和商户都认证成功后,在微信后台
微信支付
菜单中进行关联
5、接入微信支付
在微信后台
微信支付
菜单中进行接入
简要支付流程如下:
PS:难点在第 2、3、5 步,一定要仔细查看相关接口文档,否则容易出错,接下来我们按照以上 6 个步骤详细讲解在微信小程序中的支付流程
因为严格意义上来说这不属于支付流程中的步骤,但支付过程中需要用到用户唯一标识
openid
,所以建议在用户进入小程序时就进行这一步的操作
code
,并把code
传到服务器openid
和 session_key
openid
存入数据库,方便随时获取,下面的步骤也会用到config/wx.js
文件中以方便调用)PS:开发者需要自行维护用户登录状态(用户登录状态的维护本文不做展开,请自行查阅相关资料)
这步没什么好说的,当用户点击支付按钮时,给我们自己的后端接口发起一个请求,携带必要的参数(如:body
,total_fee
等),接口地址需要自行编写,如我的接口地址为/payment/order
- // http对象为wx.request()的二次封装
- import http from "../utils";
-
- // 向后端发请请求
- const res = await http.post("/payment/order", {
- body: "腾讯QQ-购买会员", // 商品描述
- total_fee: 998, // 总金额,单位为分
- });
- if (res.status === 200) {
- try {
- // 得到接口返回的数据,向微信发起支付
- const result = await wx.requestPayment({
- ...res.data,
- });
- wx.showToast({
- title: "支付成功",
- });
- console.log("支付结果:", result);
- } catch (err) {
- wx.showToast({
- title: "支付失败",
- });
- }
- }
PS:可能会有小伙伴产生疑惑,为什么不直接通过 wx.requestPayment()
在小程序端发起请求而要先请求商户自己的服务器呢?原因很简单,安全性问题,wx.requestPayment()
需要 2 个重要参数paySign
和package
,需要 appid,secret,openid,mch_key 等私密数据,这些私密的数据不应该在前端暴露出来,而是放在自己的服务器中更安全,所以需要向自己的服务器发起这个请求拿到这些参数,下一步才能真正发起支付。接下来我们来看看后端是如果操作的
后端代码使用 egg 框架(基于 NodeJS+Koa)实现,文中涉及到 egg 用法和 koa 的用法不再额外说明,请自行查阅相关资料
调用统一下单接口
获取 预支付会话标识 prepay_id
注意:该接口需要发送 xml 格式参数,同时返回 xml 格式数据,需自行转换(我使用的是
xml-js
第三方模块)
appid
,mch_id
,nonce_str
,sign_type
,body
,out_trade_no
,total_fee
,spbill_create_ip
,notify_url
,trade_type
,sign
,其中 sign
为前面所有参数加密- async order(ctx) {
- // egg框架写法
- const { service, request } = ctx;
-
- // 获取前端传入参数
- const { userid, total_fee, body } = request.body;
-
- // 引入微信配置参数(上面准备工作中保存的config/wx.js文件,包含小程序id,密钥,商户id,商户密钥)
- const { config } = require("../../config/wx");
-
- // 生成订单号(保证唯一性:我采用时间戳拼6位随机数的方式)
- const tradeNo = Date.now() + '' + randomCode(100000, 999999);
-
- // 统一下单签名参数
- const orderParams = {
- appid: config.appid, // 小程序id
- mch_id: config.mch_id, // 商户id
- nonce_str: service.wx.randomStr(), // 自定义生成随机字符方法
- sign_type: "MD5", // 加密类型
- body, // 商品简单描述,有格式要求
- out_trade_no: tradeNo, // 订单号
- total_fee, // 单位:分
- spbill_create_ip: "121.34.253.98", // 服务器ip
- notify_url: "https://你的服务器域名/payment/wxnotify", // 支付成功通知地址
- trade_type: "JSAPI", // 支付方式(小程序支付选JSAPI)
- openid: user.openid, // 用户openid,步骤0保存的数据
- };
-
- // 签名:对上面所有参数加密(签名算法请查看接口文档,下同)
- const orderSign = service.wx.sign(orderParams);
-
- // json->xml
- const xmlData = convert.js2xml(
- { xml: { ...orderParams, sign: orderSign } },
- { compact: true }
- );
-
- // 调用统一下单接口(接口没说明,但必须为post请求)
- const { data } = await ctx.curl(
- "https://api.mch.weixin.qq.com/pay/unifiedorder",
- {
- method: "post",
- data: xmlData,
- }
- );
-
- // xml->js
- const result = convert.xml2js(data, { compact: true });
-
- if (result.prepay_id) {
- // 此处可以把订单信息保存到数据库
-
- // 返回prepay_id后,接着就是把参数返回前端
- // =>为了更清晰,我把这里的代码写在下一步
- // ...
- }
- }
- // 支付签名参数
- const payParams = {
- appId: config.appid, // 商户 id
- timeStamp: Date.now(), // 时间戳
- nonceStr: this.randomStr(), // 随机字符
- package: "prepay_id=" + result.prepay_id, //预支付会话标识(格式为:prepay_id=统一下单接口返回数据)
- signType: "MD5", //签名类型(必须与上面的统一下单接口一致)
- };
- // 签名
- const paySign = service.wx.sign(payParams);
-
- // 把参数+签名返回给前端
- ctx.body = formatData({
- data: {
- timeStamp: payParams.timeStamp,
- nonceStr: payParams.nonceStr,
- package: payParams.package,
- signType: payParams.signType,
- paySign,
- },
- });
附上封装好的签名方法sign()
和生成随机字符串的方法randomStr()
,我写在service/wx.js
- "use strict";
- const { Service } = require("egg");
- const crypto = require("crypto");
- // 微信基本配置
- const { weapp } = require("../../config/wx");
- class wxService extends Service {
- randomStr(len = 24) {
- const str =
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- let result = "";
- for (let i = 0; i < len; i++) {
- result += str[Math.floor(Math.random() * str.length)];
- }
- return result;
- }
- sign(data, signType = "MD5") {
- const keys = [];
- for (const key in data) {
- if (data[key] !== undefined) {
- keys.push(key);
- }
- }
- // 字典排序=>key=value
- const stringA = keys
- .sort()
- .map((key) => `${key}=${decodeURIComponent(data[key])}`)
- .join("&");
- // 拼接商户key
- const stringSignTemp = stringA + "&key=" + weapp.mch_key;
- console.log("stringSignTemp", stringSignTemp);
- // 加密
- let hash;
- if (signType === "MD5") {
- hash = crypto.createHash("md5");
- } else {
- hash = crypto.createHmac("sha256", "laoxie");
- }
- hash.update(stringSignTemp);
- const paySign = hash.digest("hex").toUpperCase();
- return paySign;
- }
- }
- module.exports = wxService;
第 1 步的数据返回后,向微信服务器接口wx.requestPayment()
发请求,唤起支付界面,请查看第一步 try...catch 中的代码
在第 2 步向统一下单接口
发起请求时附带了一个notify_url
,此地址一定要是可外网访问的接口地址(商户自行编写),由微信服务器调用该接口,不管支付成功与否,此接口都会调用,并返回相应数据(查看接口数据
),所以商户可以在此接口中编写相关业务逻辑、如支付成功后写入数据库等操作
注意:商户需要在此接口中做接收处理,并向微信服务器返回应答(按接口规范返回特定数据)。如果微信收到商户的应答不是成功或超时,微信会认为通知失败,微信会通过一定的策略定期重新发起通知,通知频率为:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h,但微信不保证通知最终一定能成功。
- "use strict";
-
- const Controller = require("egg").Controller;
- const getRawBody = require("raw-body");
- const contentType = require("content-type");
- const { formatData, randomCode, params, formatParams } = require("../utils");
-
- class PaymentController extends Controller {
- // 微信支付回调地址
- async notify(ctx) {
- const { req } = ctx;
-
- // 微信调用该接口时传入的数据为xml,所以先转换
- const data = await getRawBody(req, {
- length: req.headers["content-length"],
- limit: "1mb",
- encoding: contentType.parse(req).parameters.charset,
- });
- const result = params.xml2js(data);
-
- // 验签:微信传入的除sign外的所有数据进行签名,拒后与sign进行对比是否一致
- // 一致说明支付成功,否则支付失败
- // 并根据不同的结果通知微信服务器(响应不同的xml数据,如下)
- const resultSign = result.sign;
- delete result.sign;
- const mySign = ctx.service.wx.sign(result);
- console.log("sign:", resultSign, mySign);
-
- ctx.set("content-type", "text/plain");
-
- if (resultSign === mySign) {
- // 修改商户订单状态
- const {
- device_info,
- openid,
- trade_type,
- bank_type,
- total_fee,
- settlement_total_fee,
- fee_type,
- transaction_id,
- time_end,
- attach,
- } = result;
-
- // 格式化自定义参数
- let myattach = {};
- if (attach) {
- myattach = params.parse(attach);
- }
-
- // 格式化支付时间:20200423161017=>2020/04/23 16:10:17
- let pay_time = time_end.replace(
- /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/,
- "$1/$2/$3 $4:$5:$6"
- );
- pay_time = new Date(pay_time);
-
- // 根据订单号更新数据库中的订单状态
- const newData = {
- device_info,
- openid,
- trade_type,
- bank_type,
- total_fee,
- settlement_total_fee,
- fee_type,
- transaction_id,
- pay_time,
- status: 1,
- ...myattach,
- };
- db.update(
- "purchase",
- {
- out_trade_no: result.out_trade_no,
- },
- {
- $set: newData,
- }
- );
-
- ctx.body = `<xml>
- <return_code><![CDATA[SUCCESS]]></return_code>
- <return_msg><![CDATA[OK]]></return_msg>
- </xml>`;
- } else {
- ctx.body = `<xml>
- <return_code><![CDATA[FAIL]]></return_code>
- <return_msg><![CDATA[ERROR]]></return_msg>
- </xml>`;
- }
- }
- }
-
- module.exports = PaymentController;
附上以上代码中会用的封装好的方法parse()
、xml2js()
、js2xml()
,我写在utils/index.js
中
- const params = {
- parse(queryString) {
- // 'a=1&b=2' => {a:1,b:2}
- return queryString.split("&").reduce((res, item) => {
- const arr = item.split("=");
- res[arr[0]] = arr[1];
- return res;
- }, {});
- },
- js2xml(data) {
- return convert.js2xml({ xml: data }, { compact: true });
- },
- xml2js(xml) {
- const result = convert.xml2js(xml, {
- compact: true,
- textKey: "value",
- cdataKey: "value",
- }).xml;
- const data = {};
- for (const key in result) {
- data[key] = result[key].value;
- }
- return data;
- },
- };
- module.exports = {
- params
- }
到此微信支付之小程序支付就完成了,过程比较繁杂,一定要一步步去实现,也许会踩坑,但相信我,这是每个程序员的必经这路,面对它,勇敢地走过去,你对能到达胜利的彼岸。
[mp] https://mp.weixin.qq.com
[pay] https://pay.weixin.qq.com
[payment] https://developers.weixin.qq.com/miniprogram/dev/api/open-api/payment/wx.requestPayment.html
[notify] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7&index=8
[login] https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
[unifiedorder] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
[code2session] https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
[sign] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。