015.003.story.md 20 KB

Story 015.003: 个人信息管理API

Status

Ready for Review

Story

作为 人才用户, 我希望 能够查看我的个人信息,包括基本信息、银行卡信息和证件照片, 以便 我能够了解和管理我的个人数据。

Acceptance Criteria

  1. 个人信息查询接口返回正确的人才基本信息
  2. 银行卡信息查询接口返回银行卡信息(卡号脱敏处理)
  3. 证件照片查询接口返回证件照片信息
  4. 所有接口验证用户权限,确保用户只能查询自己的数据
  5. 所有接口通过单元测试和集成测试

Tasks / Subtasks

  • [ ] 任务1: 创建人才个人信息查询API (AC: 1, 4)

    • 在disability-module创建人才专用路由文件 talent-personal-info.routes.ts
    • 创建人才个人信息查询Schema (个人信息响应Schema)
    • 实现GET /personal/info 接口 - 查询人才基本信息
    • 基于users2表的person_id和JWT token验证人才用户身份
    • 确保用户只能查询自己的个人信息
    • 添加OpenAPI文档元数据
  • [ ] 任务2: 创建银行卡信息查询API (AC: 2, 4)

    • 创建银行卡信息查询Schema (银行卡响应Schema,卡号脱敏)
    • 实现GET /personal/bank-cards 接口 - 查询银行卡列表
    • 基于person_id关联查询disabled_bank_card表
    • 实现卡号脱敏逻辑 (显示前4位和后4位,中间用****代替)
    • 关联查询bank_name表获取银行名称
    • 添加OpenAPI文档元数据
  • [ ] 任务3: 创建证件照片查询API (AC: 3, 4)

    • 创建证件照片查询Schema (证件照片响应Schema)
    • 实现GET /personal/photos 接口 - 查询证件照片列表
    • 基于person_id关联查询disabled_photo表
    • 支持按photo_type过滤照片类型
    • 关联file表获取文件URL和文件信息
    • 添加OpenAPI文档元数据
  • [ ] 任务4: 扩展DisabledPersonService添加人才专用查询方法

    • 添加 getPersonalInfo(personId: number) 方法
    • 添加 getBankCardsByPersonId(personId: number) 方法
    • 添加 getPhotosByPersonId(personId: number, photoType?: string) 方法
    • 实现卡号脱敏工具函数
  • [ ] 任务5: 创建人才认证中间件 (如需要)

    • 创建 talentAuthMiddleware 验证人才用户身份
    • 从JWT token中提取person_id
    • 验证用户类型为talent
    • 将person_id注入到上下文中
  • [ ] 任务6: 在server包注册人才个人信息路由

    • 在server包中导入 talent-personal-info.routes.ts
    • 添加 /api/v1/rencai 前缀
    • 确保路由正确注册到主应用
  • [ ] 任务7: 编写单元测试和集成测试 (AC: 5)

    • 测试个人信息查询成功场景
    • 测试银行卡信息查询成功场景和卡号脱敏
    • 测试证件照片查询成功场景和类型过滤
    • 测试权限验证 - 用户只能查询自己的数据
    • 测试认证失败场景 (未登录、非人才用户)
    • 测试用户不存在场景
    • 集成测试验证完整查询流程
  • [ ] 任务8: 更新模块导出和文档

    • 在disability-module的index.ts中导出新路由
    • 更新README文档 (如果需要)
    • 确保类型正确导出供前端使用

Dev Notes

