OpenID & BindCode:用户绑定流程与二维码
“推送”里最容易被低估的环节,其实不是发消息,而是收集接收者身份。
微信侧的身份是 OpenID(企业微信是 UserID),管理后台不可能凭空知道它。
因此你必须设计一条用户可完成、系统可验证、且足够稳定的绑定流程。
这篇文章的核心是:
- BindCode 为什么是必要的“桥梁”?
- 绑定为什么一定要做成幂等/状态机?
- 在测试号权限受限时,二维码怎么优雅降级?
为什么需要 BindCode(以及它解决的不是“扫码”)
BindCode 的本质不是二维码,而是一个“把外部事件关联到内部对象的关联键”。
你需要它来回答:
这次 subscribe/SCAN 事件,到底应该绑定到哪个 App?
于是流程变成:
- 管理端生成短期有效的
BindCode - 用户扫码或触发关注/扫码事件
- 微信回调把用户 OpenID 带回来
- 系统用 BindCode 找到
appId,完成绑定
一张流程图:绑定不是一次请求,是一次“事件完成”
flowchart LR
A[管理后台生成 BindCode] --> B["用户扫码/关注"]
B --> C["微信回调 POST /wechat"]
C --> D[解析事件 + 提取 scene]
D --> E[BindCode 校验]
E -->|ok| F[OpenID upsert + 索引写入]
E -->"|"expired/consumed"| G["拒绝/提示重新生成"]
注意这里的关键:
- 绑定的完成点在“回调事件”,不是在“生成二维码”。
BindCode 的生命周期:把它当成状态机
典型字段:
codeappIdcreatedAt / expiresAtstatus(pending/consumed/expired)
对应实现通常在:
node-functions/services/bindcode.service.ts
并配有较完整的测试:
node-functions/services/bindcode.service.test.ts
状态机的价值在于:它天然解决“重复事件”的问题。
- 同一个 code 被消费两次,只允许第一次成功
- 过期了就明确失败,不会造成“绑定到一半”的灰度状态
OpenID 的存储关系:幂等的关键在索引
绑定最怕重复写入,所以你需要“唯一性索引”。
常见的 KV 设计是三层:
OPENID(id) -> OpenID recordOPENID_APP(appId) -> [openIdRecordId...]OPENID_INDEX(appId, openId) -> openIdRecordId
其中 OPENID_INDEX 是幂等的核心:
- 如果 index 已存在,就不要重复创建
- 如果不存在,再创建 record 并写入 index
这样可以做到:
- 查询某个 App 的所有绑定用户
- 防重复绑定
- 删除 App 时可级联删除
二维码策略:优雅降级(不要和平台权限硬刚)
项目里 wechat.service.ts 提供了创建临时二维码的能力,但它依赖“认证服务号”权限。
这意味着在测试号场景下,你可能拿不到二维码接口权限。
更稳妥的策略是:
- 能创建二维码就用二维码(体验更好)
- 不支持时就降级为“引导用户触发回调/绑定”
要点是:绑定流程不能把“二维码可用”当成前提。
常见坑与排障
-
用户扫了码但没绑定上:
- 优先检查 bindcode 是否已过期/已被消费
- 再检查回调事件里是否真的带了 scene(不同事件字段不同)
-
重复绑定导致列表膨胀:
- 大概率是
OPENID_INDEX没用好(或者写入顺序导致竞争)
- 大概率是
启发:事件驱动系统里,状态机 + 唯一性索引 = 稳定性
- 绑定不是“写入一次”这么简单,它是外部事件驱动的写入。
- 外部事件必然重复/重试/乱序,幂等不是可选项。
