一人尔OPC
返回作品展示
AI Vibe Coding 实战:微信支付 Native 完整开发指南

AI Vibe Coding 实战:微信支付 Native 完整开发指南

1

15258862425

2026/6/11 投稿

成熟产品内容已审核

作品简介

# AI Vibe Coding 实战:微信支付 Native 完整开发指南 > **适用场景**:用 AI 搭档从零对接微信支付,包含需求分析、商户配置、代码实现、调试排错全流程。 > **技术栈**:Next.js 16 + TypeScript + Prisma + 微信支付 API V3(公钥模式) > **AI**:AI助手 攸棠 (底座LLM deepseek-v4-pro) > **开发周期**:约 2 天(需求讨论 2 小时 + 编码 4 小时 + 调试 3 小时)

详细描述

AI Vibe Coding 实战:微信支付 Native 完整开发指南

适用场景:用 AI 搭档从零对接微信支付,包含需求分析、商户配置、代码实现、调试排错全流程。 技术栈:Next.js 16 + TypeScript + Prisma + 微信支付 API V3(公钥模式) AI:AI助手 攸棠 (底座LLM deepseek-v4-pro) 开发周期:约 2 天(需求讨论 2 小时 + 编码 4 小时 + 调试 3 小时)


目录

  1. 产品需求文档
  2. AI 需求讨论实录
  3. 微信商户号对接与配置
  4. 数据库设计
  5. 后端核心库实现
  6. API 路由实现
  7. 前端支付流程实现
  8. 调试与排错实录
  9. 踩坑经验与教训
  10. 完整代码清单

1. 产品需求文档

1.1 业务背景

「一人尔 OPC 社区」的 AEO 检测功能(AI 引擎优化检测)收费功能。用户选择引擎数量后,按单价计费,通过微信支付完成付款后开始AEO检测。

1.2 核心需求

需求描述优先级
Native 扫码支付生成微信支付二维码,用户扫码付款P0
免费额度每日首次 1 引擎检测免费P1
阶梯折扣满 5 个引擎享 8 折P2
支付过期3 分钟未支付自动关闭P1
订单状态轮询支付完成后自动触发检测P1
回调通知接收微信支付异步回调,兜底更新状态P1
防金额篡改后端重新计算金额,不信任前端传来的价格P0

1.3 技术选型决策

为什么选 Native 支付而不是 JSAPI?

  • Native 支付只需商户号(mchid),不需要用户在微信内打开
  • 生成 code_url → 前端用 qrcode.react 渲染二维码
  • JSAPI 需要获取用户 openid,流程更复杂,且限制在微信浏览器内

为什么选公钥模式而不是证书模式?

  • 微信支付 API V3 支持两种签名方式
  • 公钥模式:通过商户 API 证书私钥签名,平台公钥验签
  • 证书模式已逐渐废弃,公钥模式是微信推荐的新方式

2. AI 需求讨论实录

2.1 支付场景分析

Boss:AEO 检测需要收费,按引擎数量计费,用户选几个引擎就付多少钱。你规划一下。

攸棠

  • 支付模式选 Native——用户扫码最通用,不需要在微信里打开
  • 给每个用户每天免费额度(1 次 1 引擎),降低新用户体验门槛
  • 单价 ¥0.30/引擎,满 5 个打 8 折 = ¥0.24/引擎
  • 订单过期 3 分钟,防止恶意识别二维码
  • 用 Prisma 存订单,关联到用户

Boss:单价调成 0.1。免费额度只给 1 个引擎。定价配到 config 文件里,方便以后改。

2.2 定价配置决策

最初设定单元价 0.30,后调整为 0.10。关键教训:价格不要硬编码,写进配置文件随时可调。

配置文件 data/aeo-config.json

{
  "pricing": {
    "unitPrice": 0.10,
    "discountThreshold": 5,
    "discountRate": 0.8,
    "freeDailyLimit": 1,
    "freeEngineLimit": 1
  }
}

2.3 支付流程设计

