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.
[ID] [P?] [Story] Descriptionexport 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 无关,不修。
Purpose: 确认基线干净,避免把存量问题带进本 feature
mvn -pl ruoyi-admin -am compile,确认编译通过;工作区无本 feature 之外的意外改动(git status)Purpose: 设备信任的数据层与共享构件——所有用户故事都依赖
⚠️ CRITICAL: US1/US2/US3 的实现任务不得在本阶段完成前开始
updatesql/sql.md 末尾追加 info_user_device 建表语句(带 -- 2026-09-20 ... 日期与用途注释;不执行,由开发者手动执行)ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUserDevice.java:@TableName("info_user_device"),字段 deviceId/phone/userId/createTime/updateTime,风格参照 InfoUserOauth.java(Lombok、注释规范、无 */ 序列)ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserDeviceMapper.java:extends BaseMapper<InfoUserDevice>,注释同 InfoUserOauthMapper 模式(仅 MP CRUD,无 XML)grep -rn "no.oauth.tempkey.expired" ruoyi-system/src/main/resources 定位 messages 资源文件,把 no.oauth.device.confirm(deviceConfirm 分支提示语)与 no.oauth.device.mismatch(凭证/设备不匹配)加到所有语言变体文件,文案四语对齐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,无校验注解)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 未实现)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 可并行开始
Goal: oauthLogin 未绑定时按设备信任返回 deviceConfirm 分支;新接口 oauthDeviceConfirm 免短信完成绑定登录
Independent Test: 测试内直接向 info_user_device 造一条信任记录(不依赖 US2 的记录点),断言 oauthLogin 返回 deviceConfirm、oauthDeviceConfirm 返回 token
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 ⑦ 期间已被绑定 → 容错直登返回 tokenruoyi-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);否则维持现状 needPhoneInfoUserController.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] 前缀DeviceTrustFlowTest 全绿;mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*' 通过Checkpoint: US1 独立可演示(手工插信任记录 + curl 按 quickstart 场景 A 验证)
Goal: 四种登录成功点自动 upsert 设备信任;共用设备以最近一次为准
Independent Test: mock 断言各登录成功路径调用了 recordLogin(deviceId 缺省时不调用)
DeviceTrustFlowTest 追加用例:① lodeing 短信登录成功且带 deviceId → recordLogin(deviceId, phone, userId) 被调用 ② 不带 deviceId → 不调用 ③ oauthBindPhone 绑定成功且带 deviceId → 被调用 ④ oauthLogin 已绑定直登且带 deviceId → 被调用(手机号取绑定用户)InfoUserController.lodeing(约 :941)登录成功 return 前加 deviceTrustService.recordLogin(dto.getDeviceId(), phone, user.getUserId())InfoUserController.oauthBindPhone(约 :1096)issueOauthToken 前加同款调用(dto.getDeviceId())InfoUserController.oauthLogin(约 :1063 已绑定分支)issueOauthToken 前加同款调用(手机号取 u.getPhone())DeviceTrust*Test 全绿;重复登录换号场景按 data-model.md 状态迁移人工核验Checkpoint: 真实链路下"手机号登录一次 → 三方免绑"端到端成立(quickstart 场景 A 完整闭环)
Goal: LINE 网页回调链路通过 state=deviceId 获得同等的 deviceConfirm 能力
Independent Test: mock OAuthVerifyService/RedisCache 断言回调 302 的 Location 参数分支
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 被调用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 合法才调)LineCallbackDeviceTrustTest 全绿;mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*' 通过Checkpoint: 三条链路(oauthLogin 直连 / LINE 回调 / 短信绑定)全部具备设备信任能力
specs/029-device-trust-phone-bind/contracts/api-contract.md(§1-§6)与实际实现——入参字段、返回分支、错误 key、LINE 回跳参数,发现偏差改实现或改契约并在契约中注明mvn -pl ruoyi-admin -am test -Dtest='DeviceTrust*,LineCallback*' 全绿 + mvn -pl ruoyi-admin -am compile 通过 + quickstart.md 场景 B(不带 deviceId 零差异回归)手工过一遍;确认未触碰 CLAUDE.md 废弃代码清单中的文件git status 核对本 feature 改动清单与 plan.md Project Structure 一致;把需要人工执行的事项(updatesql/sql.md 新增 DDL、前端契约移交 uni-app 团队)在交付说明中列出info_user_device 插一行即可端到端演示"同设备三方免绑"MessageUtils.message(...),禁止硬编码中文错误oauth:bind: 现有值格式不得改动(oauthBindPhone 用 indexOf('@') 解析)