feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式

This commit is contained in:
freedakgmail
2026-07-27 23:45:22 +08:00
parent 71d7ab2de5
commit 5e22a82163
21 changed files with 1246 additions and 178 deletions
+96 -44
View File
@@ -3,6 +3,7 @@ import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { decrypt } from '../lib/crypto'
import { z } from 'zod'
import OpenAI from 'openai'
const router = Router()
router.use(authMiddleware)
@@ -50,26 +51,7 @@ router.get('/config', async (req: AuthRequest, res: Response, next: NextFunction
})
}
if (!config) {
try {
config = await prisma.socialInsuranceConfig.create({
data: {
orgId: req.user!.orgId,
effectiveFrom: new Date().toISOString().slice(0, 7),
city: city || '北京',
isCurrent: true,
createdBy: req.user!.id,
},
})
} catch {
// 唯一约束冲突,查询同城市任意配置
config = await prisma.socialInsuranceConfig.findFirst({
where: { orgId: req.user!.orgId, city: city || '北京' },
orderBy: { effectiveFrom: 'desc' },
})
}
}
if (!config) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到社保配置' } })
return res.json({ success: true, data: null })
}
res.json({ success: true, data: config })
} catch (err) {
@@ -405,9 +387,7 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
})
}
if (!config) {
config = await prisma.socialInsuranceConfig.create({
data: { orgId, effectiveFrom: new Date().toISOString().slice(0, 7), city: city || '北京', createdBy: req.user!.id },
})
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的社保配置,请先在社保管理中创建` } })
}
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
@@ -496,24 +476,7 @@ router.get('/housing-config', async (req: AuthRequest, res: Response, next: Next
})
}
if (!config) {
try {
config = await prisma.housingFundConfig.create({
data: {
orgId: req.user!.orgId,
effectiveFrom: new Date().toISOString().slice(0, 7),
city: city || '北京',
createdBy: req.user!.id,
},
})
} catch {
config = await prisma.housingFundConfig.findFirst({
where: { orgId: req.user!.orgId, city: city || '北京' },
orderBy: { effectiveFrom: 'desc' },
})
}
}
if (!config) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到公积金配置' } })
return res.json({ success: true, data: null })
}
res.json({ success: true, data: config })
} catch (err) {
@@ -608,9 +571,7 @@ router.post('/housing-calculate', async (req: AuthRequest, res: Response, next:
})
}
if (!config) {
config = await prisma.housingFundConfig.create({
data: { orgId, effectiveFrom: new Date().toISOString().slice(0, 7), city: city || '北京', createdBy: req.user!.id },
})
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的公积金配置,请先在公积金管理中创建` } })
}
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
@@ -1453,4 +1414,95 @@ router.post('/special-deduction/batch', async (req: AuthRequest, res: Response,
}
})
// ========== AI 社保政策建议 ==========
const aiSuggestSchema = z.object({
city: z.string(),
effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/),
type: z.enum(['social', 'housing']).default('social'),
})
router.post('/ai-suggest', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { city, effectiveFrom, type } = aiSuggestSchema.parse(req.body)
const apiKey = process.env.DASHSCOPE_API_KEY || ''
if (!apiKey) {
return res.status(400).json({ success: false, error: { code: 'NO_API_KEY', message: 'AI服务未配置' } })
}
const client = new OpenAI({ apiKey, baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', timeout: 30 * 1000, maxRetries: 1 })
const year = effectiveFrom.split('-')[0]
const prompt = type === 'social'
? `请提供${city}${year}年度的社保缴费政策。请严格按照以下JSON格式返回,不要包含任何其他文字:
{
"baseMin": 数字,
"baseMax": 数字,
"medicalBaseMin": 数字,
"medicalBaseMax": 数字,
"pensionOrg": 数字,
"pensionEmp": 数字,
"medicalOrg": 数字,
"medicalEmp": 数字,
"unemploymentOrg": 数字,
"unemploymentEmp": 数字,
"injuryOrg": 数字,
"maternityOrg": 数字,
"extraInsurances": [
{ "name": "险种名称", "baseType": "fixed或pension或medical", "fixedAmount": 数字, "empFixedAmount": 数字, "orgRate": 数字, "empRate": 数字 }
]
}
说明:
- baseMin/baseMax: 养老/失业/工伤保险缴费基数上下限
- medicalBaseMin/medicalBaseMax: 医疗/生育保险缴费基数上下限(与养老相同则填相同值)
- 所有比例单位为百分比,如养老企业16%则填16
- extraInsurances: 大病医疗、长期护理险等附加险种,fixed类型用fixedAmount/empFixedAmount(元/月),比例类型用orgRate/empRate(百分比)
- 如果没有附加险种,返回空数组
- 请基于${year}年度${city}的最新社保政策填写`
: `请提供${city}${year}年度的住房公积金缴存政策。请严格按照以下JSON格式返回,不要包含任何其他文字:
{
"baseMin": 数字,
"baseMax": 数字,
"housingOrg": 数字,
"housingEmp": 数字
}
说明:
- baseMin/baseMax: 公积金缴存基数上下限
- housingOrg/housingEmp: 企业和个人缴存比例(百分比),如12%则填12
- 请基于${year}年度${city}的最新公积金政策填写`
const response = await client.chat.completions.create({
model: 'qwen-plus',
messages: [
{ role: 'system', content: '你是社保政策专家,精通中国各城市的社会保险和住房公积金缴费政策。请只返回JSON格式数据,不要包含markdown代码块标记。' },
{ role: 'user', content: prompt },
],
temperature: 0.1,
})
const content = response.choices[0]?.message?.content || ''
// 提取JSON(兼容markdown代码块)
let jsonStr = content.trim()
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/)
if (jsonMatch) jsonStr = jsonMatch[1].trim()
// 去除可能的非JSON前后文字
const firstBrace = jsonStr.indexOf('{')
const lastBrace = jsonStr.lastIndexOf('}')
if (firstBrace >= 0 && lastBrace >= 0) jsonStr = jsonStr.substring(firstBrace, lastBrace + 1)
const suggested = JSON.parse(jsonStr)
res.json({ success: true, data: suggested })
} catch (err: any) {
next(err)
}
})
export default router