事情是这样的,这两天我给一个很好的朋友做了一个微信公众号应用,带支付和管理后台,全部可以在微信内部闭环。我就在想,身在这个时代,也许是全民创业时代,但是,我总觉得,能收款才是闭环,不收款,你的商业逻辑也就没法闭环。所以这篇文章手把手带你走通:从一台空服务器到微信支付到账的全流程。这个过程在AI 的帮助下,我仅仅花了 1 天时间搞定。如下图,跑通的界面。

首先,我们来先算一笔账,这个应用一年的初始成本是多少
很多人觉得做一个能收钱的线上应用门槛很高,其实拆开算一下:
| 合计 | 约600元/年 |
600块一年,就能跑起一个有登录、有支付、有管理后台的完整应用。
技术选型:我用 Next.js ,但是为啥我选这套呢?
Next.js(前后端一体) + SQLite(零运维数据库) + 微信生态(登录+支付)
选型逻辑很简单:一个人能维护。
• Next.js:一个项目搞定前端页面、后端API、中间件鉴权,不用分别部署前后端 • SQLite:一个文件就是整个数据库,不用装 MySQL,不用管连接池,备份就是复制文件,当然,如果你觉得你又个几万用户,那么选 MySQL 没错。 • Prisma:ORM 层,写 TypeScript 就能操作数据库,有类型提示不容易写错 • 微信 OAuth:用户在微信里打开就自动登录,不用自己做注册登录体系 • 微信支付 JSAPI:用户在微信内直接拉起支付,体验最顺滑
一台服务器跑一个 Node 进程,PM2 守护,Nginx 反代,够了。
整体架构
用户微信 ──▶ Nginx(:443) ──▶ Next.js(:3000)
│
┌──────────────┼──────────────┐
▼ ▼ ▼
微信OAuth 业务逻辑 微信支付API
(拿openid) (Prisma+SQLite) (下单/回调)整个应用分三层:
1. 用户层:微信内 H5 页面,OAuth 静默登录拿 openid 2. 业务层:Next.js 处理核心逻辑,Prisma 读写 SQLite 3. 支付层:调微信支付 JSAPI 下单,处理异步回调
下面逐个拆解。
第一关:微信 OAuth 登录
微信生态内的用户身份标识是 openid——每个用户在你的公众号下有唯一的 openid。拿到它,就知道"谁在用你的应用"。
流程
用户访问你的页面
│
▼
中间件检查 session 里有没有 openid
│
├── 有 → 放行
│
└── 没有 → 跳转微信 OAuth 授权页
│
▼
用户同意授权(静默,无感知)
│
▼
微信回调你的服务,带上 code
│
▼
用 code 换 openid,写入 session
│
▼
跳回原页面中间件实现
关键:把鉴权逻辑放在 Next.js 中间件里,所有需要登录的页面自动保护。
// middleware.ts
exportasyncfunctionmiddleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const session = await getIronSession<SessionData>(request, response, sessionOptions);
// 管理后台:必须登录 + 管理员身份
if (pathname.startsWith("/admin")) {
if (!session.openid) {
returnredirect("/api/auth/wechat?redirect=" + pathname);
}
const admins = process.env.ADMIN_OPENIDS?.split(",") || [];
if (!admins.includes(session.openid)) {
returnredirect("/");
}
}
// C端页面:微信内要求登录,非微信放行
if (pathname.startsWith("/wechat")) {
const ua = request.headers.get("user-agent") || "";
const isWeChat = ua.includes("MicroMessenger");
if (!session.openid && isWeChat) {
returnredirect("/api/auth/wechat?redirect=" + pathname);
}
}
}这里有个细节:非微信浏览器直接放行。因为 OAuth 只在微信内生效,PC 或手机浏览器访问时跳 OAuth 会报错。让非微信用户也能看(免费或体验模式),比硬拦着体验好得多。
OAuth 路由
// app/api/auth/wechat/route.ts
exportasyncfunctionGET(request: NextRequest) {
const code = searchParams.get("code");
if (!code) {
// 第一步:跳微信授权页
const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?`
+ `appid=${APPID}&redirect_uri=${encodeURIComponent(redirectUri)}`
+ `&response_type=code&scope=snsapi_base#wechat_redirect`;
returnNextResponse.redirect(oauthUrl);
}
// 第二步:用 code 换 openid
const res = awaitfetch(
`https://api.weixin.qq.com/sns/oauth2/access_token?`
+ `appid=${APPID}&secret=${SECRET}&code=${code}&grant_type=authorization_code`
);
const data = await res.json();
session.openid = data.openid;
await session.save();
returnredirect(originalPath);
}scope=snsapi_base 是静默授权,用户无感知,不需要点"同意"。只能拿到 openid,拿不到昵称头像,但对大多数应用够用了。
第二关:微信支付接入
这是全文最核心的部分,也是第一接入最麻烦的地方,当然了,也是坑最多的地方。
前置准备
1. 企业认证的公众号(服务号),配好 JS 接口安全域名和网页授权域名,注意,域名需要先备案成功。 2. 微信商户号,在 pay.weixin.qq.com 申请,需要企业。 3. 关联 AppID 和商户号:商户平台 → 产品中心 → AppID账号管理 → 关联你的公众号 AppID,自己操作就好,不需要审核。 4. API 证书:商户平台 → 账户中心 → API安全 → 申请 API 证书,下载 apiclient_key.pem和apiclient_cert.pem5. 设置 API V3 密钥:同一页面设置 32 位密钥,记下来


