2968484d2d
- 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
171 lines
6.2 KiB
TypeScript
171 lines
6.2 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 = 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
|