wechat_pay.md 13 KB

微信支付技术文档

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 微信支付商户平台 申请,需企业资质
AppID 公众号 / 开放平台 App 应用 ID(形如 wxd930ea5d5a228f5f
商户 API 证书 apiclient_cert.pem + apiclient_key.pem(双向 TLS 用)
商户 APIv3 密钥 用于回调密文 AES-256-GCM 解密
回调地址 公网 HTTPS,用于接收支付结果通知

微信支付无正式沙箱环境,开发阶段使用真实商户号的小额测试(如 0.01 元),或使用官方 微信支付接口测试平台(部分能力)。

2.2 证书与密钥

# 商户 API 证书(微信支付商户平台 → 账户中心 → API安全 下载)
# 文件:apiclient_cert.pem(公钥)、apiclient_key.pem(私钥)
# 同时记录:商户号、APIv3密钥(32位)

3. 支付流程

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 安装与初始化

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'), // 商户私钥
});

4.2 统一下单(APP 支付)

/**
 * 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-v3transactions_app 已内部完成二次签名,可直接返回 { appId, timeStamp, nonceStr, package, signType, paySign } 给客户端。

4.3 异步通知回调(验签 + 解密)

微信支付 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 次)。

4.4 主动查询订单

// 客户端回前端后确认,或对账兜底
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 退款(可选)

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 依赖与初始化

# 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());
}

5.2 Android 配置

fluwx 已内置 WXEntryActivity / WXPayEntryActivity(activity-alias 指向 com.jarvan.fluwx.wxapi.FluwxWXEntryActivity),无需手动添加,但需确认:

<!-- android/app/build.gradle 中 applicationId 需与微信开放平台配置一致 -->

5.3 iOS 配置

<!-- 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 -->

5.4 拉起微信支付

// 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 支付
  }
}

5.5 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: '已完成支付',
  );
}

6. 接口定义

6.1 创建微信支付

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"
}

6.2 查询支付状态

GET /v1/payment/{payment_id}

{ "success": true, "note": "支付成功" }

6.3 异步通知

POST /v1/payment/notify/wechatapplication/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"
  }
}

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 未生效