fix: 全面优化安全、性能和代码质量
P0: 环境变量校验/事务保护/RBAC权限 P1: 花名册分页/密码重置验证码/ErrorBoundary/N+1查询优化 P2: 导出限制/自定义错误类/body大小限制/代码去重 P3: 自定义确认弹窗替换window.confirm
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user