微信支付(WeChat Pay)是面向中国大陆用户的国民级支付方式,使用 API v3 协议。本文档覆盖四种主流场景:
| 场景 | 接口 | 适用端 | 说明 |
|---|---|---|---|
| APP 支付 | /v3/pay/transactions/app |
iOS / Android App | 拉起微信 App 完成支付(本项目 Flutter 使用) |
| JSAPI 支付 | /v3/pay/transactions/jsapi |
公众号 / 微信内网页 | 需 openid,在微信内收银 |
| Native 支付 | /v3/pay/transactions/native |
PC Web / 桌面 | 返回 code_url,生成二维码,微信扫码支付(本项目已支持) |
| H5 支付 | /v3/pay/transactions/h5 |
移动端浏览器(非微信内) | 跳转微信收银台 |
本项目(Flutter)使用 APP 支付(fluwx.payWithWeChat)与 Native 支付(二维码扫码)。
| 项目 | 说明 |
|---|---|
| 微信商户号 mchid | 微信支付商户平台 申请,需企业资质 |
| AppID | 公众号 / 开放平台 App 应用 ID(形如 wxd930ea5d5a228f5f) |
| 商户 API 证书 | apiclient_cert.pem + apiclient_key.pem(双向 TLS 用) |
| 商户 APIv3 密钥 | 用于回调密文 AES-256-GCM 解密 |
| 回调地址 | 公网 HTTPS,用于接收支付结果通知 |
微信支付无正式沙箱环境,开发阶段使用真实商户号的小额测试(如 0.01 元),或使用官方 微信支付接口测试平台(部分能力)。
# 商户 API 证书(微信支付商户平台 → 账户中心 → API安全 下载)
# 文件:apiclient_cert.pem(公钥)、apiclient_key.pem(私钥)
# 同时记录:商户号、APIv3密钥(32位)
sequenceDiagram
participant App as Flutter App
participant Backend as 业务后端
participant WeChat as 微信支付服务端
App->>Backend: POST /v1/payment/wechatpay/ {product_id}
Backend->>Backend: 创建订单、计算金额(单位:分)
Backend->>WeChat: /v3/pay/transactions/app(API证书签名)
WeChat-->>Backend: prepay_id
Backend->>Backend: 用商户私钥二次签名(生成调起参数)
Backend-->>App: { appId, partnerId, prepayId, sign, ... }
App->>WeChat: fluwx.payWithWeChat(调起微信App)
WeChat->>Backend: 异步通知(加密, 需APIv3密钥解密)
Backend->>Backend: 验签 + 解密 + 幂等更新订单 + 发货
App->>Backend: GET /v1/payment/{payment_id} 查询最终状态
npm install wechatpay-node-v3
// src/services/wechat_service.js
const WxPay = require('wechatpay-node-v3');
const fs = require('fs');
// 单例初始化
const pay = new WxPay({
appid: process.env.WX_APPID, // App/公众号 AppID
mchid: process.env.WX_MCHID, // 商户号
publicKey: fs.readFileSync('./apiclient_cert.pem'), // 商户证书
privateKey: fs.readFileSync('./apiclient_key.pem'), // 商户私钥
});
/**
* APP 支付下单,返回客户端调起参数
* @param {string} outTradeNo 商户订单号
* @param {number} amount 金额,单位:分
* @param {string} description 商品描述
*/
async function createAppPayment({ outTradeNo, amount, description }) {
const result = await pay.transactions_app({
description,
out_trade_no: outTradeNo,
notify_url: process.env.WX_NOTIFY_URL, // 回调地址
amount: { total: amount }, // 单位:分
scene_info: { payer_client_ip: '客户端IP' },
});
// result: { status: 200, prepay_id: 'wx...' }
return result.prepay_id;
}
注意:
wechatpay-node-v3的transactions_app已内部完成二次签名,可直接返回{ appId, timeStamp, nonceStr, package, signType, paySign }给客户端。
微信支付 API v3 的通知流程与支付宝不同:先验证通知签名头,再用 APIv3 密钥 AES-256-GCM 解密 resource 字段。
// src/controllers/payment_controller.js
const express = require('express');
const router = express.Router();
/**
* 微信支付异步通知回调
* 请求头:Wechatpay-Signature, Wechatpay-Nonce, Wechatpay-Timestamp, Wechatpay-Serial
* body:{ id, event_type, resource: { ciphertext, nonce, associated_data, ... } }
*/
router.post('/v1/payment/notify/wechat',
express.raw({ type: 'application/json' }), // 必须保留原始 body 验签
async (req, res) => {
try {
// 1. 验签(工具库会校验签名头并返回是否合法)
const { event_type, resource } = JSON.parse(req.body);
// 2. 解密 resource(APIv3 密钥)
const plain = pay.decipher_gcm(
resource.ciphertext,
resource.associated_data,
resource.nonce,
process.env.WX_API_V3_KEY,
);
// plain: { out_trade_no, transaction_id, trade_state: 'SUCCESS', amount: { total }, ... }
// 3. 幂等处理
if (event_type === 'TRANSACTION.SUCCESS'
&& plain.trade_state === 'SUCCESS') {
const order = await orderRepo.findById(plain.out_trade_no);
// 校验金额
if (order && Number(order.amount) === plain.amount.total
&& order.status === 'PENDING') {
await orderService.markPaid(plain.out_trade_no, {
channel: 'wechat',
channelTradeNo: plain.transaction_id,
});
await fulfillmentService.deliver(order.id); // 幂等发货
}
}
// 4. 成功应答(微信要求 code 200 + { code: 'SUCCESS' })
return res.status(200).json({ code: 'SUCCESS', message: '成功' });
} catch (e) {
// 失败应答 code: 'FAIL',微信会重试
return res.status(500).json({ code: 'FAIL', message: e.message });
}
});
应答约定:成功返回 HTTP 200 +
{"code":"SUCCESS","message":"成功"};失败返回非 200 或code:FAIL,微信按策略重试(最多 24 次)。
// 客户端回前端后确认,或对账兜底
async function queryOrder(outTradeNo) {
// 参数顺序: out_trade_no, transaction_id, 私钥(...) 视库版本而定
const result = await pay.queryByOutTradeNo(outTradeNo);
// { trade_state: 'SUCCESS' | 'NOTPAY' | ..., transaction_id, amount: { total } }
return result;
}
async function refund({ outTradeNo, refundNo, refundAmount, totalAmount }) {
const result = await pay.refund({
out_trade_no: outTradeNo,
out_refund_no: refundNo,
amount: { refund: refundAmount, total: totalAmount, currency: 'CNY' },
});
return result;
}
本项目使用 fluwx 插件,已在 lib/service/pay_service.dart 中实现,说明如下。
# pubspec.yaml
dependencies:
fluwx: ^5.3.1
// lib/main.dart
import 'package:fluwx/fluwx.dart' as fluwx;
void main() {
// 尽早注册(AppID + iOS Universal Link)
fluwx.registerWxApi(
appId: 'wxd930ea5d5a228f5f',
universalLink: 'https://your.domain.com/wechat/link/', // iOS 必填
doOnAndroid: true,
doOnIOS: true,
);
runApp(const MyApp());
}
fluwx 已内置 WXEntryActivity / WXPayEntryActivity(activity-alias 指向 com.jarvan.fluwx.wxapi.FluwxWXEntryActivity),无需手动添加,但需确认:
<!-- android/app/build.gradle 中 applicationId 需与微信开放平台配置一致 -->
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>weixin</string>
<key>CFBundleURLSchemes</key>
<array><string>wx{AppID}</string></array>
</dict>
</array>
<key>UniversalLinks</key> <!-- 如使用 Universal Link 需配置 Associated Domains -->
// lib/service/pay_service.dart(现有实现节选)
import 'package:fluwx/fluwx.dart' as fluwx;
/// APP 支付:拉起微信
Future<void> wechatPay(PaymentProduct product) async {
final isInstalled = await fluwx.isWeChatAppInstalled();
if (!isInstalled) {
showErrorMessage('未安装微信');
return;
}
// 1. 后端统一下单,返回调起参数
final created = await APIServer().createWechatPayment(productId: product.id);
paymentId = created.paymentId;
if (PlatformTool.isAndroid() || PlatformTool.isIOS()) {
// 2. 拉起微信支付
await fluwx.payWithWeChat(
appId: created.appId!,
partnerId: created.partnerId!,
prepayId: created.prepayId!,
packageValue: created.package!,
nonceStr: created.noncestr!,
timeStamp: int.parse(created.timestamp!),
sign: created.sign!,
);
// 3. 监听微信回调结果(建议同时以后端查询为准)
fluwx.responseFromPayment.listen((resp) {
// resp.errCode: 0 成功 | -1 失败 | -2 用户取消
if (resp.errCode == 0) {
_queryFinalStatus(paymentId); // 再次查询后端确认
}
});
} else {
// 非移动端(Web/桌面):使用 Native 扫码
_showQrCodeDialog(created.codeUrl!); // 见下方 Native 支付
}
}
当运行在 Web / 桌面端,或需要 PC 场景时,后端调用 Native 接口返回 code_url,前端渲染二维码:
// lib/service/pay_service.dart(现有实现节选)
void _showQrCodeDialog(String codeUrl) {
openDialog(
context,
builder: (context) => Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: QrImageView(
data: codeUrl,
version: QrVersions.auto,
size: 200,
),
),
const Text('请使用微信扫码支付'),
],
),
onSubmit: () {
// 用户扫码完成后,轮询后端查询支付状态
APIServer().queryPaymentStatus(paymentId).then((resp) {
if (resp.success) {
showSuccessMessage('支付成功');
} else {
// 延迟 5s 再次查询
Future.delayed(const Duration(seconds: 5), _queryFinalStatus);
}
});
return true;
},
confirmText: '已完成支付',
);
}
POST /v1/payment/wechatpay/
请求:
{ "product_id": "p001", "source": "app" }
响应(APP 支付,对应模型 WechatPaymentCreatedResponse):
{
"payment_id": "pay_8f3a2b",
"sandbox": false,
"app_id": "wxd930ea5d5a228f5f",
"partner_id": "1900000109",
"prepay_id": "wx0615423208772665709493edbb4b330000",
"package": "Sign=WXPay",
"noncestr": "y8aw9vrmx8c",
"timestamp": "1609918952",
"sign": "JnFXsT4VNzlc..."
}
响应(Native 支付)额外返回 code_url:
{
"payment_id": "pay_8f3a2b",
"sandbox": false,
"code_url": "weixin://wxpay/bizpayurl?pr=9xFPmlUzz"
}
GET /v1/payment/{payment_id}
{ "success": true, "note": "支付成功" }
POST /v1/payment/notify/wechat(application/json,需保留原始 body)
请求头:Wechatpay-Signature / Wechatpay-Nonce / Wechatpay-Timestamp / Wechatpay-Serial
{
"id": "EV-...",
"event_type": "TRANSACTION.SUCCESS",
"resource": {
"ciphertext": "base64密文",
"nonce": "...",
"associated_data": "transaction",
"algorithm": "AEAD_AES_256_GCM"
}
}
Wechatpay-Signature(平台证书)后再解密。amount.total(分)必须与本地一致。out_trade_no 多次通知只发货一次(订单状态机)。200 + {"code":"SUCCESS"},否则微信会重试通知。fluwx 回调 errCode 可能不可靠,以服务端通知+查询为准。微信支付没有独立沙箱。开发建议:
| 方式 | 说明 |
|---|---|
| 小额真实测试 | 用 0.01 元真实商户号测试,测试后原路退款 |
| 自测回调 | 用 Postman 构造签名调用本地回调,或使用内网穿透(如 frp)暴露回调地址 |
| 对账 | 生产上线前务必做"回调 + 主动查询 + 商户平台对账单"三方对账 |
| 问题 | 原因/解决 |
|---|---|
INVALID_REQUEST |
参数缺失/格式错误;金额单位应为分(整数) |
| 验签失败 | 平台证书过期/中间层修改了 body;务必用原始 body 验签 |
| 解密失败 | APIv3 密钥错误;确认 ciphertext 为 Base64 原文 |
| 拉起微信后返回 -2 | 用户取消支付(正常);回调 -1 为支付失败 |
| 收不到回调 | 回调地址需公网 HTTPS;响应需符合成功约定;本地用内网穿透 |
| App 调起失败 | AppID 与开放平台不一致、包名/signature 未配置、iOS Universal Link 未生效 |