2026-10-30 增量见文末:同设备换手机号解绑。五字段数据库方案已确认;后端已实现,用户已确认测试库 SQL 执行完成;数据库并发及 App 联调待执行;涉及换号及已有绑定直登的旧描述以增量节为准。
Branch: 029-device-trust-phone-bind | Date: 2026-09-20 | Spec: spec.md
Input: Feature specification from /specs/029-device-trust-phone-bind/spec.md
同一台设备只要验证过一次手机号(短信登录 / 三方短信绑定 / 三方已绑定直登),即建立"设备→手机号"信任;之后该设备上任何未绑定三方身份登录时,返回脱敏手机号 + tempKey 让用户一键确认(免短信),确认后按现有绑定规则完成绑定并签发 token。LINE 网页回调链路通过 state 参数携带设备标识获得同等能力。只改后端,产出前端契约文档。
技术路径:新表 info_user_device(MyBatis-Plus BaseMapper,无 XML)+ 新服务 DeviceTrustService(upsert/查询/脱敏)+ InfoUserController 三接口加 deviceId 入参与 deviceConfirm 分支 + 新接口 oauthDeviceConfirm + LineCallbackController state 解析。临时凭证沿用 oauth:bind: 前缀,新增正交 key oauth:bind:dev:{tempKey} 绑定设备标识,现有解析逻辑零改动。
Language/Version: Java 21(Maven 多模块:ruoyi-admin / ruoyi-system / ruoyi-common)
Primary Dependencies: Spring Boot、MyBatis + MyBatis-Plus(baomidou)、Lombok、RedisCache(Spring Redis)、自写 JwtUtil(HS256 + Redis 会话)、MessageUtils(i18n messages)
Storage: MySQL(新表 info_user_device,DDL 只写 updatesql/sql.md 人工执行);Redis(临时凭证 oauth:bind:*,TTL 5 分钟)
Testing: JUnit 5 + Mockito,测试位于 ruoyi-admin/src/test/java/com/ruoyi/app/user/(沿用 flashdelivery 测试组织方式)。构建命令:临时 JAVA_HOME=%USERPROFILE%\.jdks\graalvm-jdk-21.0.12+7.1,mvn -pl ruoyi-admin -am test -Dtest=XxxTest -DfailIfNoSpecifiedTests=false
Target Platform: Linux 服务器部署的 Spring Boot Web 服务(越南用户端 uni-app 的后端)
Project Type: web-service(单体多模块 Maven)
Performance Goals: 与现有登录接口同量级(服务端处理 < 200ms),仅新增 1 次主键查询/写入,无新增外部调用
Constraints: Controller 参数规范(@RequestBody DTO、DTO 无 Bean Validation 注解、校验放 Controller 用 MessageUtils);模块依赖 ruoyi-admin → ruoyi-system;SQL 不直接执行;保留文件原有 CRLF 换行风格
Scale/Scope: 改 3 个 Controller/DTO 既有文件 + 新增 3 个 Java 文件 + 1 个实体/Mapper + SQL + 测试 + 前端契约文档;前端 uni-app 不在范围
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
.specify/memory/constitution.md 为未填写的空模板,无项目级 gates。以下以 CLAUDE.md 项目规范替代核对:
| 约束 | 结果 |
|---|---|
| Controller 请求参数规范(DTO + @RequestBody,无 Map 入参,无 Bean Validation 注解) | PASS — 新接口用 OAuthDeviceConfirmDto,deviceId 校验在 Controller 内 |
| 模块依赖方向 ruoyi-admin → ruoyi-system | PASS — 实体/Mapper 放 ruoyi-system,服务/编排放 ruoyi-admin |
| 数据库变更写入 updatesql/sql.md 不直接执行 | PASS — 见 tasks |
Java 注释安全(块注释无 */ 序列) |
PASS |
| 已废弃代码清单(不参考不增量修改) | PASS — 不触碰 PayController/ZaloPay 等废弃代码 |
| 后端 i18n 错误消息走 MessageUtils | PASS — 新增消息 key 加到 i18n 资源文件 |
Phase 1 设计后复核:全部 PASS,无 Complexity Tracking 需要填写的违规项。
specs/029-device-trust-phone-bind/
├── plan.md # This file (/speckit-plan command output)
├── research.md # Phase 0 output (/speckit-plan command)
├── data-model.md # Phase 1 output (/speckit-plan command)
├── quickstart.md # Phase 1 output (/speckit-plan command)
├── contracts/
│ └── api-contract.md # 前端(uni-app)接口契约
└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
ruoyi-admin/src/main/java/com/ruoyi/app/user/
├── InfoUserController.java # lodeing/oauthLogin/oauthBindPhone 记录设备信任;oauthLogin 新增 deviceConfirm 分支;新接口 oauthDeviceConfirm
├── LineCallbackController.java # state 解析为 deviceId;未绑定分支查设备信任回跳 deviceConfirm;已绑定路径记录信任
├── dto/
│ ├── OAuthLoginDto.java # +deviceId
│ ├── OAuthBindDto.java # +deviceId
│ └── OAuthDeviceConfirmDto.java # 新增 {tempKey, deviceId}
└── service/
└── DeviceTrustService.java # 新增:recordLogin(upsert)/getTrust/maskPhone
ruoyi-admin/src/test/java/com/ruoyi/app/user/
└── service/
└── DeviceTrustServiceTest.java # 设备记录/覆盖/脱敏规则
└── DeviceTrustFlowTest.java # oauthLogin 分支判定 + oauthDeviceConfirm 成功/过期/不匹配/停用路径
ruoyi-system/src/main/java/com/ruoyi/system/
├── domain/InfoUserDevice.java # 新增实体(@TableName info_user_device)
├── domain/vo/UserDTO.java # +deviceId
└── mapper/InfoUserDeviceMapper.java # 新增(MP BaseMapper,无 XML,同 InfoUserOauthMapper 模式)
ruoyi-system/src/main/resources/i18n/ # 新增消息 key(多语言)
updatesql/sql.md # 建表 DDL 追加(带日期注释)
Structure Decision: 单体多模块 Maven。数据层(实体/Mapper)按既有模式放 ruoyi-system;业务编排与接口改造全部在 ruoyi-admin 的 app.user 包(登录逻辑现状集中在 InfoUserController,保持就近、surgical);新增 DeviceTrustService 承载可单测的纯逻辑(upsert/查询/脱敏),Controller 只做参数校验与编排。
Fill ONLY if Constitution Check has violations that must be justified
无违规项。
采用 writing-plans 工作流,沿用本 spec-kit 目录。按已确认的 id、device_id、oauth_id、create_time、update_time 五字段实施,保留当前分支;本轮包含后端、测试及文档。
新增 info_user_oauth_device(字段见 data-model.md),已有 info_user_device、info_user_oauth 不增删字段。以现有 oauth 绑定主键关联,不重复保存手机号、providerUid 或 userId。迁移追加在 updatesql/sql.md 最后,由开发者手动执行。
ruoyi-system/.../domain/InfoUserOauthDevice.java 与 mapper/InfoUserOauthDeviceMapper.java:设备使用记录实体、幂等写入、按设备与绑定归属查询、按 oauth_id 批量删除;查询归属时关联 info_user_oauth.user_id,避免冗余快照过期。ruoyi-system/.../mapper/InfoUserDeviceMapper.java:提供事务内的设备行锁/并发初始化能力;InfoUserOauthMapper 必要时补充按绑定主键与旧用户同时约束的删除。自定义 SQL 放 Mapper 或对应 XML,遵循现有模块边界。ruoyi-admin/.../user/service/DeviceTrustService.java:分开“验证手机号后切换账号”与“记录第三方使用”两类调用。不能在所有 recordLogin 调用里无条件删除绑定。先锁设备并读取旧用户,再按主键锁定旧用户的用户端绑定,逐条以当前读核验本设备关联,仅对有关联的绑定清理全局绑定及其关联;最后更新设备状态。ruoyi-admin/.../user/InfoUserController.java:手机号新注册/已有账号登录及 oauthBindPhone 均接入切换处理。第三方绑定应完成凭证、验证码及账号校验后,再清理旧绑定并写入新绑定及使用记录。已有绑定并发分支必须核对本次验证用户,不能直接返回旧用户 token。oauthLogin、oauthDeviceConfirm、LineCallbackController:补齐成功登录的设备使用记录;已有绑定与设备当前用户冲突时要求手机号验证;一键确认缓存关联签发时用户,换号后旧凭证失效。首次无设备记录的已有绑定直登可建立信任。新增服务/Mapper 定向测试,扩展 DeviceTrustFlowTest、InfoUserControllerTest、LineCallbackDeviceTrustTest;覆盖 spec 增量验收全部场景。JDK21 执行相关测试及 ruoyi-admin 依赖模块打包;事务和锁语义须在开发者建表后做 MySQL 并发集成验证,Mockito 通过不能代替数据库验证。前端只交付契约,App 接入和双设备联调由前端配合。
工作区已有 040 注销翻悔期修改,涉及相同登录文件;实施时基于最新文件合并,不能覆盖其账号恢复/鉴权处理。保留 040 修改,不将其计为 029 实现;用户随后授权两个功能一并提交。