feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
// 获取通知设置
|
||||
router.get('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let setting = await prisma.notificationSetting.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
if (!setting) {
|
||||
setting = await prisma.notificationSetting.create({
|
||||
data: { orgId: req.user!.orgId },
|
||||
})
|
||||
}
|
||||
res.json({ success: true, data: setting })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新通知设置
|
||||
const settingSchema = z.object({
|
||||
contractExpiry: z.boolean().optional(),
|
||||
expiryDays: z.number().int().min(1).max(365).optional(),
|
||||
contractUnsigned: z.boolean().optional(),
|
||||
overtimeAlert: z.boolean().optional(),
|
||||
payslipReady: z.boolean().optional(),
|
||||
payrollDay: z.number().int().min(1).max(28).optional(),
|
||||
socialInsDay: z.number().int().min(1).max(28).optional(),
|
||||
housingFundDay: z.number().int().min(1).max(28).optional(),
|
||||
taxDay: z.number().int().min(1).max(28).optional(),
|
||||
wechatWebhook: z.string().url().nullable().optional(),
|
||||
emailNotify: z.boolean().optional(),
|
||||
email: z.string().email().nullable().optional(),
|
||||
})
|
||||
|
||||
router.put('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = settingSchema.parse(req.body)
|
||||
const setting = await prisma.notificationSetting.upsert({
|
||||
where: { orgId: req.user!.orgId },
|
||||
update: data,
|
||||
create: { orgId: req.user!.orgId, ...data },
|
||||
})
|
||||
res.json({ success: true, data: setting })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取通知列表
|
||||
router.get('/logs', 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 [logs, total] = await Promise.all([
|
||||
prisma.notificationLog.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.notificationLog.count({ where: { orgId: req.user!.orgId } }),
|
||||
])
|
||||
res.json({ success: true, data: { items: logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 手动触发合同到期检查
|
||||
router.post('/check-contracts', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const setting = await prisma.notificationSetting.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
const expiryDays = setting?.expiryDays || 30
|
||||
const now = new Date()
|
||||
const threshold = new Date(now.getTime() + expiryDays * 24 * 60 * 60 * 1000)
|
||||
|
||||
const contracts = await prisma.laborContract.findMany({
|
||||
where: {
|
||||
orgId: req.user!.orgId,
|
||||
endDate: { lte: threshold, gte: now },
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
})
|
||||
|
||||
const logs: any[] = []
|
||||
for (const contract of contracts) {
|
||||
const daysLeft = Math.ceil((contract.endDate!.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const title = `${contract.employee.name}的合同将在${daysLeft}天后到期`
|
||||
const content = `员工 ${contract.employee.name}(${contract.employee.department})的合同将于 ${contract.endDate!.toISOString().slice(0, 10)} 到期,请及时处理续签或终止事宜。`
|
||||
|
||||
const log = await prisma.notificationLog.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
type: 'CONTRACT_EXPIRY',
|
||||
title,
|
||||
content,
|
||||
channel: 'IN_APP',
|
||||
employeeId: contract.employeeId,
|
||||
},
|
||||
})
|
||||
logs.push(log)
|
||||
|
||||
if (setting?.wechatWebhook) {
|
||||
try {
|
||||
await fetch(setting.wechatWebhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
msgtype: 'text',
|
||||
text: { content: `【合同到期提醒】${title}\n${content}` },
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
// webhook 发送失败不阻断流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { checked: contracts.length, notified: logs.length } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 测试通知渠道
|
||||
router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { channel } = req.body as { channel: 'wechat' | 'email' }
|
||||
const setting = await prisma.notificationSetting.findUnique({ where: { orgId: req.user!.orgId } })
|
||||
if (!setting) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '通知设置不存在' } })
|
||||
|
||||
if (channel === 'wechat') {
|
||||
if (!setting.wechatWebhook) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置企业微信 Webhook' } })
|
||||
try {
|
||||
const resp = await fetch(setting.wechatWebhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ msgtype: 'text', text: { content: '【测试消息】通知渠道连接正常,配置有效。' } }),
|
||||
})
|
||||
const data = await resp.json() as any
|
||||
if (data.errcode && data.errcode !== 0) {
|
||||
return res.json({ success: false, error: { code: 'TEST_FAILED', message: `Webhook 返回错误: ${data.errmsg || data.errcode}` } })
|
||||
}
|
||||
res.json({ success: true, data: { message: '测试消息已发送到企业微信' } })
|
||||
} catch (e: any) {
|
||||
res.json({ success: false, error: { code: 'TEST_FAILED', message: `发送失败: ${e?.message || '网络错误'}` } })
|
||||
}
|
||||
} else if (channel === 'email') {
|
||||
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
|
||||
// 邮件发送(开发阶段仅返回成功)
|
||||
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
|
||||
} else {
|
||||
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不支持的通知渠道' } })
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user