用户选择引擎 → 点击"立即检测" → 后端创建订单
    ↓
免费额度?──是→ 直接开始检测
    ↓否
调用微信 Native 下单 → 获取 code_url
    ↓
前端新窗口打开支付页 → 显示二维码 + 倒计时
    ↓
用户扫码支付 → 3秒轮询检测到已支付 → 关闭支付窗口
    ↓
原页面检测到支付完成 → 开始检测
    ↓ (兜底)
微信支付回调 → 更新数据库 → 页面状态轮询生效

2.4 关键设计决策

  1. 支付页用新窗口而不是内嵌 iframe:避免跨域问题,用户体验更好
  2. 支付状态轮询用 3 秒间隔:微信对查单接口有频率限制,3 秒是安全间隔
  3. 回调通知做兜底:不能完全依赖轮询,微信回调是官方机制
  4. 服务器 IP 要做白名单:这是微信支付下单元测试时卡住的关键问题

3. 微信商户号对接与配置

3.1 申请材料

项目说明从哪里获取
APPID小程序/公众号 AppID微信开放平台 → 开发管理 → 开发设置
MCHID商户号微信支付商户平台 → 账户中心 → 商户信息
API V3 密钥32 位字符串商户平台 → API 安全 → APIv3 密钥(自己设置)
商户证书序列号证书唯一标识商户平台 → API 安全 → API 证书
商户私钥PEM 格式 RSA 私钥商户平台下载证书时获得
支付回调 URLHTTPS 公网可达自己的服务器地址

3.2 商户平台配置步骤

第 1 步:登录商户平台

https://pay.weixin.qq.com
→ 使用商户号 + 密码登录
→ 或使用超管微信扫码登录

第 2 步:设置 APIv3 密钥

账户中心 → API 安全 → APIv3 密钥 → 设置
  • 密钥必须 32 位,建议包含字母数字组合
  • 设置后会显示在页面上,请立即保存,只显示一次

第 3 步:下载 API 证书

账户中心 → API 安全 → API 证书 → 下载证书

下载后会得到一个压缩包,包含:

  • apiclient_key.pem → 商户 API 私钥(用于签名)
  • apiclient_cert.pem → 商户 API 证书公钥
  • apiclient_cert.p12 → PKCS12 格式证书(Java/PHP 用)

⚠️ 证书只下载一次,请妥善保存!

第 4 步:配置服务器 IP 白名单 ⭐ 最容易漏的一步

账户中心 → API 安全 → 服务器 IP → 添加
  • 填写生产服务器的公网 IP
  • 开发环境也要加(如果需要本地测试微信下单)
  • 不配置白名单会导致下单接口返回 IP 错误

第 5 步:配置支付回调域名

产品中心 → 开发配置 → 支付回调 URL

3.3 证书文件管理

certs/
├── apiclient_cert.pem      # API 证书公钥
├── apiclient_key.pem       # API 证书私钥(签名用)
└── apiv3_key.txt           # APIv3 密钥

⚠️ 证书文件不可提交到 Git! 已在 .gitignore 中排除 certs/。

3.4 环境变量配置

.env 文件:

# 微信支付
WECHAT_APPID=wx1125eec016a73c85
WECHAT_MCHID=1746742076
WECHAT_API_V3_KEY=你的32位v3密钥
WECHAT_CERT_SERIAL_NO=证书序列号
WECHAT_NOTIFY_URL=https://你的域名/api/payment/wechat/notify
WECHAT_PRIVATE_KEY_PATH=certs/apiclient_key.pem

4. 数据库设计

4.1 Prisma Schema

