0df8aa77d9
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
170 lines
6.3 KiB
TypeScript
170 lines
6.3 KiB
TypeScript
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
|