# 微信支付技术文档 ## 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 未生效 |