feat: 实现20260730优化方案全部功能

- AI文件审查:.docx上传提取文本,支持多种文档类型
- 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程
- 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word
- 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面
- 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤
- 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作
- Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型
- 前后端编译验证全部通过
This commit is contained in:
freedakgmail
2026-07-30 10:21:22 +08:00
parent 38b8849332
commit 42e0c650a4
24 changed files with 3639 additions and 35 deletions
@@ -0,0 +1,152 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { renderTemplate } from '../services/template.service'
const router = Router()
// 提取变量名
function extractVariables(content: string): string[] {
const matches = content.match(/\{\{(\w+)\}\}/g) || []
return [...new Set(matches.map(m => m.replace(/\{\{|\}\}/g, '')))]
}
// 列表
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { category, search } = req.query
const where: any = { orgId: req.user!.orgId, status: 'ACTIVE' }
if (category) where.category = category
if (search) where.name = { contains: String(search) }
const items = await (prisma as any).enterpriseTemplate.findMany({
where,
orderBy: { createdAt: 'desc' },
})
res.json({ success: true, data: items })
} catch (err) {
next(err)
}
})
// 新建
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { name, category, description, content } = req.body
if (!name || !category || !content) {
return res.status(400).json({ success: false, error: { code: 'MISSING_FIELDS', message: '名称、分类、内容为必填' } })
}
const variables = extractVariables(content)
const template = await (prisma as any).enterpriseTemplate.create({
data: {
orgId: req.user!.orgId,
name,
category,
description: description || null,
content,
variables,
createdBy: req.user!.id,
},
})
res.json({ success: true, data: template })
} catch (err) {
next(err)
}
})
// 详情
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const template = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!template) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
}
res.json({ success: true, data: template })
} catch (err) {
next(err)
}
})
// 更新
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const existing = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
}
const { name, category, description, content, status } = req.body
const variables = content ? extractVariables(content) : existing.variables
const updated = await (prisma as any).enterpriseTemplate.update({
where: { id: req.params.id },
data: {
...(name !== undefined && { name }),
...(category !== undefined && { category }),
...(description !== undefined && { description }),
...(content !== undefined && { content, variables }),
...(status !== undefined && { status }),
},
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 删除
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const existing = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
}
await (prisma as any).enterpriseTemplate.delete({ where: { id: req.params.id } })
res.json({ success: true, data: { message: '已删除' } })
} catch (err) {
next(err)
}
})
// 渲染
router.post('/:id/render', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const template = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!template) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
}
const { variables } = req.body as { variables: Record<string, string> }
let content = template.content
for (const [key, value] of Object.entries(variables || {})) {
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value)
}
res.json({ success: true, data: { content } })
} catch (err) {
next(err)
}
})
// 下载 Word
router.get('/:id/download', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const template = await (prisma as any).enterpriseTemplate.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!template) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
}
const encoded = encodeURIComponent(template.name + '.doc')
res.setHeader('Content-Type', 'application/msword')
res.setHeader('Content-Disposition', `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`)
res.send(template.content)
} catch (err) {
next(err)
}
})
export default router