微信小程序虚拟支付全攻略:Egg.js 后端虚拟支付参数生成实战详解

2026-05-04 55 浏览 0 评论

微信小程序虚拟支付是平台内虚拟商品/服务交易的核心能力,官方对支付参数签名、数据格式有严格规范,后端参数生成的准确性直接决定支付流程能否顺利拉起。本文基于 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,          // 用户会话签名
  },
};

三、开发避坑指南(关键总结)

  1. 签名错误 90%的原因paySig 生成时, & 符号 不要转义 ,直接使用 requestVirtualPayment&signData
  2. 环境区分 :沙箱(env=1)和正式(env=0)的 appKeyofferId 不能混用;
  3. 金额单位 :微信支付统一使用 ,必须将元转为整数,避免小数精度问题;
  4. session_key 有效性 :前端调用支付前,确保 session_key 未过期,可通过 code 主动刷新;
  5. 配置安全appKeyAppID 等敏感配置,切勿硬编码在代码中,建议存储在数据库/配置中心。

四、适用场景与扩展

本代码适用于 微信小程序虚拟商品支付 (如会员、道具、虚拟币等),基于 Egg.js 框架,轻量高效,可直接集成到现有项目中。

扩展方向:

  1. 增加配置校验逻辑,防止密钥为空;
  2. 添加签名日志,方便线上排查问题;
  3. 支持多订单、批量支付参数生成;
  4. 增加接口权限校验,提升安全性。

五、总结

微信小程序虚拟支付的核心难点在于 参数格式规范签名生成逻辑 ,本文通过 Egg.js 实现了标准化的支付参数生成服务,解决了官方文档的坑点和开发中的常见问题。

只要严格按照本文的配置、签名逻辑实现,后端即可稳定输出合法支付参数,小程序前端能顺利拉起支付,大幅降低虚拟支付开发的调试成本。代码已适配生产环境,可直接复用优化!


发布评论

发布评论前请先 登录
取消
0 评论
点赞
收藏

评论列表 0

暂无评论