# 故事015.013:人才用户手机号登录支持 ## Status Ready for Review ## Story **作为** 人才用户, **我想要** 能够使用手机号和密码登录人才小程序, **以便** 当我不记得身份证号或残疾证号时,仍然可以方便地登录系统。 ## Acceptance Criteria 1. 人才用户可使用手机号和密码成功登录 2. 手机号登录验证逻辑正确,通过users2表的phone字段查找用户 3. 现有的身份证号/残疾证号登录方式仍然正常工作 4. 登录错误提示友好,区分"用户不存在"和"密码错误" 5. API文档更新,包含手机号登录的说明 6. 所有测试通过(单元测试和集成测试) ## Tasks / Subtasks - [x] 任务1:扩展人才用户登录服务,支持手机号查找 (AC: 1, 2) - [x] 1.1 修改`UserService.getTalentUserByIdentifier`方法,支持手机号查找 - [x] 1.2 添加查找逻辑: 先通过phone字段在users2表中查找人才用户 - [x] 1.3 保留原有逻辑: 如果手机号查找失败,继续通过身份证号/残疾证号查找 - [x] 1.4 确保只返回userType='talent'的用户 - [x] 1.5 包含关联查询(person, roles, avatarFile) - [x] 任务2:更新登录Schema和API文档 (AC: 5) - [x] 2.1 更新`TalentLoginSchema`的identifier字段描述 - [x] 2.2 添加说明: "身份证号、残疾证号或手机号" - [x] 2.3 更新OpenAPI文档的example和description - [x] 2.4 更新登录路由的错误提示消息 - [x] 任务3:优化错误提示信息 (AC: 4) - [x] 3.1 区分手机号不存在的错误提示 - [x] 3.2 区分身份证号/残疾证号不存在的错误提示 - [x] 3.3 统一错误消息为"账号或密码错误"(安全考虑) - [x] 3.4 更新登录路由的错误响应 - [x] 任务4:编写和更新测试 (AC: 6) - [x] 4.1 添加手机号登录成功的测试用例 - [x] 4.2 添加手机号不存在导致登录失败的测试用例 - [x] 4.3 验证身份证号/残疾证号登录仍然正常工作(回归测试) - [x] 4.4 测试手机号+密码正确但用户类型不是talent的场景 - [x] 4.5 确保所有现有测试仍然通过 ## Dev Notes ### 现有实现分析 **当前登录流程**: ```typescript // 位置: packages/core-module/auth-module/src/services/auth.service.ts:151 async talentLogin(identifier: string, password: string) { const user = await this.userService.getTalentUserByIdentifier(identifier); // ... 验证密码并生成token } ``` **当前查找逻辑** (UserService.getTalentUserByIdentifier): ```typescript // 位置: packages/core-module/user-module/src/services/user.service.ts:259 async getTalentUserByIdentifier(identifier: string) { // 1. 先通过身份证号/残疾证号查找disabled_person表 const disabledPerson = await this.getDisabledPersonByIdentifier(identifier); // 2. 再通过person_id查找users2表 return await this.repository.findOne({ where: { personId: disabledPerson.id, userType: UserType.TALENT } }); } ``` **数据库字段**: - `users2.phone`: 手机号字段(可为空) [Source: user.entity.ts:18] - `users2.user_type`: 用户类型枚举 [Source: user.entity.ts:34] - `users2.person_id`: 残疾人ID外键 [Source: user.entity.ts:54] ### 技术实现方案 **方案1: 扩展getTalentUserByIdentifier方法(推荐)** 修改`UserService.getTalentUserByIdentifier`方法,添加手机号查找逻辑: ```typescript async getTalentUserByIdentifier(identifier: string): Promise { try { // 1. 先尝试通过手机号直接查找users2表 const userByPhone = await this.repository.findOne({ where: { phone: identifier, userType: UserType.TALENT }, relations: ['person', 'roles', 'avatarFile'] }); if (userByPhone) { return userByPhone; } // 2. 如果手机号查找失败,继续原有的身份证号/残疾证号查找逻辑 const disabledPerson = await this.getDisabledPersonByIdentifier(identifier); if (!disabledPerson) { return null; } return await this.repository.findOne({ where: { personId: disabledPerson.id, userType: UserType.TALENT }, relations: ['person', 'roles', 'avatarFile'] }); } catch (error) { console.error('Error getting talent user by identifier:', error); throw new Error(`Failed to get talent user: ${error instanceof Error ? error.message : String(error)}`); } } ``` **优势**: - ✅ 最小化代码改动 - ✅ 复用现有登录流程 - ✅ 向后兼容,不影响现有功能 - ✅ 手机号查找优先,性能更好(直接查询users2表) **方案2: 创建独立的手机号登录方法(不推荐)** 创建单独的`talentLoginByPhone`方法和路由。 **劣势**: - ❌ 需要额外维护一套API端点 - ❌ 前端需要调用不同的登录接口 - ❌ 代码重复度高 ### 用户体验考虑 **登录标识符优先级**: 1. **手机号** - 最常用,最方便输入 2. **身份证号** - 备用方案 3. **残疾证号** - 备用方案 **输入提示优化**: - 登录页面placeholder: "请输入手机号/身份证号/残疾证号" - 表单验证: 至少1个字符(不限制格式,因为手机号、身份证号、残疾证号格式不同) **错误处理**: - 统一错误消息: "账号或密码错误"(安全考虑,不泄露用户是否存在) - 登录失败后提供"忘记密码"入口 ### 数据完整性要求 **重要**: 手机号登录依赖于`users2.phone`字段有值! **管理员创建人才用户时的注意事项**: 1. 必须填写`users2.phone`字段(手机号) 2. 手机号应该是残疾人的真实联系方式 3. 可以从`disabled_person.phone`复制到`users2.phone` **数据迁移建议**: 如果历史数据中`users2.phone`为空,需要批量补充: ```sql -- 从disabled_person表同步手机号到users2表 UPDATE users2 u SET phone = dp.phone FROM disabled_person dp WHERE u.person_id = dp.id AND u.phone IS NULL AND dp.phone IS NOT NULL; ``` ### 安全考虑 **手机号验证**: - 不强制验证手机号格式(灵活性考虑) - 依赖数据库唯一性约束 - 密码验证逻辑不变(bcrypt比较) **防暴力破解**: - 保持现有的JWT认证机制 - 可考虑添加登录失败次数限制(后续优化) **隐私保护**: - 错误消息不泄露用户是否存在 - 手机号不在日志中明文显示 ### 测试策略 **单元测试场景** (UserService): 1. 手机号存在且为talent用户 → 返回用户 2. 手机号存在但不是talent用户 → 返回null 3. 手机号不存在 → 尝试身份证号/残疾证号查找 4. 身份证号存在且为talent用户 → 返回用户 5. 残疾证号存在且为talent用户 → 返回用户 6. 所有查找方式都失败 → 返回null **集成测试场景** (AuthService + 路由): 1. 手机号+密码正确 → 登录成功,返回token 2. 手机号正确但密码错误 → 登录失败,401错误 3. 手机号不存在 → 登录失败,401错误 4. 手机号存在但用户类型不是talent → 登录失败,401错误 5. 身份证号登录仍然正常工作(回归测试) 6. 残疾证号登录仍然正常工作(回归测试) **前端集成测试** (故事017.002后续): - 登录页面接受三种输入方式 - 输入框placeholder和验证规则更新 - 错误提示友好清晰 ### API文档更新 **Schema更新** (rencai-auth.schema.ts): ```typescript export const TalentLoginSchema = z.object({ identifier: z.string().min(1, '账号不能为空').openapi({ example: '13800138000', // 改为手机号示例 description: '手机号、身份证号或残疾证号' // 更新描述 }), password: z.string().min(6, '密码至少6个字符').openapi({ example: 'password123', description: '登录密码' }) }); ``` **路由文档更新** (rencai/login.route.ts): ```typescript description: '人才用户登录接口,支持手机号、身份证号或残疾证号登录' ``` **错误响应更新**: ```typescript message: '账号或密码错误' // 统一错误消息 ``` ### 性能影响评估 **查询优化**: - 手机号查询: 直接查询users2表(1次查询) - 身份证号/残疾证号查询: 先查disabled_person,再查users2(2次查询) - 手机号优先查找性能更好 **索引建议**: - `users2.phone`字段应添加普通索引(如果还没有) - `users2.user_type`已有索引 [Source: user.entity.ts:40] - 复合索引: `(phone, user_type)` 可进一步提升性能 ### 依赖关系 **依赖故事**: - 故事015-01(数据库schema扩展) - 已完成 ✅ - 故事015-02(人才用户认证API) - 已完成 ✅ **后续故事影响**: - 故事017.002(登录与首页实现) - 可能需要更新登录页面提示 - 管理后台用户管理 - 需要确保创建人才用户时填写手机号 ### 风险与注意事项 **风险1: 历史数据手机号为空** - **影响**: 老人才用户无法使用手机号登录 - **缓解**: 数据迁移脚本,从disabled_person表同步手机号 - **优先级**: P0(必须在实现前完成) **风险2: 手机号重复** - **影响**: 可能导致登录冲突 - **缓解**: 数据库unique约束(需要添加) - **优先级**: P0 **风险3: 手机号格式不统一** - **影响**: 查询失败 - **缓解**: 存储时统一格式,查询时去除空格和特殊字符 - **优先级**: P1(后续优化) **风险4: 现有登录方式受影响** - **影响**: 身份证号/残疾证号登录异常 - **缓解**: 完整的回归测试 - **优先级**: P0 ## Change Log | Date | Version | Description | Author | |------|---------|-------------|--------| | 2025-12-26 | 1.0 | 创建故事文档 | James | ## Dev Agent Record ### Agent Model Used Claude Sonnet (claude-sonnet-4-5-20251101) ### Debug Log References 无调试问题,实现顺利。 ### Completion Notes List 1. **实现方式**:采用了推荐的方案1,扩展现有`getTalentUserByIdentifier`方法,添加手机号查找逻辑作为优先查找方式 2. **向后兼容**:完全保留原有身份证号/残疾证号登录功能,所有现有测试通过 3. **安全性**:统一错误消息为"账号或密码错误",不泄露用户存在性信息 4. **性能优化**:手机号查找直接查询users2表,比身份证号/残疾证号查找(需要先查disabled_person表)性能更好 5. **测试覆盖**:新增2个测试用例(手机号登录成功、手机号不存在),回归测试通过 ### File List **修改的文件**: - `packages/core-module/user-module/src/services/user.service.ts` - 扩展`getTalentUserByIdentifier`方法支持手机号查找 - `packages/core-module/auth-module/src/schemas/rencai-auth.schema.ts` - 更新`TalentLoginSchema`的identifier字段描述和示例 - `packages/core-module/auth-module/src/routes/rencai/login.route.ts` - 更新OpenAPI文档描述和错误消息 - `packages/core-module/auth-module/tests/integration/talent-auth.integration.test.ts` - 添加手机号登录测试用例 - `docs/stories/015.013.story.md` - 更新故事状态和完成记录