注意,这个地方先申请最底下的 V3密钥,然后在申请商户 API 证书,V2,那个不太需要了,那个是过时的 API ,不用理他。申请证书的需要一个工具,流程会引导你下载。几分钟整完,也不是很难。

支付时序
这是完整的一次支付流程:
前端 你的服务器 微信支付API
│ │ │
│ 1. 点击支付 │ │
│───────────────────────▶│ │
│ │ 2. 创建预支付订单 │
│ │───────────────────────▶│
│ │ │
│ │ 3. 返回 prepay_id │
│ │◀───────────────────────│
│ 4. 返回支付参数 │ │
│◀───────────────────────│ │
│ │ │
│ 5. wx.requestPayment │ │
│ (拉起微信支付弹窗) │ │
│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
│ │ │
│ 6. 用户完成支付 │ │
│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │ │
│ │ 7. 异步回调通知(加密) │
│ │◀───────────────────────│
│ │ │
│ │ 8. 解密,更新订单状态 │
│ │ │
│ 9. 轮询支付状态 │ │
│───────────────────────▶│ │
│ 10. 返回 PAID │ │
│◀───────────────────────│ │
│ │ │
│ 11. 跳转结果页 ✓ │ │注意第 7 步:微信的支付结果不是同步返回的。用户付完钱,微信会异步 POST 到你配置的回调地址。前端靠轮询来确认支付状态。
后端:创建预支付订单
// lib/wxpay.ts — 初始化支付客户端
importWxPayfrom"wechatpay-node-v3";
const wxpay = newWxPay({
appid: process.env.WECHAT_APPID,
mchid: process.env.WX_MCH_ID,
publicKey: fs.readFileSync("/path/to/apiclient_cert.pem"), // 公钥证书
privateKey: fs.readFileSync("/path/to/apiclient_key.pem"), // 私钥
});这里是第一个大坑:publicKey 要传 公钥证书(apiclient_cert.pem),其实很好理解,证书有点网站的 HTTPS 证书,就是给人家去认证的,但是不小心传错了,排查起来会比较麻烦,无厘头。如果把私钥同时传给两个字段,会报:
Error: Could not convert certificate from PEM: invalid type
这个错误信息完全看不出是哪个文件传错了,都是 pem,很容易在这卡几分钟。
// app/api/pay/create/route.ts
exportasyncfunctionPOST(request: NextRequest) {
const { orderId } = await request.json();
const order = await prisma.order.findUnique({ where: { id: orderId } });
// 调用微信 JSAPI 下单
const result = await wxpay.transactions_jsapi({
description: "你的商品描述",
out_trade_no: order.id,
notify_url: "https://yourdomain.com/api/pay/notify",
amount: { total: order.amount, currency: "CNY" },
payer: { openid: order.openid },
});
// wechatpay-node-v3 直接返回前端需要的支付参数
// { appId, timeStamp, nonceStr, package, signType, paySign }
returnNextResponse.json({ payParams: result.data });
}第二个坑:wechatpay-node-v3 的 transactions_jsapi 返回的不是原始 prepay_id,而是已经签好名的完整 JSAPI 参数。如果你还自己拿 prepay_id 去拼签名,反而会报参数错误。直接 result.data 透传给前端就行。
第三个坑:APPID_MCHID_NOT_MATCH。AppID 和商户号必须在微信商户平台上绑定。去 pay.weixin.qq.com → 产品中心 → AppID账号管理,确认关联关系。
前端:拉起支付
// 前端调用 wx.requestPayment
function onPay(payParams) {
wx.chooseWXPay({
timestamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign,
success: () => { /* 开始轮询支付状态 */ },
fail: () => { /* 用户取消或失败 */ },
});
}后端:处理支付回调
第四个坑,也是最隐蔽的一个:微信 V3 的回调通知是 AES-256-GCM 加密 的。
你收到的回调长这样:
{
"event_type":"TRANSACTION.SUCCESS",
"resource":{
"algorithm":"AEAD_AES_256_GCM",
"ciphertext":"a01ruYbf7C7ItSb9G6zYtH...(一大串Base64)",
"nonce":"RZEIMd1L1xBM",
"associated_data":"transaction"
}
}看到 ciphertext 了吗?里面才是真正的支付结果(包含 out_trade_no、金额等)。很多教程没提这一步,如果你不解密直接去读 out_trade_no,永远是 undefined。
解密代码:
import crypto from"crypto";
functiondecryptResource(ciphertext: string, nonce: string, associatedData: string) {
const apiV3Key = process.env.WX_API_V3_KEY; // 32位密钥
const buf = Buffer.from(ciphertext, "base64");
// 密文最后16字节是 authTag
const authTag = buf.subarray(buf.length - 16);
const encrypted = buf.subarray(0, buf.length - 16);
const decipher = crypto.createDecipheriv("aes-256-gcm", apiV3Key, nonce);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(associatedData));
returnBuffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
}解密后拿到明文:
{
"out_trade_no": "你的订单号",
"trade_state": "SUCCESS",
"amount": { "total": 1 }
}然后更新订单状态为 PAID,完事。
前端:轮询确认
// 用户付完钱后轮询
const timer = setInterval(async () => {
const res = await fetch(`/api/pay/status?orderId=${orderId}`);
const data = await res.json();
if (data.status === "PAID") {
clearInterval(timer);
router.push(`/paid/${orderId}`);
}
}, 2000);为什么不直接信任前端的 success 回调?因为那个回调只表示"用户操作完成了",不等于"钱真的到账了"。以服务端回调结果为准,这是支付系统的铁律。

