wx-gateway · 2026.05

一份代码,两个公众号,六个对外接口,把入向和出向消息全部抽象成统一契约。

爸爸在 mvp-deployer 上跑的微信公众号网关,解决"一个公众号只能配一个回调 URL"的硬约束——下游业务方共享公众号能力,但互不感知。

实例数2 个独立部署
接入业务6 个 active app
对外接口6 internal API
技术栈Next.js 15 · Prisma · Postgres
01

双实例

公众号回调 URL 一对一,所以每个公众号一份独立部署。代码共享,差别只在 env。
wx-gateway
服务公众号「造悟者」
AppID
wx225bf76b06064faa
域名
wx.mvp.restry.cn
端口
3794
数据库
wx_gateway
主实例
wx-gateway-pucs
服务公众号「莆阳网络科技」
AppID
wxe780b027c2c56921
域名
wxmsg.mvp.restry.cn
端口
3800
数据库
wx_gateway_pucs
多业务方共享
02

架构

入向消息按 binding 路由独占;出向消息按 grant 越权检查。token 集中托管。
微信公众号 cgi-bin / callback WX-GATEWAY 入向 Fanout /wx/callback 出向 Internal API /internal/wx-* access_token 集中托管 · cron 续期 · advisory lock 防并发 cgi-bin/token · errcode 40001 自动重刷 UserAppBinding openid → app(一对一) 入向消息独占路由 MessageRoute EventKey_prefix → app click / scan 事件路由 Admin Dashboard Apps · Invites · Users · Routes · Menu · Outbound · Payments geniuspulse-prod 面试业务 cspy_admin 客服工单 nexora-devhub 开发者中心 copilot-proxy 代理服务 …6 active apps
03

对外接口

六个 internal API,统一 HMAC 签名,业务方一份 WX_GATEWAY_SECRET 通用。
GET
/internal/wx-token
access_token 集中托管
业务方拿 access_token,网关 cron 续期 + advisory lock 防多家挤掉。禁止业务方自己调 cgi-bin/token。
wx-token|ts|appName
POST
/internal/wx-push
主动推消息
客服消息或订阅模板,网关代调微信 cgi-bin/message/*。越权检查:只能给绑到自己 app 的 openid 推。
wx-push|ts|appName
GET
/internal/wx-config
自检配置快照
一次拿到 app 元信息 + fanout endpoint/route + 绑定用户数 + token 状态。出问题第一时间自查。
wx-config|ts|appName
GET
/internal/wx-qrcode
业务方专属永久二维码
每个 app 一张永久 SCAN 二维码,scene_str = appName。任何人扫这码自动 binding 到本 app,粉丝来源决定归属。
wx-qrcode|ts|appName
GET
/internal/wx-userinfo
查用户身份
是否关注 + nickname/avatar/unionid/subscribeTime。openid 入签防越权探测,60s 缓存防滥用。
wx-userinfo|ts|appName|openid
GET
/internal/wx-menu
查公众号菜单
返完整 buttons + myButtons(server 预筛只列绑到本 app 的 click 按钮)。收到 click 事件后查上下文写智能回复。
wx-menu|ts|appName
04

核心机制

解决多业务方共享一个公众号必然遇到的三个问题。
入向消息
UserAppBinding 路由独占
openid → app 一对一,fanout 拿到 binding 后只把消息派给唯一归属 app,其他业务方收不到。OAuth/扫码自动写入,最后扫者赢。
出向消息
永久二维码 + scene 绑定
每个 app 一张永久码,贴海报或邀请页。用户扫即关注 + 自动 binding。彻底解决"用户在 A 站登过又去 B 站登 binding 被覆盖"的归属错乱。
事件路由
菜单 click → app prefix
菜单按钮 key 带业务前缀,fanout 按 EventKey_prefix 路由到指定 endpoint。admin Menu UI 配按钮时直接 select 绑哪个 app,Save 自动写 MessageRoute。
05

业务方接入生命周期

从拿到邀请码到主动 push 消息,七步闭环。

邀请码注册

爸爸在 admin Invites tab 颁发 32-hex 邀请码 + appNamePrefix + hostPattern。业务方调 POST /apps/register 拿 secret,只显示一次。

前端扫码登录

业务方页面调 POST /wx/qr/{app} 拿临时二维码 + token,SSE /wx/poll/{token} 等扫码确认,confirmed 后跳业务方 finalize。

finalize 验签 + upsert User

业务方 /api/wx/finalize 用 WX_GATEWAY_SECRET 验 HMAC,5min skew,upsert 自家 User 表,起 session。

永久二维码引流

业务方调 /internal/wx-qrcode 拿自己专属永久码,贴海报。任何扫码者自动 binding 到本 app,后续消息归属明确。

查身份引导关注

业务方调 /internal/wx-userinfo 看用户 subscribed 状态,未关注引导关注送积分(配合永久码效果最好)。

收消息 / click / SCAN

网关 fanout 把入向消息派给业务方 endpoint。click 事件按菜单绑定路由,SCAN/subscribe 按 scene_str 路由,业务方写 webhook 接住。

主动推送通知

业务方调 /internal/wx-push 推任务完成 / 客服回复 / 订阅模板。越权检查保证只推自己的用户。所有出向落 OutboundMessageLog。

06

Admin 后台

爸爸的运维入口,扫码登录后管所有业务方资源。
Apps
/admin/apps
业务方注册表,active/revoked 状态,每个 app 的永久二维码 + 强制完善信息开关 + Rotate Secret。
Invites
/admin/invites
签发邀请码,限定 namePrefix / hostPattern / usesLeft / expiresAt。明文 code 只显示一次。
Users
/admin/users
所有 openid + binding + 角色,可手动改 binding(防 OAuth 自动覆盖造成的归属漂移)。
Endpoints & Routes
/admin/fanout
业务方下游 callback URL + MessageRoute 匹配规则(MsgType / Event / EventKey_prefix)。
Menu
/admin/menu
公众号菜单 JSON 编辑 + 实时预览 + click 按钮直接绑 app + Push to WeChat。每个实例只改自己公众号。
Outbound
/admin/outbound
所有 wx-push 出向日志,errcode 红/绿标识,按 appName/type/openid 筛选,行点击展 payload。
Fanout Logs
/admin/fanout-logs
入向消息派发记录,boundApp / matchedRouteName / downstreamStatus,排查"消息没到"的第一站。
Payments
/admin/payments
微信支付 v1 订单(personal_qr 人工审核),approve/reject/redeliver-webhook,支持 JSAPI 收款。
07

已知问题

在路上,本周内修。
设计 bug
UserAppBinding
一对一模型让多 app 客户失去 push 权
用户在 A 登录后又去 B 登录,binding 覆盖成 B,A 失去推送权——但用户实际还是 A 的客户。解法:拆 UserAppGrant(N:N)管越权,binding 仍管 fanout 路由。
待派 CC
~30 min · 加表 + 三处写入 + 两接口越权切换