浏览代码

docs(epic-015): 新增故事015.013 - 人才用户手机号登录支持

功能说明:
- 允许人才用户使用手机号登录,提升用户体验
- 保持向后兼容,身份证号/残疾证号登录仍然可用
- 使用现有users2.phone字段,无需数据库变更

技术方案:
- 扩展UserService.getTalentUserByIdentifier方法
- 手机号查找优先(1次查询),失败后继续身份证号/残疾证号查找
- 更新登录Schema和API文档

数据要求:
- 管理员创建人才用户时必须填写手机号
- 提供数据迁移SQL脚本从disabled_person表同步手机号

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 3 周之前
父节点
当前提交
e692fae613
共有 2 个文件被更改,包括 374 次插入4 次删除
  1. 71 4
      docs/prd/epic-015-talent-mini-program-api-support.md
  2. 303 0
      docs/stories/015.013.story.md

+ 71 - 4
docs/prd/epic-015-talent-mini-program-api-support.md

@@ -40,7 +40,7 @@
 **新增/变更内容:**
 为人才小程序补充6大类API接口,包括:
 1. 数据库schema扩展:扩展用户表支持人才用户类型、添加打卡记录相关字段
-2. 人才用户认证API:支持人才用户身份证号/残疾证号密码登录、获取人才用户信息
+2. 人才用户认证API:支持人才用户手机号/身份证号/残疾证号密码登录、获取人才用户信息
 3. 个人信息查询API:人才基本信息查询、银行卡信息查询、证件照片查询
 4. 考勤记录API:打卡记录查询、考勤统计、考勤日历、打卡明细
 5. 就业信息API:当前就业状态查询、薪资记录查询、就业历史查询
@@ -394,6 +394,72 @@
 
 **建议:** 此故事作为基础设施任务已由各故事分别覆盖,无需单独实现。保持各故事的文档和测试要求即可。
 
+### 故事015-13:人才用户手机号登录支持 🆕
+**状态**: ✅ Approved
+**优先级**: P1 - 用户体验改进(强烈建议)
+
+**背景:** 当前人才用户登录仅支持身份证号/残疾证号,用户反馈使用不方便,希望支持手机号登录,提升登录体验。
+
+**用户价值:**
+- 手机号更容易记忆,不需要翻看证件
+- 手机号输入更快捷,提升用户体验
+- 符合主流APP登录习惯(手机号+密码)
+
+**技术方案:**
+- 扩展`UserService.getTalentUserByIdentifier`方法,支持手机号查找
+- 手机号查找优先级:先通过`users2.phone`查找,失败后继续身份证号/残疾证号查找
+- 使用现有`users2`表的`phone`字段(无需数据库变更)
+- 保持向后兼容,身份证号/残疾证号登录仍然可用
+
+**任务列表:**
+1. **扩展登录服务方法** (UserService):
+   - 修改`getTalentUserByIdentifier`方法,添加手机号查找逻辑
+   - 先通过`phone`字段在`users2`表查找`userType='talent'`的用户
+   - 如果手机号查找失败,继续原有的身份证号/残疾证号查找逻辑
+   - 确保包含关联查询(person, roles, avatarFile)
+
+2. **更新Schema和API文档**:
+   - 更新`TalentLoginSchema`的identifier字段描述为"手机号、身份证号或残疾证号"
+   - 更新OpenAPI文档的example和description
+   - 更新登录路由的错误提示消息
+
+3. **优化错误提示**:
+   - 统一错误消息为"账号或密码错误"(安全考虑)
+   - 更新登录路由的错误响应
+
+4. **编写测试**:
+   - 添加手机号登录成功的测试用例
+   - 添加手机号不存在导致登录失败的测试用例
+   - 回归测试:验证身份证号/残疾证号登录仍然正常
+   - 测试手机号+密码正确但用户类型不是talent的场景
+   - 确保所有现有测试仍然通过
+
+**数据完整性要求:**
+- **重要**: 手机号登录依赖于`users2.phone`字段有值
+- 管理员创建人才用户时必须填写手机号
+- 建议从`disabled_person.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;
+```
+
+**验收标准:**
+- [ ] 人才用户可使用手机号和密码成功登录
+- [ ] 手机号登录验证逻辑正确,通过users2表的phone字段查找用户
+- [ ] 现有的身份证号/残疾证号登录方式仍然正常工作
+- [ ] 登录错误提示友好,统一为"账号或密码错误"
+- [ ] API文档更新,包含手机号登录的说明
+- [ ] 所有测试通过(单元测试和集成测试)
+
+**详细设计文档**: [docs/stories/015.013.story.md](../stories/015.013.story.md)
+
 ## 史诗进度
 
 **当前状态:** 史诗执行阶段,3个核心故事已完成,5个核心故事待实现。
