Files
TurboHR/backend/src/routes/settings.routes.ts
T
freedakgmail 9512b555ee feat: 电子签全场景集成+场景筛选+设置开关
- ESignRecord 模型新增 scene 字段(CONTRACT/RESIGNATION/POLICY/PAYSLIP/ONBOARDING)
- Organization 模型新增3个电子签开关:esignPolicyEnabled/esignPayslipEnabled/esignOnboardingEnabled
- 设置页面企业信息新增电子签署设置区域,3个开关各自独立,缺省关闭
- 规章制度签收:开启电子签后,员工阅读确认时自动创建POLICY场景签署记录
- 工资条确认:开启电子签后,员工确认工资条时自动创建PAYSLIP场景签署记录
- 入职文件签署:开启电子签后,HR审批通过入职流程时自动创建ONBOARDING场景签署记录
- 电子签署列表增加场景筛选下拉(全部场景/劳动合同/离职协议/规章制度/工资条/入职文件)
- 管理端和员工端列表均展示场景标签(基于scene字段,替代硬编码判断)
- 合同和离职流程的esign调用已加scene参数
2026-08-05 07:47:00 +08:00

247 lines
9.0 KiB
TypeScript

import { Router } from 'express'
import bcrypt from 'bcryptjs'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { requireAdmin } from '../middleware/rbac'
import { z } from 'zod'
import { checkAndUpdateRetirementPolicy, confirmRetirementPolicy } from '../services/retirement.service'
const router = Router()
router.use(authMiddleware)
const updateUserSchema = z.object({
name: z.string().min(1).optional(),
phone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
email: z.string().email().optional(),
role: z.enum(['ADMIN', 'HR', 'VIEWER']).optional(),
})
const createUserSchema = z.object({
name: z.string().min(1, '姓名不能为空'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
password: z.string().min(6, '密码至少6位'),
role: z.enum(['ADMIN', 'HR', 'VIEWER']).default('HR'),
})
// 获取企业信息
router.get('/org', async (req: AuthRequest, res, next) => {
try {
const org = await prisma.organization.findUnique({
where: { id: req.user!.orgId },
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, createdAt: true },
})
res.json({ success: true, data: org })
} catch (err) {
next(err)
}
})
// 更新企业信息
router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const { name, payrollFrequency, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled } = req.body as { name?: string; payrollFrequency?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean }
const updateData: any = {}
if (name) updateData.name = name
if (payrollFrequency !== undefined) updateData.payrollFrequency = payrollFrequency
if (city !== undefined) updateData.city = city
if (contactName !== undefined) updateData.contactName = contactName
if (contactPhone !== undefined) updateData.contactPhone = contactPhone
if (retirementReminderEnabled !== undefined) updateData.retirementReminderEnabled = retirementReminderEnabled
if (esignPolicyEnabled !== undefined) updateData.esignPolicyEnabled = esignPolicyEnabled
if (esignPayslipEnabled !== undefined) updateData.esignPayslipEnabled = esignPayslipEnabled
if (esignOnboardingEnabled !== undefined) updateData.esignOnboardingEnabled = esignOnboardingEnabled
const org = await prisma.organization.update({
where: { id: req.user!.orgId },
data: updateData,
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true },
})
res.json({ success: true, data: org })
} catch (err) {
next(err)
}
})
// 获取用户列表
router.get('/users', async (req: AuthRequest, res, next) => {
try {
const users = await prisma.user.findMany({
where: { orgId: req.user!.orgId },
select: { id: true, name: true, phone: true, email: true, role: true, disabled: true, createdAt: true, lastLoginAt: true },
orderBy: { createdAt: 'asc' },
})
res.json({ success: true, data: users })
} catch (err) {
next(err)
}
})
// 添加用户
router.post('/users', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const data = createUserSchema.parse(req.body)
const existing = await prisma.user.findFirst({ where: { phone: data.phone, orgId: req.user!.orgId } })
if (existing) {
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
}
const passwordHash = await bcrypt.hash(data.password, 10)
const user = await prisma.user.create({
data: {
orgId: req.user!.orgId,
name: data.name,
phone: data.phone,
passwordHash,
role: data.role,
},
select: { id: true, name: true, phone: true, role: true },
})
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// 更新用户
router.put('/users/:id', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const data = updateUserSchema.parse(req.body)
const user = await prisma.user.update({
where: { id: req.params.id },
data: data,
select: { id: true, name: true, phone: true, email: true, role: true, disabled: true },
})
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// 删除用户
router.delete('/users/:id', requireAdmin, async (req: AuthRequest, res, next) => {
try {
if (req.params.id === req.user!.id) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不能删除自己' } })
}
await prisma.user.delete({ where: { id: req.params.id } })
res.json({ success: true })
} catch (err) {
next(err)
}
})
// 禁用/启用用户
router.patch('/users/:id/toggle-disable', requireAdmin, async (req: AuthRequest, res, next) => {
try {
if (req.params.id === req.user!.id) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不能禁用自己' } })
}
const existing = await prisma.user.findUnique({ where: { id: req.params.id } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '用户不存在' } })
}
const user = await prisma.user.update({
where: { id: req.params.id },
data: { disabled: !existing.disabled },
select: { id: true, name: true, disabled: true },
})
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// 切换套餐
router.put('/plan', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const { plan } = req.body as { plan: 'FREE' | 'PRO' | 'ENTERPRISE' }
if (!['FREE', 'PRO', 'ENTERPRISE'].includes(plan)) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无效的套餐' } })
}
const maxEmployees = plan === 'FREE' ? 10 : plan === 'PRO' ? 100 : 999999
const org = await prisma.organization.update({
where: { id: req.user!.orgId },
data: { plan, maxEmployees },
select: { id: true, name: true, plan: true, maxEmployees: true },
})
res.json({ success: true, data: org })
} catch (err) {
next(err)
}
})
// 用量统计
router.get('/usage', async (req: AuthRequest, res, next) => {
try {
const orgId = req.user!.orgId
const [employeeCount, aiConversations, contracts] = await Promise.all([
prisma.employee.count({ where: { orgId } }),
prisma.aIConversation.count({ where: { orgId } }),
prisma.laborContract.count({ where: { orgId } }),
])
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { plan: true, maxEmployees: true } })
res.json({
success: true,
data: {
plan: org?.plan || 'FREE',
maxEmployees: org?.maxEmployees || 10,
employeeCount,
aiConversations,
contracts,
employeeUsage: `${employeeCount}/${org?.maxEmployees || 10}`,
},
})
} catch (err) {
next(err)
}
})
export default router
// ========== 退休政策 ==========
// 获取当前生效的退休政策和待确认政策
router.get('/retirement-policy', async (req: AuthRequest, res, next) => {
try {
const orgId = req.user!.orgId
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { retirementReminderEnabled: true } })
// 如果开启了退休提醒,懒加载检查是否需要获取新政策
if (org?.retirementReminderEnabled) {
await checkAndUpdateRetirementPolicy(orgId).catch(() => {})
}
// 查询当前生效的政策
const confirmedPolicy = await prisma.retirementPolicy.findFirst({
where: { orgId, status: 'CONFIRMED' },
orderBy: { createdAt: 'desc' },
})
// 查询待确认的政策
const pendingPolicy = await prisma.retirementPolicy.findFirst({
where: { orgId, status: 'PENDING' },
orderBy: { createdAt: 'desc' },
})
res.json({
success: true,
data: {
enabled: org?.retirementReminderEnabled || false,
confirmed: confirmedPolicy,
pending: pendingPolicy,
},
})
} catch (err) {
next(err)
}
})
// 确认退休政策
router.post('/retirement-policy/:id/confirm', requireAdmin, async (req: AuthRequest, res, next) => {
try {
await confirmRetirementPolicy(req.user!.orgId, req.params.id)
res.json({ success: true, message: '退休政策已确认生效' })
} catch (err: any) {
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: err.message || '确认失败' } })
}
})