📋 目录

前言

本教程详细介绍如何在全栈应用中集成微信支付 Native 支付方式,实现会员充值、课程购买等付费功能。

技术栈

  • 前端: React + TypeScript
  • 后端: Edge Functions + Hono
  • 支付: 微信支付 Native (扫码支付)
  • 数据库: PostgreSQL

业务场景

  • 会员充值(月度/季度/年度会员)
  • 荣耀合伙人购买
  • 其他增值服务

一、微信支付申请流程

1.1 注册微信支付商户号

  1. 访问 微信支付商户平台
  2. 点击"立即注册" → 选择"企业商户"
  3. 填写企业基本信息:
    • 营业执照信息
    • 法人身份证信息
    • 对公账户信息
    • 超级管理员信息

1.2 等待审核

  • 审核周期:1-7 个工作日
  • 审核通过后会收到开通通知

1.3 完成账户验证

  • 微信会向对公账户打款(0.01-0.99元)
  • 登录商户平台输入打款金额完成验证

1.4 申请产品功能

  1. 登录商户平台
  2. 进入"产品中心" → "我的产品"
  3. 申请开通 Native 支付(扫码支付)

二、获取必要的配置参数

2.1 AppID(应用 ID)

获取路径:微信公众平台 → 开发 → 基本配置

示例:wx1234567890abcdef

2.2 商户号(MCH_ID)

获取路径:微信支付商户平台 → 账户中心 → 商户信息

示例:1234567890

2.3 API 密钥(API Key)

设置路径

  1. 微信支付商户平台 → 账户中心 → API安全
  2. 设置 APIv3 密钥(32位字符串)
  3. 重要:密钥设置后无法查看,请妥善保存
示例:abcdefghijklmnopqrstuvwxyz123456

2.4 API 证书

下载路径:微信支付商户平台 → 账户中心 → API安全 → 申请API证书

需要下载的文件:

  • apiclient_cert.pem - 证书文件
  • apiclient_key.pem - 私钥文件
  • apiclient_cert.p12 - PKCS12 格式证书

证书序列号获取

# 使用 OpenSSL 查看证书序列号
openssl x509 -in apiclient_cert.pem -noout -serial

三、环境变量配置

3.1 环境变量设置

在项目中设置以下环境变量:

变量名说明示例
WECHAT_APP_ID微信 AppIDwx1234567890abcdef
WECHAT_MCH_ID微信商户号1234567890
WECHAT_APP_SECRET微信 AppSecretabc123def456...
WECHAT_PRIVATE_KEYAPI 私钥内容-----BEGIN PRIVATE KEY-----...
WECHAT_CERT_SERIALAPI 证书序列号5F3E...

3.2 设置步骤

  1. 登录 Dashboard
  2. 选择您的项目
  3. 进入 Project Settings → Edge Functions → Secrets
  4. 点击 Add new secret,逐个添加上述环境变量

3.3 私钥格式注意事项

私钥需要保持完整的 PEM 格式,包括头尾标记:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
(省略中间内容)
...xyz
-----END PRIVATE KEY-----

四、后端服务器实现

4.1 项目结构

//functions/server/
├── index.tsx              # 主服务器入口
├── wechat_pay.tsx         # 微信支付核心逻辑
└── kv_store.tsx           # 数据库操作工具

4.2 核心代码实现

4.2.1 微信支付工具类 (wechat_pay.tsx)
import * as crypto from "node:crypto";

export class WechatPay {
  private appId: string;
  private mchId: string;
  private privateKey: crypto.KeyObject;
  private certSerial: string;

  constructor() {
    this.appId = Deno.env.get("WECHAT_APP_ID")!;
    this.mchId = Deno.env.get("WECHAT_MCH_ID")!;
    this.certSerial = Deno.env.get("WECHAT_CERT_SERIAL")!;

    const privateKeyPem = Deno.env.get("WECHAT_PRIVATE_KEY")!;
    this.privateKey = crypto.createPrivateKey(privateKeyPem);
  }

