# Tasks: 设备信任免绑手机号 **Input**: Design documents from `/specs/029-device-trust-phone-bind/`(spec.md / plan.md / research.md / data-model.md / contracts/api-contract.md / quickstart.md) **Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ **Tests**: 本 feature 的 spec(SC-005)明确要求服务层自动化测试,且采用 TDD:每阶段先写测试确认红,再实现转绿。 **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. ## Format: `[ID] [P?] [Story] Description` - **[P]**: Can run in parallel (different files, no dependencies) - **[Story]**: Which user story this task belongs to (e.g. US1, US2, US3) - Include exact file paths in descriptions ## 构建与测试命令(所有验证任务通用) ```bash export JAVA_HOME="$HOME/.jdks/graalvm-jdk-21.0.12+7.1" # 本机实际路径,勿改全局 export PATH="$JAVA_HOME/bin:$PATH" mvn -pl ruoyi-admin -am test -Dtest='XxxTest' -Dsurefire.failIfNoSpecifiedTests=false ``` 已知基线红测试:flash payType 白名单(createRejectsUnsupportedPayType)为预期失败,与本 feature 无关,不修。 --- ## Phase 1: Setup (Shared Infrastructure) **Purpose**: 确认基线干净,避免把存量问题带进本 feature - [x] T001 基线验证:按上方命令跑 `mvn -pl ruoyi-admin -am compile`,确认编译通过;工作区无本 feature 之外的意外改动(`git status`) --- ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: 设备信任的数据层与共享构件——所有用户故事都依赖 **⚠️ CRITICAL**: US1/US2/US3 的实现任务不得在本阶段完成前开始 - [x] T002 按 data-model.md 的 DDL 在 `updatesql/sql.md` 末尾追加 `info_user_device` 建表语句(带 `-- 2026-09-20 ...` 日期与用途注释;**不执行**,由开发者手动执行) - [x] T003 [P] 新建实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUserDevice.java`:`@TableName("info_user_device")`,字段 deviceId/phone/userId/createTime/updateTime,风格参照 `InfoUserOauth.java`(Lombok、注释规范、无 `*/` 序列) - [x] T004 [P] 新建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserDeviceMapper.java`:`extends BaseMapper`,注释同 `InfoUserOauthMapper` 模式(仅 MP CRUD,无 XML) - [x] T005 [P] 新增 i18n 消息 key:先 `grep -rn "no.oauth.tempkey.expired" ruoyi-system/src/main/resources` 定位 messages 资源文件,把 `no.oauth.device.confirm`(deviceConfirm 分支提示语)与 `no.oauth.device.mismatch`(凭证/设备不匹配)加到**所有语言变体文件**,文案四语对齐 - [x] T006 [P] DTO 变更:`ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/UserDTO.java`、`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/OAuthLoginDto.java`、`OAuthBindDto.java` 各加 `private String deviceId;`(注释:设备唯一标识,App 首启生成,可选 ≤64);新建 `ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/OAuthDeviceConfirmDto.java`(字段 tempKey + deviceId,Lombok @Data,无校验注解) - [x] T007 TDD 先红:新建 `ruoyi-admin/src/test/java/com/ruoyi/app/user/service/DeviceTrustServiceTest.java`,Mockito 单测:① `isValidDeviceId`(null/空/64 位/65 位)② `recordLogin` 首次 insert(create_time=update_time=now)③ 再次同号登录仅刷新 update_time ④ 换号登录覆盖 phone/user_id ⑤ 非法 deviceId 静默跳程不调 mapper ⑥ `maskPhone`(正常 10 位、短号原样返回)。跑测试确认**编译通过但断言红**(service 未实现) - [x] T008 新建 `ruoyi-admin/src/main/java/com/ruoyi/app/user/service/DeviceTrustService.java` 实现 `isValidDeviceId/recordLogin/getTrustedPhone(deviceId)/maskPhone(phone)`(recordLogin=selectById 命中则 update phone/user_id/update_time,否则 insert;依赖注入 `InfoUserDeviceMapper`),使 T007 转绿 **Checkpoint**: 数据层+服务就绪,`DeviceTrustServiceTest` 全绿;US1/US2/US3 可并行开始 --- ## Phase 3: User Story 1 - 同设备三方登录免短信一键绑定 (Priority: P1) 🎯 MVP **Goal**: `oauthLogin` 未绑定时按设备信任返回 `deviceConfirm` 分支;新接口 `oauthDeviceConfirm` 免短信完成绑定登录 **Independent Test**: 测试内直接向 `info_user_device` 造一条信任记录(不依赖 US2 的记录点),断言 oauthLogin 返回 deviceConfirm、oauthDeviceConfirm 返回 token ### Tests for User Story 1(先写、先红) - [x] T009 TDD 先红:新建 `ruoyi-admin/src/test/java/com/ruoyi/app/user/DeviceTrustFlowTest.java`(参考 `FlashDeliveryControllerContractTest` 的 Mockito 实例化 Controller 方式),覆盖:① oauthLogin 未绑定+deviceId 有信任 → data 含 `status="deviceConfirm"`、tempKey、`maskedPhone="098****4321"`,且 Redis 写入 `oauth:bind:{tempKey}` 与 `oauth:bind:dev:{tempKey}` 两键 ② oauthLogin 未绑定+无信任/无 deviceId → 仍是 `needPhone`(回归) ③ oauthDeviceConfirm 成功:token 返回 + `info_user_oauth` insert + 两个 Redis key 删除 + 已有用户关联/新用户创建(含 createUserWallet) ④ tempKey 过期 → `no.oauth.tempkey.expired` ⑤ dev 键缺失或 deviceId 不等 → `no.oauth.device.mismatch` 且无绑定 ⑥ 信任手机号账号 status≠0 → `no.user.stop` ⑦ 期间已被绑定 → 容错直登返回 token ### Implementation for User Story 1 - [x] T010 改 `ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java` 的 `oauthLogin`:未绑定分支内,`dto.getDeviceId()` 合法且 `deviceTrustService.getTrustedPhone` 命中时,同时写 `oauth:bind:{tempKey}=provider@providerUid`(现状不变)与 `oauth:bind:dev:{tempKey}=deviceId`(同 5 分钟 TTL),返回 `{status:"deviceConfirm", tempKey, maskedPhone}`(提示语 `no.oauth.device.confirm`);否则维持现状 needPhone - [x] T011 在 `InfoUserController.java` 新增 `@Anonymous @PostMapping("/oauthDeviceConfirm")`,入参 `@RequestBody OAuthDeviceConfirmDto`:校验 tempKey/deviceId 非空(`no.oauth.tempkey.missing`)→ 读 `oauth:bind:{tempKey}`(空=`no.oauth.tempkey.expired`)→ 读 `oauth:bind:dev:{tempKey}` 并与入参 deviceId 相等、且设备信任记录仍存在(否则 `no.oauth.device.mismatch`)→ 复用 `oauthBindPhone` 的"期间已绑定容错直登/get-or-create 用户(停用拒绑 no.user.stop)/insert info_user_oauth"逻辑 → 删两个 Redis key → `deviceTrustService.recordLogin` → `issueOauthToken`;日志风格对齐现有 `[OAuth]` 前缀 - [x] T012 转绿与回归:`DeviceTrustFlowTest` 全绿;`mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*'` 通过 **Checkpoint**: US1 独立可演示(手工插信任记录 + curl 按 quickstart 场景 A 验证) --- ## Phase 4: User Story 2 - 设备信任的建立与更新 (Priority: P2) **Goal**: 四种登录成功点自动 upsert 设备信任;共用设备以最近一次为准 **Independent Test**: mock 断言各登录成功路径调用了 `recordLogin`(deviceId 缺省时不调用) ### Tests for User Story 2(先写、先红) - [x] T013 TDD 先红:在 `DeviceTrustFlowTest` 追加用例:① `lodeing` 短信登录成功且带 deviceId → `recordLogin(deviceId, phone, userId)` 被调用 ② 不带 deviceId → 不调用 ③ `oauthBindPhone` 绑定成功且带 deviceId → 被调用 ④ `oauthLogin` 已绑定直登且带 deviceId → 被调用(手机号取绑定用户) ### Implementation for User Story 2 - [x] T014 `InfoUserController.lodeing`(约 :941)登录成功 return 前加 `deviceTrustService.recordLogin(dto.getDeviceId(), phone, user.getUserId())` - [x] T015 `InfoUserController.oauthBindPhone`(约 :1096)`issueOauthToken` 前加同款调用(dto.getDeviceId()) - [x] T016 `InfoUserController.oauthLogin`(约 :1063 已绑定分支)`issueOauthToken` 前加同款调用(手机号取 `u.getPhone()`) - [x] T017 转绿:`DeviceTrust*Test` 全绿;重复登录换号场景按 data-model.md 状态迁移人工核验 **Checkpoint**: 真实链路下"手机号登录一次 → 三方免绑"端到端成立(quickstart 场景 A 完整闭环) --- ## Phase 5: User Story 3 - LINE 回调免绑 (Priority: P3) **Goal**: LINE 网页回调链路通过 `state=deviceId` 获得同等的 deviceConfirm 能力 **Independent Test**: mock OAuthVerifyService/RedisCache 断言回调 302 的 Location 参数分支 ### Tests for User Story 3(先写、先红) - [x] T018 TDD 先红:新建 `ruoyi-admin/src/test/java/com/ruoyi/app/user/LineCallbackDeviceTrustTest.java`:① 未绑定+state 为有信任的 deviceId → Location 含 `deviceConfirm=1`、tempKey、URL 编码的 maskedPhone,且两个 Redis key 写入 ② 未绑定+state 为陌生值/空 → Location 仍为 `needPhone=1&tempKey`(回归) ③ 已绑定+state=deviceId → Location 含 token 且 `recordLogin` 被调用 ### Implementation for User Story 3 - [x] T019 改 `ruoyi-admin/src/main/java/com/ruoyi/app/user/LineCallbackController.java`:`state` 语义变为 deviceId(注释说明前端契约,保留原参数名);未绑定分支查 `deviceTrustService.getTrustedPhone(state)`,命中 → 写两键 + 302 `appRedirect + "?deviceConfirm=1&tempKey=..&maskedPhone=.."`(enc 编码),未命中 → 现状 needPhone;已绑定分支发 token 前加 `recordLogin(state, u.getPhone(), u.getUserId())`(deviceId 合法才调) - [x] T020 转绿:`LineCallbackDeviceTrustTest` 全绿;`mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*'` 通过 **Checkpoint**: 三条链路(oauthLogin 直连 / LINE 回调 / 短信绑定)全部具备设备信任能力 --- ## Phase 6: Polish & Cross-Cutting Concerns - [x] T021 契约一致性核对:逐条比对 `specs/029-device-trust-phone-bind/contracts/api-contract.md`(§1-§6)与实际实现——入参字段、返回分支、错误 key、LINE 回跳参数,发现偏差改实现或改契约并在契约中注明 - [x] T022 全量验证:`mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*'` 全绿 + `mvn -pl ruoyi-admin -am compile` 通过 + quickstart.md 场景 B(不带 deviceId 零差异回归)手工过一遍;确认未触碰 CLAUDE.md 废弃代码清单中的文件 - [x] T023 收尾:`git status` 核对本 feature 改动清单与 plan.md Project Structure 一致;把需要人工执行的事项(`updatesql/sql.md` 新增 DDL、前端契约移交 uni-app 团队)在交付说明中列出 --- ## Dependencies & Execution Order ### Phase Dependencies - **Setup (Phase 1)**: 无依赖,立即开始 - **Foundational (Phase 2)**: 依赖 Phase 1;**阻塞所有用户故事** - **US1 (Phase 3) / US2 (Phase 4) / US3 (Phase 5)**: 均依赖 Phase 2 完成;彼此可并行(不同方法/文件区域),单人则按 P1→P2→P3 - **Polish (Phase 6)**: 依赖全部所需故事完成 ### User Story Dependencies - **US1**: 独立可测(测试直接造 info_user_device 数据,不依赖 US2) - **US2**: 独立可测(mock 断言 recordLogin 调用);真实端到端闭环需 US1+US2 都完成 - **US3**: 独立可测;依赖 Phase 2 的 DeviceTrustService(所有故事共享) ### Within Each User Story - 测试任务先写并确认红 → 实现转绿(TDD) - 同一 Controller 文件的任务(T010/T011、T014-T016)不做并行标记,避免编辑冲突 ### Parallel Opportunities - Phase 2 的 T003/T004/T005/T006 互不相关可并行(不同文件) - US1 的测试任务与 US2 的测试任务分属不同测试文件可并行 - 多人时 US1/US3 可分人(InfoUserController vs LineCallbackController),US2 与 US1 同文件建议同人 --- ## Implementation Strategy ### MVP First (Phase 1-3 + 手工造数据) 1. Phase 1 基线 → Phase 2 数据层 → Phase 3 US1 2. 手工向 `info_user_device` 插一行即可端到端演示"同设备三方免绑" 3. US2 完成后无需手工造数据,真实登录即建立信任 ### Incremental Delivery 1. Setup + Foundational → 数据层就绪(可先交付 SQL 给 DBA 排期) 2. +US1 → 免绑核心(MVP) 3. +US2 → 信任自动建立(完整闭环) 4. +US3 → LINE 链路补齐 5. Polish → 契约核对 + 回归 + 交付说明 --- ## Notes - 所有 Java/资源文件保留原有 CRLF 换行风格,禁止全文件重排 - Controller 校验一律 `MessageUtils.message(...)`,禁止硬编码中文错误 - `oauth:bind:` 现有值格式**不得改动**(`oauthBindPhone` 用 `indexOf('@')` 解析) - 不触碰 CLAUDE.md 废弃代码清单中的任何文件