015.013.story.md 11 KB

故事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)

    • 1.1 修改UserService.getTalentUserByIdentifier方法,支持手机号查找
    • 1.2 添加查找逻辑: 先通过phone字段在users2表中查找人才用户
    • 1.3 保留原有逻辑: 如果手机号查找失败,继续通过身份证号/残疾证号查找
    • 1.4 确保只返回userType='talent'的用户
    • 1.5 包含关联查询(person, roles, avatarFile)
  • [x] 任务2:更新登录Schema和API文档 (AC: 5)

    • 2.1 更新TalentLoginSchema的identifier字段描述
    • 2.2 添加说明: "身份证号、残疾证号或手机号"
    • 2.3 更新OpenAPI文档的example和description
    • 2.4 更新登录路由的错误提示消息
  • [x] 任务3:优化错误提示信息 (AC: 4)

    • 3.1 区分手机号不存在的错误提示
    • 3.2 区分身份证号/残疾证号不存在的错误提示
    • 3.3 统一错误消息为"账号或密码错误"(安全考虑)
    • 3.4 更新登录路由的错误响应
  • [x] 任务4:编写和更新测试 (AC: 6)

    • 4.1 添加手机号登录成功的测试用例
    • 4.2 添加手机号不存在导致登录失败的测试用例
    • 4.3 验证身份证号/残疾证号登录仍然正常工作(回归测试)
    • 4.4 测试手机号+密码正确但用户类型不是talent的场景
    • 4.5 确保所有现有测试仍然通过

Dev Notes

现有实现分析

当前登录流程:

// 位置: 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):

// 位置: 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方法,添加手机号查找逻辑:

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为空,需要批量补充:

-- 从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):

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):

description: '人才用户登录接口,支持手机号、身份证号或残疾证号登录'

错误响应更新:

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 - 更新故事状态和完成记录