Files
TurboHR/backend/src/routes/notification.routes.ts
T
freedakgmail 2968484d2d 优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换
- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割
- AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割
- xlsx改为动态导入, OvertimeTab从345KB降至12.7KB
- api-services.ts: 请求参数 any→Record<string,unknown>
- 移除前端3处console.log残留
- 后端console替换为pino logger
- 前后端未使用import/变量清理
- Zod schema验证: termination/platform/special-status/work-process
- 新增 leave.routes.ts, acceptance-test.routes.ts
- UI组件: PageGuide, QueryError, Stepper
2026-08-04 07:53:37 +08:00

171 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
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)
}
})
// 测试通知渠道
const testChannelSchema = z.object({
channel: z.enum(['wechat', 'email']),
})
router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { channel } = testChannelSchema.parse(req.body)
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 (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
}
} catch (err) {
next(err)
}
})
export default router