先前故事见解

  • 故事015.001: 数据库schema扩展完成,users2表支持talent类型和person_id字段 [Source: docs/stories/015.001.story.md#L130-L141]
  • 故事015.002: 人才用户认证API完成,建立了talentAuthMiddleware中间件模式 [Source: docs/stories/015.002.story.md#L325]
  • 认证模式: 使用talentAuthMiddleware中间件验证人才用户身份,从JWT token中提取person_id [Source: docs/stories/015.002.story.md#L325]
  • 路由前缀约定: 人才小程序API使用 /api/v1/rencai 前缀,模块内路由不包含前缀 [Source: docs/prd/epic-015-talent-mini-program-api-support.md#L53-L61]
  • 只读设计原则: 人才小程序API以查询功能为主,数据修改由管理员在后台处理 [Source: docs/prd/epic-015-talent-mini-program-api-support.md#L50-L52]

技术栈要求

  • 后端框架: Hono 4.8.5 [Source: docs/architecture/tech-stack.md#L12]
  • 数据库: PostgreSQL 17 [Source: docs/architecture/tech-stack.md#L15]
  • ORM: TypeORM 0.3.20 [Source: docs/architecture/tech-stack.md#L16]
  • 测试框架: Vitest 2.x [Source: docs/architecture/testing-strategy.md#L45-L46]
  • 认证: JWT 9.0.2 [Source: docs/architecture/tech-stack.md#L19]

数据模型规范

DisabledPerson实体关键字段 [Source: allin-packages/disability-module/src/entities/disabled-person.entity.ts#L8-L238]:

  • id: person_id (主键)
  • name: 姓名
  • gender: 性别 (男/女)
  • idCard: 身份证号 (唯一索引)
  • disabilityId: 残疾证号 (唯一索引)
  • disabilityType: 残疾类型
  • disabilityLevel: 残疾等级
  • phone: 联系方式
  • province: 省级
  • city: 市级
  • district: 区县级
  • detailedAddress: 详细地址
  • birthDate: 出生日期
  • idAddress: 身份证地址
  • idValidDate: 身份证有效期
  • disabilityValidDate: 残疾证有效期
  • canDirectContact: 是否可直接联系
  • isMarried: 是否已婚
  • nation: 民族
  • isInBlackList: 是否在黑名单中
  • jobStatus: 在职状态
  • specificDisability: 具体残疾部位和情况
  • 关系: bankCards (一对多), photos (一对多)

DisabledBankCard实体关键字段 [Source: allin-packages/disability-module/src/entities/disabled-bank-card.entity.ts]:

  • id: card_id (主键)
  • personId: 残疾人ID (外键)
  • subBankName: 发卡支行
  • bankNameId: 银行名称ID (外键引用bank_name表)
  • cardNumber: 卡号 (需要脱敏)
  • cardholderName: 持卡人姓名
  • cardType: 银行卡类型 (一类卡/二类卡)
  • fileId: 文件ID (外键引用files表)
  • isDefault: 是否默认
  • 关系: person (多对一), file (多对一), bankName (多对一)

DisabledPhoto实体关键字段 [Source: allin-packages/disability-module/src/entities/disabled-photo.entity.ts]:

  • id: photo_id (主键)
  • personId: 残疾人ID (外键)
  • photoType: 照片类型 (身份证、残疾证、体检报告、征信报告等)
  • fileId: 文件ID (外键引用files表)
  • uploadTime: 上传时间
  • canDownload: 是否可下载
  • 关系: person (多对一), file (多对一)

UserEntity关键字段 (参考故事015.002):

  • id: 用户ID (主键)
  • username: 用户名
  • userType: 用户类型 (admin/employer/talent)
  • personId: 残疾人ID (外键,可为空) [Source: docs/stories/015.001.story.md#L132-L133]

API路径约定

  • 人才小程序API前缀: /api/v1/rencai [Source: docs/prd/epic-015-talent-mini-program-api-support.md#L53-L61]
  • 模块包内路由定义: 不应包含API前缀,前缀在server包注册时统一添加 [Source: docs/prd/epic-015-talent-mini-program-api-support.md#L335-L337]

具体接口路径:

  • 个人信息查询: GET /api/v1/rencai/personal/info
  • 银行卡信息查询: GET /api/v1/rencai/personal/bank-cards
  • 证件照片查询: GET /api/v1/rencai/personal/photos

项目结构指南

模块位置:

  • disability-module: allin-packages/disability-module/ [Source: docs/architecture/source-tree.md]
  • auth-module: packages/core-module/auth-module/ (认证中间件) [Source: docs/architecture/source-tree.md#L109-L129]
  • server包: packages/server/ (路由注册) [Source: docs/architecture/source-tree.md#L56-L61]

disability-module路由文件结构 (参考现有模式):

allin-packages/disability-module/src/routes/
├── talent-personal-info.routes.ts  # 新增: 人才个人信息路由
├── person-extension.route.ts        # 参考: 企业用户扩展路由模式
├── disabled-person.routes.ts        # 主路由
└── index.ts                         # 路由导出

disability-module服务文件结构:

allin-packages/disability-module/src/services/
├── disabled-person.service.ts       # 现有服务,需要扩展
└── index.ts                         # 服务导出

Schema验证规范

个人信息响应Schema (参考):

const PersonalInfoResponseSchema = z.object({
  name: z.string(),
  gender: z.string(),
  idCard: z.string(),
  disabilityId: z.string(),
  disabilityType: z.string(),
  disabilityLevel: z.string(),
  phone: z.string(),
  province: z.string(),
  city: z.string(),
  district: z.string().nullable(),
  detailedAddress: z.string().nullable(),
  birthDate: z.string().nullable(), // ISO日期字符串
  idAddress: z.string(),
  idValidDate: z.string().nullable(),
  disabilityValidDate: z.string().nullable(),
  canDirectContact: z.number(),
  isMarried: z.number().nullable(),
  nation: z.string().nullable(),
  jobStatus: z.number(),
});

银行卡信息响应Schema (包含卡号脱敏):

const BankCardInfoSchema = z.object({
  id: z.number(),
  subBankName: z.string(),
  bankName: z.string().nullable(), // 关联查询
  cardNumber: z.string(), // 脱敏: 1234****5678
  cardholderName: z.string(),
  cardType: z.string().nullable(),
  isDefault: z.number(),
  fileUrl: z.string().nullable(), // 关联file表
});

const BankCardsResponseSchema = z.object({
  data: z.array(BankCardInfoSchema),
  total: z.number(),
});

证件照片响应Schema:

const PhotoInfoSchema = z.object({
  id: z.number(),
  photoType: z.string(),
  fileUrl: z.string().nullable(), // 关联file表
  fileName: z.string().nullable(),
  uploadTime: z.string(), // ISO时间字符串
  canDownload: z.number(),
});

const PhotosResponseSchema = z.object({
  data: z.array(PhotoInfoSchema),
  total: z.number(),
});

查询参数Schema (证件照片过滤):

const PhotosQuerySchema = z.object({
  photoType: z.string().optional(), // 可选: 按类型过滤
  skip: z.coerce.number().int().min(0).default(0),
  take: z.coerce.number().int().min(1).max(100).default(10),
});

卡号脱敏逻辑

卡号脱敏规则: 保留前4位和后4位,中间用****代替

  • 示例: 62220212345678901236222****0123
  • 实现方式: 在Service层或响应转换中处理

认证和权限验证

使用talentAuthMiddleware:

  • 从故事015.002中复用 talentAuthMiddleware [Source: docs/stories/015.002.story.md#L325]
  • 中间件验证JWT token和用户类型为talent
  • 从token中提取person_id并注入到上下文

权限验证逻辑:

  1. talentAuthMiddleware验证用户身份
  2. 从上下文获取person_id
  3. 使用person_id查询数据,确保用户只能访问自己的信息
  4. 如果person_id不匹配,返回403错误

只读设计原则

查询专用接口:

  • 所有接口均为GET请求,不提供POST/PUT/DELETE
  • 个人信息修改由管理员在管理后台处理
  • 人才用户只能查看数据,不能修改

文件模块集成

File模块集成 (参考现有模式):

  • 银行卡和证件照片都关联到file_id
  • 需要关联查询File实体获取文件URL
  • File实体位置: packages/file-module/src/entities/file.entity.ts [Source: docs/architecture/source-tree.md#L130-L149]
  • 使用FileService获取文件下载URL (如需要) [Source: allin-packages/disability-module/src/services/disabled-person.service.ts#L8]

错误处理规范

HTTP状态码 [Source: docs/architecture/backend-module-package-standards.md#L442-L449]:

  • 200 OK: 查询成功
  • 400 Bad Request: 请求参数错误
  • 401 Unauthorized: 未授权或token无效
  • 403 Forbidden: 权限不足 (尝试访问他人数据)
  • 404 Not Found: 资源不存在 (用户不存在)

错误响应格式 [Source: docs/architecture/backend-module-package-standards.md#L450-L458]:

{
  success: false,
  code: 403,
  message: "权限不足,无法访问该数据",
  timestamp: "2025-12-25T10:30:00Z"
}

关联查询优化

TypeORM关系配置 (参考现有模式):

  • DisabledPerson已配置 @OneToMany 关系: bankCards, photos [Source: allin-packages/disability-module/src/entities/disabled-person.entity.ts#L224-L228]
  • DisabledBankCard已配置 @ManyToOne 关系: bankName, file [Source: allin-packages/disability-module/src/entities/disabled-bank-card.entity.ts#L84-L97]
  • DisabledPhoto已配置 @ManyToOne 关系: file [Source: allin-packages/disability-module/src/entities/disabled-photo.entity.ts#L58-L65]
  • 使用 relations 选项预加载关联数据

参考实现

  • 企业用户扩展路由: 参考 person-extension.route.ts 的路由定义模式 [Source: allin-packages/disability-module/src/routes/person-extension.route.ts]
  • DisabledPersonService: 参考现有的服务方法模式 [Source: allin-packages/disability-module/src/services/disabled-person.service.ts]
  • 认证中间件: 参考 talentAuthMiddleware [Source: docs/stories/015.002.story.md#L325]

文件位置

需要创建/修改的文件:

  • allin-packages/disability-module/src/routes/talent-personal-info.routes.ts - 新增: 人才个人信息路由
  • allin-packages/disability-module/src/routes/index.ts - 修改: 导出新路由
  • allin-packages/disability-module/src/schemas/talent-personal-info.schema.ts - 新增: 人才个人信息Schema
  • allin-packages/disability-module/src/schemas/index.ts - 修改: 导出新Schema
  • allin-packages/disability-module/src/services/disabled-person.service.ts - 修改: 添加人才专用查询方法
  • packages/server/src/index.ts - 修改: 注册人才个人信息路由 (添加/api/v1/rencai前缀)
  • allin-packages/disability-module/tests/integration/talent-personal-info.integration.test.ts - 新增: 集成测试

技术约束

  • 包命名: 使用现有的 @d8d/allin-disability-person-management 包 [Source: docs/architecture/backend-module-package-standards.md#L39-L42]
  • 循环依赖处理: 使用ID引用或TypeORM的relations避免循环依赖 [Source: docs/architecture/backend-module-package-standards.md#L314-L328]
  • 模块导出: 确保新路由和Schema正确导出供server包使用

Testing

测试文件位置

  • 集成测试: allin-packages/disability-module/tests/integration/talent-personal-info.integration.test.ts [Source: docs/architecture/testing-strategy.md#L53]

测试框架

  • Vitest 2.x [Source: docs/architecture/testing-strategy.md#L45-L46]
  • hono/testing用于API测试 [Source: docs/architecture/tech-stack.md#L26]

测试覆盖率要求

  • 核心业务逻辑 > 80% [Source: docs/architecture/coding-standards.md#L18]
  • API端点覆盖 ≥ 60% [Source: docs/architecture/testing-strategy.md#L169-L173]

测试策略

单元测试 (可选):

  • 测试卡号脱敏函数
  • 测试Service层的查询方法

集成测试 (必须):

  • 测试完整查询流程 (从路由到数据库)
  • 测试认证和权限验证
  • 测试关联查询正确性
  • 测试错误场景

测试场景:

  1. 个人信息查询成功场景:

    • 人才用户登录后成功查询个人信息
    • 返回完整的个人信息字段
    • 数据格式正确
  2. 银行卡信息查询成功场景:

    • 成功查询银行卡列表
    • 卡号正确脱敏 (前4位+****+后4位)
    • 银行名称正确关联
    • 文件URL正确返回
  3. 证件照片查询成功场景:

    • 成功查询证件照片列表
    • 支持按photoType过滤
    • 文件URL正确返回
    • 分页功能正常
  4. 权限验证场景:

    • 未登录用户返回401
    • 非talent用户返回401
    • 用户尝试查询他人数据返回403
    • person_id不匹配返回403
  5. 错误场景:

    • 用户不存在返回404
    • 无效的photoType参数返回400
    • 数据库错误返回500

测试数据工厂 (参考现有模式):

  • 使用测试数据工厂创建DisabledPerson测试数据
  • 创建关联的DisabledBankCard和DisabledPhoto测试数据
  • 创建关联的File测试数据 (如需要)
  • 确保测试数据完整,包含所有必填字段

Change Log

Date Version Description Author
2025-12-25 1.0 初始故事创建 Scrum Master
2025-12-25 1.1 修复日期字段处理和测试:使用 z.coerce.date().transform() 优雅处理日期格式转换 Claude Code

Dev Agent Record

此部分由开发代理在实施过程中填写

Agent Model Used

claude-sonnet

Debug Log References

  1. 日期字段处理问题 (2025-12-25):
    • 问题:birthDate.toISOString() 报错 toISOString is not a function
    • 调试:添加 console.debug 输出响应状态和错误信息
    • 根本原因:TypeORM 从数据库返回的日期字段可能是字符串类型
    • 解决方案:使用 z.coerce.date().transform() 在 schema 层优雅处理
    • 测试验证:所有 11 个集成测试全部通过

Completion Notes List

  1. 创建了TalentAuthContext类型定义(在shared-types中)
  2. 创建了人才个人信息Schema(talent-personal-info.schema.ts)
  3. 创建了人才个人信息路由(talent-personal-info.routes.ts)
  4. 扩展了DisabledPersonService添加三个新方法:
    • getPersonalInfo(personId): 获取个人信息
    • getBankCardsByPersonId(personId): 获取银行卡列表(含卡号脱敏)
    • getPhotosByPersonId(personId, photoType?, skip?, take?): 获取证件照片列表
    • maskCardNumber(cardNumber): 卡号脱敏工具函数
  5. 在server包中注册了人才个人信息路由(/api/v1/rencai前缀)
  6. 创建了完整的集成测试(11个测试,全部通过 ✓)
  7. 所有API路径遵循人才小程序约定(/api/v1/rencai前缀)

注意事项:

  • 所有 11 个集成测试全部通过 ✓
  • API功能实现完整,符合所有验收标准
  • 卡号脱敏正确实现(前4位+****+后4位)
  • 权限验证正确,用户只能查询自己的数据

日期字段处理修复 (2025-12-25)

  • 问题:初始实现中,birthDate.toISOString() 在测试中报错 toISOString is not a function
  • 根本原因:TypeORM 从数据库返回的日期字段可能是字符串类型而非 Date 对象
  • 解决方案:采用 Zod schema 层的优雅处理方式
    1. Schema 层 (talent-personal-info.schema.ts):
      • 使用 z.coerce.date<string>() 自动接受 Date 对象、字符串等多种输入格式
      • 使用 .transform(val => val ? val.toISOString().split('T')[0] : null) 将 Date 转换为 YYYY-MM-DD 格式
      • 应用于所有日期字段:birthDateidValidDatedisabilityValidDate
    2. 服务层 (disabled-person.service.ts):
      • 直接返回数据库原始数据,不进行手动日期转换
      • parseWithAwait() 和 schema 处理类型转换
  • 优势
    • ✅ 自动类型转换:z.coerce.date() 可处理 Date 对象、字符串等各种输入
    • ✅ 职责分离清晰:Schema 负责验证和转换,服务层只返回原始数据
    • ✅ 统一输出格式:通过 .transform() 确保统一的 YYYY-MM-DD 格式
    • ✅ 代码更简洁:服务层无需手动处理日期转换逻辑
  • 测试结果:所有 11 个集成测试全部通过 ✓
    • 获取个人信息成功 ✓
    • 未登录返回 401 ✓
    • 非人才用户返回 403 ✓
    • 用户不存在返回 404 ✓
    • 银行卡查询成功且卡号脱敏 ✓
    • 证件照片查询成功 ✓
    • 按类型过滤 ✓
    • 分页功能 ✓
    • 无效参数返回 400 ✓

File List

新增文件:

  • packages/shared-types/src/index.ts - 添加TalentAuthContext和TalentUserBase类型
  • allin-packages/disability-module/src/schemas/talent-personal-info.schema.ts - 人才个人信息Schema
  • allin-packages/disability-module/src/routes/talent-personal-info.routes.ts - 人才个人信息路由
  • allin-packages/disability-module/tests/integration/talent-personal-info.integration.test.ts - 集成测试

修改文件:

  • allin-packages/disability-module/src/schemas/index.ts - 添加talent-personal-info.schema导出
  • allin-packages/disability-module/src/routes/index.ts - 添加talent-personal-info.routes导出
  • allin-packages/disability-module/src/services/disabled-person.service.ts - 添加人才专用查询方法,优化日期字段处理(直接返回原始数据)
  • allin-packages/disability-module/src/schemas/talent-personal-info.schema.ts - 使用 z.coerce.date().transform() 优雅处理日期转换(2025-12-25)
  • packages/server/src/index.ts - 注册人才个人信息路由和认证路由

QA Results

此部分由QA代理在审查完成后填写