feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -4,6 +4,7 @@ import { chat, chatStream, reviewContract, matchCase, predictRisks, predictRisks
|
||||
import { seedKnowledgeBase, addKnowledge, searchKnowledge, ensureRAGTable } from '../services/rag.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { z } from 'zod'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -542,4 +543,140 @@ router.delete('/rag/:id', authMiddleware, async (req: AuthRequest, res, next) =>
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 合同到期决策助手 ==========
|
||||
|
||||
router.post('/contract-decision', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeId } = req.body as { employeeId: string }
|
||||
if (!employeeId) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 employeeId' } })
|
||||
}
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId: req.user!.orgId },
|
||||
include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
|
||||
})
|
||||
if (!emp) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
|
||||
const contract = emp.contracts[0]
|
||||
if (!contract || !contract.endDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '该员工无固定期限合同或未签合同,不适用到期决策' } })
|
||||
}
|
||||
|
||||
// 解密薪资
|
||||
let salary = 0
|
||||
try { salary = Number(decrypt(emp.monthlySalary)) || 0 } catch { salary = Number(emp.monthlySalary) || 0 }
|
||||
|
||||
const today = new Date()
|
||||
const hireDate = emp.hireDate
|
||||
const endDate = contract.endDate
|
||||
const totalMonths = (endDate.getFullYear() - hireDate.getFullYear()) * 12 + (endDate.getMonth() - hireDate.getMonth())
|
||||
const years = Math.floor(totalMonths / 12)
|
||||
const remainingMonths = totalMonths % 12
|
||||
let n = years
|
||||
if (remainingMonths >= 6) n = years + 1
|
||||
else if (remainingMonths > 0) n = years + 0.5
|
||||
if (n <= 0) n = 0.5
|
||||
|
||||
const daysToExpiry = Math.ceil((endDate.getTime() - today.getTime()) / 86400000)
|
||||
|
||||
// 三选项成本对比
|
||||
const options = [
|
||||
{
|
||||
key: 'RENEW',
|
||||
title: '续签合同',
|
||||
cost: 0,
|
||||
description: '与员工续签劳动合同,保持劳动关系延续',
|
||||
legalRisk: '低',
|
||||
details: {
|
||||
compensation: 0,
|
||||
noticePeriod: '无需通知',
|
||||
notes: '续签时如维持或提高条件,员工拒绝则无需补偿;降低条件员工拒绝需支付 N',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'EXPIRE_NO_RENEW',
|
||||
title: '到期不续签',
|
||||
cost: salary * n,
|
||||
description: '合同到期后公司决定不续签,需支付经济补偿金 N',
|
||||
legalRisk: '中',
|
||||
details: {
|
||||
compensation: salary * n,
|
||||
n,
|
||||
monthlySalary: salary,
|
||||
noticePeriod: '建议提前30天书面通知',
|
||||
notes: '公司提出不续签需支付经济补偿金(N);员工主动提出不续签则无需支付',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'EXPIRE_WAIT',
|
||||
title: '逾期不处理(风险最高)',
|
||||
cost: salary * (n + 1),
|
||||
description: '合同到期后继续用工但不签新合同,可能被认定为事实劳动关系',
|
||||
legalRisk: '高',
|
||||
details: {
|
||||
compensation: salary * (n + 1),
|
||||
n,
|
||||
monthlySalary: salary,
|
||||
notes: '逾期超过1个月未续签,员工可主张双倍工资;满1年视为已订立无固定期限劳动合同',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 构建 AI 建议请求
|
||||
const decisionContext = `员工合同到期决策分析:
|
||||
- 姓名:${emp.name}
|
||||
- 部门:${emp.department}
|
||||
- 入职日期:${hireDate.toISOString().slice(0, 10)}
|
||||
- 合同到期日:${endDate.toISOString().slice(0, 10)}(距今${daysToExpiry}天)
|
||||
- 月薪:¥${salary.toFixed(2)}
|
||||
- 工龄:${years}年${remainingMonths}月(经济补偿月数 N=${n})
|
||||
- 特殊状态:${emp.isPregnant ? '孕期/哺乳期 ' : ''}${emp.isInMedicalPeriod ? '医疗期 ' : ''}${emp.isWorkInjured ? '工伤' : '无'}
|
||||
|
||||
三选项成本对比:
|
||||
1. 续签:成本 ¥0
|
||||
2. 到期不续签:成本 ¥${(salary * n).toFixed(2)}(经济补偿 N=${n})
|
||||
3. 逾期不处理:风险成本 ¥${(salary * (n + 1)).toFixed(2)}(双倍工资风险)
|
||||
|
||||
请给出专业建议,分析每个选项的法律风险和实际影响,推荐最优方案。`
|
||||
|
||||
let aiAdvice = ''
|
||||
try {
|
||||
await checkUsageLimit(req.user!.orgId, 'chat')
|
||||
const result = await chat([{ role: 'user', content: decisionContext }], '')
|
||||
aiAdvice = result
|
||||
await recordUsage(req.user!.orgId, req.user!.id, 'chat')
|
||||
} catch {
|
||||
aiAdvice = 'AI 建议生成失败,请参考以上成本对比数据自行判断。'
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
employee: {
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
department: emp.department,
|
||||
hireDate: hireDate.toISOString().slice(0, 10),
|
||||
contractEndDate: endDate.toISOString().slice(0, 10),
|
||||
daysToExpiry,
|
||||
monthlySalary: salary,
|
||||
workYears: years,
|
||||
workMonths: remainingMonths,
|
||||
n,
|
||||
isPregnant: emp.isPregnant,
|
||||
isInMedicalPeriod: emp.isInMedicalPeriod,
|
||||
isWorkInjured: emp.isWorkInjured,
|
||||
},
|
||||
options,
|
||||
aiAdvice,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
createAttendanceConfirmation,
|
||||
batchCreateAttendanceConfirmations,
|
||||
getAttendanceConfirmations,
|
||||
confirmAttendance,
|
||||
getAttendanceStats,
|
||||
} from '../services/attendance.service'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/** 获取月度考勤确认列表 */
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = req.query.month as string
|
||||
if (!month) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })
|
||||
}
|
||||
const status = req.query.status as string | undefined
|
||||
const data = await getAttendanceConfirmations(req.user!.orgId, month, status)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取考勤确认统计 */
|
||||
router.get('/stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = req.query.month as string
|
||||
if (!month) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })
|
||||
}
|
||||
const data = await getAttendanceStats(req.user!.orgId, month)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 批量创建考勤确认记录 */
|
||||
router.post('/batch', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
items: z.array(z.object({
|
||||
employeeId: z.string(),
|
||||
workDays: z.number().int().min(0),
|
||||
weekdayHours: z.number().min(0),
|
||||
weekendHours: z.number().min(0),
|
||||
holidayHours: z.number().min(0),
|
||||
overtimePay: z.number().min(0),
|
||||
})),
|
||||
})
|
||||
const { month, items } = schema.parse(req.body)
|
||||
const result = await batchCreateAttendanceConfirmations(req.user!.orgId, req.user!.id, month, items)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 员工确认考勤(员工端) */
|
||||
router.post('/confirm', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
employeeId: z.string(),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
disputeNote: z.string().optional(),
|
||||
})
|
||||
const { employeeId, month, disputeNote } = schema.parse(req.body)
|
||||
const ip = req.ip || req.socket.remoteAddress || ''
|
||||
const result = await confirmAttendance(req.user!.orgId, employeeId, month, ip, disputeNote)
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'ATTENDANCE',
|
||||
refId: result.id,
|
||||
employeeId,
|
||||
events: [{ action: '考勤确认', timestamp: new Date().toISOString(), ip, userAgent: req.headers['user-agent'], ...(disputeNote ? { location: disputeNote } : {}) }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'NOT_FOUND') {
|
||||
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
/**
|
||||
* 查询审计日志
|
||||
* 支持按操作类型、实体类型、时间范围筛选
|
||||
*/
|
||||
router.get('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const action = req.query.action as string | undefined
|
||||
const entity = req.query.entity as string | undefined
|
||||
const userId = req.query.userId as string | undefined
|
||||
const dateFrom = req.query.dateFrom as string | undefined
|
||||
const dateTo = req.query.dateTo as string | undefined
|
||||
|
||||
const where: any = { orgId: req.user!.orgId }
|
||||
if (action) where.action = { contains: action, mode: 'insensitive' }
|
||||
if (entity) where.entity = entity
|
||||
if (userId) where.userId = userId
|
||||
if (dateFrom || dateTo) {
|
||||
where.createdAt = {}
|
||||
if (dateFrom) where.createdAt.gte = new Date(dateFrom)
|
||||
if (dateTo) where.createdAt.lte = new Date(`${dateTo}T23:59:59`)
|
||||
}
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.auditLog.count({ where }),
|
||||
])
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { items: logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取审计日志统计(按操作类型分组)
|
||||
*/
|
||||
router.get('/stats', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const dateFrom = req.query.dateFrom as string | undefined
|
||||
const dateTo = req.query.dateTo as string | undefined
|
||||
|
||||
const where: any = { orgId: req.user!.orgId }
|
||||
if (dateFrom || dateTo) {
|
||||
where.createdAt = {}
|
||||
if (dateFrom) where.createdAt.gte = new Date(dateFrom)
|
||||
if (dateTo) where.createdAt.lte = new Date(`${dateTo}T23:59:59`)
|
||||
}
|
||||
|
||||
const stats = await prisma.auditLog.groupBy({
|
||||
by: ['action'],
|
||||
where,
|
||||
_count: { action: true },
|
||||
orderBy: { _count: { action: 'desc' } },
|
||||
take: 20,
|
||||
})
|
||||
|
||||
res.json({ success: true, data: stats })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -4,11 +4,10 @@ import { register, login, refresh, resetPassword } from '../services/auth.servic
|
||||
import { authLimiter, loginLimiter } from '../middleware/rateLimit'
|
||||
import prisma from '../lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { setCode, getCode, deleteCode } from '../lib/codeStore'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const codeStore = new Map<string, { code: string; expiresAt: number }>()
|
||||
|
||||
router.post('/register', authLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = registerSchema.parse(req.body)
|
||||
@@ -48,7 +47,7 @@ router.post('/forgot-password/send-code', authLimiter, async (req, res, next) =>
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '该手机号未注册' } })
|
||||
}
|
||||
const code = Math.random().toString().slice(2, 8)
|
||||
codeStore.set(data.phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 })
|
||||
await setCode(data.phone, code)
|
||||
res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -59,14 +58,14 @@ router.post('/forgot-password/send-code', authLimiter, async (req, res, next) =>
|
||||
router.post('/forgot-password/verify', authLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = verifyCodeSchema.parse(req.body)
|
||||
const stored = codeStore.get(data.phone)
|
||||
if (!stored || stored.expiresAt < Date.now()) {
|
||||
const stored = await getCode(data.phone)
|
||||
if (!stored) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
||||
}
|
||||
if (stored.code !== data.code) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: '验证码错误' } })
|
||||
}
|
||||
codeStore.delete(data.phone)
|
||||
await deleteCode(data.phone)
|
||||
const passwordHash = await bcrypt.hash(data.newPassword, 10)
|
||||
await prisma.user.updateMany({
|
||||
where: { phone: data.phone },
|
||||
@@ -81,11 +80,11 @@ router.post('/forgot-password/verify', authLimiter, async (req, res, next) => {
|
||||
router.post('/reset-password', authLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = resetPasswordSchema.parse(req.body)
|
||||
const stored = codeStore.get(data.phone)
|
||||
if (!stored || stored.expiresAt < Date.now()) {
|
||||
const stored = await getCode(data.phone)
|
||||
if (!stored) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
||||
}
|
||||
codeStore.delete(data.phone)
|
||||
await deleteCode(data.phone)
|
||||
const result = await resetPassword(data.phone, data.newPassword)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { getDashboardData } from '../services/risk.service'
|
||||
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getComplianceScore, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
@@ -77,4 +77,98 @@ router.patch('/todos/batch-ignore', authMiddleware, async (req: AuthRequest, res
|
||||
}
|
||||
})
|
||||
|
||||
// HR 月度日历
|
||||
router.get('/calendar', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const data = await getMonthlyCalendar(req.user!.orgId, month)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 人力成本深度分析
|
||||
router.get('/cost-analysis', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const data = await getCostAnalysis(req.user!.orgId, month)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 合规健康度评分 + AI 建议卡片流
|
||||
router.get('/compliance-score', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = await getComplianceScore(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 用工体检诊断 — 获取当前诊断
|
||||
router.get('/health-check', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = await getHealthCheck(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 用工体检诊断 — 保存报告
|
||||
router.post('/health-check/save', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const report = await saveHealthCheckReport(req.user!.orgId, req.user!.id)
|
||||
res.json({ success: true, data: { id: (report as any).id } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 用工体检诊断 — 历史报告
|
||||
router.get('/health-check/history', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = await getHealthCheckHistory(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 年度价值报告 — 获取当年报告
|
||||
router.get('/annual-value', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const year = parseInt(req.query.year as string) || new Date().getFullYear()
|
||||
const data = await getAnnualValueReport(req.user!.orgId, year)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 年度价值报告 — 保存
|
||||
router.post('/annual-value/save', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const year = parseInt(req.body.year) || new Date().getFullYear()
|
||||
const report = await saveAnnualValueReport(req.user!.orgId, req.user!.id, year)
|
||||
res.json({ success: true, data: { id: (report as any).id } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 年度价值报告 — 历史
|
||||
router.get('/annual-value/history', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = await getAnnualValueReportHistory(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import {
|
||||
createEmployeeSchema,
|
||||
@@ -49,6 +50,14 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
const data = createEmployeeSchema.parse(req.body)
|
||||
const result = await createEmployee(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'CREATE', 'EMPLOYEE', result.id, { name: data.name })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'ONBOARD',
|
||||
refId: result.id,
|
||||
employeeId: result.id,
|
||||
events: [{ action: '员工入职登记', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -199,6 +208,14 @@ router.post('/contracts', authMiddleware, async (req: AuthRequest, res, next) =>
|
||||
const data = addContractSchema.parse(req.body)
|
||||
const result = await addContract(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'ADD_CONTRACT', 'CONTRACT', result.id, { employeeId: data.employeeId })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'CONTRACT_SIGN',
|
||||
refId: result.id,
|
||||
employeeId: data.employeeId,
|
||||
events: [{ action: '合同签订', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { getEvidenceByRef, getEvidenceByEmployee, getEvidenceDetail, verifyEvidence, getEvidenceList, verifyAllEvidence } from '../services/evidence.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* 获取组织级证据链列表
|
||||
* GET /api/v1/evidence?category=ALL&page=1&pageSize=20
|
||||
*/
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const category = (req.query.category as string) || 'ALL'
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const data = await getEvidenceList(req.user!.orgId, category, page, pageSize)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证全部证据链完整性
|
||||
* GET /api/v1/evidence/verify-all
|
||||
*/
|
||||
router.get('/verify-all', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = await verifyAllEvidence(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取某操作的完整证据链
|
||||
* GET /api/v1/evidence/:category/:refId
|
||||
*/
|
||||
router.get('/:category/:refId', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const evidence = await getEvidenceByRef(req.user!.orgId, req.params.category, req.params.refId)
|
||||
res.json({ success: true, data: evidence })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取某员工所有证据链
|
||||
* GET /api/v1/evidence/employee/:id
|
||||
*/
|
||||
router.get('/employee/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const evidence = await getEvidenceByEmployee(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: evidence })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取证据链详情
|
||||
* GET /api/v1/evidence/detail/:id
|
||||
*/
|
||||
router.get('/detail/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const evidence = await getEvidenceDetail(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: evidence })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证证据链完整性
|
||||
* GET /api/v1/evidence/verify/:id
|
||||
*/
|
||||
router.get('/verify/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = await verifyEvidence(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
calcBatchEntry,
|
||||
getPayrollRiskWarnings,
|
||||
generatePayslipFromBatches,
|
||||
prePayrollCheck,
|
||||
} from '../services/payroll.service'
|
||||
|
||||
const router = Router()
|
||||
@@ -670,4 +671,17 @@ router.get('/batches/:batchId/export', async (req: AuthRequest, res: Response, n
|
||||
}
|
||||
})
|
||||
|
||||
// 算薪前 AI 校验
|
||||
router.get('/batches/:batchId/pre-check', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = await prePayrollCheck(req.user!.orgId, req.params.batchId)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'NOT_FOUND') {
|
||||
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import {
|
||||
createPolicy, getPolicies, getPolicyDetail, updatePolicy,
|
||||
advanceDemocracyStep, deletePolicy,
|
||||
} from '../services/policy.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/** 获取制度列表 */
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = req.query.status as string | undefined
|
||||
const policies = await getPolicies(req.user!.orgId, status)
|
||||
res.json({ success: true, data: policies })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取制度详情 */
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const policy = await getPolicyDetail(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: policy })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 创建制度 */
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
title: z.string().min(1),
|
||||
content: z.string().min(1),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
const data = schema.parse(req.body)
|
||||
const policy = await createPolicy(req.user!.orgId, req.user!.id, data)
|
||||
res.json({ success: true, data: { id: policy.id } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 更新制度 */
|
||||
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
title: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
const data = schema.parse(req.body)
|
||||
await updatePolicy(req.user!.orgId, req.params.id, data)
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 推进民主程序步骤 */
|
||||
router.post('/:id/advance-step', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const schema = z.object({ step: z.number().int().min(1).max(4), note: z.string().optional() })
|
||||
const { step, note } = schema.parse(req.body)
|
||||
await advanceDemocracyStep(req.user!.orgId, req.params.id, step, note)
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 删除制度 */
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await deletePolicy(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取制度的阅读签收统计 */
|
||||
router.get('/:id/read-stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const policy = await prisma.policyDocument.findFirst({ where: { id: req.params.id, orgId }, select: { id: true } })
|
||||
if (!policy) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '制度不存在' } })
|
||||
}
|
||||
const [totalEmployees, readRecords] = await Promise.all([
|
||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
||||
prisma.policyReadRecord.findMany({
|
||||
where: { policyId: req.params.id, orgId },
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { readAt: 'desc' },
|
||||
}),
|
||||
])
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: totalEmployees,
|
||||
readCount: readRecords.length,
|
||||
unreadCount: totalEmployees - readRecords.length,
|
||||
records: readRecords.map(r => ({
|
||||
employeeId: r.employeeId,
|
||||
employeeName: r.employee.name,
|
||||
department: r.employee.department,
|
||||
readAt: r.readAt.toISOString(),
|
||||
ip: r.ip,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -6,12 +6,11 @@ import fs from 'fs'
|
||||
import prisma from '../lib/prisma'
|
||||
import { signAccessToken, verifyAccessToken } from '../lib/jwt'
|
||||
import { portalLoginSchema, portalSendCodeSchema, portalVerifyCodeSchema, onboardingSchema, contractConfirmSchema, contractSendCodeSchema } from '../schemas/portal.schema'
|
||||
import { setCode, getCode, deleteCode, updateCode, checkRateLimit } from '../lib/codeStore'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 验证码临时存储(生产环境应使用 Redis)
|
||||
const codeStore = new Map<string, { code: string; expiresAt: number; failCount: number; lastSentAt: number }>()
|
||||
|
||||
// 员工端认证中间件
|
||||
function portalAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization
|
||||
@@ -63,12 +62,12 @@ router.post('/send-code', async (req, res, next) => {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '该手机号未在系统中登记' } })
|
||||
}
|
||||
// 频率限制:60秒内不可重复发送
|
||||
const existing = codeStore.get(data.phone)
|
||||
if (existing && existing.lastSentAt && Date.now() - existing.lastSentAt < 60 * 1000) {
|
||||
const allowed = await checkRateLimit(data.phone)
|
||||
if (!allowed) {
|
||||
return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT', message: '验证码发送过于频繁,请60秒后重试' } })
|
||||
}
|
||||
const code = Math.random().toString().slice(2, 8)
|
||||
codeStore.set(data.phone, { code, expiresAt: Date.now() + 5 * 60 * 1000, failCount: 0, lastSentAt: Date.now() })
|
||||
await setCode(data.phone, code)
|
||||
res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -79,20 +78,20 @@ router.post('/send-code', async (req, res, next) => {
|
||||
router.post('/verify-code', async (req, res, next) => {
|
||||
try {
|
||||
const data = portalVerifyCodeSchema.parse(req.body)
|
||||
const stored = codeStore.get(data.phone)
|
||||
if (!stored || stored.expiresAt < Date.now()) {
|
||||
const stored = await getCode(data.phone)
|
||||
if (!stored) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
||||
}
|
||||
// 错误次数限制:5次后锁定
|
||||
if (stored.failCount >= 5) {
|
||||
codeStore.delete(data.phone)
|
||||
await deleteCode(data.phone)
|
||||
return res.status(400).json({ success: false, error: { code: 'TOO_MANY_ATTEMPTS', message: '验证码错误次数过多,请重新获取验证码' } })
|
||||
}
|
||||
if (stored.code !== data.code) {
|
||||
stored.failCount++
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: `验证码错误(剩余${5 - stored.failCount}次机会)` } })
|
||||
await updateCode(data.phone, { failCount: stored.failCount + 1 })
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: `验证码错误(剩余${5 - stored.failCount - 1}次机会)` } })
|
||||
}
|
||||
codeStore.delete(data.phone)
|
||||
await deleteCode(data.phone)
|
||||
const employee = await prisma.employee.findFirst({ where: { phone: data.phone, status: 'ACTIVE' } })
|
||||
if (!employee) {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
@@ -158,6 +157,14 @@ router.post('/payslip/:id/confirm', portalAuth, async (req: any, res, next) => {
|
||||
channel: 'IN_APP',
|
||||
},
|
||||
})
|
||||
await createEvidence({
|
||||
orgId: req.employee.orgId,
|
||||
category: 'PAYSLIP_CONFIRM',
|
||||
refId: payslip.id,
|
||||
employeeId: req.employee.id,
|
||||
events: [{ action: '工资条确认', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.employee.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -231,7 +238,7 @@ router.post('/contract-confirm/send-code', async (req, res, next) => {
|
||||
return res.status(400).json({ success: false, error: { code: 'NO_PHONE', message: '员工手机号未登记,无法发送验证码' } })
|
||||
}
|
||||
const code = Math.random().toString().slice(2, 8)
|
||||
codeStore.set(`contract-${data.token}`, { code, expiresAt: Date.now() + 5 * 60 * 1000, failCount: 0, lastSentAt: Date.now() })
|
||||
await setCode(`contract-${data.token}`, code)
|
||||
res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -250,19 +257,19 @@ router.post('/contract-confirm', async (req, res, next) => {
|
||||
return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } })
|
||||
}
|
||||
// 验证码校验
|
||||
const stored = codeStore.get(`contract-${data.token}`)
|
||||
if (!stored || stored.expiresAt < Date.now()) {
|
||||
const stored = await getCode(`contract-${data.token}`)
|
||||
if (!stored) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
||||
}
|
||||
if (stored.failCount >= 5) {
|
||||
codeStore.delete(`contract-${data.token}`)
|
||||
await deleteCode(`contract-${data.token}`)
|
||||
return res.status(400).json({ success: false, error: { code: 'TOO_MANY_ATTEMPTS', message: '验证码错误次数过多,请重新获取验证码' } })
|
||||
}
|
||||
if (stored.code !== data.verifyCode) {
|
||||
stored.failCount++
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: `验证码错误(剩余${5 - stored.failCount}次机会)` } })
|
||||
await updateCode(`contract-${data.token}`, { failCount: stored.failCount + 1 })
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: `验证码错误(剩余${5 - stored.failCount - 1}次机会)` } })
|
||||
}
|
||||
codeStore.delete(`contract-${data.token}`)
|
||||
await deleteCode(`contract-${data.token}`)
|
||||
|
||||
const userAgent = req.headers['user-agent'] || ''
|
||||
const signEvidence = JSON.stringify({
|
||||
@@ -278,6 +285,14 @@ router.post('/contract-confirm', async (req, res, next) => {
|
||||
where: { id: link.contractId },
|
||||
data: { attachmentName: `confirmed:${new Date().toISOString()}|evidence:${signEvidence}` },
|
||||
})
|
||||
await createEvidence({
|
||||
orgId: link.contract.orgId,
|
||||
category: 'CONTRACT_SIGN',
|
||||
refId: link.contractId,
|
||||
employeeId: link.contract.employeeId,
|
||||
events: [{ action: '合同签署确认', timestamp: new Date().toISOString(), ip: req.ip, userAgent, smsCode: data.verifyCode }],
|
||||
createdBy: link.contract.employeeId,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: { message: '合同签署确认成功' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -422,4 +437,111 @@ router.post('/onboarding/:token/upload', onboardingUpload.single('file'), async
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 员工端:规章制度公示 ==========
|
||||
|
||||
/** 获取已公示制度列表(含当前员工阅读状态) */
|
||||
router.get('/policies', portalAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { orgId, id: employeeId } = (req as any).employee
|
||||
const policies = await prisma.policyDocument.findMany({
|
||||
where: { orgId, status: 'PUBLISHED' },
|
||||
orderBy: { publishedAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
type: true,
|
||||
publishedAt: true,
|
||||
readRecords: {
|
||||
where: { employeeId },
|
||||
select: { id: true, readAt: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
const result = policies.map(p => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
type: p.type,
|
||||
publishedAt: p.publishedAt?.toISOString() || null,
|
||||
hasRead: p.readRecords.length > 0,
|
||||
readAt: p.readRecords[0]?.readAt?.toISOString() || null,
|
||||
}))
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取制度详情(已公示) */
|
||||
router.get('/policies/:id', portalAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { orgId, id: employeeId } = (req as any).employee
|
||||
const policy = await prisma.policyDocument.findFirst({
|
||||
where: { id: req.params.id, orgId, status: 'PUBLISHED' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
type: true,
|
||||
publishedAt: true,
|
||||
readRecords: {
|
||||
where: { employeeId },
|
||||
select: { id: true, readAt: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!policy) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '制度不存在或未公示' } })
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: policy.id,
|
||||
title: policy.title,
|
||||
content: policy.content,
|
||||
type: policy.type,
|
||||
publishedAt: policy.publishedAt?.toISOString() || null,
|
||||
hasRead: policy.readRecords.length > 0,
|
||||
readAt: policy.readRecords[0]?.readAt?.toISOString() || null,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 提交阅读确认(签收) */
|
||||
router.post('/policies/:id/read', portalAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { orgId, id: employeeId } = (req as any).employee
|
||||
const policy = await prisma.policyDocument.findFirst({
|
||||
where: { id: req.params.id, orgId, status: 'PUBLISHED' },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!policy) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '制度不存在或未公示' } })
|
||||
}
|
||||
|
||||
// 幂等:已存在则返回已有记录
|
||||
const existing = await prisma.policyReadRecord.findUnique({
|
||||
where: { policyId_employeeId: { policyId: req.params.id, employeeId } },
|
||||
})
|
||||
if (existing) {
|
||||
return res.json({ success: true, data: { readAt: existing.readAt.toISOString() } })
|
||||
}
|
||||
|
||||
const record = await prisma.policyReadRecord.create({
|
||||
data: {
|
||||
policyId: req.params.id,
|
||||
orgId,
|
||||
employeeId,
|
||||
ip: req.ip || req.socket.remoteAddress,
|
||||
userAgent: req.headers['user-agent'] || null,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: { readAt: record.readAt.toISOString() } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt, encrypt } from '../lib/crypto'
|
||||
import { getContractStatus } from '../services/contract.service'
|
||||
@@ -523,6 +524,14 @@ router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'DISCIPLINARY',
|
||||
refId: record.id,
|
||||
employeeId: req.params.employeeId,
|
||||
events: [{ action: '违纪记录创建', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { getAllTemplates, getTemplateById, getTemplatesByCategory, renderTemplate } from '../services/template.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/** 获取所有模板列表 */
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const category = req.query.category as string | undefined
|
||||
const templates = category ? getTemplatesByCategory(category) : getAllTemplates()
|
||||
res.json({ success: true, data: templates })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取模板详情 */
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const template = getTemplateById(req.params.id)
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
res.json({ success: true, data: template })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 渲染模板 */
|
||||
router.post('/:id/render', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { variables } = req.body as { variables: Record<string, string> }
|
||||
const content = renderTemplate(req.params.id, variables || {})
|
||||
if (!content) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
res.json({ success: true, data: { content } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -2,8 +2,9 @@ import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
||||
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems } from '../services/termination.service'
|
||||
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -63,6 +64,14 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
const data = terminationChecklistSchema.parse(req.body)
|
||||
const result = await createTermination(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'TERMINATE', 'EMPLOYEE', data.employeeId, { reason: data.reason })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'TERMINATION',
|
||||
refId: result.id,
|
||||
employeeId: data.employeeId,
|
||||
events: [{ action: '解聘流程启动', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -246,6 +255,14 @@ router.post('/draft/:id/execute', authMiddleware, async (req: AuthRequest, res,
|
||||
try {
|
||||
const result = await executeTermination(req.user!.orgId, req.params.id, req.user!.id)
|
||||
await auditLog(req, 'EXECUTE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {})
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'TERMINATION',
|
||||
refId: req.params.id,
|
||||
employeeId: (result as any)?.employeeId,
|
||||
events: [{ action: '解聘执行', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).catch(() => {})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
@@ -269,4 +286,18 @@ router.post('/draft/:id/cancel', authMiddleware, async (req: AuthRequest, res, n
|
||||
}
|
||||
})
|
||||
|
||||
// 步骤前置校验
|
||||
router.get('/draft/:id/validate-step', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const step = parseInt(req.query.step as string) || 1
|
||||
const result = await validateTerminationStep(req.user!.orgId, req.params.id, step)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'NOT_FOUND') {
|
||||
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user