model AeoOrder {
  id            String    @id @default(cuid())
  orderNo       String    @unique @map("order_no")
  userId        String    @map("user_id")
  detectType    Int       @map("detect_type")    
  detectValue   String    @map("detect_value")
  engineCount   Int       @map("engine_count")
  engineKeys    Json      @map("engine_keys")
  unitPrice     Float     @map("unit_price")
  discount      Float     @default(1.0)
  totalAmount   Float     @map("total_amount")
  payStatus     Int       @default(0) @map("pay_status")
  // 0=待支付 1=已支付 2=已过期 3=已关闭
  payMethod     Int       @default(1) @map("pay_method")  // 1=微信
  codeUrl       String?   @db.Text @map("code_url")
  transactionId String?   @map("transaction_id")
  createdAt     DateTime  @default(now()) @map("created_at")
  expireAt      DateTime  @map("expire_at")
  payAt         DateTime? @map("pay_at")
  checkId       String?   @map("check_id")
  user          User      @relation(fields: [userId], references: [id])

  @@map("aeo_orders")
  @@index([userId, createdAt])
  @@index([orderNo])
}

4.2 设计要点

  1. orderNo 是业务主键:不只 DB unique,微信接口也用它定位订单
  2. payStatus 用 Int 不用 Enum:方便扩展状态(退款中、部分退款)
  3. engineKeys 用 JSON 存数组:Prisma 原生支持 JSON,MySQL 存为 TEXT
  4. codeUrl 用 @db.Text:微信返回的 code_url 可能很长
  5. 索引[userId, createdAt] 查用户今日订单(免费额度),[orderNo] 定位订单

5. 后端核心库实现

5.1 文件结构

lib/wechatpay.ts    # ~280 行
├── 配置读取        # .env → APPID/MCHID/KEY
├── 签名生成        # RSA-SHA256
├── Authorization   # 请求头构建
├── Native 下单     # POST /v3/pay/transactions/native
├── 订单查询        # GET /v3/pay/transactions/out-trade-no/{no}
├── 关闭订单        # POST /v3/pay/transactions/out-trade-no/{no}/close
├── 平台证书获取    # GET /v3/certificates(12h 缓存)
├── 回调签名验证    # 平台证书公钥验签
└── 回调数据解密    # AEAD_AES_256_GCM

5.2 核心:签名生成

微信支付 API V3 的签名是最容易出错的部分:

function sign(method, path, timestamp, nonce, body) {
  // 签名串:5行,每行以 \n 结尾
  const signStr = `${method}\n${path}\n${timestamp}\n${nonce}\n${body}\n`;
  const signer = crypto.createSign("RSA-SHA256");
  signer.update(signStr);
  signer.end();
  return signer.sign(PRIVATE_KEY, "base64");
}

签名串规则(5 行,每行以 \n 结尾):

  1. 请求方法(GET/POST/PATCH/DELETE,大写)
  2. URL 路径(如 /v3/pay/transactions/native,不含域名)
  3. 时间戳(Unix 秒)
  4. 随机串(32 位)
  5. 请求体(JSON 字符串,GET 请求为空字符串)

Authorization 请求头格式:

WECHATPAY2-SHA256-RSA2048 mchid="商户号",nonce_str="随机串",
  signature="签名Base64",timestamp="时间戳",serial_no="证书序列号"

⚠️ serial_no 是商户 API 证书序列号,不是平台证书序列号!

5.3 平台证书获取与缓存

let platformCerts = [];
let certsLastFetch = 0;
const CERTS_TTL = 12 * 60 * 60 * 1000; // 12h 刷新

async function fetchPlatformCertificates() {
  if (platformCerts.length > 0 && Date.now() - certsLastFetch < CERTS_TTL) {
    return platformCerts; // 缓存命中
  }
  // GET /v3/certificates → AEAD_AES_256_GCM 解密证书
}

5.4 Native 下单

export async function createNativeOrder(params) {
  const body = {
    appid: APPID,
    mchid: MCHID,
    description: params.description,
    out_trade_no: params.outTradeNo,
    notify_url: NOTIFY_URL,
    amount: { total: params.amount, currency: "CNY" },
    time_expire: params.timeExpire,
    scene_info: { payer_client_ip: params.payerClientIp },
  };
  return await request("POST", "/v3/pay/transactions/native", body);
}

