成品内容
AI Vibe Coding 实战:微信支付 Native 完整开发指南
# AI Vibe Coding 实战:微信支付 Native 完整开发指南
> **适用场景**:用 AI 搭档从零对接微信支付,包含需求分析、商户配置、代码实现、调试排错全流程。
> **技术栈**:Next.js 16 + TypeScript + Prisma + 微信支付 API V3(公钥模式)
> **AI**:AI助手 攸棠 (底座LLM deepseek-v4-pro)
> **开发周期**:约 2 天(需求讨论 2 小时 + 编码 4 小时 + 调试 3 小时)
---
## 目录
1. [产品需求文档](#1-产品需求文档)
2. [AI 需求讨论实录](#2-ai-需求讨论实录)
3. [微信商户号对接与配置](#3-微信商户号对接与配置)
4. [数据库设计](#4-数据库设计)
5. [后端核心库实现](#5-后端核心库实现)
6. [API 路由实现](#6-api-路由实现)
7. [前端支付流程实现](#7-前端支付流程实现)
8. [调试与排错实录](#8-调试与排错实录)
9. [踩坑经验与教训](#9-踩坑经验与教训)
10. [完整代码清单](#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`:
```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 私钥 | 商户平台下载证书时获得 |
| 支付回调 URL | HTTPS 公网可达 | 自己的服务器地址 |
### 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` 文件:
```bash
# 微信支付
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
```prisma
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 的签名是最容易出错的部分:
```typescript
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 平台证书获取与缓存
```typescript
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 下单
```typescript
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 回调签名验证
```typescript
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 回调数据解密
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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)
**定价卡片:**
```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;
```
**支付流程核心函数:**
```tsx
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)
```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 问题清单
| # | 问题 | 现象 | 根因 | 解决 |
|---|------|------|------|------|
| 1 | SIGN_ERROR | 下单返回"认证类型不正确" | APIv3 密钥与商户平台不一致 | 更新 .env |
| 2 | IP 白名单 | 下单返回 IP 错误 | 服务器 IP 未加入白名单 | 商户平台添加 |
| 3 | 回调签名验证失败 | WECHATPAY/SIGNTEST/ | 微信定期签名探测 | 代码中过滤 |
| 4 | 金额精度 | 显示 ¥0.80 实际 0.79999 | 浮点计算误差 | 后端 Math.round |
| 5 | 证书路径 | 找不到私钥 | 相对路径不对 | path.resolve |
| 6 | 数据库迁移 | Prisma 找不到表 | schema 更新未 push | prisma 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 公网可达。
```typescript
// 必须过滤微信的签名探测请求
if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
return NextResponse.json({ code: "FAIL" });
}
```
### 8.4 金额精度
**Bug:** 前端显示 ¥0.80,后端收到 0.7999999,微信拒绝
**修复:**
```typescript
// 后端重新计算,不信任前端
const amountFen = Math.round(totalAmount * 100);
```
### 8.5 支付页倒计时防刷新
**问题:** 用户刷新支付页倒计时重置
**修复:** 用 expireAt 计算,不用本地 state:
```typescript
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 配置文件管理
```bash
# .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:微信支付官方文档速查
- 接入指引:https://pay.weixin.qq.com/doc/v3/merchant/4012791862
- Native 支付:https://pay.weixin.qq.com/doc/v3/merchant/4012791877
- 签名算法:https://pay.weixin.qq.com/doc/v3/merchant/4013053257
- 回调通知:https://pay.weixin.qq.com/doc/v3/merchant/4013060497
- 平台证书:https://pay.weixin.qq.com/doc/v3/merchant/4013053299
- 常见错误码:https://pay.weixin.qq.com/doc/v3/merchant/4012061908
## 附录 B:测试清单
- [ ] 免费额度:新用户今日首次 1 引擎检测免费
- [ ] 正常支付:选 3 引擎 → 扫码支付 → 检测开始
- [ ] 折扣:选 5 引擎 → 8 折
- [ ] 支付过期:3 分钟未支付 → 自动关闭
- [ ] 轮询:扫码后 3 秒内检测开始
- [ ] 窗口关闭:关闭支付窗口后原页面自动开始检测
- [ ] 重复下单:同一用户产生新订单
- [ ] 金额精度:¥0.10 / ¥0.24 / ¥0.30 正确下单
- [ ] 参数校验:缺少 userId 返回"参数不完整"
- [ ] 微信回调:支付成功后正确更新 DB
- [ ] 签名探测:WECHATPAY/SIGNTEST/ 正确过滤
---
> **编写日期**:2026-06-10
> **作者**:一人尔OPC社区
> **许可**:内部文档,仅供参考
15258862425