tasks.md 12 KB

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

构建与测试命令(所有验证任务通用)

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

  • T001 基线验证:按上方命令跑 mvn -pl ruoyi-admin -am compile,确认编译通过;工作区无本 feature 之外的意外改动(git status

Phase 2: Foundational (Blocking Prerequisites)

Purpose: 设备信任的数据层与共享构件——所有用户故事都依赖

⚠️ CRITICAL: US1/US2/US3 的实现任务不得在本阶段完成前开始

  • T002 按 data-model.md 的 DDL 在 updatesql/sql.md 末尾追加 info_user_device 建表语句(带 -- 2026-09-20 ... 日期与用途注释;不执行,由开发者手动执行)
  • 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、注释规范、无 */ 序列)
  • T004 [P] 新建 ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserDeviceMapper.javaextends BaseMapper<InfoUserDevice>,注释同 InfoUserOauthMapper 模式(仅 MP CRUD,无 XML)
  • 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(凭证/设备不匹配)加到所有语言变体文件,文案四语对齐
  • T006 [P] DTO 变更:ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/UserDTO.javaruoyi-admin/src/main/java/com/ruoyi/app/user/dto/OAuthLoginDto.javaOAuthBindDto.java 各加 private String deviceId;(注释:设备唯一标识,App 首启生成,可选 ≤64);新建 ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/OAuthDeviceConfirmDto.java(字段 tempKey + deviceId,Lombok @Data,无校验注解)
  • 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 未实现)
  • 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(先写、先红)

  • 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

  • T010 改 ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.javaoauthLogin:未绑定分支内,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
  • 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.recordLoginissueOauthToken;日志风格对齐现有 [OAuth] 前缀
  • 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(先写、先红)

  • T013 TDD 先红:在 DeviceTrustFlowTest 追加用例:① lodeing 短信登录成功且带 deviceId → recordLogin(deviceId, phone, userId) 被调用 ② 不带 deviceId → 不调用 ③ oauthBindPhone 绑定成功且带 deviceId → 被调用 ④ oauthLogin 已绑定直登且带 deviceId → 被调用(手机号取绑定用户)

Implementation for User Story 2

  • T014 InfoUserController.lodeing(约 :941)登录成功 return 前加 deviceTrustService.recordLogin(dto.getDeviceId(), phone, user.getUserId())
  • T015 InfoUserController.oauthBindPhone(约 :1096)issueOauthToken 前加同款调用(dto.getDeviceId())
  • T016 InfoUserController.oauthLogin(约 :1063 已绑定分支)issueOauthToken 前加同款调用(手机号取 u.getPhone()
  • 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(先写、先红)

  • 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

  • T019 改 ruoyi-admin/src/main/java/com/ruoyi/app/user/LineCallbackController.javastate 语义变为 deviceId(注释说明前端契约,保留原参数名);未绑定分支查 deviceTrustService.getTrustedPhone(state),命中 → 写两键 + 302 appRedirect + "?deviceConfirm=1&tempKey=..&maskedPhone=.."(enc 编码),未命中 → 现状 needPhone;已绑定分支发 token 前加 recordLogin(state, u.getPhone(), u.getUserId())(deviceId 合法才调)
  • T020 转绿:LineCallbackDeviceTrustTest 全绿;mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*' 通过

Checkpoint: 三条链路(oauthLogin 直连 / LINE 回调 / 短信绑定)全部具备设备信任能力


Phase 6: Polish & Cross-Cutting Concerns

  • T021 契约一致性核对:逐条比对 specs/029-device-trust-phone-bind/contracts/api-contract.md(§1-§6)与实际实现——入参字段、返回分支、错误 key、LINE 回跳参数,发现偏差改实现或改契约并在契约中注明
  • T022 全量验证:mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*' 全绿 + mvn -pl ruoyi-admin -am compile 通过 + quickstart.md 场景 B(不带 deviceId 零差异回归)手工过一遍;确认未触碰 CLAUDE.md 废弃代码清单中的文件
  • 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: 现有值格式不得改动oauthBindPhoneindexOf('@') 解析)
  • 不触碰 CLAUDE.md 废弃代码清单中的任何文件