微信小程序接入微信支付全流程指南(CloudBase / JSAPI / 真机可用)

适用读者 :已有小程序与云开发基础, 但从未接入过微信支付。(文章末尾有代码示例)
目标 :基于 CloudBase 完成 JSAPI 支付 接入,并在 体验版真机完成支付验证(含回调)。


目录

[1. 前置准备](#1. 前置准备)

[2. 私钥转换与安全管理](#2. 私钥转换与安全管理)

[3. 配置支付回调(HTTP 入口)](#3. 配置支付回调(HTTP 入口))

[4. 云函数](#4. 云函数)

[5. 小程序前端调起支付](#5. 小程序前端调起支付)

[6. 回调](#6. 回调)

[7. 部署与真机验证 Checklist](#7. 部署与真机验证 Checklist)

[8. 常见问题与排查(FAQ)](#8. 常见问题与排查(FAQ))

[9. 后续增强建议](#9. 后续增强建议)

[10. 附录:](#10. 附录:)完整代码清单与目录结构


1. 前置准备

开通商户与能力

  • 小程序开通微信支付,并与商户号 mchid 绑定。

  • 开通 API v3,记录 APIv3 密钥(32 字符)。

下载证书(商户证书)

  • 商户平台 → 账户中心 → API 安全 → 下载 API 证书:得到 apiclient_cert.pem、apiclient_key.pem。

  • 若生成证书时设置了密码,务必记住,后续用于私钥解密。

OpenSSL

  • macOS / Linux 默认自带;Windows 建议使用 Git Bash 或 WSL。

获取证书序列号(serial_no)

复制代码
# 输出大写 HEX(无冒号)即为 serial_no
openssl x509 -in apiclient_cert.pem -noout -serial | cut -d= -f2 | tr '[:lower:]' '[:upper:]' | tr -d ':'

2. 私钥转换与安全管理

微信导出的 apiclient_key.pem 常为 加密 PKCS#1 。Node.js 18 在云函数环境下使用更便利的是 无密码 PKCS#8

复制代码
# 若原始私钥有密码,执行时会提示输入
openssl pkcs8 -topk8 -inform PEM -outform PEM \
  -in apiclient_key.pem \
  -out apiclient_key_plain.pem \
  -nocrypt

环境变量建议(.env 示例)

复制代码
WECHAT_APPID=wx_your_appid
WECHAT_MCHID=your_mchid
WECHAT_API_V3_KEY=32_chars_api_v3_key
WECHAT_SERIAL_NO=YOUR_MCH_CERT_SERIAL
WECHAT_NOTIFY_URL=https://<cloudbase-domain>/pay/wechat/notify

PAY_PRODUCT_DESCRIPTION=...
PAY_PRODUCT_ID=quit-plan-annual

# 二选一:以 Base64 保存内容,或提供云环境内的安全路径
WECHAT_PRIVATE_KEY_BASE64=<可选:apiclient_key_plain.pem 的 Base64>
WECHAT_PRIVATE_KEY_PATH=/secure/path/apiclient_key_plain.pem

务必将 .env 加入 .gitignore,私钥只保存在部署环境。
✅ CloudBase 推荐使用" 环境变量 / 密钥管理服务"保存敏感信息。
❌ 不要把 .pem / 密钥明文提交到仓库。


3. 配置支付回调(HTTP 入口)

  1. 在 cloudfunctions/wechatPayNotify 目录新建云函数(HTTP 触发),先返回占位 JSON:{code:"SUCCESS"}。

  2. CloudBase 控制台 → HTTP 访问服务,启用并添加路由:

    • 路径:/pay/wechat/notify

    • 资源:云函数 wechatPayNotify

  3. 浏览器访问 https://<云开发域名>/pay/wechat/notify,看到 JSON 即通。

说明:真实环境需 验签 + 解密 + 幂等写库,第 6 节会给出占位骨架与待办。


4. 云函数

createCheckout

(JSAPI 下单)

4.1 关键点

  • 由后端云函数完成下单签名,前端仅调用 wx.requestPayment。

  • Authorization 头 需按 WECHATPAY2-SHA256-RSA2048 规范自行构造。

  • 签名串格式(下单请求)

    HTTP_METHOD + "\n" +
    HTTP_PATH_WITH_QUERY + "\n" +
    TIMESTAMP + "\n" +
    NONCE_STR + "\n" +
    REQUEST_BODY + "\n"

    • 例如:POST\n/v3/pay/transactions/jsapi\n1699999999\n随机串\n{"amount":...}\n
  • JSAPI 前端二次签名(paySign)

    message = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + package + "\n"
    paySign = RSA-SHA256(privateKey, message)

4.2 云函数实现(Node.js 18,无外部依赖)

见文末"附录:完整代码"。核心流程:

  1. 校验环境变量(缺失则返回 skipPayment:true,用于体验期防误触)。

  2. 读取私钥(优先 WECHAT_PRIVATE_KEY_BASE64,否则读取 WECHAT_PRIVATE_KEY_PATH)。

  3. 生成 out_trade_no、nonceStr、timestamp。

  4. 构造下单请求体(金额单位:分payer.openid 必传)。

  5. 计算 Authorization,请求 https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi。

  6. 拿到 prepay_id 后,生成前端 timeStamp/nonceStr/package/paySign 返回。


5. 小程序前端调起支付

复制代码
// pages/pay/index.js
Page({
  data:{},
  async onPayTap() {
    try {
      const res = await wx.cloud.callFunction({
        name: 'createCheckout',
        data: { planId: 'quit-plan-annual' }, // 你的业务参数
      })
      const { skipPayment, paymentParams, message } = res.result || {}
      if (skipPayment) {
        wx.showToast({ title: '当前为体验模式,已跳过支付', icon: 'none' })
        return
      }
      if (!paymentParams) {
        throw new Error(message || '获取支付参数失败')
      }
      const payRes = await wx.requestPayment(paymentParams)
      console.log('pay success', payRes)
      wx.showToast({ title: '支付成功' })
    } catch (err) {
      console.error(err)
      wx.showToast({ title: err.message || '支付失败', icon: 'none' })
    }
  }
})

注意:wx.cloud.callFunction 默认会注入 wxContext,后端可以从 context.OPENID 获取用户 openid。


6. 回调

wechatPayNotify:占位骨架与幂等

正式环境必做

  • 读取回调请求头 Wechatpay-Signature / Wechatpay-Timestamp / Wechatpay-Nonce / Wechatpay-Serial,用 微信支付平台证书验签。

  • 解密 resource.ciphertext(AES-256-GCM,密钥为 WECHAT_API_V3_KEY)得到订单资源。

  • 根据 out_trade_no / amount.total / 交易状态做 幂等更新(避免重复通知造成多次发货)。

本文先给一个可运行的占位实现,返回"SUCCESS"告知微信"已接收"。上线前把 // TODO 部分补齐(见附录代码)。


7. 部署与真机验证 Checklist

  1. 部署云函数:createCheckout / wechatPayNotify(微信开发者工具 → 上传并部署)。

  2. 环境变量:在 CloudBase 控制台补齐并确认生效。

  3. 商户平台:配置"支付结果通知 URL"为 https://<cloudbase-domain>/pay/wechat/notify。

  4. 体验版真机:扫码预览 → 进入支付页 → 测试。

  5. 结果观察:支付成功页、云函数日志是否打印到 prepay_id 与回调;回调函数是否触发成功。

  6. 常见报错:见下节 FAQ。


8. 常见问题与排查(FAQ)

现象 / 报错 可能原因 排查与解决
error:1E08010C:DECODER routines::unsupported 私钥仍为加密 PKCS#1或粘贴导致换行错乱 用 openssl pkcs8 -topk8 -nocrypt 转换为 无密码 PKCS#8;检查 BEGIN/END 与换行。
mchid is not defined 构造 Authorization 时变量名与传参不一致 签名函数参数与模板字面量使用同名变量(mchIdvs mchid)。
弹窗提示"测试环境"或无法调起 云函数返回 skipPayment:true 或前端参数为空 检查环境变量、确认已成功拿到 prepay_id 并返回 paymentParams。
403 / 401 Authorization 签名错误 / serial_no 不匹配 重核签名串格式 与时间戳/随机串;确认使用的是商户证书的 serial_no。
openid 为空 前端未带上 wxContext wx.cloud.callFunction 默认自动注入;云函数读取 context.OPENID。
无法安装 wx-server-sdk 云函数网络限制 本文代码不依赖该 SDK;HTTP 调用与签名使用内置 https/crypto。

9. 后续增强建议

  • 完善回调 :平台证书拉取与缓存、验签、解密、幂等写库、业务状态流转。

  • 订单后台:下单/退款/续费/对账工具;导出报表。

  • 通知与运营:成功订单发送服务通知 / 企业微信提醒;失败重试与预警。

  • 安全治理:周期性轮换 API v3 密钥与证书;日志脱敏;最小权限。

  • 灰度与风控:白名单/黑名单、下单频控、设备指纹与风控评分。


10. 附录:

完整代码清单与目录结构

10.1 目录结构

复制代码
miniapp/
├─ cloudfunctions/
│  ├─ createCheckout/
│  │  ├─ index.js
│  │  └─ package.json   # 可空(无第三方依赖)
│  └─ wechatPayNotify/
│     ├─ index.js
│     └─ package.json
├─ miniprogram/
│  └─ pages/pay/index.js
└─ .env (仅本地与云环境;请勿入库)

10.2

cloudfunctions/createCheckout/index.js

复制代码
/* 云函数:createCheckout  --- JSAPI 下单 + 二次签名(RSA) */
const https = require('https')
const crypto = require('crypto')

function readPrivateKey() {
  const b64 = process.env.WECHAT_PRIVATE_KEY_BASE64
  if (b64 && b64.trim()) {
    const pem = Buffer.from(b64, 'base64').toString('utf8')
    return normalizePem(pem)
  }
  const fs = require('fs')
  const p = process.env.WECHAT_PRIVATE_KEY_PATH
  if (!p) throw new Error('Missing WECHAT_PRIVATE_KEY_{BASE64|PATH}')
  return normalizePem(fs.readFileSync(p, 'utf8'))
}

function normalizePem(pem) {
  // 兼容单行 / 多行,确保换行正确
  const body = pem
    .replace(/\r/g, '')
    .replace(/-----(BEGIN|END) PRIVATE KEY-----/g, m => `\n${m}\n`)
    .replace(/[\t ]+/g, ' ')
    .trim()
  // 粗略归一化
  return body.includes('BEGIN PRIVATE KEY') ? body : pem
}

function rsaSign(privateKey, message) {
  const sign = crypto.createSign('RSA-SHA256')
  sign.update(message)
  sign.end()
  return sign.sign(privateKey, 'base64')
}

function randomStr(len = 16) {
  return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').slice(0, len)
}

function requestWechatPay(path, body, auth) {
  const data = JSON.stringify(body)
  const options = {
    hostname: 'api.mch.weixin.qq.com',
    port: 443,
    path,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'Authorization': auth,
      'User-Agent': 'CloudBase-Miniprogram-Checkout',
      'Content-Length': Buffer.byteLength(data)
    }
  }
  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let chunks = ''
      res.on('data', d => (chunks += d))
      res.on('end', () => {
        try {
          const json = JSON.parse(chunks || '{}')
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(json)
          } else {
            reject(new Error(`HTTP ${res.statusCode}: ${chunks}`))
          }
        } catch (e) {
          reject(new Error(`Bad JSON: ${chunks}`))
        }
      })
    })
    req.on('error', reject)
    req.write(data)
    req.end()
  })
}

exports.main = async (event, context) => {
  // 1) 关键环境变量校验
  const {
    WECHAT_APPID: appId,
    WECHAT_MCHID: mchId,
    WECHAT_API_V3_KEY: apiV3Key,         // 回调解密会用到
    WECHAT_SERIAL_NO: serialNo,
    WECHAT_NOTIFY_URL: notifyUrl,
    PAY_PRODUCT_DESCRIPTION: description = '商品描述',
  } = process.env

  // 可根据你的发布策略,体验版/开发环境允许跳过支付
  const allowSkip = !appId || !mchId || !serialNo || !notifyUrl
  if (allowSkip) {
    return { skipPayment: true, message: '支付环境变量未就绪,跳过支付(体验模式)' }
  }

  // 2) 读取 openid(需 wx.cloud 默认 wxContext)
  const openid = context.OPENID || event.OPENID || event?.userInfo?.openId
  if (!openid) {
    return { message: '无法获取 OPENID' }
  }

  // 3) 私钥读取
  const privateKey = readPrivateKey()

  // 4) 业务参数(示例:年费 99 元)
  const totalFen = 9900 // 单位分;根据 event.planId 或后端定价表映射
  const outTradeNo = `QP${Date.now()}${randomStr(6).toUpperCase()}`
  const body = {
    appid: appId,
    mchid: mchId,
    description,
    out_trade_no: outTradeNo,
    notify_url: notifyUrl,
    amount: { total: totalFen, currency: 'CNY' },
    payer: { openid }
  }

  // 5) 构造下单签名(WECHATPAY2-SHA256-RSA2048)
  const timestamp = String(Math.floor(Date.now() / 1000))
  const nonceStr = randomStr(32)
  const method = 'POST'
  const path = '/v3/pay/transactions/jsapi'
  const message = [
    method,
    path,
    timestamp,
    nonceStr,
    JSON.stringify(body)
  ].join('\n') + '\n'
  const signature = rsaSign(privateKey, message)
  const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${serialNo}",signature="${signature}"`

  // 6) 发起下单
  const resp = await requestWechatPay(path, body, authorization)
  const prepayId = resp?.prepay_id
  if (!prepayId) {
    return { message: '未获取到 prepay_id' }
  }

  // 7) 生成前端调起参数(JSAPI 二次签名)
  const timeStamp = String(Math.floor(Date.now() / 1000))
  const nonceStr2 = randomStr(16)
  const pkg = `prepay_id=${prepayId}`
  const payMsg = [appId, timeStamp, nonceStr2, pkg].join('\n') + '\n'
  const paySign = rsaSign(privateKey, payMsg)

  return {
    skipPayment: false,
    order: {
      outTradeNo: outTradeNo,
      total: totalFen
    },
    paymentParams: {
      timeStamp,
      nonceStr: nonceStr2,
      package: pkg,
      signType: 'RSA',
      paySign
    }
  }
}

10.3

cloudfunctions/wechatPayNotify/index.js

(占位,可运行)

复制代码
/* 云函数:wechatPayNotify --- 回调入口(占位版本) */
exports.main = async (event, context) => {
  // CloudBase HTTP 触发时:
  // - event.headers    回调请求头
  // - event.body       原始 JSON 字符串
  // - event.httpMethod HTTP 方法
  try {
    // 1) 读取头部(验签材料)
    const headers = event.headers || {}
    const wechatSig = headers['Wechatpay-Signature'] || headers['wechatpay-signature']
    const wechatTs = headers['Wechatpay-Timestamp'] || headers['wechatpay-timestamp']
    const wechatNonce = headers['Wechatpay-Nonce'] || headers['wechatpay-nonce']
    const wechatSerial = headers['Wechatpay-Serial'] || headers['wechatpay-serial']

    // 2) 解析 body
    const body = typeof event.body === 'string' ? JSON.parse(event.body) : (event.body || {})
    // body.resource.algorithm === 'AEAD_AES_256_GCM'
    // body.resource.ciphertext / nonce / associated_data

    // TODO:使用【微信支付平台证书】公钥进行验签
    // TODO:使用 APIv3 Key (process.env.WECHAT_API_V3_KEY) 对 resource 进行 AES-256-GCM 解密
    // TODO:获取 out_trade_no, total, trade_state 等,做幂等更新

    // 先返回占位成功,表示"已接收",避免微信重试
    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: 'SUCCESS', message: 'OK' })
    }
  } catch (e) {
    console.error('notify error:', e)
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: 'FAIL', message: e.message })
    }
  }
}