  /**
   * 生成签名
   */
  private sign(message: string): string {
    const sign = crypto.createSign("RSA-SHA256");
    sign.update(message);
    return sign.sign(this.privateKey, "base64");
  }

  /**
   * 构建 Authorization 头
   */
  private buildAuthorization(
    method: string,
    url: string,
    body: string
  ): string {
    const timestamp = Math.floor(Date.now() / 1000);
    const nonce = crypto.randomBytes(16).toString("hex");
    
    const signatureStr = [
      method,
      url,
      timestamp,
      nonce,
      body,
    ].join("\n") + "\n";

    const signature = this.sign(signatureStr);

    return `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.certSerial}"`;
  }

  /**
   * 创建 Native 支付订单
   */
  async createNativeOrder(params: {
    outTradeNo: string;
    description: string;
    amount: number;
    notifyUrl: string;
  }) {
    const url = "/v3/pay/transactions/native";
    const fullUrl = `https://api.mch.weixin.qq.com${url}`;

    const body = {
      appid: this.appId,
      mchid: this.mchId,
      description: params.description,
      out_trade_no: params.outTradeNo,
      notify_url: params.notifyUrl,
      amount: {
        total: params.amount,
        currency: "CNY",
      },
    };

    const bodyStr = JSON.stringify(body);
    const authorization = this.buildAuthorization("POST", url, bodyStr);

    const response = await fetch(fullUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": authorization,
        "User-Agent": "YourApp/1.0",
      },
      body: bodyStr,
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`微信支付API错误: ${response.status} ${errorText}`);
    }

    return await response.json();
  }

  /**
   * 查询订单状态
   */
  async queryOrder(outTradeNo: string) {
    const url = `/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.mchId}`;
    const fullUrl = `https://api.mch.weixin.qq.com${url}`;

    const authorization = this.buildAuthorization("GET", url, "");

    const response = await fetch(fullUrl, {
      method: "GET",
      headers: {
        "Accept": "application/json",
        "Authorization": authorization,
        "User-Agent": "YourApp/1.0",
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`查询订单失败: ${response.status} ${errorText}`);
    }

    return await response.json();
  }

  /**
   * 验证回调签名
   */
  verifyNotification(
    timestamp: string,
    nonce: string,
    body: string,
    signature: string,
    serial: string
  ): boolean {
    const message = `${timestamp}\n${nonce}\n${body}\n`;
    
    // 这里简化处理,实际应该使用平台证书验证
    // 完整实现需要下载并缓存微信支付平台证书
    return true;
  }

  /**
   * 解密回调数据
   */
  decryptNotification(
    ciphertext: string,
    nonce: string,
    associatedData: string
  ): string {
    const apiKey = Deno.env.get("WECHAT_APP_SECRET")!;
    
    const decipher = crypto.createDecipheriv(
      "aes-256-gcm",
      apiKey,
      nonce
    );
    
    decipher.setAuthTag(Buffer.from(ciphertext.slice(-16), "base64"));
    decipher.setAAD(Buffer.from(associatedData));

    let decrypted = decipher.update(
      ciphertext.slice(0, -16),
      "base64",
      "utf8"
    );
    decrypted += decipher.final("utf8");

    return decrypted;
  }
}
4.2.2 支付路由 (index.tsx)
import { Hono } from "npm:hono";
import { cors } from "npm:hono/cors";
import { logger } from "npm:hono/logger";
import { WechatPay } from "./wechat_pay.tsx";
import * as kv from "./kv_store.tsx";

const app = new Hono();

// 中间件
app.use("*", cors());
app.use("*", logger(console.log));

const wechatPay = new WechatPay();

/**
 * 创建支付订单
 */
