# 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 响应遵循统一格式，前端解析统一