@@ -411,11 +477,12 @@
 - [ ] **故事015-10**:路由路径规范与API客户端 - **待实现**
 - [ ] **故事015-11**:高级功能与优化 - **P2 - 延期**(后期优化)
 - [ ] **故事015-12**:API文档与测试完善 - **冗余**(基础设施已覆盖)
+- [ ] **故事015-13**:人才用户手机号登录支持 - **待实现** 🆕
 
-**总体进度:** 3/12 故事完成(25%)
-**MVP进度:** 3/8 核心故事完成(38%,排除015-04、015-08、015-11延期和015-12冗余)
+**总体进度:** 3/13 故事完成(23%)
+**MVP进度:** 3/9 核心故事完成(33%,排除015-04、015-08、015-11延期和015-12冗余)
 
-**最近更新:** 2025-12-25 - 故事015-03已完成:个人信息查询API、银行卡信息查询API(卡号脱敏)、证件照片查询API、所有11个集成测试通过。2025-12-25 - 故事015-02已完成:人才用户认证API、JWT登录、退出登录、获取用户信息、认证中间件、所有16个测试通过。2025-12-24 - 故事015-01已完成:UserType枚举扩展、personId字段添加、TypeORM实体和Schema更新、测试通过。
+**最近更新:** 2025-12-26 - 故事015.013已创建:人才用户手机号登录支持,允许人才用户使用手机号/身份证号/残疾证号登录,提升用户体验。2025-12-25 - 故事015-03已完成:个人信息查询API、银行卡信息查询API(卡号脱敏)、证件照片查询API、所有11个集成测试通过。2025-12-25 - 故事015-02已完成:人才用户认证API、JWT登录、退出登录、获取用户信息、认证中间件、所有16个测试通过。2025-12-24 - 故事015-01已完成:UserType枚举扩展、personId字段添加、TypeORM实体和Schema更新、测试通过。
 
 ---
 

+ 303 - 0
docs/stories/015.013.story.md

@@ -0,0 +1,303 @@
+# 故事015.013:人才用户手机号登录支持
+
+## Status
+
+Approved
+
+## Story
+
+**作为** 人才用户,
+**我想要** 能够使用手机号和密码登录人才小程序,
+**以便** 当我不记得身份证号或残疾证号时,仍然可以方便地登录系统。
+
+## Acceptance Criteria
+
+1. 人才用户可使用手机号和密码成功登录
+2. 手机号登录验证逻辑正确,通过users2表的phone字段查找用户
+3. 现有的身份证号/残疾证号登录方式仍然正常工作
+4. 登录错误提示友好,区分"用户不存在"和"密码错误"
+5. API文档更新,包含手机号登录的说明
+6. 所有测试通过(单元测试和集成测试)
+
+## Tasks / Subtasks
+
+- [ ] 任务1:扩展人才用户登录服务,支持手机号查找 (AC: 1, 2)
+  - [ ] 1.1 修改`UserService.getTalentUserByIdentifier`方法,支持手机号查找
+  - [ ] 1.2 添加查找逻辑: 先通过phone字段在users2表中查找人才用户
+  - [ ] 1.3 保留原有逻辑: 如果手机号查找失败,继续通过身份证号/残疾证号查找
+  - [ ] 1.4 确保只返回userType='talent'的用户
+  - [ ] 1.5 包含关联查询(person, roles, avatarFile)
+
+- [ ] 任务2:更新登录Schema和API文档 (AC: 5)
+  - [ ] 2.1 更新`TalentLoginSchema`的identifier字段描述
+  - [ ] 2.2 添加说明: "身份证号、残疾证号或手机号"
+  - [ ] 2.3 更新OpenAPI文档的example和description
+  - [ ] 2.4 更新登录路由的错误提示消息
+
+- [ ] 任务3:优化错误提示信息 (AC: 4)
+  - [ ] 3.1 区分手机号不存在的错误提示
+  - [ ] 3.2 区分身份证号/残疾证号不存在的错误提示
+  - [ ] 3.3 统一错误消息为"账号或密码错误"(安全考虑)
+  - [ ] 3.4 更新登录路由的错误响应
+
+- [ ] 任务4:编写和更新测试 (AC: 6)
+  - [ ] 4.1 添加手机号登录成功的测试用例
+  - [ ] 4.2 添加手机号不存在导致登录失败的测试用例
+  - [ ] 4.3 验证身份证号/残疾证号登录仍然正常工作(回归测试)
+  - [ ] 4.4 测试手机号+密码正确但用户类型不是talent的场景
+  - [ ] 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<UserEntity | null> {
+  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
+
+_待实现时填写_
+
+### Debug Log References
+
+_待实现时填写_
+
+### Completion Notes List
+
+_待实现时填写_
+
+### File List
+
+_待实现时填写_