1. 开通微信支付?
要在小程序里集成微信支付,小程序自身必须是通过微信认证的。从微信公众平台扫码进去,点击左侧菜单的微信认证
:
大家按照流程认证就好:
- 认证的主体类型。
- 认证费用,需要30元。
1.2. 检查小程序 AppId
- 要将.env里,改为认证通过的小程序AppId。
- 此外,微信开发者工具里,点击右上角的详情,也要将这里的AppId改掉。
1.3. 注册微信支付商户号
认证通过后,还需要额外注册微信支付商户号
,才能实现支付。
请大家访问微信支付官网,右上角有个接入微信支付
按钮,点进去:
1.4. 开通产品
注册成功后,回到微信支付官网。扫码登录后,点击产品中心。这里和支付宝一样,也需要开通对应的产品。在小程序里支付,需要开通的是JSAPI支付:
1.5. 开发配置
接着点击开发配置,这里可以在JSAPI支付
里,添加线上接口
地址。这样将来小程序上线了,才能正常支付。例如我这里填的是:https://api.clwy.cn`
1.6. 获取商户号
页面顶上有个商户号,这个值在对接微信支付的时候会用到。
打开.env,增加
WECHAT_MCH_ID=你的商户号
1.7.关联小程序
继续点击AppID账号管理
,这里要关联自己的小程序,才能在小程序里支付。点击关联AppID
:
里面填通过认证的小程序AppID
:
1.8. 商户 APIv2 密钥
继续点击账户中心,左侧选择API 安全,在这里要添加密钥。
密钥现在是分了v2
和v3
版本。v2
密钥非常简单,自己设置一串32位
的字符就好,里面支持数字和大小写字母。大家可以自己随便打一串,或者直接让AI
生成都可以。
至于v3
的,跟支付宝类似,需要下载微信的秘钥工具,再生成各种公钥私钥什么的,非常繁琐。为了简单,我们这里就直接用v2
版本进行开发。
v2
密钥设置好了以后,再打开.env
文件,添加:
WECHAT_MCH_KEY=你的商户APIv2密钥
1.9. 微信支付通知地址
微信支付和支付宝一样,在支付成功后,也要设置一个通知地址,用来更新订单状态。.env
里,继续加上:
WECHAT_NOTIFY_URL=你的微信
我设置为https://api.clwy.cn/wechat/notify
,大家不要照着我的填,请填自己的线上接口地址。
地址最后的路径是notify
,很显然,回头会在微信的路由文件里,新增一个notify
路由。
1.10. 检查环境变量
WECHAT_APPID=微信小程序APPID
WECHAT_SECRET=微信小程序SECRET
WECHAT_MCH_ID=微信小程序支付商户号
WECHAT_MCH_KEY=商户APIv2密钥
WECHAT_NOTIFY_URL=微信支付通知地址
2. 集成微信支付
2.1. 安装 tenpay
npm i tenpay
2.2. 实例化微信支付
接着,就要将微信的环境变量用起来了,参考文档的实例化这一节。
在utils
目录,新增一个wechat.js
文件,里面加上对应的配置:
const tenpay = require('tenpay');
const config = {
appid: process.env.WECHAT_APPID, // 小程序 appid
mchid: process.env.WECHAT_MCH_ID, // 微信商户号
partnerKey: process.env.WECHAT_MCH_KEY, // 微信支付安全密钥
notify_url: process.env.WECHAT_NOTIFY_URL // 支付通知地址
};
// 调用 tenpay,生成微信 JSSDK 支付参数
const wechatSdk = new tenpay(config, true);
module.exports = wechatSdk;
2.3. 实现微信支付
继续看文档的获取微信 JSSDK 支付参数这里:
打开routes/wechat.js
文件,顶部先引用一下,刚才初始化的微信支付 SDK:
const wechatApi = require('../utils/wechat');
再增加一个路由,
/**
* 微信支付
* POST /wechat/pay
*/
router.post('/pay', userAuth, async function (req, res, next) {
try {
// 支付订单信息
const order = await getOrder(req);
const { outTradeNo, totalAmount, subject } = order;
// 查询当前用户,因为发起支付,需要用户的 openid
const user = await User.findByPk(req.userId);
// 生成微信支付参数
const result = await wechatApi.getPayParams({
out_trade_no: outTradeNo, // 商户内部订单号
body: subject, // 商品简单描述
total_fee: totalAmount * 100, // 因为微信支付以「分」为单位,所以需要 * 100
openid: user.openid // 付款用户的 openid
});
// 返回给小程序
success(res, '获取微信支付参数成功。', { result });
} catch (error) {
failure(res, error);
}
});
- 先查询一下要支付的订单。
- 然后查一下当前用户。因为发起支付,必须要当前用户的
openid
。 - 接着就是调用
getPayParams
了,这块代码与文档的参数完全一样。 - 但是要注意了,微信支付里的金额是以
分
为单位。而数据库订单表里存的是以元
为单位。所以要乘以100
,这才是分
。 - 最后就将生成的微信支付参数返回出去。
接着在下面,还要增加一个查询订单的公共方法:
/**
* 公共方法:查询当前订单
* @param req
* @returns {Promise<*>}
*/
async function getOrder(req) {
const { outTradeNo } = req.body;
if (!outTradeNo) {
throw new BadRequest('订单号不能为空。');
}
const order = await Order.findOne({
where: {
outTradeNo: outTradeNo,
userId: req.userId,
},
});
// 用户只能查看自己的订单
if (!order) {
throw new NotFound(`订单号: ${ outTradeNo } 的订单未找到。`);
}
if (order.status > 0) {
throw new BadRequest('订单已支付或取消。');
}
return order;
}
3.微信支付通知
当微信支付成功后,微信服务器会主动发起回调请求,我们在代码里设置好的通知地址,告诉我们订单支付完成了
看到文档的 中间件・微信消息通知 这里
3.1. 解析 xml
里面说,要先写个app.use
,用来解析xml
格式的数据。这说明,微信通知发过来的数据格式就是xml
。
我们按照文档的说明,打开根目录的app.js
文件,增加点内容:
const bodyParser = require('body-parser');
app.use(bodyParser.text({ type: '*/xml' }));
3.2 微信通知中间件
继续看文档:
- 让我们在
通知对应的路由
里,增加个中间件
。 - 从
req.weixin
里,得到的info
,就是微信发送过来的数据。 - 最下面还有个说明,使用r
es.replay
。如果参数
是空
,表示回复微信服务器,已收到通知
。这样微信就不会重复发出通知
了。 - 如果传其他信息,就表示出错了。
照文档,打开routes/wechat.js
文件,增加一个路由
/**
* 微信支付通知
* POST /wechat/notify
*/
router.post('/notify', wechatApi.middlewareForExpress('pay'), async function (req, res) {
try {
const info = req.weixin;
logger.warn('微信支付成功:', info);
res.reply(''); // 回复微信服务器,表示已收到通知
} catch (error) {
logger.warn('微信支付失败:', error);
failure(res, error);
}
});
打开数据库客户端
,找到logs
表,可以看到微信发过来的通知
了。
点开meta
字段,复制
出来,在编辑器里随便建个info.json
,粘贴进去,格式化一下:
{
"service": "clwy-api",
"appid": "wxf10e4d931aad21c9",
"bank_type": "OTHERS",
"cash_fee": "1",
"fee_type": "CNY",
"is_subscribe": "N",
"mch_id": "1230390602",
"nonce_str": "QKBbtYcLFQirsTT6",
"openid": "oyDWp5VtDeHOrce1mRIStv-VN9Ag",
"out_trade_no": "c063e7a6cd9b406e824912a9802bf63f",
"result_code": "SUCCESS",
"return_code": "SUCCESS",
"sign": "C0D31206A6B60E3F2CA1A4071E9761A5",
"time_end": "20250228105657",
"total_fee": "1",
"trade_type": "JSAPI",
"transaction_id": "4200002560202502284847194037"
}
- return_code 和 result_code:显示的都是
SUCCESS
。显然这两个,可以用来判断支付状态
。 - out_trade_no:是
订单号
。 - transaction_id:是流水号,对应我们数据库订单表的
tradeNo
字段。微信这里与支付宝里的命名是不同的,但都是一个意思。 - time_end:就是
支付的时间
了。
3.3 完善支付通知
router.post('/notify', wechatApi.middlewareForExpress('pay'), async function (req, res) {
try {
const info = req.weixin;
if (info.return_code === 'SUCCESS' && info.result_code === 'SUCCESS') {
const {out_trade_no, transaction_id, time_end} = info;
// 支付时间转成 DATETIME 所需格式
const paidAt = moment(time_end, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss');
await paidSuccess(out_trade_no, transaction_id, paidAt);
res.reply(''); // 回复微信服务器,表示已收到通知
} else {
// 支付失败或其他情况处理逻辑
res.reply('错误消息');
}
} catch (error) {
logger.warn('微信支付失败:', error);
failure(res, error);
}
});
3.4 paidSuccess封装
/**
* 支付成功后,更新订单状态和会员信息
* @param outTradeNo
* @param tradeNo
* @param paidAt
* @returns {Promise<void>}
*/
async function paidSuccess(outTradeNo, tradeNo, paidAt) {
try {
// 开启事务
await sequelize.transaction(async (t) => {
// 查询当前订单(在事务中)
const order = await Order.findOne({
where: { outTradeNo: outTradeNo },
transaction: t,
lock: true, // 增加排它锁
});
// 对于状态已更新的订单,直接返回。防止用户重复请求,重复增加大会员有效期
if (order.status > 0) {
return;
}
// 更新订单状态(在事务中)
await order.update(
{
tradeNo: tradeNo, // 流水号
status: 1, // 订单状态:已支付
paymentMethod: 1 , // 支付方式:微信支付
paidAt: paidAt, // 支付时间
},
{ transaction: t }
);
// 查询订单对应的用户(在事务中)
const user = await User.findByPk(order.userId, {
transaction: t,
lock: true, // 增加排它锁
});
// 将用户组设置为大会员。可防止管理员创建订单,并将用户组修改为大会员
if (user.role === 0) {
user.role = 1;
}
// 使用moment.js,增加大会员有效期
user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date())
.add(order.membershipMonths, 'months')
.toDate();
// 保存用户信息(在事务中)
await user.save({ transaction: t });
});
} catch (error) {
// 将错误抛出,让上层处理
throw error;
}
}
4.前端代码
pages/orders/pay/index.js
async handlePay() {
// 只有登录后,才能支付
const userSignedIn = wx.getStorageSync('userSignedIn')
if (!userSignedIn) {
wx.showToast({
title: '请先登录',
icon: 'error',
})
return
}
// 先创建订单
const orderInfo = await post('/orders', {
membershipId: 1
})
// 获取到订单号
const { outTradeNo } = orderInfo.data.order
// 发起支付
const payInfo = await post(`/wechat/pay`, { outTradeNo })
const { result } = payInfo.data
wx.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
success: (res) => {
wx.showToast({
title: '支付成功',
icon: 'success',
})
},
fail: (res) => {
wx.showToast({
title: '支付失败',
icon: 'error',
})
}
})
}