一人尔OPC

作品展示

浏览 OPC 创业者的创意与产品

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

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社区 > **许可**:内部文档,仅供参考

1525886242515258862425