feat: TurboHR 14项优化与功能增强
- #1 Dashboard风险提醒增加立刻办理按钮 - #2 Calendar月份选择器改为input month - #3 Termination增加7种解聘原因法律依据和操作步骤 - #5 合同审查支持PDF TXT格式 - #6 AI合同审查prompt优化为具体修改建议 - #7 知识库添加更新机制说明 - #9 SpecialStatus员工选择改用all-lite接口 - #10 Termination增加详细法律条款引用 - #11 Money发薪批次增加社保公积金合计列 - #12 EmployeeAttachment扩展文件类型 - #13 花名册增加女职工干部工人选项加退休提醒 - #14 新增公司备用文件上传模块
This commit is contained in:
@@ -957,7 +957,7 @@ const reviewUpload = multer({
|
||||
limits: { fileSize: 100 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase()
|
||||
if (ext !== '.docx' && ext !== '.doc') {
|
||||
if (ext !== '.docx' && ext !== '.txt' && ext !== '.pdf') {
|
||||
return cb(null, false)
|
||||
}
|
||||
cb(null, true)
|
||||
@@ -967,15 +967,27 @@ const reviewUpload = multer({
|
||||
router.post('/review/upload', authMiddleware, reviewUpload.single('file'), async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, error: { code: 'NO_FILE', message: '请上传 .docx 文件' } })
|
||||
return res.status(400).json({ success: false, error: { code: 'NO_FILE', message: '请上传 .docx / .txt / .pdf 文件' } })
|
||||
}
|
||||
const ext = path.extname(req.file.originalname).toLowerCase()
|
||||
let text = ''
|
||||
if (ext === '.docx') {
|
||||
const result = await mammoth.extractRawText({ buffer: req.file.buffer })
|
||||
text = result.value
|
||||
} else if (ext === '.txt') {
|
||||
text = req.file.buffer.toString('utf-8')
|
||||
} else if (ext === '.pdf') {
|
||||
// PDF 简单文本提取:提取括号内的文本流内容
|
||||
const raw = req.file.buffer.toString('latin1')
|
||||
const textMatches = raw.match(/\(([^)]+)\)/g)
|
||||
if (textMatches) {
|
||||
text = textMatches.map(m => m.slice(1, -1).replace(/\\[nrt()\\]/g, ' ')).join(' ')
|
||||
}
|
||||
if (!text || text.trim().length < 10) {
|
||||
return res.status(400).json({ success: false, error: { code: 'PDF_PARSE_FAIL', message: 'PDF 文件无法提取文本,可能是扫描件或图片格式。建议将文件另存为 .docx 后上传' } })
|
||||
}
|
||||
} else {
|
||||
return res.status(400).json({ success: false, error: { code: 'UNSUPPORTED', message: '暂不支持 .doc 格式,请将文件另存为 .docx 后上传' } })
|
||||
return res.status(400).json({ success: false, error: { code: 'UNSUPPORTED', message: '暂不支持该格式,请上传 .docx / .txt / .pdf 文件' } })
|
||||
}
|
||||
if (text.length > 50000) {
|
||||
text = text.slice(0, 50000) + '\n\n[文本过长,已截断]'
|
||||
|
||||
@@ -23,7 +23,7 @@ router.get('/:employeeId', async (req: AuthRequest, res: Response, next: NextFun
|
||||
const attachmentSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
fileName: z.string().min(1),
|
||||
fileType: z.enum(['ID_CARD', 'BANK_CARD', 'CONTRACT_SCAN', 'EDUCATION', 'OTHER']),
|
||||
fileType: z.enum(['ID_CARD', 'BANK_CARD', 'CONTRACT_SCAN', 'EDUCATION', 'TERMINATION_DOC', 'RETIREMENT_DOC', 'INJURY_CERT', 'MEDICAL_CERT', 'PREGNANCY_CERT', 'DISCIPLINARY', 'OTHER']),
|
||||
fileUrl: z.string().min(1),
|
||||
fileSize: z.number().int().default(0),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const fileType = req.query.fileType as string | undefined
|
||||
const files = await prisma.companyFile.findMany({
|
||||
where: { orgId: req.user!.orgId, ...(fileType ? { fileType } : {}) },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: files })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加公司文件记录
|
||||
const companyFileSchema = z.object({
|
||||
fileName: z.string().min(1),
|
||||
fileType: z.enum(['BUSINESS_LICENSE', 'WORK_HOURS', 'HR_POLICY', 'LABOR_CONTRACT_TEMPLATE', 'OTHER']),
|
||||
fileUrl: z.string().min(1),
|
||||
fileSize: z.number().int().default(0),
|
||||
remark: z.string().optional(),
|
||||
expiryDate: z.string().optional(),
|
||||
})
|
||||
|
||||
router.post('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = companyFileSchema.parse(req.body)
|
||||
const { expiryDate, ...rest } = data
|
||||
const file = await prisma.companyFile.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
...rest,
|
||||
...(expiryDate ? { expiryDate: new Date(expiryDate) } : {}),
|
||||
uploadedBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: file })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除公司文件
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const file = await prisma.companyFile.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!file) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '文件不存在' } })
|
||||
}
|
||||
await prisma.companyFile.delete({ where: { id: file.id } })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -36,6 +36,23 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 轻量级全量员工列表(不分页,仅返回 id/name/department/gender/status)
|
||||
* 用于特殊状态台账、发薪批次等需要选择全部员工的场景
|
||||
*/
|
||||
router.get('/all-lite', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId: req.user!.orgId, status: { in: ['ACTIVE', 'RESIGNED'] } },
|
||||
select: { id: true, name: true, department: true, gender: true, status: true },
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
res.json({ success: true, data: employees })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employee = await getEmployeeDetail(req.user!.orgId, req.params.id)
|
||||
|
||||
Reference in New Issue
Block a user