app.post("/make-server-3174ebb5/wechat/create-order", async (c) => {
  try {
    const { productType, amount, description, userId } = await c.req.json();

    // 生成唯一订单号
    const outTradeNo = `ORDER_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;

    // 创建微信支付订单
    const result = await wechatPay.createNativeOrder({
      outTradeNo,
      description,
      amount: Math.floor(amount * 100), // 转为分
      notifyUrl: `https://${Deno.env.get("_URL")}/functions/v1/make-server-3174ebb5/wechat/notify`,
    });

    // 保存订单信息到数据库
    await kv.set(`wechat_order:${outTradeNo}`, {
      outTradeNo,
      productType,
      amount,
      description,
      userId,
      codeUrl: result.code_url,
      status: "pending",
      createdAt: new Date().toISOString(),
    });

    return c.json({
      success: true,
      outTradeNo,
      codeUrl: result.code_url,
    });
  } catch (error: any) {
    console.error("创建订单失败:", error);
    return c.json({
      success: false,
      error: error.message,
    }, 500);
  }
});

/**
 * 查询订单状态
 */
app.get("/make-server-3174ebb5/wechat/query-order/:outTradeNo", async (c) => {
  try {
    const { outTradeNo } = c.req.param();

    // 先查本地数据库
    const localOrder = await kv.get(`wechat_order:${outTradeNo}`);
    
    if (localOrder?.status === "paid") {
      return c.json({
        success: true,
        status: "paid",
        order: localOrder,
      });
    }

    // 查询微信支付
    const result = await wechatPay.queryOrder(outTradeNo);

    if (result.trade_state === "SUCCESS") {
      // 更新本地订单状态
      await kv.set(`wechat_order:${outTradeNo}`, {
        ...localOrder,
        status: "paid",
        paidAt: new Date().toISOString(),
        transactionId: result.transaction_id,
      });

      // 处理业务逻辑(开通会员等)
      await handleOrderSuccess(localOrder);
    }

    return c.json({
      success: true,
      status: result.trade_state === "SUCCESS" ? "paid" : "pending",
      order: result,
    });
  } catch (error: any) {
    console.error("查询订单失败:", error);
    return c.json({
      success: false,
      error: error.message,
    }, 500);
  }
});

/**
 * 微信支付回调
 */
app.post("/make-server-3174ebb5/wechat/notify", async (c) => {
  try {
    const body = await c.req.text();
    const data = JSON.parse(body);

    console.log("收到微信支付回调:", data);

    // 验证签名
    const { timestamp, nonce, signature, serial } = c.req.header();
    const isValid = wechatPay.verifyNotification(
      timestamp!,
      nonce!,
      body,
      signature!,
      serial!
    );

    if (!isValid) {
      return c.json({ code: "FAIL", message: "签名验证失败" });
    }

    // 解密数据
    const { resource } = data;
    const decrypted = wechatPay.decryptNotification(
      resource.ciphertext,
      resource.nonce,
      resource.associated_data
    );

    const orderData = JSON.parse(decrypted);
    const outTradeNo = orderData.out_trade_no;

    // 更新订单状态
    const order = await kv.get(`wechat_order:${outTradeNo}`);
    await kv.set(`wechat_order:${outTradeNo}`, {
      ...order,
      status: "paid",
      paidAt: new Date().toISOString(),
      transactionId: orderData.transaction_id,
    });

    // 处理业务逻辑
    await handleOrderSuccess(order);

    return c.json({ code: "SUCCESS", message: "成功" });
  } catch (error: any) {
    console.error("处理回调失败:", error);
    return c.json({ code: "FAIL", message: error.message });
  }
});

/**
 * 处理订单成功后的业务逻辑
 */
async function handleOrderSuccess(order: any) {
  const { userId, productType } = order;

  if (productType === "monthly_member") {
    // 开通月度会员
    const expiresAt = new Date();
    expiresAt.setMonth(expiresAt.getMonth() + 1);
    
    await kv.set(`user_member:${userId}`, {
      type: "monthly",
      startedAt: new Date().toISOString(),
      expiresAt: expiresAt.toISOString(),
    });
  } else if (productType === "partner") {
    // 开通荣耀合伙人
    await kv.set(`user_partner:${userId}`, {
      status: "active",
      joinedAt: new Date().toISOString(),
    });
  }

  console.log(`✅ 业务处理完成: ${productType} for user ${userId}`);
}

