# 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** (参考):
```typescript
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** (包含卡号脱敏):
```typescript
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**:
```typescript
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** (证件照片过滤):
```typescript
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位,中间用****代替
- 示例: `6222021234567890123` → `6222****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]:
```typescript
{
  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` 格式
     - 应用于所有日期字段：`birthDate`、`idValidDate`、`disabilityValidDate`
  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代理在审查完成后填写*