关键参数:

  • amount.total:单位是,¥0.10 = 10
  • time_expire:ISO 8601 格式,如 2026-06-10T12:00:00+08:00
  • scene_info.payer_client_ip:非必填但建议传,避免风控
  • notify_url:必须 HTTPS 公网可达

5.5 回调签名验证

export async function verifyNotifySignature(headers, body) {
  // 防签名探测
  if (signature.startsWith("WECHATPAY/SIGNTEST/")) return false;

  // 获取平台证书公钥
  const certs = await fetchPlatformCertificates();
  const cert = certs.find(c => c.serial_no === serial);

  // 验签:签名串 = timestamp\nnonce\nbody\n
  const signStr = `${timestamp}\n${nonce}\n${body}\n`;
  const verifier = crypto.createVerify("RSA-SHA256");
  verifier.update(signStr);
  return verifier.verify(cert.publicKey, signature, "base64");
}

5.6 回调数据解密

export function decryptNotifyResource(resource) {
  const data = Buffer.from(resource.ciphertext, "base64");
  const authTag = data.subarray(data.length - 16);  // 最后16字节
  const encrypted = data.subarray(0, data.length - 16);

  const decipher = crypto.createDecipheriv(
    "aes-256-gcm",
    Buffer.from(API_V3_KEY),
    Buffer.from(resource.nonce)
  );
  decipher.setAuthTag(authTag);
  decipher.setAAD(Buffer.from(resource.associated_data));

  const plaintext = Buffer.concat([decipher.update(encrypted), decipher.final()]);
  return JSON.parse(plaintext.toString());
}

6. API 路由实现

6.1 创建订单 POST /api/payment/order

export async function POST(request) {
  const { userId, detectType, detectValue, engineKeys,
          engineCount, unitPrice, discount, totalAmount } = body;

  // 1. 参数校验
  if (!userId || !detectValue || !engineCount || !totalAmount)
    return error("参数不完整");

  // 2. 免费额度检查
  const todayOrders = await prisma.aeoOrder.count({
    where: { userId, payStatus: { in: [1] }, createdAt: { gte: today } }
  });
  if (todayOrders === 0 && engineCount <= pricing.freeEngineLimit) {
    // 免费!直接标记已支付 → 跳过微信下单
    return { orderNo, isFree: true };
  }

  // 3. 金额精度校验(防前端篡改)
  const amountFen = Math.round(totalAmount * 100);
  if (amountFen <= 0) return error("金额无效");

  // 4. 微信 Native 下单
  const { code_url } = await createNativeOrder({ ... });

  // 5. 保存订单
  await prisma.aeoOrder.create({ ... });

  return { orderNo, codeUrl: code_url, isFree: false };
}

6.2 查询订单状态 GET /api/payment/order/status

export async function GET(request) {
  const order = await prisma.aeoOrder.findUnique({ where: { orderNo } });

  if (order.payStatus === 1) return order; // 已支付

  // 过期检查
  if (new Date() > new Date(order.expireAt)) {
    await prisma.aeoOrder.update({ data: { payStatus: 3 } });
    return { payStatus: 3 };
  }

  // 主动查微信状态(兜底)
  const wxResult = await queryOrderByOutTradeNo(orderNo);
  if (wxResult.trade_state === "SUCCESS") {
    await prisma.aeoOrder.update({ data: { payStatus: 1, payAt: new Date() } });
  }
  return order;
}

6.3 支付回调 POST /api/payment/wechat/notify

export async function POST(request) {
  // 1. 验证签名
  if (!await verifyNotifySignature(headers, body)) return error(401);

  // 2. 解密数据
  const decrypted = decryptNotifyResource(notify.resource);

  // 3. 更新订单
  if (decrypted.trade_state === "SUCCESS") {
    await prisma.aeoOrder.update({
      where: { orderNo: decrypted.out_trade_no },
      data: { payStatus: 1, transactionId: decrypted.transaction_id, payAt: new Date() }
    });
  }

  // 4. 返回微信要求的格式
  return NextResponse.json({ code: "SUCCESS", message: "成功" });
}

7. 前端支付流程实现

