项目矩阵 平台基建 Ewx-gateway
EEagle 主维护 · DDora 协查 · 平台基建

wx-gateway

多业务方共享的微信公众号网关 · 2026 H1 平台层基建

微信公众号有一条硬约束:1 个公众号只能配 1 个回调 URL、只能存在 1 份有效 access_token。MVP 工厂里跑着 8 个想接同一个号的原型 — 在 wx-gateway 之前,谁先 cron 续期谁就把别人的 token 踢成 40001,每个项目还各自存一份微信密钥、各自维护 openid 表、各自踩同一组平台 PII 政策的坑。wx-gateway 把微信公众号原生 API 全部包装成内部 HMAC API:8 个业务方共用 1 个公众号、共用 1 份中央 access_token、不碰原生 SDK,接入零配置,密钥泄漏面从 8 份压回 1 份。

访问 wx.mvp.restry.cn 莆阳实例 wxmsg.mvp.restry.cn Restry/wx-gateway · private
8
业务方接入
2
公众号实例 · 双部署
123
commits · 19 天
1
中央 access_token

核心能力 · Five Capabilities

5 件事,业务方再也不用自己做

每个能力先回答"业务方原本要付什么代价",再补一行技术细节。所有能力共享一条铁律:业务方禁止自己调微信原生 API,token / push / qrcode / userinfo / template / menu 一律走 /internal/*,网关统一兜底验签、限流、平台 PII 政策。

统一登录凭证
8 业务方共用一个
业务方不再各自 cgi-bin/token 互踢成 40001,也不再各存一份微信 secret。HMAC GET 网关就拿到当前 5 分钟窗的 token。
/internal/wx-token · pg_advisory_xact_lock · 100 min cron
扫码即登录
业务方零配置
业务方一个 POST 拿到带参二维码 + SSE 长连接状态流。scene_str = appName 1:1 映射,微信 10 万张永久 QR 配额集中分配再不烧空。
POST /wx/qr/[app] · GET /wx/poll/[token] · SSE
OAuth 包装拿用户
绕开平台 PII 坑
业务方不再被 cgi-bin/user/info 2021+ 不返 nickname 的政策坑死。网关强制走 snsapi_userinfo,回调内自写绑定,业务方零感知拿 unionid。
/wx/oauth/* · /sns/userinfo · UserAppBinding
出站消息 & 模板
收口出站验签
业务方推消息直接 POST 网关,不再各自维护 access_token + 模板 ID 表。服务号模板 + 订阅通知统一 sync,占位符自动拆字段。
/internal/wx-push · message/template · wxaapi/newtmpl
一码两机
同代码 × 双公众号
想再接一个公众号? 不需要改代码 — 同份代码部署成两个实例分别绑两个号、两个 DB,顶部色块用 INSTANCE_ACCENT 区分,业务方按命名前缀分流。
wx-gateway · wx-gateway-pucs · 2 PM2 procs

架构 · Same code × 2 instances × 8 apps

一张图看清:微信平台 → 网关双实例 → 8 个业务方

同一份代码 deploy 成两个独立 PM2 进程,各绑一个公众号、一个 Postgres 库。微信侧只看到两个标准回调;业务方侧只看到一组 /internal/* HMAC API。

WeChat Open Platform
微信公众平台 每号:1 callback URL · 1 access_token
回调 + 事件推送
Gateway Instances (same code, 2 deployments)
Instance A · 造悟者
wx-gateway
wx.mvp.restry.cn :3794
/internal/wx-token /wx/qr/[app] /wx/oauth/start /internal/wx-push /internal/wx-userinfo /internal/wx-menu
公众号「造悟者」· 认证服务号 · DB: wx_gateway
Instance B · 莆阳
wx-gateway-pucs
wxmsg.mvp.restry.cn :3800
/internal/wx-token /wx/qr/[app] /wx/oauth/start /internal/wx-push /internal/wx-userinfo /internal/wx-menu
公众号「莆阳网络科技」· 订阅号 · DB: wx_gateway_pucs
fanout (MsgType + scene_str routing)
Business Apps (consumers · HMAC-SHA256 per-app)
ph
PackHorizon
pack
PackSmith
echo
Echo·5 分钟
copilot
api proxy
design
studio prod
menshen
门神面板
cspy_*
莆阳工单
nexora_*
nexora-loop
Instance A · 造悟者(认证服务号) Instance B · 莆阳(订阅号)

关键里程碑 · Timeline

19 天 · 123 commits · 4 个结构性里程碑

从 4 月底支付集成开始,5 月做完 access_token 中央化、per-app HMAC、1:N binding 三块基础设施,中间穿一次"大收敛日"删冗余表与冗余页。⭐ 标记的是结构性里程碑。

2026-04-29 Panel 重构落地:Invites / Scans / Payments / Admins 全部走 card list + dialog,旧 admin 页退役。同日旧 wx-msg-fanout 项目停用。
2026-04-30 ★ 里程碑微信支付 phase A 全链路 · 平台证书 store + JSAPI prepay + v3 回调验签 + 5 分钟 query cron + 统一 /pay/checkout/[id] 收银台 — 10+ commits 一天打完。
2026-05-02 Fanout phase A: MessageRoute 表 + admin tabs,对造悟者实例零影响。
2026-05-03 ★ 里程碑access_token 中央托管上线 · /internal/wx-token + pg_advisory_xact_lock 串行化 + parseScene 改 lastIndexOf 修下划线 app 名(d6f2c1c · 0940f95)。
2026-05-08 README 重写成完整功能矩阵 + 暗色 C4 Container SVG 架构图(取代 mermaid)。
2026-05-12 ★ 里程碑per-app HMAC 取代共享密钥(9752488)· 一个业务方失陷不波及其他;/internal/wx-push 出站消息代理上线;admin 不再污染 UserAppBinding。
2026-05-13 自助 internal API 大爆发一天: wx-config / wx-userinfo / wx-menu / qrcode 全开 · profile-gate scanned/rejected 状态机 · click-key → app 绑定。
2026-05-14 大收敛日:删 FanoutEndpoint / DownstreamHealth 两张表 + Outbound/Fanout Logs 两个独立页;Messages 改统一视图;新接入零配置 — 14 commits 一天。
2026-05-14 模板消息支持完成: message/template + 订阅通知 统一 sync + per-kind test-send + 占位符自动拆字段输入。
2026-05-15 Users tab 加 app filter + 48h 客服窗口 Chat dialog(5s poll)并入 panel 主导航。
2026-05-17 ★ 里程碑1:N 用户-App 绑定上线(145657f)· 同一 openid 可绑多个业务方;admin 自动同步全 App;panel users 可逐项管理 — 至此 wx-gateway 稳态收口。

名场面 · 1 个最值钱的坑(深度)

access_token 从 N 份并发刷拉回 1 份串行化

这条坑文档没写,只能踩。修法看似一行 SQL,但它背后是"微信公众号 access_token 同 AppID 仅一份 + cron 续期与首次 fetch 一定会撞"两条约束叠加 — 任何不串行化的方案都注定在凌晨某个时刻把所有业务方一起踢成 40001

HALL OF FAME · WeChat MP Gotcha #1

多业务方共用一个 access_token:为什么必须 pg_advisory_xact_lock

症状
业务方上线第一天:扫码登录有时正常、有时 40001 invalid credential。日志里多个业务方的 cron 在同一秒去刷 /cgi-bin/token,每次刷新都把上一个调用者的 token 瞬间作废。客服每周收 3 起"登不上"工单。
根因
微信公众号同 AppID 只存一份 access_token — 这是微信侧的硬约束,文档没写"并发不安全",但只要两个进程同时调 cgi-bin/token,后调的返回会让先调的那份立刻失效。在 N 个业务方各自 cron 续期的世界里,这不是概率问题,是必然问题。
修法
(1) 网关独占 cgi-bin/token,业务方一律 HMAC GET /internal/wx-token 取。(2) 网关内部 100 分钟 cron 主动续期;首次 fetch 与 cron 并发用 Postgres advisory 事务锁串行化。(3) 用 $executeRaw 而非 $queryRaw — advisory_lock 返 void,Prisma 拿 void 会抛(fb7441b)。
commit
d6f2c1c 上线 · fb7441b 改 $executeRaw · 9752488 后续把 INTERNAL_TOKEN_SECRET 共享密钥进一步拆成 per-app HMAC,失陷不连坐。
// lib/internal-auth/wx-token.ts — 拿 token 必经此路
await prisma.$executeRaw`SELECT pg_advisory_xact_lock(${LOCK_KEY_WX_TOKEN})`
const cached = await prisma.wxAccessTokenCache.findUnique({...})
if (cached && cached.expiresAt > new Date(Date.now() + 5 * 60_000)) return cached
// 否则 fetch /cgi-bin/token 并 upsert · 整个事务持锁,并发 fetch 自动串行
沉淀:这条坑加上 OAuth PII 政策、fanout owners=1 invariant 两个亲戚,统一沉淀成 hermes skill wechat-mp-multi-tenant-factswechat-mp-profile-pii-gotcha — 下次任何 agent 接微信先读;再加 selftest T-CONTRACT-1 跑 integrate skill 的 sample-client 锁三方契约,改 HMAC payload 时网关代码 + selftest + integrate skill 必须同改,否则业务方全员断签。

📊 当前现状 · Status

五个维度,逐条带具体数字 / 表名 / commit / domain

不写"一切正常 / 反馈正面"这类空话。下面五个分组每条都对应一个可 grep 到的真实事实:生产部署 / 数据规模 / 接入业务方 / 最近 7 天变动 / 运维状态。

生产部署

  • 主实例 · wx.mvp.restry.cn :3794 · 公众号「造悟者」wx225bf76b06064faa · 已认证服务号
  • 莆阳实例 · wxmsg.mvp.restry.cn :3800 · 公众号「莆阳网络科技」· 订阅号
  • PM2 进程:wx-gateway(主)+ wx-gateway-pucs(莆阳),两者均 online
  • 部署链路:mvp-deployer zip → manifest → 异步 build → Caddy 自动 HTTPS
  • 实例区分:INSTANCE_LABEL / ACCENT / EMOJI 三个 env,顶部色块自动渲染

数据规模

  • 21 张表(prisma/schema.prisma):App · AppInvite · AdminUser · UserAppBinding · WxLoginToken · WxMenu · WxTemplate · WxAccessTokenCache · ScanLog
  • 支付域:Payment · PaymentWebhookDelivery · AppPaymentChannel · WxpayPlatformCert · PaymentRefund
  • 消息域:MessageRoute · FanoutLog · OutboundMessageLog
  • 两个独立 Postgres 库:wx_gateway(造悟者)· wx_gateway_pucs(莆阳)
  • fanout 大收敛:已删 FanoutEndpoint · DownstreamHealth 两张表,callbackUrl 收编进 App

接入业务方(造悟者实例 · 8 个)

  • ph PackHorizon · pack PackSmith · echo Echo·5 分钟 AI · admin 网关后台 · selftest 网关自检
  • copilot-proxy(api proxy)· design-studio-prod(design 工厂)· menshen-ui(门神面板)
  • App 列表存于 DB App 表,status='active' 自动加入路由;APPS_JSON env 仅作 fallback

最近 7 天主要变动(git log)

  • 9c1c7cc fix(deploy): exclude local .env files from zip — 部署 zip 不再带 .env,防意外覆盖线上
  • 956e681 feat(api): add DELETE /admin/users/[openid]/binding endpoint — admin 可清单解绑
  • 472b5bb feat(panel/users): show & manage multiple app bindings per user — 1:N 配套 UI
  • 84bb24f fix(migration): strip non-binding drift from 1:N migration — 修迁移脚本副作用
  • 145657f feat(binding): 1:N user-app binding + auto-sync admins to all apps — ★ 1:N 主体上线
  • 4249c32 feat(wx-template): admin-curated enabled flag; fix subscribe endpoint
  • 82ec4aa docs: add project memory.md — 第一份 hermes 项目记忆落盘
  • fea57bd feat(panel/users): app filter + Chat dialog with 48h window + 5s poll

运维状态

  • selftest 30+ case 持续跑(/wx/selftest):每次代码改 / app 增删必跑,当前全 pass
  • access_token 中央缓存 + advisory lock 上线一个月:40001 不再复发,跨业务方 token 互踢工单清零
  • 1:N binding 5/17 上线后稳定,admin 自动同步全 App,panel 可逐项 DELETE,无 drift
  • wxpay 平台证书:cert-store 自动轮换 + 5 分钟 query-cron 主动核对未回调订单
  • T-CONTRACT-1 三方契约 selftest 已就位:改 HMAC payload 时,网关 + selftest + integrate skill 三方必须同改

🚧 未结清债务 · Open Debt

8 条具体待办 · 每条带优先级 / 工作量 / 现状

不写"P1 待办 4 条"这种模糊罗列。每条 4 列:优先级(P0 立刻 / P1 本周 / P2 排期) · 工作量(S < 0.5d · M 1-2d · L 3d+) · 待办 · 现状是什么(看了就知道离修好还差什么)。来源:.hermes/memory.md / README 已知问题 / 近 30 commits / 实际运维。

优先级 工作量 待办 现状(还差什么)
P0 M 部署 task log 屏蔽 prod secrets 已知:cspy 项目部署 phase log 里曾发现 33 条明文 secret/token/password。需在 mvp-deployer phase log 写入前做 *_SECRET|*_TOKEN|*_PASSWORD|.*KEY 关键字 mask,本项目 deploy hook 一并验证。
P1 L 莆阳实例 wxpay 接入 主实例 phase A 已通(平台证书 + JSAPI + 回调 + query cron)。莆阳分支待复制:DB schema 已就位,需配莆阳商户号 + 平台证书 + 回调白名单,再过一遍 selftest。
P1 M 永久 QR 配额监控告警 当前已用约 8/10 万张,微信侧无 webhook 告警。需 daily cron 查 cgi-bin/qrcode/quota 写 metric,余量 < 2 万触发飞书告警。
P1 M template 模板暴露 internal API 目前仅 admin panel 可 test-send。业务方仍走 /internal/wx-push 自己拼模板 ID;需新增 /internal/wx-template/send 让业务方用模板 key 调用,网关查表代填 ID。
P1 S 1:N binding 跨业务方迁移工具 老用户 1:1 → 1:N 已自动迁移完。但跨业务方手动 promote / move 仍只能写 SQL,admin UI 无入口;需 /admin/users/[openid]/transfer + panel 按钮。
P2 M template 出站失败重试 单次失败仅写 OutboundMessageLog 不重发。需加 retry 列 + 5 分钟 cron 扫 status='failed' 重发(指数退避,最多 3 次)。
P2 M 客服 48h Chat 走 internal API 当前 48h 客服窗口 Chat dialog 只在 panel 内嵌(5s poll),业务方调不到。需 /internal/wx-customer-msgcustomservice/sendmsg
P2 S INSTANCE_LABEL i18n 顶部色块标签写死中文(造悟者 / 莆阳),后续若接英文业务方需抽 i18n;目前仅 2 实例,暂可拖。

🛠 技术栈 · Stack

RuntimeNext.js · RSC · pnpm · PM2 DBPostgres · Prisma · advisory_xact_lock AuthHMAC-SHA256 per-app · base64url PaymentWxPay v3 · cert auto-rotate WeChatcgi-bin/* · /sns/userinfo · wxaapi/newtmpl Opsmvp-deployer · Caddy QAselftest · T-CONTRACT-1