# 微信支付技术文档
## 1. 概述
微信支付(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 支付**(二维码扫码)。
## 2. 申请与配置
### 2.1 需要申请的内容
| 项目 | 说明 |
| --- | --- |
| 微信商户号 mchid | [微信支付商户平台](https://pay.weixin.qq.com) 申请,需企业资质 |
| AppID | 公众号 / 开放平台 App 应用 ID(形如 `wxd930ea5d5a228f5f`) |
| 商户 API 证书 | `apiclient_cert.pem` + `apiclient_key.pem`(双向 TLS 用) |
| 商户 APIv3 密钥 | 用于回调密文 AES-256-GCM 解密 |
| 回调地址 | 公网 HTTPS,用于接收支付结果通知 |
> 微信支付**无正式沙箱环境**,开发阶段使用真实商户号的小额测试(如 0.01 元),或使用官方 [微信支付接口测试平台](https://pay.weixin.qq.com)(部分能力)。
### 2.2 证书与密钥
```bash
# 商户 API 证书(微信支付商户平台 → 账户中心 → API安全 下载)
# 文件:apiclient_cert.pem(公钥)、apiclient_key.pem(私钥)
# 同时记录:商户号、APIv3密钥(32位)
```
## 3. 支付流程
```mermaid
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} 查询最终状态
```
## 4. 后端集成(Node.js/Express)
### 4.1 安装与初始化
```bash
npm install wechatpay-node-v3
```
```javascript
// 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'), // 商户私钥
});
```
### 4.2 统一下单(APP 支付)
```javascript
/**
* 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 }` 给客户端。
### 4.3 异步通知回调(验签 + 解密)
微信支付 API v3 的通知流程与支付宝不同:**先验证通知签名头,再用 APIv3 密钥 AES-256-GCM 解密 resource 字段**。
```javascript
// 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 次)。
### 4.4 主动查询订单
```javascript
// 客户端回前端后确认,或对账兜底
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;
}
```
### 4.5 退款(可选)
```javascript
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;
}
```
## 5. 前端集成(Flutter)
本项目使用 **fluwx** 插件,已在 `lib/service/pay_service.dart` 中实现,说明如下。
### 5.1 依赖与初始化
```yaml
# pubspec.yaml
dependencies:
fluwx: ^5.3.1
```
```dart
// 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());
}
```
### 5.2 Android 配置
fluwx 已内置 `WXEntryActivity` / `WXPayEntryActivity`(activity-alias 指向 `com.jarvan.fluwx.wxapi.FluwxWXEntryActivity`),无需手动添加,但需确认:
```xml
```
### 5.3 iOS 配置
```xml
CFBundleURLTypes
CFBundleURLName
weixin
CFBundleURLSchemes
wx{AppID}
UniversalLinks
```
### 5.4 拉起微信支付
```dart
// lib/service/pay_service.dart(现有实现节选)
import 'package:fluwx/fluwx.dart' as fluwx;
/// APP 支付:拉起微信
Future 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 支付
}
}
```
### 5.5 Native 支付(扫码)
当运行在 Web / 桌面端,或需要 PC 场景时,后端调用 Native 接口返回 `code_url`,前端渲染二维码:
```dart
// 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: '已完成支付',
);
}
```
## 6. 接口定义
### 6.1 创建微信支付
`POST /v1/payment/wechatpay/`
请求:
```json
{ "product_id": "p001", "source": "app" }
```
响应(APP 支付,对应模型 `WechatPaymentCreatedResponse`):
```json
{
"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`:
```json
{
"payment_id": "pay_8f3a2b",
"sandbox": false,
"code_url": "weixin://wxpay/bizpayurl?pr=9xFPmlUzz"
}
```
### 6.2 查询支付状态
`GET /v1/payment/{payment_id}`
```json
{ "success": true, "note": "支付成功" }
```
### 6.3 异步通知
`POST /v1/payment/notify/wechat`(`application/json`,需保留原始 body)
请求头:`Wechatpay-Signature` / `Wechatpay-Nonce` / `Wechatpay-Timestamp` / `Wechatpay-Serial`
```json
{
"id": "EV-...",
"event_type": "TRANSACTION.SUCCESS",
"resource": {
"ciphertext": "base64密文",
"nonce": "...",
"associated_data": "transaction",
"algorithm": "AEAD_AES_256_GCM"
}
}
```
## 7. 安全注意事项
1. **APIv3 密钥与商户私钥只存后端**,回调解密依赖 APIv3 密钥。
2. **回调必须验签**:校验 `Wechatpay-Signature`(平台证书)后再解密。
3. **解密后校验金额与订单**:`amount.total`(分)必须与本地一致。
4. **幂等**:同 `out_trade_no` 多次通知只发货一次(订单状态机)。
5. **应答约定**:成功 `200 + {"code":"SUCCESS"}`,否则微信会重试通知。
6. **客户端结果仅供参考**:`fluwx` 回调 `errCode` 可能不可靠,以服务端通知+查询为准。
## 8. 沙箱与测试
微信支付**没有独立沙箱**。开发建议:
| 方式 | 说明 |
| --- | --- |
| 小额真实测试 | 用 0.01 元真实商户号测试,测试后原路退款 |
| 自测回调 | 用 Postman 构造签名调用本地回调,或使用内网穿透(如 frp)暴露回调地址 |
| 对账 | 生产上线前务必做"回调 + 主动查询 + 商户平台对账单"三方对账 |
## 9. 常见问题
| 问题 | 原因/解决 |
| --- | --- |
| `INVALID_REQUEST` | 参数缺失/格式错误;金额单位应为**分**(整数) |
| 验签失败 | 平台证书过期/中间层修改了 body;务必用原始 body 验签 |
| 解密失败 | APIv3 密钥错误;确认 `ciphertext` 为 Base64 原文 |
| 拉起微信后返回 -2 | 用户取消支付(正常);回调 `-1` 为支付失败 |
| 收不到回调 | 回调地址需公网 HTTPS;响应需符合成功约定;本地用内网穿透 |
| App 调起失败 | AppID 与开放平台不一致、包名/signature 未配置、iOS Universal Link 未生效 |