第三关:管理后台
收了钱,还得能看到。管理后台本质就是:用 openid 鉴权 + 一套 CRUD 页面。
// lib/admin-auth.ts
exportasyncfunctionisAdmin() {
const session = awaitgetSession();
const envAdmins = process.env.ADMIN_OPENIDS?.split(",") || [];
// 先查环境变量(快),再查数据库(灵活)
const isEnvAdmin = envAdmins.includes(session.openid);
const isDbAdmin = await prisma.adminUser.findUnique({ where: { openid: session.openid } });
return isEnvAdmin || !!isDbAdmin;
}管理员身份两级校验:环境变量写死的(你自己),这就初始蛋,蛋生鸡,哈哈。+ 数据库动态添加的(你的合伙人)。
后台功能不用复杂,核心就几个:
• 订单列表:看谁付了钱、什么时候付的、多少钱 • 数据看板:今日/本周/本月收入 • 内容管理:编辑你的商品/服务内容 • 系统设置:调价格、改公告 • 管理员管理:加人、删人
用 shadcn/ui 搭界面,差不多顺利点1小时能撸完。


部署:一把梭
# 服务器上
git pull
npm install
npx prisma db push
npm run build
# PM2 守护进程
pm2 start ecosystem.config.js
pm2 saveNginx 配置:
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}写在最后
全民创业的时代,技术不该是门槛。一台轻量服务器、一个认证公众号、几百行代码,就能搭起一个从登录到支付到管理的完整闭环。
真正的门槛从来不是技术,是你愿不愿意动手把第一个版本跑起来。
本文链接:https://kinber.cn/post/6329.html 转载需授权!
推荐本站淘宝优惠价购买喜欢的宝贝:

支付宝微信扫一扫,打赏作者吧~