7.1 检测页改造(app/aeo/page.tsx)

定价卡片:

const unitPrice = 0.10;  // 从 aeo-config.json
const count = selected.size;
const discount = count >= 5 ? 0.8 : 1.0;
const total = count * unitPrice * discount;

支付流程核心函数:

async function handleCheck(e) {
  // 1. 已登录 → 创建订单
  const orderRes = await fetch("/api/payment/order", {
    body: JSON.stringify({ userId, detectType, detectValue, engineKeys, ... })
  });
  const orderData = await orderRes.json();

  // 2. 免费 → 直接检测
  if (orderData.isFree) {
    startDetection(..., orderData.orderNo);
    return;
  }

  // 3. 付费 → 打开支付页
  window.open(`/aeo/pay?orderNo=${orderData.orderNo}`, "_blank");
  setShowPaymentOverlay(true);

  // 4. 监控支付窗口关闭 → 检测支付状态 → 自动开始检测
  const checkClosed = setInterval(async () => {
    if (payWindow.closed) {
      const statusData = await fetch(`/api/payment/order/status?orderNo=...`);
      if (statusData.payStatus === 1) startDetection(...);
    }
  }, 1000);
}

7.2 支付页面(app/aeo/pay/page.tsx)

function PayContent() {
  const [countdown, setCountdown] = useState(180); // 3分钟

  // 加载订单 → 用 expireAt 计算安全倒计时(防刷新重置)
  useEffect(() => {
    fetch(`/api/payment/order/status?orderNo=...`).then(r => {
      const remaining = Math.max(0,
        Math.floor((new Date(data.expireAt).getTime() - Date.now()) / 1000));
      setCountdown(remaining);
    });
  }, []);

  // 轮询支付状态(3秒间隔)
  useEffect(() => {
    setInterval(async () => {
      const data = await fetch(`/api/payment/order/status?...`);
      if (data.payStatus === 1) window.close(); // 已支付,关闭
    }, 3000);
  }, []);

  return (
    <>
      <QRCodeSVG value={codeUrl} size={200} />
      <Countdown expired={isExpired} remaining={countdown} />
    </>
  );
}

8. 调试与排错实录

8.1 问题清单

#问题现象根因解决
1SIGN_ERROR下单返回"认证类型不正确"APIv3 密钥与商户平台不一致更新 .env
2IP 白名单下单返回 IP 错误服务器 IP 未加入白名单商户平台添加
3回调签名验证失败WECHATPAY/SIGNTEST/微信定期签名探测代码中过滤
4金额精度显示 ¥0.80 实际 0.79999浮点计算误差后端 Math.round
5证书路径找不到私钥相对路径不对path.resolve
6数据库迁移Prisma 找不到表schema 更新未 pushprisma db push
7订单号碰撞极低概率同毫秒同随机加 OPC 前缀

8.2 SIGN_ERROR 排查(卡最久)

现象: [WeChatPay] 下单失败: Http头Authorization认证类型不正确

排查:

  1. 签名串格式 ✓(每行以 \n 结尾)
  2. Authorization 拼写 ✓
  3. 证书序列号 ✓(商户 API 证书,非平台证书)
  4. 私钥 PEM 格式 ✓
  5. 最终发现:APIv3 密钥在 .env 里写错了 ← 就是这个

调试技巧: 在 sign 函数中打印签名串和 Authorization 请求头,对照微信官方文档逐字核对。

8.3 回调通知调试

本地开发需要内网穿透(ngrok/frp),因为微信回调需要 HTTPS 公网可达。

// 必须过滤微信的签名探测请求
if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
  return NextResponse.json({ code: "FAIL" });
}

8.4 金额精度

Bug: 前端显示 ¥0.80,后端收到 0.7999999,微信拒绝

修复:

// 后端重新计算,不信任前端
const amountFen = Math.round(totalAmount * 100);

8.5 支付页倒计时防刷新

问题: 用户刷新支付页倒计时重置

修复: 用 expireAt 计算,不用本地 state:

