fix: 全面优化安全、性能和代码质量

P0: 环境变量校验/事务保护/RBAC权限
P1: 花名册分页/密码重置验证码/ErrorBoundary/N+1查询优化
P2: 导出限制/自定义错误类/body大小限制/代码去重
P3: 自定义确认弹窗替换window.confirm
This commit is contained in:
freedakgmail
2026-07-24 22:28:58 +08:00
parent 9ec21fedea
commit 5c12f28ac7
23 changed files with 634 additions and 366 deletions
+183 -200
View File
@@ -136,150 +136,125 @@ export function assessRisk(employee: any, reason: string): { level: RiskAssessme
return { level, warnings }
}
async function createTerminationRecord(
orgId: string,
userId: string,
data: any,
recordData: {
type: 'TERMINATION' | 'RESIGNATION'
reason: TerminationReason | 'RESIGNATION'
compensation?: number
riskLevel: RiskAssessment
checklist: any
remark?: string | null
resignationReason?: string | null
},
conflictMsg: string,
) {
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
const latestTerm = await prisma.terminationRecord.findFirst({
where: { employeeId: data.employeeId },
orderBy: { terminationDate: 'desc' },
})
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
throw { code: 'CONFLICT', message: conflictMsg }
}
const termDate = new Date(data.terminationDate)
const termMonth = dateToMonth(termDate)
const socialInsEndMonth = data.socialInsEndMonth || termMonth
const housingFundEndMonth = data.housingFundEndMonth || termMonth
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
return await prisma.$transaction(async (tx) => {
const record = await tx.terminationRecord.create({
data: {
orgId,
employeeId: data.employeeId,
terminationDate: termDate,
socialInsEndMonth,
housingFundEndMonth,
createdBy: userId,
...recordData,
},
})
await tx.employeeSocialInsRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
await tx.employeeHousingFundRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
await tx.employee.update({
where: { id: data.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await tx.riskItem.updateMany({
where: { employeeId: data.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
return { id: record.id }
})
}
export async function createTermination(orgId: string, userId: string, data: any) {
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次解聘
const latestTerm = await prisma.terminationRecord.findFirst({
where: { employeeId: data.employeeId },
orderBy: { terminationDate: 'desc' },
})
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣' }
}
const { level } = assessRisk(employee, data.reason)
const termDate = new Date(data.terminationDate)
const termMonth = dateToMonth(termDate)
const socialInsEndMonth = data.socialInsEndMonth || termMonth
const housingFundEndMonth = data.housingFundEndMonth || termMonth
const record = await prisma.terminationRecord.create({
data: {
orgId,
employeeId: data.employeeId,
return createTerminationRecord(
orgId,
userId,
data,
{
type: 'TERMINATION',
reason: data.reason,
terminationDate: termDate,
compensation: data.compensation || 0,
socialInsEndMonth,
housingFundEndMonth,
riskLevel: level,
checklist: data.checklist || {},
remark: data.remark,
createdBy: userId,
},
})
// 关闭社保缴费记录(设置 endMonth)
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 根据解聘日期判断在职/离职状态
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: data.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await prisma.riskItem.updateMany({
where: { employeeId: data.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
return { id: record.id }
'该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣',
)
}
// 员工主动离职
export async function createResignation(orgId: string, userId: string, data: any) {
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次离职
const latestTerm = await prisma.terminationRecord.findFirst({
where: { employeeId: data.employeeId },
orderBy: { terminationDate: 'desc' },
})
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次办理请先重新雇佣' }
}
const termDate = new Date(data.terminationDate)
const termMonth = dateToMonth(termDate)
const socialInsEndMonth = data.socialInsEndMonth || termMonth
const housingFundEndMonth = data.housingFundEndMonth || termMonth
const record = await prisma.terminationRecord.create({
data: {
orgId,
employeeId: data.employeeId,
return createTerminationRecord(
orgId,
userId,
data,
{
type: 'RESIGNATION',
reason: 'RESIGNATION',
terminationDate: termDate,
resignationReason: data.resignationReason || null,
compensation: 0,
socialInsEndMonth,
housingFundEndMonth,
riskLevel: 'SAFE',
checklist: {},
remark: data.remark || null,
createdBy: userId,
},
})
// 关闭社保缴费记录
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 根据离职日期判断在职/离职状态
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: data.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await prisma.riskItem.updateMany({
where: { employeeId: data.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
return { id: record.id }
'该员工已有离职/解聘记录,如需再次办理请先重新雇佣',
)
}
// 撤回离职/解聘(仅未到日期可撤回)
@@ -439,73 +414,84 @@ export async function batchTerminate(
const success: string[] = []
const failed: Array<{ employeeId: string; reason: string }> = []
const employeeIds = items.map((i) => i.employeeId)
const employees = await prisma.employee.findMany({
where: { id: { in: employeeIds }, orgId },
})
const empMap = new Map(employees.map((e) => [e.id, e]))
const existingTerms = await prisma.terminationRecord.findMany({
where: { employeeId: { in: employeeIds } },
orderBy: { terminationDate: 'desc' },
})
const latestTermMap = new Map<string, Date>()
for (const t of existingTerms) {
if (!latestTermMap.has(t.employeeId)) {
latestTermMap.set(t.employeeId, t.terminationDate)
}
}
const today = new Date()
today.setHours(0, 0, 0, 0)
for (const item of items) {
try {
const termDate = new Date(item.terminationDate)
const termMonth = dateToMonth(termDate)
// 校验:已有离职/解聘记录
const latestTerm = await prisma.terminationRecord.findFirst({
where: { employeeId: item.employeeId },
orderBy: { terminationDate: 'desc' },
})
const employee = await prisma.employee.findFirst({ where: { id: item.employeeId, orgId } })
const employee = empMap.get(item.employeeId)
if (!employee) {
failed.push({ employeeId: item.employeeId, reason: '员工不存在' })
continue
}
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
const latestTermDate = latestTermMap.get(item.employeeId)
if (latestTermDate && latestTermDate >= employee.hireDate) {
failed.push({ employeeId: item.employeeId, reason: '该员工已有离职/解聘记录' })
continue
}
const termDate = new Date(item.terminationDate)
const termMonth = dateToMonth(termDate)
const { level } = assessRisk(employee, item.reason)
await prisma.terminationRecord.create({
data: {
orgId,
employeeId: item.employeeId,
type: 'TERMINATION',
reason: item.reason as TerminationReason,
terminationDate: termDate,
compensation: item.compensation || 0,
socialInsEndMonth: termMonth,
housingFundEndMonth: termMonth,
riskLevel: level,
checklist: {},
remark: '批量解聘',
createdBy: userId,
},
})
// 关闭社保和公积金
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: termMonth },
})
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: termMonth },
})
// 更新员工状态
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: item.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth: termMonth,
housingFundEndMonth: termMonth,
},
})
await prisma.$transaction(async (tx) => {
await tx.terminationRecord.create({
data: {
orgId,
employeeId: item.employeeId,
type: 'TERMINATION',
reason: item.reason as TerminationReason,
terminationDate: termDate,
compensation: item.compensation || 0,
socialInsEndMonth: termMonth,
housingFundEndMonth: termMonth,
riskLevel: level,
checklist: {},
remark: '批量解聘',
createdBy: userId,
},
})
// 关闭风险项
await prisma.riskItem.updateMany({
where: { employeeId: item.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
await tx.employeeSocialInsRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: termMonth },
})
await tx.employeeHousingFundRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: termMonth },
})
await tx.employee.update({
where: { id: item.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth: termMonth,
housingFundEndMonth: termMonth,
},
})
await tx.riskItem.updateMany({
where: { employeeId: item.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
})
success.push(item.employeeId)
@@ -687,42 +673,39 @@ export async function executeTermination(orgId: string, recordId: string, userId
const socialInsEndMonth = record.socialInsEndMonth || termMonth
const housingFundEndMonth = record.housingFundEndMonth || termMonth
// 关闭社保缴费记录
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: record.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: record.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 更新员工状态
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: record.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await prisma.$transaction(async (tx) => {
await tx.employeeSocialInsRecord.updateMany({
where: { employeeId: record.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭风险项
await prisma.riskItem.updateMany({
where: { employeeId: record.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
await tx.employeeHousingFundRecord.updateMany({
where: { employeeId: record.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 标记为已完成
await prisma.terminationRecord.update({
where: { id: recordId },
data: { status: 'COMPLETED', updatedBy: userId },
await tx.employee.update({
where: { id: record.employeeId },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await tx.riskItem.updateMany({
where: { employeeId: record.employeeId, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
await tx.terminationRecord.update({
where: { id: recordId },
data: { status: 'COMPLETED', updatedBy: userId },
})
})
return { id: recordId }