fix: 新增后端 /api/v1/salary/dashboard 路由

- 创建 salary.routes.ts,提供薪酬分析数据
- 基于 Payslip 模型计算部门对比、月度趋势、同比环比
- 在 app.ts 中挂载 /api/v1/salary 路由
This commit is contained in:
freedakgmail
2026-08-04 07:58:21 +08:00
parent 2968484d2d
commit e02e3dcd81
2 changed files with 90 additions and 0 deletions
+2
View File
@@ -65,6 +65,7 @@ import specialStatusRoutes from './routes/special-status.routes'
import companyFileRoutes from './routes/company-file.routes'
import acceptanceTestRoutes from './routes/acceptance-test.routes'
import leaveRoutes from './routes/leave.routes'
import salaryRoutes from './routes/salary.routes'
app.use('/api/v1/auth', authRoutes)
app.use('/api/v1/dashboard', dashboardRoutes)
app.use('/api/v1/employees', employeeRoutes)
@@ -93,6 +94,7 @@ app.use('/api/v1/special-statuses', specialStatusRoutes)
app.use('/api/v1/company-files', companyFileRoutes)
app.use('/api/v1/acceptance-tests', acceptanceTestRoutes)
app.use('/api/v1/leaves', leaveRoutes)
app.use('/api/v1/salary', salaryRoutes)
app.use(errorHandler)
+88
View File
@@ -0,0 +1,88 @@
import { Router } from 'express'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
const router = Router()
router.get('/dashboard', authMiddleware, async (req: AuthRequest, res) => {
try {
const orgId = req.user!.orgId
const year = Number(req.query.year) || new Date().getFullYear()
const yearPrefix = `${year}-`
// 获取该年度所有工资条
const payslips = await prisma.payslip.findMany({
where: {
orgId,
month: { startsWith: yearPrefix },
},
include: {
employee: { select: { department: true } },
},
})
// 概览
const totalEmployees = new Set(payslips.map(p => p.employeeId)).size
const allNetPays = payslips.map(p => p.netPay).sort((a, b) => a - b)
const avgSalary = payslips.length > 0 ? payslips.reduce((s, p) => s + p.netPay, 0) / payslips.length : 0
const medianSalary = allNetPays.length > 0
? allNetPays.length % 2 === 0
? (allNetPays[allNetPays.length / 2 - 1] + allNetPays[allNetPays.length / 2]) / 2
: allNetPays[Math.floor(allNetPays.length / 2)]
: 0
const totalAnnual = payslips.reduce((s, p) => s + p.netPay, 0)
// 同比:与上一年度对比
const prevYearPrefix = `${year - 1}-`
const prevPayslips = await prisma.payslip.findMany({
where: {
orgId,
month: { startsWith: prevYearPrefix },
},
})
const prevTotal = prevPayslips.reduce((s, p) => s + p.netPay, 0)
const yoy = prevTotal > 0 ? ((totalAnnual - prevTotal) / prevTotal) * 100 : undefined
// 环比:与上个月对比(12月 vs 11月)
const decPayslips = payslips.filter(p => p.month === `${year}-12`)
const novPayslips = payslips.filter(p => p.month === `${year}-11`)
const decTotal = decPayslips.reduce((s, p) => s + p.netPay, 0)
const novTotal = novPayslips.reduce((s, p) => s + p.netPay, 0)
const mom = novTotal > 0 ? ((decTotal - novTotal) / novTotal) * 100 : undefined
// 部门薪酬对比
const deptMap = new Map<string, { count: number; total: number }>()
for (const p of payslips) {
const dept = p.employee?.department || '未分配'
const cur = deptMap.get(dept) || { count: 0, total: 0 }
cur.count += 1
cur.total += p.netPay
deptMap.set(dept, cur)
}
const departments = Array.from(deptMap.entries())
.map(([name, v]) => ({ name, count: v.count, avgSalary: v.count > 0 ? v.total / v.count : 0 }))
.sort((a, b) => b.avgSalary - a.avgSalary)
// 月度趋势
const monthMap = new Map<string, number>()
for (const p of payslips) {
monthMap.set(p.month, (monthMap.get(p.month) || 0) + p.netPay)
}
const monthlyTrend = Array.from(monthMap.entries())
.map(([month, total]) => ({ month: month.slice(5), total }))
.sort((a, b) => a.month.localeCompare(b.month))
res.json({
success: true,
data: {
summary: { totalEmployees, avgSalary, medianSalary, totalAnnual, yoy, mom },
departments,
monthlyTrend,
},
})
} catch (err) {
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: '获取薪酬分析数据失败' } })
}
})
export default router