const remaining = Math.max(0,
  Math.floor((new Date(data.expireAt).getTime() - Date.now()) / 1000));

9. 踩坑经验与教训

9.1 微信支付 API V3 核心知识点

  1. 签名串 5 行均以 \n 结尾,包括最后的 body 行
  2. serial_no 是商户 API 证书序列号,非平台证书序列号
  3. 金额单位是分,¥1.00 = 100
  4. 服务器 IP 必须白名单,否则下单报错
  5. 微信会发送签名测试请求 WECHATPAY/SIGNTEST/,需过滤
  6. 平台证书 12 小时刷新一次,需缓存
  7. GET 请求签名串 body 为空字符串,但末尾同样需要 \n

9.2 AI Vibe Coding 实操经验

有效做法:

  • 📝 让 AI 先写完整架构,再逐模块实现——先有骨架再填肉
  • 🔍 错误信息直接丢给 AI 分析——比 Google 快 10 倍
  • 📋 让 AI 写开发状态文档——方便追踪进度
  • ⚠️ 涉及钱的代码要人工二次确认——AI 可能漏掉金额篡改防护

踩过的坑:

  • AI 对微信 API 文档理解可能有偏差,关键参数需对照官方文档
  • AI 生成的代码可能有 TypeScript 类型错误,需 npm run build 检查
  • AI 不会自动处理证书路径,需明确告知路径结构
  • 支付回调签名验证逻辑需特别注意边界情况

9.3 安全红线

  1. 金额防篡改:后端重新计算,不信前端
  2. 签名验证:回调必验微信签名
  3. HTTPS 必须:回调 URL 必须是 HTTPS
  4. 幂等处理:回调可能重复到达,按 transaction_id 去重
  5. 日志脱敏:不打印 APIv3 密钥和私钥

9.4 配置文件管理

# .gitignore 必须包含
certs/
.env
*.pem
*.p12

生产环境:.env 和证书手动拷贝到服务器,不提交 Git。


10. 完整代码清单

新增文件

文件行数说明
lib/wechatpay.ts~280微信支付核心库(签名/下单/查单/回调)
app/api/payment/order/route.ts~170创建订单(免费额度+微信下单)
app/api/payment/order/status/route.ts~80查询订单状态(含微信主动查单)
app/api/payment/order/cancel/route.ts~40取消订单
app/api/payment/wechat/notify/route.ts~70支付回调(验签+解密+更新DB)
app/aeo/pay/page.tsx~250支付页面(二维码+倒计时+轮询)

修改文件

文件修改
app/aeo/page.tsx定价卡片、支付流程、遮罩层、orderNo 关联
app/api/aeo/check/route.ts接收 orderNo 参数
app/api/aeo/save/route.ts保存检测记录时关联 orderNo
prisma/schema.prisma新增 AeoOrder 模型
data/aeo-config.json新增 pricing 配置段
.env新增微信支付配置
.gitignore排除 certs/

依赖包

只需 qrcode.react@^4(前端二维码)。后端全用 Node.js 内置 crypto,零额外依赖。


附录 A:微信支付官方文档速查

附录 B:测试清单

  • 免费额度:新用户今日首次 1 引擎检测免费
  • 正常支付:选 3 引擎 → 扫码支付 → 检测开始
  • 折扣:选 5 引擎 → 8 折
  • 支付过期:3 分钟未支付 → 自动关闭
  • 轮询:扫码后 3 秒内检测开始
  • 窗口关闭:关闭支付窗口后原页面自动开始检测
  • 重复下单:同一用户产生新订单
  • 金额精度:¥0.10 / ¥0.24 / ¥0.30 正确下单
  • 参数校验:缺少 userId 返回"参数不完整"
  • 微信回调:支付成功后正确更新 DB
  • 签名探测:WECHATPAY/SIGNTEST/ 正确过滤

编写日期:2026-06-10 作者:一人尔OPC社区 许可:内部文档,仅供参考

1

15258862425

一人公司创业者

评论区 (共0条)

登录后参与评论