feat: 完成优化1-6全部功能 — Portal安全/AI增强/Settings导入导出/社保公积金版本化
- 优化-1: 社保公积金独立配置+版本化缴费记录+多城市支持+迁移脚本 - 优化-2: AI流式输出/RAG集成/风险角标/审计日志/二维码/批量续签/忘记密码/语音输入/PDF导出/速率限制/套餐人数上限 - 优化-3: 批次重命名/费用实时预览/模拟版本管理/续签合规预检/社保重置/搜索分页/批量解聘/到期预警/税率试算 - 优化-4: 会话历史/待办批量/结果关联档案/风险下钻/预测上下文/附件校验/Tab级联/薪税导出 - 优化-5: 表单回填/用户编辑禁用/导入预览/选择性导出/通知测试/错误日志导出/脱敏导出/gzip压缩 - 优化-6: 工资条确认通知HR/AI上下文增强/电子签名/用量限制修复/入职文件上传/RAG管理/工资趋势/用量事务/验证码加固/审查结构化/链接撤回/超时机制/确认重发/案例转待办
This commit is contained in:
@@ -3,48 +3,155 @@ import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import ExcelJS from 'exceljs'
|
||||
import { createGzip } from 'zlib'
|
||||
import { Writable } from 'stream'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 敏感字段脱敏
|
||||
function maskIdCard(idCard: string | null): string | null {
|
||||
if (!idCard) return null
|
||||
if (idCard.length >= 11) return idCard.slice(0, 3) + '*'.repeat(idCard.length - 7) + idCard.slice(-4)
|
||||
return idCard
|
||||
}
|
||||
function maskBankAccount(account: string | null): string | null {
|
||||
if (!account) return null
|
||||
if (account.length > 4) return '*'.repeat(account.length - 4) + account.slice(-4)
|
||||
return account
|
||||
}
|
||||
|
||||
// 导出全部数据(支持模块选择、格式选择、脱敏)
|
||||
router.get('/all', authMiddleware, async (req: AuthRequest, res: Response, next) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const format = (req.query.format as string) || 'json'
|
||||
const mask = req.query.mask === 'true' || req.user!.role !== 'ADMIN'
|
||||
const modules = (req.query.modules as string || 'employees,contracts,terminations,payrollBatches,payslips,socialRecords,housingRecords,riskItems').split(',')
|
||||
|
||||
const [employees, contracts, terminations, payrollBatches, payslips, socialRecords, housingRecords, riskItems] = await Promise.all([
|
||||
prisma.employee.findMany({ where: { orgId } }),
|
||||
prisma.laborContract.findMany({ where: { orgId } }),
|
||||
prisma.terminationRecord.findMany({ where: { orgId } }),
|
||||
prisma.payrollBatch.findMany({ where: { orgId } }),
|
||||
prisma.payslip.findMany({ where: { orgId } }),
|
||||
prisma.employeeSocialInsRecord.findMany({ where: { orgId } }),
|
||||
prisma.employeeHousingFundRecord.findMany({ where: { orgId } }),
|
||||
prisma.riskItem.findMany({ where: { orgId } }),
|
||||
])
|
||||
|
||||
const safeEmployees = employees.map((e) => {
|
||||
let salary = 0
|
||||
try { salary = Number(decrypt(e.monthlySalary)) || 0 } catch { salary = Number(e.monthlySalary) || 0 }
|
||||
let idCard = null
|
||||
try { if (e.idCardNumber) idCard = decrypt(e.idCardNumber) } catch { idCard = e.idCardNumber }
|
||||
return { ...e, monthlySalary: salary, idCardNumber: idCard }
|
||||
})
|
||||
|
||||
const data = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
orgId,
|
||||
employees: safeEmployees,
|
||||
contracts,
|
||||
terminations,
|
||||
payrollBatches,
|
||||
payslips,
|
||||
socialRecords,
|
||||
housingRecords,
|
||||
riskItems,
|
||||
const fetchMap: Record<string, () => Promise<any>> = {
|
||||
employees: () => prisma.employee.findMany({ where: { orgId } }),
|
||||
contracts: () => prisma.laborContract.findMany({ where: { orgId } }),
|
||||
terminations: () => prisma.terminationRecord.findMany({ where: { orgId } }),
|
||||
payrollBatches: () => prisma.payrollBatch.findMany({ where: { orgId } }),
|
||||
payslips: () => prisma.payslip.findMany({ where: { orgId } }),
|
||||
socialRecords: () => prisma.employeeSocialInsRecord.findMany({ where: { orgId } }),
|
||||
housingRecords: () => prisma.employeeHousingFundRecord.findMany({ where: { orgId } }),
|
||||
riskItems: () => prisma.riskItem.findMany({ where: { orgId } }),
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="export-${new Date().toISOString().slice(0, 10)}.json"`)
|
||||
res.json(data)
|
||||
const useGzip = req.query.gzip !== 'false'
|
||||
const batchSize = 500
|
||||
|
||||
if (format === 'excel') {
|
||||
const data: any = { exportedAt: new Date().toISOString(), orgId }
|
||||
|
||||
if (modules.includes('employees')) {
|
||||
const employees = await fetchMap.employees()
|
||||
data.employees = employees.map((e: any) => {
|
||||
let salary = 0
|
||||
try { salary = Number(decrypt(e.monthlySalary)) || 0 } catch { salary = Number(e.monthlySalary) || 0 }
|
||||
let idCard: string | null = null
|
||||
try { if (e.idCardNumber) idCard = decrypt(e.idCardNumber) } catch { idCard = e.idCardNumber }
|
||||
let bankAccount: string | null = null
|
||||
try { if (e.bankAccount) bankAccount = decrypt(e.bankAccount) } catch { bankAccount = e.bankAccount }
|
||||
if (mask) {
|
||||
idCard = maskIdCard(idCard)
|
||||
bankAccount = maskBankAccount(bankAccount)
|
||||
if (salary) salary = 0
|
||||
}
|
||||
return { ...e, monthlySalary: salary, idCardNumber: idCard, bankAccount }
|
||||
})
|
||||
}
|
||||
|
||||
for (const mod of modules) {
|
||||
if (mod === 'employees') continue
|
||||
if (fetchMap[mod]) {
|
||||
data[mod] = await fetchMap[mod]()
|
||||
}
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
for (const mod of modules) {
|
||||
if (!data[mod] || !data[mod].length) continue
|
||||
const ws = workbook.addWorksheet(mod.slice(0, 31))
|
||||
const rows = data[mod]
|
||||
const keys = Object.keys(rows[0]).filter(k => typeof rows[0][k] !== 'object')
|
||||
ws.columns = keys.map(k => ({ header: k, key: k, width: 18 }))
|
||||
ws.getRow(1).font = { bold: true }
|
||||
for (const row of rows) {
|
||||
const flat: any = {}
|
||||
for (const k of keys) flat[k] = typeof row[k] === 'object' ? JSON.stringify(row[k]) : row[k]
|
||||
ws.addRow(flat)
|
||||
}
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="export-${new Date().toISOString().slice(0, 10)}.xlsx"`)
|
||||
await workbook.xlsx.write(res)
|
||||
res.end()
|
||||
} else {
|
||||
// JSON 流式导出 + gzip 压缩
|
||||
if (useGzip) {
|
||||
res.setHeader('Content-Encoding', 'gzip')
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="export-${new Date().toISOString().slice(0, 10)}.json.gz"`)
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="export-${new Date().toISOString().slice(0, 10)}.json"`)
|
||||
}
|
||||
|
||||
const gzip = useGzip ? createGzip() : null
|
||||
const output: Writable = gzip || res
|
||||
if (gzip) { gzip.pipe(res) }
|
||||
|
||||
const write = (chunk: string) => {
|
||||
output.write(Buffer.from(chunk))
|
||||
}
|
||||
|
||||
write('{"exportedAt":"' + new Date().toISOString() + '","orgId":"' + orgId + '"')
|
||||
|
||||
for (const mod of modules) {
|
||||
write(',"' + mod + '":[')
|
||||
|
||||
if (mod === 'employees') {
|
||||
// 员工数据分批查询,避免内存溢出
|
||||
let skip = 0
|
||||
let first = true
|
||||
while (true) {
|
||||
const batch = await prisma.employee.findMany({ where: { orgId }, skip, take: batchSize })
|
||||
if (batch.length === 0) break
|
||||
for (const e of batch) {
|
||||
let salary = 0
|
||||
try { salary = Number(decrypt(e.monthlySalary)) || 0 } catch { salary = Number(e.monthlySalary) || 0 }
|
||||
let idCard: string | null = null
|
||||
try { if (e.idCardNumber) idCard = decrypt(e.idCardNumber) } catch { idCard = e.idCardNumber }
|
||||
let bankAccount: string | null = null
|
||||
try { if (e.bankAccount) bankAccount = decrypt(e.bankAccount) } catch { bankAccount = e.bankAccount }
|
||||
if (mask) {
|
||||
idCard = maskIdCard(idCard)
|
||||
bankAccount = maskBankAccount(bankAccount)
|
||||
if (salary) salary = 0
|
||||
}
|
||||
const row = { ...e, monthlySalary: salary, idCardNumber: idCard, bankAccount }
|
||||
write((first ? '' : ',') + JSON.stringify(row))
|
||||
first = false
|
||||
}
|
||||
skip += batchSize
|
||||
if (batch.length < batchSize) break
|
||||
}
|
||||
} else if (fetchMap[mod]) {
|
||||
const rows = await fetchMap[mod]()
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
write((i === 0 ? '' : ',') + JSON.stringify(rows[i]))
|
||||
}
|
||||
}
|
||||
|
||||
write(']')
|
||||
}
|
||||
|
||||
write('}')
|
||||
if (gzip) gzip.end()
|
||||
else res.end()
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user