10.4

miniprogram/pages/pay/index.js

(示例页面)

复制代码
Page({
  async onPayTap() {
    wx.showLoading({ title: '发起支付...' })
    try {
      const res = await wx.cloud.callFunction({
        name: 'createCheckout',
        data: { planId: 'quit-plan-annual' },
      })
      const { skipPayment, paymentParams, message } = res.result || {}
      if (skipPayment) {
        wx.hideLoading()
        return wx.showToast({ title: '体验模式,未发起支付', icon: 'none' })
      }
      if (!paymentParams) {
        throw new Error(message || '获取支付参数失败')
      }
      await wx.requestPayment(paymentParams)
      wx.hideLoading()
      wx.showToast({ title: '支付成功' })
    } catch (err) {
      wx.hideLoading()
      console.error(err)
      wx.showToast({ title: err.message || '支付失败', icon: 'none' })
    }
  }
})

10.5

package.json

(两处函数均可为空依赖)

复制代码
{
  "name": "cloudfunction",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {}
}

相关推荐
FinClip4 小时前
微信AI小程序“亿元计划”来了!你的APP如何一键接入,抢先变现?
前端·微信小程序·app
开开心心_Every6 小时前
免费进销存管理软件:云端本地双部署
java·游戏·微信·eclipse·pdf·excel·语音识别
菜鸟学习成功之路-李飞8 小时前
免费开源一款作文批改小程序模版,下载即可二开
小程序·开源
说私域8 小时前
电商价格战下的创新破局:定制开发开源AI智能名片S2B2C商城小程序的应用与价值
人工智能·小程序·开源
week_泽8 小时前
小程序云函数全面总结笔记_5
笔记·小程序
说私域9 小时前
融合“开源链动2+1模式AI智能名片S2B2C商城小程序”:同城自媒体赋能商家私域流量增长的新路径
人工智能·小程序·开源
计算机毕设指导69 小时前
基于微信小程序的考研资源共享系统【源码文末联系】
java·spring boot·后端·考研·微信小程序·小程序·maven
week_泽9 小时前
小程序云数据库增加操作_3
数据库·小程序
沉默-_-9 小时前
从小程序前端到Spring后端:新手上路必须理清的核心概念图
java·前端·后端·spring·微信小程序
week_泽10 小时前
百战商城商品数据云函数化改造总结_百战_3
数据库·笔记·微信小程序·小程序