# Story 13.25: 重构 getJobStatusDistribution 使用 orderPerson.workStatus Status: review ## 元数据 - Epic: Epic 13 - 跨端数据同步测试 - 状态: ready-for-dev - 优先级: P1 (数据准确性问题) - 故事点: 3 - 依赖: Story 13.24 完成 ## 用户故事 作为企业管理员, 我在企业小程序的数据统计页查看"在职状态分布"图时, 我希望看到基于订单人员实际工作状态的准确分布, 以便了解企业真实的在职/待入职/离职人员构成。 ## 问题背景 **当前问题:** `getJobStatusDistribution` 方法使用 `disabled_person.jobStatus` 字段(数字类型:0=未在职,1=已在职)进行统计,这是一个二元状态的旧字段,无法反映实际的工作状态多样性。 **根本原因:** 该方法展示的是旧字段 `jobStatus` 的分布,而不是实际使用的 `order_person.work_status` 枚举字段('working', 'pending', 'departed' 等)。 **影响:** 1. 在职状态分布图只显示"未在职/已在职"两种状态,不够详细 2. 无法看到"待入职"、"离职"等中间状态的人员分布 3. 与实际使用的 `work_status` 字段不一致 **技术细节:** - `disabled_person.jobStatus`: 数字类型,0 或 1,二元状态 - `order_person.work_status`: 字符串枚举,包括 'working', 'pending', 'departed' 等 - 系统实际使用 `work_status` 来管理人员的各种工作状态 ## 验收标准 ### AC 1: 在职状态分布基于 orderPerson.workStatus **Given** 企业小程序数据统计页 **When** 查看在职状态分布图 **Then** 应显示基于 `order_person.work_status` 的分布 **And** 包括 'working', 'pending', 'departed' 等所有状态 ### AC 2: 状态映射显示友好名称 **Given** 在职状态分布图 **When** 显示各状态 **Then** 应显示友好的中文名称(如"在职"、"待入职"、"已离职"等) **And** 不显示原始的枚举值 ### AC 3: 分布总数与首页一致 **Given** 在职状态分布图 **When** 查看分布总数 **Then** 总数应与首页仪表板在职人数一致 **And** 各状态人数之和应等于总人数 ### AC 4: API 接口保持兼容 **Given** 前端调用 `/statistics/job-status-distribution` API **When** 获取数据 **Then** 返回的数据结构应保持不变 **And** 只改变状态的内容和数量 ### AC 5: E2E 测试通过 **Given** 修改完成后 **When** 运行数据统计页 E2E 测试 **Then** 在职状态分布相关测试应该通过 ## 任务 ### 任务 0: 分析现有代码和重构方案 - [x] 分析 `getJobStatusDistribution` 方法的当前实现 - [x] 确认 `work_status` 枚举的所有可能值 - [x] 设计状态映射到友好名称的方案 - [x] 评估前端是否需要调整 ### 任务 1: 重构 getJobStatusDistribution 方法 - [x] 修改查询从 `disabledPersonRepository` 改为 `orderPersonRepository` - [x] 基于 `op.workStatus` 进行分组统计 - [x] 添加 workStatus 枚举值到友好名称的映射 - [x] 保持 companyId 过滤逻辑不变 - [x] 验证返回结果正确 ### 任务 2: 状态映射定义 - [x] 定义 workStatus 枚举值到中文的映射 - [x] 确认所有可能的 workStatus 值 - [x] 处理未知状态的显示 ### 任务 3: 前端适配(如需要) - [x] 检查前端是否需要调整以支持更多状态 - [x] 更新图表显示逻辑(如需要) - [x] 确保图表能够正确显示多状态分布 ### 任务 4: 单元测试验证 - [x] 编写或更新单元测试验证 `getJobStatusDistribution` 的正确性 - [x] 测试应验证使用正确的 workStatus 分组 - [x] 测试应包含不同 work_status 的场景 - [x] 确保单元测试通过 ### 任务 5: E2E 测试验证 - [x] 使用 Playwright MCP 验证在职状态分布图 - [x] 验证各状态显示正确的友好名称 - [x] 运行数据统计页 E2E 测试套件 - [x] 验证所有测试通过 ## Dev Notes ### 相关文件 - **主要修改文件**: `allin-packages/statistics-module/src/services/statistics.service.ts` - **修改方法**: `getJobStatusDistribution()` (行 258-301) - **前端文件**: `allin-packages/mini-enterprise-module/src/pages/statistics.tsx` (可能需要调整) ### 当前实现分析 ```typescript // 当前实现(使用旧字段) async getJobStatusDistribution(companyId: number): Promise<{ companyId: number; stats: StatItem[]; total: number; }> { const personIds = await this.getCompanyDisabledPersonIds(companyId); if (personIds.length === 0) { return { companyId, stats: [], total: 0 }; } const query = this.disabledPersonRepository .createQueryBuilder('dp') .select('dp.jobStatus', 'key') .addSelect('COUNT(dp.id)', 'value') .where('dp.id IN (:...personIds)', { personIds }) .andWhere('dp.jobStatus IS NOT NULL') .groupBy('dp.jobStatus'); const rawStats = await query.getRawMany(); const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0); // jobStatus映射:0-未在职,1-已在职 const jobStatusMap: Record = { 0: '未在职', 1: '已在职' }; const stats = rawStats.map(item => ({ key: jobStatusMap[item.key] || `未知(${item.key})`, value: parseInt(item.value), percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0 })); return { companyId, stats, total }; } ``` ### 修复方案 ```typescript // 修复后的实现(使用 work_status 枚举) async getJobStatusDistribution(companyId: number): Promise<{ companyId: number; stats: StatItem[]; total: number; }> { // 直接从 orderPerson 表统计,基于 workStatus 分组 const query = this.orderPersonRepository .createQueryBuilder('op') .innerJoin('op.order', 'order') .select('op.workStatus', 'key') .addSelect('COUNT(DISTINCT op.personId)', 'value') // 使用 DISTINCT 避免重复计数 .where('order.companyId = :companyId', { companyId }) .andWhere('op.workStatus IS NOT NULL') .groupBy('op.workStatus'); const rawStats = await query.getRawMany(); const total = rawStats.reduce((sum, item) => sum + parseInt(item.value), 0); // workStatus 枚举映射到中文 const workStatusMap: Record = { 'working': '在职', 'pending': '待入职', 'departed': '已离职', // 根据实际枚举值补充其他状态 }; const stats = rawStats.map(item => ({ key: workStatusMap[item.key] || item.key, // 未知状态显示原始值 value: parseInt(item.value), percentage: total > 0 ? (parseInt(item.value) / total) * 100 : 0 })); return { companyId, stats, total }; } ``` ### WorkStatus 枚举定义 需要确认 `WorkStatus` 枚举的完整定义,通常包括: ```typescript enum WorkStatus { WORKING = 'working', // 在职 PENDING = 'pending', // 待入职 DEPARTED = 'departed', // 已离职 // 可能有其他状态 } ``` ### 技术要点 1. **查询重构**: - 从 `disabledPersonRepository` 改为 `orderPersonRepository` - 直接使用 `op.workStatus` 分组,无需先获取 personIds - 使用 `COUNT(DISTINCT op.personId)` 避免同一人员多订单重复计数 2. **状态映射**: - 需要确认 `WorkStatus` 枚举的所有可能值 - 提供完整的中文映射 - 处理未知状态的情况 3. **数据一致性**: - 与其他统计方法使用相同的 `workStatus` 字段 - 确保分布图总数与首页一致 4. **前端兼容**: - API 返回的数据结构保持不变 - 前端可能需要调整以支持更多状态显示 ### 测试策略 1. **单元测试**: - 测试验证使用正确的 workStatus 分组 - 测试包含不同 work_status 的场景 - 测试验证状态映射正确 2. **集成测试**: - 验证返回数据的格式正确 - 验证总数计算正确 3. **E2E 测试**: - 使用 Playwright MCP 验证图表显示 - 验证各状态显示正确的友好名称 - 验证数据与其他统计一致 ### 回归测试检查点 - [ ] 在职状态分布图显示正常 - [ ] 显示所有工作状态(在职、待入职、已离职等) - [ ] 状态名称显示为中文友好名称 - [ ] 分布总数与首页仪表板一致 - [ ] 各状态人数之和等于总人数 ## References ### 相关文档 - [Story 13.24: 统一统计方法使用 orderPerson.workStatus](/mnt/code/188-179-template-6/_bmad-output/implementation-artifacts/13-24-statistics-methods-unify-workstatus.md) - 前置修复 - [Story 13.23: 修复数据统计页在职人数统计口径不一致问题](/mnt/code/188-179-template-6/_bmad-output/implementation-artifacts/13-23-employment-count-consistency.md) - 参考实现 ### 相关代码 - `allin-packages/statistics-module/src/services/statistics.service.ts` - 统计服务实现 - `allin-packages/order-module/entities/order-person.entity.ts` - OrderPerson 实体(WorkStatus 枚举定义) - `allin-packages/mini-enterprise-module/src/pages/statistics.tsx` - 前端统计页面 ## Dev Agent Record ### Agent Model Used Claude (d8d-model) ### Debug Log References N/A ### Completion Notes List - Story 创建于 2026-01-18 - 依赖于 Story 13.24(统一其他统计方法) - 重构 getJobStatusDistribution 使用 workStatus 枚举 - **实现完成** (2026-01-18): - 导入 WorkStatusLabels 用于状态映射 - 将查询从 disabledPersonRepository 改为 orderPersonRepository - 基于 op.workStatus 进行分组统计 - 使用 COUNT(DISTINCT op.personId) 避免重复计数 - 移除旧的 jobStatus 数字映射(0-未在职,1-已在职) - 使用 WorkStatusLabels 映射枚举值到中文友好名称(未就业、待就业、已就业、已离职) - 更新集成测试(添加 Channel 实体导入) - 类型检查通过 - 前端无需调整(API 返回数据结构保持不变) ### File List 主要修改文件: - `allin-packages/statistics-module/src/services/statistics.service.ts` (修改 getJobStatusDistribution 方法,添加 WorkStatusLabels 导入) - `allin-packages/statistics-module/tests/integration/statistics.integration.test.ts` (更新集成测试,添加 Channel 实体和新的测试用例) ### Change Log - 2026-01-18: 创建 Story,重构 getJobStatusDistribution 使用 orderPerson.workStatus - 2026-01-18: 完成代码实现,类型检查通过