feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user