# Data Model: 设备信任免绑手机号 > 2026-10-30 增量见文末:同设备换手机号解绑。五字段数据库方案已确认;后端已实现,用户已确认测试库 SQL 执行完成;数据库并发及 App 联调待执行;涉及换号及已有绑定直登的旧描述以增量节为准。 **Date**: 2026-09-20 | **Spec**: [spec.md](spec.md) ## 新增实体:InfoUserDevice(表 `info_user_device`) 一台设备一行,`device_id` 主键;记录该设备**最近一次**手机号验证通过的登录。 | 字段 | 类型 | 约束 | 说明 | |---|---|---|---| | device_id | VARCHAR(64) | PK,NOT NULL | 设备唯一标识(App 首启生成持久化,≤64) | | phone | VARCHAR(20) | NOT NULL | 最近一次验证通过的手机号(账号主键,越南 +84) | | user_id | BIGINT | NOT NULL | phone 对应的 info_user.user_id | | create_time | DATETIME | NULL | 首次建立信任时间 | | update_time | DATETIME | NULL | 最近一次登录刷新时间 | **DDL(写入 `updatesql/sql.md`,人工执行):** ```sql -- 2026-09-20 设备绑定手机号信任表(同设备三方登录免绑,029-device-trust-phone-bind) CREATE TABLE info_user_device ( device_id VARCHAR(64) NOT NULL COMMENT '设备唯一标识(App首启生成持久化)', phone VARCHAR(20) NOT NULL COMMENT '本设备最近一次验证通过的手机号', user_id BIGINT NOT NULL COMMENT '对应用户ID(info_user.user_id)', create_time DATETIME DEFAULT NULL COMMENT '首次建立信任时间', update_time DATETIME DEFAULT NULL COMMENT '最近一次登录刷新时间', PRIMARY KEY (device_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备-手机号信任表(029)'; ``` ### 校验与写入规则 - 写入前置校验(Service 层):deviceId 非空且长度 ≤64,否则跳过记录(不报错,静默降级);phone、userId 必须来自**已验证通过的登录成功路径**。 - **Upsert 语义**:`selectById(deviceId)` 命中 → 更新 phone/user_id/update_time(共用设备换号 = 最近一次覆盖);未命中 → insert,create_time = update_time = now。 - 删除:本期不提供解除信任接口(YAGNI);误绑可通过确认框取消避免,后续需要再加。 ### 状态迁移(单行生命周期) ```text [不存在] --首次验证登录(带deviceId)--> [信任 phone_A] [信任 phone_A] --phone_B 在本设备登录成功--> [信任 phone_B](覆盖) [信任 phone_X] --同号再次登录--> [信任 phone_X](仅刷新 update_time) ``` ## 复用/扩展:临时凭证(Redis) | Key | 值 | TTL | 写入点 | 消费点 | |---|---|---|---|---| | `oauth:bind:{tempKey}` | `provider@providerUid`(**现状不变**) | 5 min | oauthLogin / LINE 回调 未绑定分支 | oauthBindPhone / oauthDeviceConfirm | | `oauth:bind:dev:{tempKey}`(**新增**) | `deviceId`(仅 deviceConfirm 分支写入) | 5 min | oauthLogin / LINE 回调 未绑定且设备有信任 | oauthDeviceConfirm | - 两个 key 同 TTL 写入;`oauthDeviceConfirm` 消费时要求两键齐全,且 `dev` 值与入参 deviceId 相等,否则按"凭证与设备不匹配"拒绝。 - 确认成功后两个 key 一并删除(与 oauthBindPhone 删除现有 key 一致)。 ## 复用不变:InfoUserOauth(表 `info_user_oauth`) `UNIQUE(provider, provider_uid)` 语义不变。一键确认 = 向此表新增记录(provider + providerUid → 设备信任手机号对应 userId),插入逻辑与 `oauthBindPhone` 相同。 ## 脱敏规则(maskedPhone) 复用现有 `maskPhone`:长度 ≥8 时返回 `前3位 + "****" + 后4位`(如 `098****4321`);长度不足 8 原样返回。DeviceTrustService 提供该方法,InfoUserController 现有私有实现改调共用(行为不变)。 ## 新增消息 key(i18n 资源文件,多语言) | key | 用途 | |---|---| | `no.oauth.device.confirm` | deviceConfirm 分支的成功提示语 | | `no.oauth.device.mismatch` | 确认接口:tempKey 非设备确认凭证 / dev 键缺失 / deviceId 不匹配 / 设备信任记录不存在 | ## 2026-10-30 新增设备与第三方绑定关联表(五字段结构已确认) | 表 | 本次变化 | 职责 | |---|---|---| | info_user_device | 不改字段 | 一台设备最近验证登录的手机号及用户 | | info_user_oauth | 不改字段及原有唯一约束 | 一个第三方身份绑定一个用户 | | info_user_oauth_device | 新建 | 记录一项现有第三方绑定在哪些设备成功使用过 | ### info_user_oauth_device | 字段 | 类型 | 约束/用途 | |---|---|---| | id | BIGINT | 自增主键,沿用项目实体主键风格 | | device_id | VARCHAR(64) | 非空,与登录请求和 info_user_device.device_id 使用同一值 | | oauth_id | BIGINT | 非空,关联 info_user_oauth.id | | create_time | DATETIME | 首次记录成功使用的时间 | | update_time | DATETIME | 最近一次成功使用的时间 | 唯一索引 `(device_id, oauth_id)`:同一设备同一绑定只保留一行,后续成功登录更新时间。普通索引 `(oauth_id)`:全局绑定删除后,按绑定 ID 清理全部设备关联。 切换账号以验证成功的 user_id 与设备旧 user_id 比较,手机号使用账号记录中的规范值;不能仅因同一手机号的格式差异误判换号。 不额外保存手机号或 user_id;按 oauth_id 查询现有绑定的 user_id 和 provider,避免绑定归属变化后使用冗余旧数据误删。不新增物理外键,关联完整性由同一事务内的写入/删除维护。表不存第三方 token,也不采集设备型号等信息。 ### 事务示例 D1→A,关联行为 (D1, X→A)、(D1, Y→A);B 验证码登录成功后,删除 X→A 与 Y→A 及其全部关联,再更新 D1→B。若本次为 Google Z 绑定 B,则在同一事务内新增 Z→B 和 (D1, Z→B),不能被旧账号清理步骤误删。 同一绑定可有 D1/D2 两行,始终只对应一个 user_id。解绑是删除这一条全局绑定;其他设备以后需重新绑定。设备信任记录不因第三方解绑被全部清空。 ### 上线约定 本次仅新增一个表,DDL 见 updatesql/sql.md 的 2026-10-30 / 029 节。开发者计划上线前清理旧绑定,清理范围应区分用户端与商家/骑手渠道;本轮不生成或执行清空数据语句,不回填旧关系。新表从空表开始,之后成功的第三方登录/绑定写入关联。 Redis 确认凭证需要关联签发时 user_id,防止设备换号后旧确认弹窗绑定到新用户;这属于缓存契约调整,不增加 MySQL 字段。