Files
TurboHR/docs/api-consistency-refactor.md
T
selfrelease fb924dea98 refactor: API口径统一 — 统计函数/员工列表/前端服务层
后端:
- /dashboard/compliance-score 改用 getHealthCheck,与体检诊断同口径
- 新增 /employees/list 端点(不分页,供各页面下拉选择)
- /employees/all-lite 增加 position 字段和 status 筛选参数
- getHealthCheck 未签合同改为查无合同记录的员工(非UNSIGNED类型)

前端:
- 新建 lib/api-services.ts 统一 API 服务层
- employeeApi/rosterApi/dashboardApi/attendanceApi 统一封装
- 6个页面迁移到统一 API 调用:
  - Attendance: employeeApi.list + rosterApi.departments
  - Termination: rosterApi.list + rosterApi.departments
  - Money: rosterApi.list + rosterApi.departments
  - PortalQRModal: employeeApi.list
  - AIAssistant: 3处 employeeApi.list 替代 /roster?pageSize=999
  - Contracts: rosterApi.departments
  - Dashboard: rosterApi.expiringContracts + dashboardApi.healthCheck/workforceStats
2026-08-01 15:29:13 +08:00

236 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TurboHR API 口径统一重构方案
> 2026-08-01 · 全量梳理 + 分步执行
## 一、问题总览
### 1.1 后端统计函数重复(口径不一致)
| 函数 | 端点 | 维度 | 问题 |
|------|------|------|------|
| `getDashboardData` | `/dashboard` | todos 去重 | 基础数据源,其他应复用 |
| `getComplianceScore` | `/dashboard/compliance-score` | 5 维度(旧) | 与评分标准说明不一致,应废弃 |
| `getHealthCheck` | `/dashboard/health-check` | 6 维度(新) | 与评分标准说明一致 |
**重复查询的指标**`unsignedContracts``expiringContracts``policiesWithoutPublish``socialConfig``housingConfig``totalEmployees` — 三个函数各自独立查数据库,口径不完全一致。
### 1.2 `/roster` API 被滥用为通用员工列表
| 页面 | 调用方式 | 用途 | 问题 |
|------|---------|------|------|
| `Roster.tsx` | `/roster` + 分页参数 | 花名册列表 + `globalRiskStats` | ✓ 正确用法 |
| `Contracts.tsx` | `/roster` + contractStatus | 合同管理列表 | ❌ 应有专用合同 API |
| `Attendance.tsx` | `/roster?pageSize=200` | 考勤页选员工 | ❌ pageSize 硬编码 |
| `AIAssistant.tsx` | `/roster?pageSize=999` | AI 对话引用员工 | ❌ pageSize 硬编码 |
| `Compensation.tsx` | `/roster` | 薪酬管理选员工 | ❌ 无分页参数 |
| `Termination.tsx` | `/roster` | 解聘选员工 | ❌ 无分页参数 |
| `Money.tsx` | `/roster` + status=ACTIVE | 薪酬看板 | ❌ 无分页参数 |
| `PortalQRModal.tsx` | `/roster?pageSize=999` | 二维码模态框 | ❌ pageSize 硬编码 |
### 1.3 前端响应解析不统一
`/roster` 返回 `{ success, data, pagination, globalRiskStats }`,但各页面解析方式不同:
| 页面 | 解析方式 |
|------|---------|
| `Roster.tsx` | `res`(完整响应) |
| `AIAssistant.tsx` | `res.data?.items \|\| res.data \|\| []` |
| `Attendance.tsx` | `res.data` |
| `Compensation.tsx` | `res.data` |
| `Money.tsx` | `res.data \|\| []` |
| `PortalQRModal.tsx` | `res.data?.data \|\| res.data \|\| []` |
### 1.4 后端路由职责重叠
| 路由 | 职责 | 重叠 |
|------|------|------|
| `roster.routes.ts` | 花名册 CRUD + 合同状态 + 部门列表 + 风险统计 | 合同管理、员工列表 |
| `employee.routes.ts` | 员工 CRUD | 与 roster 的员工创建/编辑重叠 |
| `dashboard.routes.ts` | 仪表盘 + 风险中心 + 合规评分 + 体检诊断 + 工作台 | 统计逻辑分散 |
---
## 二、重构方案
### Phase 1: 后端统计函数统一(低风险)
**目标**:废弃 `getComplianceScore`Dashboard 合规评分改用 `getHealthCheck`
#### 1.1 废弃 `getComplianceScore`
- `/dashboard/compliance-score` 端点改为调用 `getHealthCheck`,返回相同结构
- 前端 `Dashboard.tsx``complianceScore` 查询改为使用 `health-check` 数据
- 保留 `getComplianceScore` 函数体但标记 `@deprecated`,后续删除
#### 1.2 提取共享统计基础函数
```typescript
// backend/src/services/stats.service.ts
/** 获取组织级基础统计数据(所有统计函数共享) */
export async function getOrgBaseStats(orgId: string) {
const now = new Date()
const [
totalEmployees,
unsignedEmployees, // 无合同员工
expiringContracts, // 30天内到期
policiesWithoutPublish, // 未公示制度
totalPolicies,
socialConfig,
housingConfig,
employeesNoSocial, // 社保异常(按员工去重)
overtimeExcessive, // 超时加班
unconfirmedPayslips,
totalPayslips,
totalTerminations,
completedTerminations,
terminationsWithChecklist,
disciplinaryRecords,
attendanceRecords,
trainingRecords,
] = await Promise.all([...])
return { totalEmployees, unsignedEmployees, expiringContracts, ... }
}
```
- `getDashboardData``getHealthCheck` 均调用 `getOrgBaseStats`,确保口径一致
### Phase 2: 新建员工列表专用 API(中风险)
**目标**`/roster` 专用于花名册分页列表,新建 `/employees/list` 供其他页面获取不分页员工列表。
#### 2.1 新增 `/employees/list` 端点
```typescript
// employee.routes.ts
// 获取员工列表(不分页,供下拉选择、引用等场景)
// 支持 status、department 筛选
router.get('/list', authMiddleware, async (req, res) => {
const { status, department } = req.query
const employees = await prisma.employee.findMany({
where: { orgId: req.user!.orgId, ...(status && { status: String(status) }) },
select: { id: true, name: true, department: true, position: true, status: true },
orderBy: { name: 'asc' },
})
res.json({ success: true, data: employees })
})
```
#### 2.2 前端迁移
| 页面 | 原调用 | 新调用 |
|------|--------|--------|
| `Attendance.tsx` | `/roster?pageSize=200` | `/employees/list?status=ACTIVE` |
| `AIAssistant.tsx` | `/roster?pageSize=999` | `/employees/list?status=ACTIVE` |
| `Compensation.tsx` | `/roster` | `/employees/list?status=ACTIVE` |
| `Termination.tsx` | `/roster` | `/employees/list?status=ACTIVE` |
| `Money.tsx` | `/roster?status=ACTIVE` | `/employees/list?status=ACTIVE` |
| `PortalQRModal.tsx` | `/roster?pageSize=999` | `/employees/list?status=ACTIVE` |
### Phase 3: 前端 API 层统一封装(低风险)
**目标**:建立类型安全的 API 调用层,统一响应解析。
#### 3.1 创建 API 服务模块
```typescript
// frontend/src/lib/api-services.ts
// 统一响应解析
function unwrap<T>(res: any): T {
return res.data?.data ?? res.data ?? res
}
export const rosterApi = {
list: (params: RosterParams) => api.get('/roster', { params }).then(unwrap<RosterResponse>),
departments: () => api.get('/roster/departments').then(unwrap<string[]>()),
contractTypes: () => api.get('/roster/contract-types').then(unwrap<ContractType[]>()),
}
export const employeeApi = {
list: (params?: { status?: string; department?: string }) =>
api.get('/employees/list', { params }).then(unwrap<EmployeeOption[]>()),
profile: (id: string) => api.get(`/roster/${id}/profile`).then(unwrap<EmployeeProfile>()),
}
export const dashboardApi = {
data: () => api.get('/dashboard').then(unwrap<DashboardData>()),
healthCheck: () => api.get('/dashboard/health-check').then(unwrap<HealthCheckData>()),
risks: () => api.get('/dashboard/risks').then(unwrap<RiskItem[]>()),
calendar: (month: string) => api.get(`/dashboard/calendar?month=${month}`).then(unwrap()),
}
```
#### 3.2 各页面迁移调用
逐步将各页面从 `api.get('/xxx')` 改为 `xxxApi.method()`,确保响应解析统一。
### Phase 4: 后端路由职责清理(中风险)
**目标**:消除路由间职责重叠。
| 调整 | 说明 |
|------|------|
| `roster.routes.ts` 移除员工 CRUD | 员工创建/编辑/删除统一走 `employee.routes.ts` |
| `Contracts.tsx` 改用 `/employees` + 合同子资源 | 合同管理不再复用花名册列表 |
| `dashboard.routes.ts` 统计逻辑提取到 service | 路由层只做参数校验和响应封装 |
---
## 三、执行优先级
| 优先级 | Phase | 风险 | 预计改动 |
|--------|-------|------|---------|
| P0 | Phase 1.1 废弃 getComplianceScore | 低 | 后端 2 文件 + 前端 1 文件 |
| P1 | Phase 2 新建 /employees/list | 中 | 后端 1 文件 + 前端 6 文件 |
| P2 | Phase 1.2 提取共享统计函数 | 低 | 后端 1 新文件 + 2 改动 |
| P3 | Phase 3 前端 API 服务层 | 低 | 前端 1 新文件 + 逐步迁移 |
| P4 | Phase 4 路由职责清理 | 中 | 后端多文件重构 |
---
## 四、统一响应规范
### 4.1 所有 API 响应格式
```typescript
// 列表类(分页)
{
success: true,
data: T[],
pagination: { page, pageSize, total, totalPages },
// 可选附加字段
globalRiskStats?: { expiring, expired, unsigned }
}
// 列表类(不分页)
{
success: true,
data: T[]
}
// 单对象
{
success: true,
data: T
}
// 错误
{
success: false,
error: { code, message, trace_id }
}
```
### 4.2 前端统一解析
所有 API 调用通过 `unwrap<T>()` 解析,取 `data` 字段,不再各页面自行判断 `res.data?.items || res.data || []`
---
## 五、验收标准
1. `/dashboard``/dashboard/compliance-score``/dashboard/health-check` 三个端点的统计数据口径完全一致
2. `/roster` 仅用于花名册分页列表,其他页面获取员工列表统一用 `/employees/list`
3. 前端所有 API 调用通过 `lib/api-services.ts` 统一封装,无裸 `api.get()` 调用
4. 所有 API 响应遵循统一格式,前端解析统一