微信小程序虚拟支付全攻略:Egg.js 后端虚拟支付参数生成实战详解
微信小程序虚拟支付是平台内虚拟商品/服务交易的核心能力,官方对支付参数签名、数据格式有严格规范,后端参数生成的准确性直接决定支付流程能否顺利拉起。本文基于 Egg.js 框架 ,手把手拆解虚拟支付后端配置、签名逻辑、参数组装的完整实现,解决开发中签名错误、环境适配、session_key 更新等核心痛点,代码可直接复用至生产环境。
一、前置知识与核心配置说明
在开发前,我们需要先明确微信小程序虚拟支付的核心配置项,这是整个支付流程的基础。所有配置会统一存储,后端通过读取配置实现多环境(沙箱/正式)切换。
1. 虚拟支付核心配置结构
这是后端存储的小程序虚拟支付配置 JSON,包含小程序、支付平台的关键凭证, 切勿泄露 appKey 等敏感信息 :
{
"name": "支付配置名称",
"AppID": "wxa8***********b", // 微信小程序 AppID
"offerId": "14505****93", // 微信支付平台分配的商品 ID
"env": 1, // 环境标识:1=沙箱测试环境 0=正式生产环境
"appKey_dev": "EEm***********57YXaBO", // 沙箱环境密钥
"appKey_prod": "8Mo7***********tLy" // 正式环境密钥
}2. 核心依赖说明
代码基于 Egg.js 框架开发,依赖两个核心模块:
egg.Controller:Egg.js 控制器基类,处理接口请求crypto:Node.js 原生加密模块,用于生成支付签名(无需额外安装)
二、完整代码实现与逐段拆解
我们将代码拆分为 配置获取 、 session_key 更新 、 订单校验 、 签名生成 、 接口返回 五个核心模块,逐段解析逻辑。
完整代码
// 微信小程序 虚拟支付 支付参数生成
/* eslint-disable prefer-const */
'use strict';
const Controller = require('egg').Controller;
const crypto = require('crypto');
module.exports = class extends Controller {
// 1. 获取虚拟支付配置
async getConfig() {
const { AppID } = { ...this.ctx.request.body, ...this.ctx.query };
let virtualPay = await this.ctx.service.option.getOption('virtualPay');
virtualPay = JSON.parse(virtualPay);
if (AppID) return virtualPay.find(v => v.AppID === AppID);
return virtualPay[0];
}
// 2. 生成小程序虚拟支付所需参数
async getPayParams() {
const query = this.ctx.query;
// 读取支付配置信息
const config = await this.getConfig();
// 初始化 session_key,支持通过 code 更新
let sessionKey = this.ctx.auth.session_key;
if (query.code) {
const skres = await this.ctx.service.wxspAuth.sessionKey(query.code);
let noopipr = { id: this.ctx.auth.id };
// 根据来源区分存储不同的 session_key
if (query.source === 1) noopipr.session_key = skres.session_key;
if (query.source === 2) noopipr.session_key2 = skres.session_key;
if (query.source === 3) noopipr.session_key3 = skres.session_key;
// 覆盖当前会话的 session_key
sessionKey = skres.session_key;
}
// 获取订单信息并校验
const order = await this.ctx.service.order.getById(query.order_id);
if (this.ctx.helper.isEmpty(order)) this.ctx.returnError('订单信息错误');
// 计算支付金额:沙箱固定 4 分,正式环境使用实际订单金额(单位:分)
const order_amount = config.env === 1 ? 4 : ((order.price - order.price_offer) * 100).toFixed();
// 1. 构造微信支付要求的签名字段数据
const signParams = {
offerId: config.offerId,
outTradeNo: order.order_no, // 商户内部订单号
productId: order.item_json.code, // 虚拟商品 ID
goodsPrice: Number((order.price * 100).toFixed()), // 商品原价(单位:分)
activitySellingPrice: Number(order_amount), // 实际支付金额(单位:分)
buyQuantity: 1, // 购买数量
currencyType: 'CNY', // 货币类型:固定人民币
attach: '{}', // 附加参数,无特殊需求传空 JSON
env: config.env, // 支付环境
};
const signData = JSON.stringify(signParams);
// 2. 生成 paySig:使用 appKey 加密,核心坑点:&符号无需转义
let appKey = config.env === 1 ? config.appKey_dev : config.appKey_prod;
const paySig = crypto
.createHmac('sha256', appKey)
.update('requestVirtualPayment&' + signData)
.digest('hex');
// 3. 生成 signature:使用用户 session_key 加密
const signature = crypto.createHmac('sha256', sessionKey).update(signData).digest('hex');
// 统一返回格式给小程序前端
this.ctx.body = {
code: 0,
message: '',
success: true,
data: {
mode: 'short_series_goods', // 支付模式:虚拟商品固定值
signData,
paySig,
signature,
},
};
}
};模块 1:支付配置获取方法 getConfig()
该方法负责从数据库/配置中心读取虚拟支付配置,支持多小程序切换:
async getConfig() {
// 合并 GET/POST 请求参数,获取小程序 AppID
const { AppID } = { ...this.ctx.request.body, ...this.ctx.query };
// 从服务层读取存储的虚拟支付配置(JSON 字符串)
let virtualPay = await this.ctx.service.option.getOption('virtualPay');
virtualPay = JSON.parse(virtualPay);
// 根据 AppID 匹配对应配置,无匹配则返回默认第一条
if (AppID) return virtualPay.find(v => v.AppID === AppID);
return virtualPay[0];
}核心作用 :解耦配置与业务逻辑,支持多小程序、多环境配置灵活切换,无需修改代码。
模块 2:session_key 动态更新逻辑
微信小程序虚拟支付要求使用用户的 session_key 生成签名,该字段会过期,因此接口支持通过 code 动态刷新:
// 初始化 session_key
let sessionKey = this.ctx.auth.session_key;
// 前端传入 code 时,刷新 session_key
if (query.code) {
const skres = await this.ctx.service.wxspAuth.sessionKey(query.code);
let noopipr = { id: this.ctx.auth.id };
// 区分来源,存储不同 session_key(适配多场景需求)
if (query.source === 1) noopipr.session_key = skres.session_key;
if (query.source === 2) noopipr.session_key2 = skres.session_key;
if (query.source === 3) noopipr.session_key3 = skres.session_key;
// 更新当前会话的 session_key
sessionKey = skres.session_key;
}关键说明 : session_key 是微信为用户会话生成的密钥,是生成 signature 的必填参数,过期会导致签名验证失败。
模块 3:订单信息校验与金额计算
支付参数必须绑定有效订单,这里做了订单合法性校验和金额单位转换(微信支付单位为 分 ):
// 根据订单 ID 获取订单详情
const order = await this.ctx.service.order.getById(query.order_id);
// 订单为空则抛出错误
if (this.ctx.helper.isEmpty(order)) this.ctx.returnError('订单信息错误');
// 金额规则:沙箱环境固定 4 分(测试专用),正式环境计算实际支付金额
const order_amount = config.env === 1 ? 4 : ((order.price - order.price_offer) * 100).toFixed();开发提示 :沙箱环境不产生真实支付,固定小额金额可快速测试流程;正式环境必须使用真实订单金额。
模块 4:核心支付参数构造 signData
这是微信虚拟支付的 标准数据格式 ,所有字段必须严格按照官方要求传递,不可缺失/修改字段名:
const signParams = {
offerId: config.offerId, // 支付平台分配 ID
outTradeNo: order.order_no, // 商户订单号(唯一)
productId: order.item_json.code, // 虚拟商品编码
goodsPrice: Number((order.price * 100).toFixed()), // 商品原价
activitySellingPrice: Number(order_amount), // 实付金额
buyQuantity: 1, // 购买数量
currencyType: 'CNY', // 固定人民币
attach: '{}', // 附加参数
env: config.env, // 环境标识
};
// 转为 JSON 字符串,用于签名计算
const signData = JSON.stringify(signParams);模块 5:双重签名生成(核心难点)
微信虚拟支付需要生成 两个签名 : paySig (服务端签名)和 signature (用户会话签名),这是开发中最容易出错的环节。
1. 生成 paySig
使用环境对应的 appKey 加密, 官方文档坑点 :文档要求将 & 转义为 & ,实测不转义才正确:
// 自动切换沙箱/正式密钥
let appKey = config.env === 1 ? config.appKey_dev : config.appKey_prod;
const paySig = crypto
.createHmac('sha256', appKey) // 加密算法:sha256
.update('requestVirtualPayment&' + signData) // 固定前缀+签名字符串
.digest('hex'); // 输出 16 进制字符串2. 生成 signature
使用用户的 session_key 加密,无需添加前缀,直接加密 signData :
const signature = crypto.createHmac('sha256', sessionKey).update(signData).digest('hex');模块 6:统一接口返回格式
后端将核心参数返回给小程序前端,前端直接调用微信支付 API 即可拉起支付:
this.ctx.body = {
code: 0, // 业务状态码:0=成功
message: '', // 错误信息
success: true, // 成功标识
data: {
mode: 'short_series_goods', // 虚拟商品固定支付模式
signData, // 签名字符串
paySig, // 服务端签名
signature, // 用户会话签名
},
};三、开发避坑指南(关键总结)
- 签名错误 90%的原因 :
paySig生成时,&符号 不要转义 ,直接使用requestVirtualPayment&signData; - 环境区分 :沙箱(env=1)和正式(env=0)的
appKey、offerId不能混用; - 金额单位 :微信支付统一使用 分 ,必须将元转为整数,避免小数精度问题;
- session_key 有效性 :前端调用支付前,确保
session_key未过期,可通过code主动刷新; - 配置安全 :
appKey、AppID等敏感配置,切勿硬编码在代码中,建议存储在数据库/配置中心。
四、适用场景与扩展
本代码适用于 微信小程序虚拟商品支付 (如会员、道具、虚拟币等),基于 Egg.js 框架,轻量高效,可直接集成到现有项目中。
扩展方向:
- 增加配置校验逻辑,防止密钥为空;
- 添加签名日志,方便线上排查问题;
- 支持多订单、批量支付参数生成;
- 增加接口权限校验,提升安全性。
五、总结
微信小程序虚拟支付的核心难点在于 参数格式规范 和 签名生成逻辑 ,本文通过 Egg.js 实现了标准化的支付参数生成服务,解决了官方文档的坑点和开发中的常见问题。
只要严格按照本文的配置、签名逻辑实现,后端即可稳定输出合法支付参数,小程序前端能顺利拉起支付,大幅降低虚拟支付开发的调试成本。代码已适配生产环境,可直接复用优化!