Deno.serve(app.fetch);

五、前端调用流程

5.1 创建支付组件

import { useState } from 'react';
import QRCode from 'qrcode';

export function PaymentModal({ productType, amount, description, userId, onClose }) {
  const [qrCodeUrl, setQrCodeUrl] = useState('');
  const [status, setStatus] = useState<'creating' | 'pending' | 'paid'>('creating');
  const [outTradeNo, setOutTradeNo] = useState('');

  // 创建订单
  const createOrder = async () => {
    try {
      const response = await fetch(
        `https://your-project.co/functions/v1/make-server-3174ebb5/wechat/create-order`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${publicAnonKey}`,
          },
          body: JSON.stringify({
            productType,
            amount,
            description,
            userId,
          }),
        }
      );

      const data = await response.json();

      if (data.success) {
        setOutTradeNo(data.outTradeNo);
        
        // 生成二维码
        const qr = await QRCode.toDataURL(data.codeUrl);
        setQrCodeUrl(qr);
        setStatus('pending');

        // 开始轮询订单状态
        startPolling(data.outTradeNo);
      }
    } catch (error) {
      console.error('创建订单失败:', error);
    }
  };

  // 轮询订单状态
  const startPolling = (orderNo: string) => {
    const interval = setInterval(async () => {
      try {
        const response = await fetch(
          `https://your-project.co/functions/v1/make-server-3174ebb5/wechat/query-order/${orderNo}`,
          {
            headers: {
              'Authorization': `Bearer ${publicAnonKey}`,
            },
          }
        );

        const data = await response.json();

        if (data.status === 'paid') {
          clearInterval(interval);
          setStatus('paid');
          
          // 3秒后关闭弹窗
          setTimeout(() => {
            onClose(true); // 支付成功
          }, 3000);
        }
      } catch (error) {
        console.error('查询订单失败:', error);
      }
    }, 2000); // 每2秒查询一次

    // 5分钟后停止轮询
    setTimeout(() => clearInterval(interval), 5 * 60 * 1000);
  };

  useEffect(() => {
    createOrder();
  }, []);

  return (
    

{status === 'creating' &&

正在创建订单...

} {status === 'pending' && (

请使用微信扫码支付

金额: ¥{amount}

等待支付中...

)} {status === 'paid' && (

✅ 支付成功!

会员已开通

)}

); }

5.2 使用示例

import { PaymentModal } from './PaymentModal';

function MembershipPage() {
  const [showPayment, setShowPayment] = useState(false);

  const handleBuy = () => {
    setShowPayment(true);
  };

  return (
    

购买月度会员 - ¥29.9 {showPayment && ( { setShowPayment(false); if (success) { // 刷新会员状态 } }} /> )}

); }

六、测试与验证

6.1 开发环境测试

  1. 配置沙箱环境
    • 微信支付提供沙箱环境用于测试
    • 访问:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=23_1
  2. 使用测试账号
    • 下载微信支付测试工具
    • 使用测试商户号和密钥

6.2 测试流程

# 1. 启动本地服务
functions serve

# 2. 创建订单
curl -X POST http://localhost:54321/functions/v1/make-server-3174ebb5/wechat/create-order \
  -H "Content-Type: application/json" \
  -d '{
    "productType": "monthly_member",
    "amount": 29.9,
    "description": "测试订单",
    "userId": "test_user"
  }'

# 3. 查询订单
curl http://localhost:54321/functions/v1/make-server-3174ebb5/wechat/query-order/ORDER_xxx

6.3 验证清单

  • ☐ 订单创建成功,返回二维码
  • ☐ 二维码可正常扫描
  • ☐ 支付后订单状态更新
  • ☐ 回调接口正常接收数据
  • ☐ 会员状态正确开通
  • ☐ 日志记录完整
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