UserAppBinding 一对一 → 一对多拆表

爸爸提的 bug:用户在两个 app 都登录,但只有最后一个能 push。

问题复现

⚠ 当前 binding 是 openid PK,后写覆盖前者
T0
用户在 App A 扫码登录
binding = A
T1
A 给用户推消息
✓ 200 ok
T2
用户在 App B 扫码登录
binding = B(覆盖)
T3
A 想给同一用户再推
403 not_bound_to_app
用户**实际同时是 A 和 B 的客户**(都登录过),但 binding 模型只能记一个 → A 失去推送权, 虽然用户从未离开 A 站。

根因 — 一个表干两件事

① fanout 路由(被动消息)

问题
用户发"你好" → 网关该转给谁?
合理模型
一对一(只能转给一家)
✓ 当前 binding 满足

② 主动 push 越权

问题
A 能给"也是 A 客户"的用户推吗?
合理模型
一对多(用户可属于多 app)
✗ 一对一模型把多 app 客户压成一个
两个职责对模型的要求**直接冲突**——塞一张表必然有一个出错。当前出错的是 push 越权。

解法 — 拆成两表,各司其职

UserAppBinding
保留 · 不动
openidPK appNameString boundAtDateTime
一对一,**最后扫者赢**。
职责:决定被动消息 fanout 给谁。
不变:fanout 路由继续读这表,行为完全不变。
UserAppGrant
新增
openidString appNameString grantedAtDateTime sourceoauth | qrcode | subscribe | admin @@id[openid, appName]
**一对多**,只 add 不删(除非 admin revoke)。
职责:授权矩阵——这用户给过哪些 app 接触权。
写入:OAuth/扫码/subscribe 任一动作 → INSERT IGNORE。

两表如何协作 · 数据流

WRITES OAuth callback 用户在 X 站扫码登录 SCAN/subscribe 事件 扫永久码 / 关注 admin Users tab 手动改 binding UserAppBinding UPSERT(openid) 最后写者赢 · 一对一 UserAppGrant INSERT IGNORE 只 add 不删 · 一对多 READS fanout 路由 被动消息派给谁 /internal/wx-push 主动推越权检查 /internal/wx-userinfo 查身份越权检查
UserAppBinding(被动路由,一对一)
UserAppGrant(主动越权,一对多)

3 个 internal API 越权改向

POST /internal/wx-push
UserAppBinding UserAppGrant
调用方在 grant 表 → 允许 push。admin 旁路保留。
GET /internal/wx-userinfo
UserAppBinding UserAppGrant
同上,改查 grant 表防 PII 探测仍然有效。
fanout(被动消息路由)
UserAppBinding(不变)
仍然一对一,最新登录的 app 收消息。逻辑零改动。
admin Users tab
两表都展示 + 可改
显示 binding 主属 app + grant 列表(可 revoke)。

改造前后对照

场景 现在 改后
用户在 A 登录,A 推消息 200 推送成功 200 推送成功
用户又去 B 登录,B 推消息 200 推送成功 200 推送成功
A 再推同一用户(B 登录后) 403 not_bound_to_app 200 推送成功
用户发消息回来 → B(最新登录) → B(不变)
从未登录过的业务方推该用户 403 403 仍越权
用户取消关注公众号 微信侧返 errcode 43004 微信侧返 errcode 43004(不变)

迁移成本

代码改动
  • schema 加 UserAppGrant 表 + db push
  • OAuth callback / SCAN / subscribe 三处加 grant INSERT IGNORE
  • wx-push & wx-userinfo 越权检查改查 grant
  • admin Users tab 显示 grant 列表 + revoke 按钮
  • data migration:把现有 binding 行回填进 grant
不改的
  • fanout 路由代码(读 UserAppBinding 不变)
  • HMAC 签名/token/qrcode/menu/config 接口
  • 业务方接入文档(只新增"用户可属多 app"说明)
  • 所有对外 URL/响应结构
派 CC 实施? 一次任务搞定:加表 / 三处写入 / 两接口越权切换 / 数据迁移 / Users tab 加 grant 列表 / selftest。
~30 min