fb924dea98
后端: - /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
8.4 KiB
8.4 KiB
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 提取共享统计基础函数
// 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 端点
// 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 || []。
五、验收标准
/dashboard、/dashboard/compliance-score、/dashboard/health-check三个端点的统计数据口径完全一致/roster仅用于花名册分页列表,其他页面获取员工列表统一用/employees/list- 前端所有 API 调用通过
lib/api-services.ts统一封装,无裸api.get()调用 - 所有 API 响应遵循统一格式,前端解析统一