核心能力 · Five Capabilities
每个能力先回答"业务方原本要付什么代价",再补一行技术细节。所有能力共享一条铁律:业务方禁止自己调微信原生 API,token / push / qrcode / userinfo / template / menu 一律走 /internal/*,网关统一兜底验签、限流、平台 PII 政策。
架构 · Same code × 2 instances × 8 apps
同一份代码 deploy 成两个独立 PM2 进程,各绑一个公众号、一个 Postgres 库。微信侧只看到两个标准回调;业务方侧只看到一组 /internal/* HMAC API。
关键里程碑 · Timeline
从 4 月底支付集成开始,5 月做完 access_token 中央化、per-app HMAC、1:N binding 三块基础设施,中间穿一次"大收敛日"删冗余表与冗余页。⭐ 标记的是结构性里程碑。
名场面 · 1 个最值钱的坑(深度)
这条坑文档没写,只能踩。修法看似一行 SQL,但它背后是"微信公众号 access_token 同 AppID 仅一份 + cron 续期与首次 fetch 一定会撞"两条约束叠加 — 任何不串行化的方案都注定在凌晨某个时刻把所有业务方一起踢成 40001。
多业务方共用一个 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 自动串行
📊 当前现状 · Status
不写"一切正常 / 反馈正面"这类空话。下面五个分组每条都对应一个可 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
不写"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-msg 走 customservice/sendmsg。 |
| P2 | S | INSTANCE_LABEL i18n | 顶部色块标签写死中文(造悟者 / 莆阳),后续若接英文业务方需抽 i18n;目前仅 2 实例,暂可拖。 |