2. 账号体系数据模型设计
账号体系的数据模型不是“把用户信息存进一张表”这么简单。生产环境中,一个账号往往同时具备密码登录、手机号验证码登录、微信/Google 等第三方登录、多设备在线、多端会话并存等能力。如果把这些信息全部塞进一张宽表,会立刻遇到三个问题:第三方绑定数量不确定、设备与会话数量持续增长、凭证(密码/密钥)与用户资料的生命周期完全不同。因此本节采用“用户主表 + 凭证表 + 第三方绑定表 + 设备表 + 会话表”的五表结构,把稳定身份、认证凭据、外部身份、物理载体、登录态分层管理。
五张表的职责与关系
用户表(user)是账号的“根”,只存稳定且唯一的身份信息:内部用户 ID(uid)、账号状态、注册来源、创建时间、软删除标记。它不存密码、不存 token、不存设备信息。凭证表(credential)存“证明你是你”的材料,例如密码哈希、盐值、密码算法版本、最近修改时间;一个用户可以有多种凭证类型(password、pin、recovery_code)。第三方绑定表(third_party_bind)存外部身份映射:平台(wechat/google/apple)、外部唯一标识 open_id/union_id、绑定时间、解绑状态。设备表(device)存物理或逻辑设备:设备指纹、机型、系统版本、首次出现时间、可信标记。会话表(session)存登录态:会话 ID、所属 uid、所属设备、刷新令牌哈希、过期时间、状态。
关系上:user 与 credential 是一对多(同一用户可同时有密码和 PIN);user 与 third_party_bind 是一对多;user 与 device 是一对多;device 与 session 是一对多(同一设备可先后产生多个会话);user 与 session 是一对多。所有子表通过 uid 外键关联到 user,删除用户时以 uid 为锚点做级联软删除。
唯一索引:把业务规则写进数据库
唯一索引是账号体系最便宜的一致性保障。核心约束有四条:user 表对手机号、邮箱分别建唯一索引(若允许为空,需注意多数数据库允许多个 NULL,因此“未绑定”不会冲突);credential 表建 (uid, type) 唯一索引,防止同一用户出现两条 password 记录;third_party_bind 表建 (platform, open_id) 唯一索引,这是防止“同一微信被绑定到两个账号”的关键;device 表建 (uid, device_fp) 唯一索引,避免同一设备在同一账号下重复登记。session 表通常不建业务唯一索引,但要对 refresh_hash 建唯一索引,防止刷新令牌被复制后重复使用。
-- 关键唯一索引示例(MySQL 语法)
ALTER TABLE user ADD UNIQUE KEY uk_phone (phone);
ALTER TABLE user ADD UNIQUE KEY uk_email (email);
ALTER TABLE credential ADD UNIQUE KEY uk_uid_type (uid, type);
ALTER TABLE third_party_bind ADD UNIQUE KEY uk_platform_openid (platform, open_id);
ALTER TABLE device ADD UNIQUE KEY uk_uid_fp (uid, device_fp);
ALTER TABLE session ADD UNIQUE KEY uk_refresh_hash (refresh_hash);
注意软删除与唯一索引的冲突:如果用户注销后把 deleted_at 置为时间戳,但 phone 仍保留原值,那么该手机号无法再注册新账号。常见解法有两种:一是注销时把 phone 改写为 phone + "_deleted_" + uid 之类的占位值;二是使用“部分唯一索引”(PostgreSQL 支持 WHERE deleted_at IS NULL),MySQL 则可用生成列配合唯一索引模拟。
软删除与状态机
账号数据不建议物理删除,原因有三:审计与合规需要保留操作痕迹;第三方绑定解绑后短期内可能被重新绑定,需要历史记录;误删恢复是真实存在的客服场景。软删除统一用 deleted_at 字段,NULL 表示未删除。查询时所有业务 SQL 必须带 deleted_at IS NULL,建议在 ORM 层用全局 Scope 统一注入,避免遗漏。
状态机与软删除是两个维度:status 描述账号“能不能用”,deleted_at 描述账号“是否已注销”。ACTIVE 可登录;SUSPENDED 因风控被临时限制,可申诉恢复;LOCKED 因多次密码错误或安全事件被锁定,需验证身份解锁;DELETED 是终态,只能走恢复流程。状态迁移必须集中在一个服务方法里完成,禁止各业务模块直接 UPDATE status,否则很容易出现“已注销账号还能登录”的漏洞。
多端数据一致性考量
多端场景下最典型的不一致是:用户在 A 端修改密码,B 端的旧会话仍然有效。解决思路是“会话版本号 + 主动失效”。在 user 表增加 credential_version,每次修改密码或解绑第三方时自增;session 表记录签发时的版本号。校验会话时比较两者,不一致则强制下线。另一种做法是修改密码后按 uid 批量把该用户所有 session 置为 REVOKED,但要注意多设备同时在线时不能误伤正在使用的设备——通常保留当前设备会话,其余全部失效。
设备表与第三方绑定表的一致性同样重要:解绑微信时,应同时把 third_party_bind 对应记录置为解绑状态,并检查该用户是否还有其它可用登录方式,若没有则提示先设置密码,避免账号变成“无法登录”的孤儿账号。这类跨表校验建议放在同一个事务中,或使用领域事件 + 补偿任务保证最终一致。
把五张表的职责、唯一索引、软删除、状态机和会话版本号设计清楚,后续的登录、注册、改密、多端互踢才有稳定的数据基础。数据模型一旦上线再改,代价远高于前期多花半天推演。