背景介绍
如果你作为一名前端工程师,想自己实现一个带支付功能的小程序,你还准备后端用Java帮你实现带支付功能的接口吗?大可不行,动起手来,让我们自己用Node.js实现一个微信小程序的接口。
环境搭建
- 前端框架:我选择了Tarojs
- 后端框架:我选择了Eggjs 作为一个前端工程师,必要的框架搭建我就不一一赘述了。
准备工作
- 小程序支付需要自行开通微信支付功能
- 微信支付文档
小程序端发起支付流程
发起支付流程图
调用创建订单的接口,返回订单ID
network.post('/wx/create/order', createParams).then(res => {
const {code} = res
const { data: { appid, prepay_id } } = res;
if (code === 0) {
launchPay({ appid, prepay_id })
} else {
Taro.showToast({
title: '创建订单失败'
})
}
}).finally(() => {
Taro.hideLoading()
})
根据预支付交易会话标识(prepay_id)
和openId
js版本的签名算法 - getSign
// 加密支付
export const getSign = (params, partnerKey) => {
const keys = Object.keys(params);
keys.sort((a, b) => a.charAt(0).charCodeAt() - b.charAt(0).charCodeAt());
const stringA = keys.map(it => it + '=' + params[it]).join('&');
const stringSignTemp = `${stringA}&key=${partnerKey}`; //注:key为商户平台设置的密钥key
return md5(stringSignTemp).toUpperCase();
}
Taro.requestPayment发起小程序的支付,launchPay
方法的具体实现:
export const launchPay = (data: PayData) => {
const { appid, prepay_id } = data;
const timeStamp = getCurrentStimp();
const params = {
appId: appid, // 小程序ID
timeStamp: timeStamp.toString(), // 时间戳
nonceStr: Math.random().toString(36).substr(2), // 随机串
package: `prepay_id=${prepay_id}`, // 数据包
signType: 'MD5', //签名方式
};
// 用户登录后返回的商户平台设置的密钥key
const partner_key = Taro.getStorageSync('partner_key');
//
const paySign = getSign(params, partner_key);
Object.assign(params, { paySign });
Taro.requestPayment({
...params,
success: res => {
Taro.showToast({
title: "支付成功",
icon: "success",
duration: 1500,
mask: false,
success: res => {
console.log('支付成功');
console.log(res, '支付成功');
Taro.showToast({ icon: 'success', title: '支付成功' })
}
});
},
fail: res => {
console.log(res, '支付失败');
Taro.showToast({ icon: 'none', title: '支付失败' })
},
complete: res => {
console.log(res);
}
});
}
Node.js服务端支付接口具体实现
支付回调信息配置
// payInfo
const payInfo = {
domain: 'http://a5iqw7.natappfree.cc', // 内网穿透的临时地址
notify_url: '/wx/order/pay/callback',
};
创建订单
import tenpay from 'tenpay';
// wx/create/order
async createApply() {
const body = this.ctx.request.body;
const { openid: user_id } = this.ctx.state.user;
const order = {
note,
order_id,
user_id,
create_time: stemp,
all_price,
people_number,
table_number,
order_type,
table_cost,
pack_cost,
status: ORDER_STATUS_KEYS.WAIT_PAY,
create_at: stemp,
update_at: stemp,
};
// 插入订单表
await this.ctx.service.wx.order.create(order);
// 微信统一下单
const { payInfo: { domain, notify_url } } = this.ctx.app.config;
const { partner_key, mchid, appid } = await this.ctx.service.shop.merchant.model.findOne({
where: { id: this.ctx._shop.id },
});
const payConfig = {
appid,
mchid,
partnerKey: partner_key, // partnerKey
};
const PAYAPI = new tenpay(payConfig);
const payParams = {
out_trade_no: order_id,
body: 'xxxxx支付信息',
total_fee: all_price, // 单位为分,最小为1
openid: user_id,
notify_url: domain + notify_url,
};
const result = await PAYAPI.unifiedOrder(payParams);
this.ctx.success({
data: result,
});
}
支付回调方法,当用户支付成功后,微信会多次调用我们提供的回调地址
* 接口:/wx/order/pay/callback
* 实现:
* 加密方法和处理支付时间的方法:
```js
export const getSign = (params: any, partnerKey: string) => {
const keys = Object.keys(params);
keys.sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0));
const stringA = keys.map(it => it + '=' + params\[it]).join('&');
const stringSignTemp = `${stringA}&key=${partnerKey}`; //注:key为商户平台设置的密钥key
return md5(stringSignTemp).toUpperCase();
}
// 处理微信的支付时间
export const toPayTime = (date: string) => {
if (date.length !== 14) return date;
const YYYY = date.substring(0, 4);
const MM = date.substring(4, 6);
const DD = date.substring(6, 8);
const HH = date.substring(8, 10);
const mm = date.substring(10, 12);
const ss = date.substring(12, 14);
return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
};
```
/**
* 支付回调
* return_code SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
* appid 小程序ID
* mch_id 商户号
* nonce_str 随机字符串
* sign 签名
* sign_type 签名类型
* trade_type JSAPI
* openid 用户标识
* bank_type 付款银行
* total_fee 订单金额
* cash_fee 现金支付金额
* transaction_id 微信支付订单号
* order_id 商户订单号
* time_end 支付完成时间
*/
async payCallback() {
const { xml } = JSON.parse(xml2json.toJson(this.ctx.request.body));
const params: any = {};
Object.keys(xml).forEach(it => {
params[it] = xml[it].toString();
});
console.log(params, 'params')
const {
return_code,
mch_id,
openid: user_id,
sign,
out_trade_no: order_id,
bank_type,
total_fee,
cash_fee,
transaction_id,
time_end,
} = params;
const signBody = { ...params };
delete signBody.sign;
Object.keys(signBody).forEach(it => {
if (!signBody[it]) {
delete signBody[it];
}
});
const order = await this.ctx.model.Order.findOne({
where: {
user_id,
order_id,
}
});
const { partner_key } = await this.ctx.service.shop.merchant.model.findOne({
where: { id: order.shop_id }
});
const paySign = getSign(signBody, partner_key);
// 支付成功
if (return_code === 'SUCCESS') {
// 校验签名
if (paySign === sign) {
console.log('签名对比成功');
// 待支付状态才支付
if (order && order.status === ORDER_STATUS_KEYS.WAIT_PAY) {
await this.ctx.model.Order
.update({
mch_id,
total_fee,
bank_type,
cash_fee,
transaction_id,
pay_time: toPayTime(time_end),
pay_status: 1, // 支付状态
status: ORDER_STATUS_KEYS.PAY
},
{
where: {
user_id,
order_id,
}
}
);
this.ctx.success({
return_code: 'SUCCESS',
return_msg: 'OK'
})
} else {
this.ctx.failure({
return_code: 'FAIL',
return_msg: '签名失败'
});
}
} else {
this.ctx.failure({
return_code: 'FAIL',
return_msg: '支付失败'
});
}
}
}
微信的支付回调如何调试?
- 本地开发调试的时候,可以开一个内网穿透
总结
本文主要讲述了从前端小程序调用创建订单接口,到Nodejs后端接口返回prepay_id,然后前端发起支付,然后前端加密支付信息,微信调用我们提供的回调接口,我们对比加密信息一致,完成整个支付流程。