2
0
Эх сурвалжийг харах

docs(story): 创建故事015.005 - 人才就业信息API

为人才小程序提供就业信息查询API,包括:
- 当前就业状态查询(企业名称、岗位、薪资等)
- 薪资记录查询(支持按月过滤)
- 就业历史查询(完整工作记录)
- 薪资视频查询(工资视频、个税视频)

关键特性:
- 基于order-module扩展,复用现有实体和索引
- 使用talentAuthMiddleware认证中间件
- 遵循只读设计原则(人才用户仅查询)
- 完整的数据模型和Schema定义
- 全面的测试策略(7个测试场景)
- 性能优化(利用现有索引,< 100ms响应)

状态:Approved
通过故事草稿检查清单验证(9.5/10)

🤖 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 долоо хоног өмнө
parent
commit
c9537f2358

+ 475 - 0
docs/stories/015.005.story.md

@@ -0,0 +1,475 @@
+# Story 015.005: 就业信息API
+
+## Status
+Approved
+
+## Story
+**作为** 人才用户,
+**我想要** 能够查看我的就业信息,包括当前就业状态、薪资记录和就业历史,
+**以便于** 我能够了解和管理我的工作情况
+
+## Acceptance Criteria
+1. 当前就业状态查询接口返回正确的工作信息(企业名称、岗位名称、入职日期、工作状态、订单编号、薪资水平)
+2. 薪资记录查询接口返回历史薪资记录,支持按月份查询
+3. 就业历史查询接口返回个人的就业历史记录,按时间倒序排列
+4. 薪资视频查询接口返回薪资相关视频记录(工资视频、个税视频),支持按月份查询
+5. 所有接口验证用户权限,确保用户只能查询自己的数据
+6. 查询性能优化,添加必要的数据索引
+7. 所有接口通过单元测试和集成测试
+
+## Tasks / Subtasks
+
+- [ ] 任务1: 创建人才就业信息Schema (AC: 1, 2, 3)
+  - [ ] 在order-module创建人才就业Schema文件 `talent-employment.schema.ts`
+  - [ ] 创建当前就业状态响应Schema (EmploymentStatusResponseSchema)
+  - [ ] 创建薪资记录响应Schema (SalaryRecordResponseSchema)
+  - [ ] 创建就业历史响应Schema (EmploymentHistoryResponseSchema)
+  - [ ] 创建薪资视频响应Schema (SalaryVideoResponseSchema)
+  - [ ] 创建查询参数Schema (月份过滤、分页等)
+  - [ ] 添加OpenAPI文档元数据
+
+- [ ] 任务2: 扩展OrderService添加人才专用查询方法 (AC: 1, 2, 3, 4)
+  - [ ] 添加 `getCurrentEmploymentStatus(personId: number)` 方法 - 获取当前就业状态
+  - [ ] 添加 `getSalaryRecords(personId: number, month?: string)` 方法 - 获取薪资记录
+  - [ ] 添加 `getEmploymentHistory(personId: number)` 方法 - 获取就业历史
+  - [ ] 添加 `getSalaryVideos(personId: number, month?: string)` 方法 - 获取薪资视频
+  - [ ] 关联查询employment_order、company表获取企业信息
+  - [ ] 关联查询order_person_asset表获取视频信息
+  - [ ] 关联file表获取视频URL
+
+- [ ] 任务3: 创建人才就业信息API路由 (AC: 1, 2, 3, 4)
+  - [ ] 在order-module创建人才就业路由文件 `talent-employment.routes.ts`
+  - [ ] 实现GET `/employment/status` 接口 - 查询当前就业状态
+  - [ ] 实现GET `/employment/salary-records` 接口 - 查询薪资记录
+  - [ ] 实现GET `/employment/history` 接口 - 查询就业历史
+  - [ ] 实现GET `/employment/salary-videos` 接口 - 查询薪资视频
+  - [ ] 基于person_id和JWT token验证人才用户身份
+  - [ ] 确保用户只能查询自己的就业信息
+  - [ ] 添加OpenAPI文档元数据
+
+- [ ] 任务4: 使用talentAuthMiddleware认证中间件
+  - [ ] 在人才就业路由中应用talentAuthMiddleware
+  - [ ] 从JWT token中提取person_id
+  - [ ] 验证用户类型为talent
+  - [ ] 将person_id注入到上下文中用于查询
+
+- [ ] 任务5: 添加数据库查询性能优化 (AC: 6)
+  - [ ] 确认order_person表已有索引: `['personId', 'workStatus']`, `['personId', 'joinDate', 'workStatus']`
+  - [ ] 确认order_person_asset表已有索引: `['personId', 'assetType']`, `['relatedTime']`
+  - [ ] 验证查询性能,确保响应时间 < 100ms
+  - [ ] 使用TypeORM relations预加载关联数据
+  - [ ] 避免N+1查询问题
+
+- [ ] 任务6: 在server包注册人才就业路由
+  - [ ] 在server包中导入 `talent-employment.routes.ts`
+  - [ ] 添加 `/api/v1/rencai` 前缀
+  - [ ] 确保路由正确注册到主应用
+
+- [ ] 任务7: 编写单元测试和集成测试 (AC: 5, 7)
+  - [ ] 测试当前就业状态查询成功场景
+  - [ ] 测试薪资记录查询成功场景和月份过滤
+  - [ ] 测试就业历史查询成功场景和时间排序
+  - [ ] 测试薪资视频查询成功场景和类型过滤
+  - [ ] 测试权限验证 - 用户只能查询自己的数据
+  - [ ] 测试认证失败场景 (未登录、非人才用户)
+  - [ ] 测试用户不存在场景
+  - [ ] 测试无就业记录场景
+  - [ ] 集成测试验证完整查询流程
+  - [ ] 性能测试验证查询响应时间
+
+- [ ] 任务8: 更新模块导出和文档
+  - [ ] 在order-module的index.ts中导出新路由和Schema
+  - [ ] 更新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]
+- **故事015.003**: 个人信息管理API完成,建立了人才专用API路由模式 [Source: docs/stories/015.003.story.md]
+- **认证模式**: 使用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]
+
+### 数据模型规范
+
+**OrderPerson实体关键字段** [Source: allin-packages/order-module/src/entities/order-person.entity.ts]:
+- `id`: op_id (主键)
+- `orderId`: 订单ID (外键引用employment_order表)
+- `personId`: 残疾人ID (外键)
+- `joinDate`: 入职日期
+- `actualStartDate`: 实际入职日期
+- `leaveDate`: 离职日期
+- `workStatus`: 工作状态 (枚举: not_working, pre_working, working, resigned)
+- `salaryDetail`: 个人薪资 (decimal类型)
+- 关系: `order` (多对一), `person` (多对一)
+
+**EmploymentOrder实体关键字段** [Source: allin-packages/order-module/src/entities/employment-order.entity.ts]:
+- `id`: order_id (主键)
+- `orderName`: 订单名称
+- `platformId`: 用人平台ID
+- `companyId`: 用人单位ID (外键)
+- `channelId`: 渠道ID
+- `expectedStartDate`: 预计开始日期
+- `actualStartDate`: 实际开始日期
+- `actualEndDate`: 实际结束日期
+- `orderStatus`: 订单状态 (枚举: draft, confirmed, in_progress, completed, cancelled)
+- `workStatus`: 工作状态 (枚举: not_working, pre_working, working, resigned)
+- `createTime`: 创建时间
+- `updateTime`: 更新时间
+- 关系: `orderPersons` (一对多)
+
+**OrderPersonAsset实体关键字段** [Source: allin-packages/order-module/src/entities/order-person-asset.entity.ts]:
+- `id`: op_id (主键)
+- `orderId`: 订单ID (外键)
+- `personId`: 残疾人ID (外键)
+- `assetType`: 资产类型 (枚举: tax, salary, job_result, contract_sign, disability_cert, other, salary_video, tax_video, checkin_video, work_video)
+- `assetFileType`: 资产文件类型 (枚举: image, video)
+- `fileId`: 文件ID (外键引用files表)
+- `status`: 视频审核状态 (pending, verified, rejected)
+- `relatedTime`: 关联时间
+- 关系: `order` (多对一), `file` (多对一)
+
+**Company实体关键字段** (参考company-module):
+- `id`: company_id (主键)
+- `companyName`: 企业名称
+- `companyId`: 企业ID (外键引用platform表)
+
+**DisabledPerson实体关键字段** (参考disability-module):
+- `id`: person_id (主键)
+- `name`: 姓名
+
+**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/employment/status`
+- 薪资记录查询: `GET /api/v1/rencai/employment/salary-records`
+- 就业历史查询: `GET /api/v1/rencai/employment/history`
+- 薪资视频查询: `GET /api/v1/rencai/employment/salary-videos`
+
+### 项目结构指南
+**模块位置**:
+- order-module: `allin-packages/order-module/` [Source: docs/architecture/source-tree.md]
+- disability-module: `allin-packages/disability-module/` (参考人才个人信息API)
+- 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]
+
+**order-module路由文件结构** (参考现有模式):
+```
+allin-packages/order-module/src/routes/
+├── talent-employment.routes.ts  # 新增: 人才就业路由
+├── order-custom.routes.ts        # 现有: 企业用户扩展路由
+├── order-crud.routes.ts          # 现有: CRUD路由
+└── index.ts                      # 路由导出
+```
+
+**order-module服务文件结构**:
+```
+allin-packages/order-module/src/services/
+├── order.service.ts              # 现有服务,需要扩展
+└── index.ts                      # 服务导出
+```
+
+**order-module Schema文件结构**:
+```
+allin-packages/order-module/src/schemas/
+├── talent-employment.schema.ts   # 新增: 人才就业Schema
+├── order.schema.ts               # 现有: 订单Schema
+└── index.ts                      # Schema导出
+```
+
+### Schema验证规范
+
+**当前就业状态响应Schema**:
+```typescript
+const EmploymentStatusResponseSchema = z.object({
+  companyName: z.string(), // 关联company表
+  orderId: z.number(),
+  orderName: z.string().nullable(),
+  positionName: z.string().nullable(), // 订单名称作为岗位名称
+  joinDate: z.string(), // ISO日期字符串 YYYY-MM-DD
+  workStatus: z.string(), // 枚举: not_working, pre_working, working, resigned
+  salaryLevel: z.number(), // 来自salaryDetail
+  actualStartDate: z.string().nullable(),
+});
+```
+
+**薪资记录响应Schema**:
+```typescript
+const SalaryRecordSchema = z.object({
+  orderId: z.number(),
+  orderName: z.string().nullable(),
+  companyName: z.string().nullable(), // 关联company表
+  salaryAmount: z.number(), // 来自salaryDetail
+  joinDate: z.string(), // ISO日期字符串 YYYY-MM-DD
+  month: z.string(), // 格式: YYYY-MM
+});
+
+const SalaryRecordsResponseSchema = z.object({
+  data: z.array(SalaryRecordSchema),
+  total: z.number(),
+});
+```
+
+**就业历史响应Schema**:
+```typescript
+const EmploymentHistoryItemSchema = z.object({
+  orderId: z.number(),
+  orderName: z.string().nullable(),
+  companyName: z.string().nullable(), // 关联company表
+  positionName: z.string().nullable(),
+  joinDate: z.string(), // ISO日期字符串 YYYY-MM-DD
+  leaveDate: z.string().nullable(), // ISO日期字符串 YYYY-MM-DD
+  workStatus: z.string(), // 枚举
+  salaryLevel: z.number(),
+});
+
+const EmploymentHistoryResponseSchema = z.object({
+  data: z.array(EmploymentHistoryItemSchema),
+  total: z.number(),
+});
+```
+
+**薪资视频响应Schema**:
+```typescript
+const SalaryVideoSchema = z.object({
+  id: z.number(),
+  assetType: z.string(), // salary_video 或 tax_video
+  assetFileType: z.string(), // video
+  fileUrl: z.string().nullable(), // 关联file表
+  fileName: z.string().nullable(),
+  status: z.string(), // pending, verified, rejected
+  relatedTime: z.string(), // ISO时间字符串
+  month: z.string(), // 格式: YYYY-MM (从relatedTime提取)
+});
+
+const SalaryVideosResponseSchema = z.object({
+  data: z.array(SalaryVideoSchema),
+  total: z.number(),
+});
+```
+
+**查询参数Schema**:
+```typescript
+const SalaryQuerySchema = z.object({
+  month: z.string().regex(/^\d{4}-\d{2}$/).optional(), // 格式: YYYY-MM
+  skip: z.coerce.number().int().min(0).default(0),
+  take: z.coerce.number().int().min(1).max(100).default(10),
+});
+
+const EmploymentHistoryQuerySchema = z.object({
+  skip: z.coerce.number().int().min(0).default(0),
+  take: z.coerce.number().int().min(1).max(100).default(20), // 就业历史可能较多,默认20条
+});
+```
+
+### 业务逻辑规范
+
+**当前就业状态查询逻辑**:
+1. 从JWT token获取person_id
+2. 查询order_person表,条件: person_id = ? AND work_status IN ('pre_working', 'working')
+3. 如果有记录,取最新的一条(按join_date降序)
+4. 如果没有记录,返回空状态(未就业)
+5. 关联查询employment_order表获取订单信息
+6. 关联查询company表获取企业名称
+7. 返回当前就业状态
+
+**薪资记录查询逻辑**:
+1. 从JWT token获取person_id
+2. 查询order_person表,条件: person_id = ?,按join_date降序
+3. 如果有month参数,过滤join_date在指定月份的记录
+4. 关联查询employment_order表获取订单信息
+5. 关联查询company表获取企业名称
+6. 返回薪资记录列表,支持分页
+
+**就业历史查询逻辑**:
+1. 从JWT token获取person_id
+2. 查询order_person表,条件: person_id = ?,按join_date降序排列
+3. 关联查询employment_order表获取订单信息
+4. 关联查询company表获取企业名称
+5. 返回就业历史列表,包含所有状态的记录(包括离职记录)
+6. 支持分页
+
+**薪资视频查询逻辑**:
+1. 从JWT token获取person_id
+2. 查询order_person_asset表,条件: person_id = ? AND asset_type IN ('salary_video', 'tax_video')
+3. 如果有month参数,过滤related_time在指定月份的记录
+4. 关联查询file表获取文件URL
+5. 返回薪资视频列表,按related_time降序排列
+6. 支持分页
+
+### 认证和权限验证
+**使用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/order-module/src/services/order.service.ts#L14-L15]
+
+### 数据库索引优化
+**现有索引** (已在实体定义中):
+- OrderPerson: `['personId', 'workStatus']`, `['personId', 'joinDate', 'workStatus']` [Source: allin-packages/order-module/src/entities/order-person.entity.ts#L7-L12]
+- OrderPersonAsset: `['personId', 'assetType']`, `['relatedTime']` [Source: allin-packages/order-module/src/entities/order-person-asset.entity.ts#L7-L11]
+
+**查询性能优化**:
+- 使用现有索引优化查询性能
+- 使用TypeORM的relations预加载关联数据
+- 避免N+1查询问题
+- 验证查询响应时间 < 100ms
+
+### 关联查询优化
+**TypeORM关系配置** (参考现有模式):
+- OrderPerson已配置 `@ManyToOne` 关系: `order`, `person` [Source: allin-packages/order-module/src/entities/order-person.entity.ts#L82-L89]
+- EmploymentOrder已配置 `@OneToMany` 关系: `orderPersons` [Source: allin-packages/order-module/src/entities/employment-order.entity.ts#L109-L110]
+- OrderPersonAsset已配置 `@ManyToOne` 关系: `order`, `file` [Source: allin-packages/order-module/src/entities/order-person-asset.entity.ts#L99-L112]
+- 使用 `relations` 选项预加载关联数据
+
+### 参考实现
+- **人才个人信息路由**: 参考 `talent-personal-info.routes.ts` 的路由定义模式 [Source: allin-packages/disability-module/src/routes/talent-personal-info.routes.ts]
+- **OrderService**: 参考现有的服务方法模式 [Source: allin-packages/order-module/src/services/order.service.ts]
+- **认证中间件**: 参考 `talentAuthMiddleware` [Source: docs/stories/015.002.story.md#L325]
+
+### 文件位置
+**需要创建/修改的文件**:
+- `allin-packages/order-module/src/routes/talent-employment.routes.ts` - 新增: 人才就业路由
+- `allin-packages/order-module/src/routes/index.ts` - 修改: 导出新路由
+- `allin-packages/order-module/src/schemas/talent-employment.schema.ts` - 新增: 人才就业Schema
+- `allin-packages/order-module/src/schemas/index.ts` - 修改: 导出新Schema
+- `allin-packages/order-module/src/services/order.service.ts` - 修改: 添加人才专用查询方法
+- `packages/server/src/index.ts` - 修改: 注册人才就业路由 (添加`/api/v1/rencai`前缀)
+- `allin-packages/order-module/tests/integration/talent-employment.integration.test.ts` - 新增: 集成测试
+
+### 技术约束
+- **包命名**: 使用现有的 `@d8d/allin-order-module` 包 [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/order-module/tests/integration/talent-employment.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. **薪资记录查询成功场景**:
+   - 成功查询薪资记录列表
+   - 按join_date降序排列
+   - 企业名称正确关联
+   - 支持按月份过滤
+   - 分页功能正常
+
+3. **就业历史查询成功场景**:
+   - 成功查询就业历史列表
+   - 按join_date降序排列
+   - 包含所有状态的记录(在职、离职)
+   - 企业名称正确关联
+   - 分页功能正常
+
+4. **薪资视频查询成功场景**:
+   - 成功查询薪资视频列表
+   - 按related_time降序排列
+   - 文件URL正确返回
+   - 支持按类型过滤(salary_video, tax_video)
+   - 支持按月份过滤
+   - 分页功能正常
+
+5. **权限验证场景**:
+   - 未登录用户返回401
+   - 非talent用户返回401
+   - 用户尝试查询他人数据返回403
+   - person_id不匹配返回403
+
+6. **错误场景**:
+   - 用户不存在返回404
+   - 无效的月份参数返回400
+   - 数据库错误返回500
+   - 无就业记录时返回空列表
+
+7. **性能测试场景**:
+   - 验证查询响应时间 < 100ms
+   - 验证大数据量场景下的分页性能
+   - 验证索引使用情况
+
+**测试数据工厂** (参考现有模式):
+- 使用测试数据工厂创建EmploymentOrder测试数据
+- 创建关联的OrderPerson测试数据
+- 创建关联的Company测试数据 (如需要)
+- 创建关联的OrderPersonAsset测试数据 (用于薪资视频测试)
+- 创建关联的File测试数据 (如需要)
+- 确保测试数据完整,包含所有必填字段
+
+## Change Log
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2025-12-25 | 1.0 | 初始故事创建 | Scrum Master |
+
+## Dev Agent Record
+*此部分由开发代理在实施过程中填写*
+
+### Agent Model Used
+
+### Debug Log References
+
+### Completion Notes List
+
+### File List
+
+## QA Results
+*此部分由QA代理在审查完成后填写*