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

8.4 KiB
Raw Blame History

TurboHR API 口径统一重构方案

2026-08-01 · 全量梳理 + 分步执行

一、问题总览

1.1 后端统计函数重复(口径不一致)

函数 端点 维度 问题
getDashboardData /dashboard todos 去重 基础数据源,其他应复用
getComplianceScore /dashboard/compliance-score 5 维度(旧) 与评分标准说明不一致,应废弃
getHealthCheck /dashboard/health-check 6 维度(新) 与评分标准说明一致

重复查询的指标unsignedContractsexpiringContractspoliciesWithoutPublishsocialConfighousingConfigtotalEmployees — 三个函数各自独立查数据库,口径不完全一致。

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: 后端统计函数统一(低风险)

目标:废弃 getComplianceScoreDashboard 合规评分改用 getHealthCheck

1.1 废弃 getComplianceScore

  • /dashboard/compliance-score 端点改为调用 getHealthCheck,返回相同结构
  • 前端 Dashboard.tsxcomplianceScore 查询改为使用 health-check 数据
  • 保留 getComplianceScore 函数体但标记 @deprecated,后续删除

1.2 提取共享统计基础函数

// 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, ... }
}
  • getDashboardDatagetHealthCheck 均调用 getOrgBaseStats,确保口径一致

Phase 2: 新建员工列表专用 API(中风险)

目标/roster 专用于花名册分页列表,新建 /employees/list 供其他页面获取不分页员工列表。

2.1 新增 /employees/list 端点

// 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 服务模块

// 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 响应格式

// 列表类(分页)
{
  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 响应遵循统一格式,前端解析统一