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