diff --git a/backend/scripts/assign-accounts.ts b/backend/scripts/assign-accounts.ts new file mode 100644 index 0000000..4192533 --- /dev/null +++ b/backend/scripts/assign-accounts.ts @@ -0,0 +1,154 @@ +/** + * 给全部员工对应上正确的社保和公积金账户 + * 逻辑:按员工所在城市的账户匹配,回退到默认账户 + */ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +async function main() { + // 1. 查所有账户(按城市分组) + const accounts = await prisma.socialAccount.findMany({ + select: { id: true, name: true, type: true, city: true, isDefault: true }, + }) + console.log('=== 全部账户 ===') + for (const a of accounts) { + console.log(`${a.type} | ${a.name} | ${a.city} | default=${a.isDefault} | ${a.id}`) + } + + // 按城市+类型建索引 + const socialByCity: Record = {} + const housingByCity: Record = {} + let defaultSocial: string | null = null + let defaultHousing: string | null = null + + for (const a of accounts) { + if (a.type === 'SOCIAL') { + if (a.isDefault) defaultSocial = a.id + if (a.city) socialByCity[a.city] = a.id + } else { + if (a.isDefault) defaultHousing = a.id + if (a.city) housingByCity[a.city] = a.id + } + } + + // 2. 查所有部门 + const depts = await prisma.department.findMany({ + select: { id: true, name: true, level: true, parentId: true, socialAccountId: true, housingAccountId: true }, + }) + console.log('\n=== 部门账户关联(修改前)===') + for (const d of depts) { + console.log(`${d.name} | level=${d.level} | social=${d.socialAccountId || '无'} | housing=${d.housingAccountId || '无'}`) + } + + // 3. 查所有员工及其部门和城市 + const employees = await prisma.employee.findMany({ + select: { id: true, name: true, department: true, departmentId: true, city: true, dept: true }, + }) + console.log(`\n=== 员工总数: ${employees.length} ===`) + + // 4. 为每个员工找到根部门,检查是否有账户 + // 如果根部门没有账户,按员工城市匹配账户,更新根部门 + const rootDeptMap = new Map() + + for (const emp of employees) { + // 向上找根部门 + let dept: any = emp.dept + if (!dept && emp.departmentId) { + dept = await prisma.department.findUnique({ where: { id: emp.departmentId } }) + } + while (dept && dept.level > 0 && dept.parentId) { + dept = await prisma.department.findUnique({ where: { id: dept.parentId } }) + } + if (dept) { + const existing = rootDeptMap.get(dept.id) + if (existing) { + existing.empCount++ + } else { + rootDeptMap.set(dept.id, { name: dept.name, city: emp.city || '', empCount: 1 }) + } + } + } + + console.log('\n=== 根部门及员工城市 ===') + for (const [deptId, info] of rootDeptMap) { + const dept = await prisma.department.findUnique({ where: { id: deptId }, select: { socialAccountId: true, housingAccountId: true } }) + console.log(`${info.name} | 员工数=${info.empCount} | 员工城市=${info.city || '无'} | social=${dept?.socialAccountId || '无'} | housing=${dept?.housingAccountId || '无'}`) + } + + // 5. 为没有账户的根部门按员工城市匹配账户 + let updated = 0 + for (const [deptId, info] of rootDeptMap) { + const dept = await prisma.department.findUnique({ where: { id: deptId }, select: { socialAccountId: true, housingAccountId: true, name: true } }) + + let socialId = dept?.socialAccountId || null + let housingId = dept?.housingAccountId || null + + // 社保账户 + if (!socialId) { + if (info.city && socialByCity[info.city]) { + socialId = socialByCity[info.city] + } else if (defaultSocial) { + socialId = defaultSocial + } + } + + // 公积金账户 + if (!housingId) { + if (info.city && housingByCity[info.city]) { + housingId = housingByCity[info.city] + } else if (defaultHousing) { + housingId = defaultHousing + } + } + + if ((socialId && !dept?.socialAccountId) || (housingId && !dept?.housingAccountId)) { + await prisma.department.update({ + where: { id: deptId }, + data: { + ...(socialId && !dept?.socialAccountId ? { socialAccountId: socialId } : {}), + ...(housingId && !dept?.housingAccountId ? { housingAccountId: housingId } : {}), + }, + }) + console.log(`[更新] 根部门 ${dept?.name} → social=${socialId || '无'} housing=${housingId || '无'}`) + updated++ + } + } + + console.log(`\n更新了 ${updated} 个根部门的账户关联`) + + // 6. 验证:重新查每个员工是否有账户 + let noSocial = 0 + let noHousing = 0 + let hasBoth = 0 + for (const emp of employees) { + let dept: any = emp.dept + if (!dept && emp.departmentId) { + dept = await prisma.department.findUnique({ where: { id: emp.departmentId } }) + } + while (dept && dept.level > 0 && dept.parentId) { + dept = await prisma.department.findUnique({ where: { id: dept.parentId } }) + } + + let socialId = dept?.socialAccountId || null + let housingId = dept?.housingAccountId || null + + // 回退到默认 + if (!socialId) socialId = defaultSocial + if (!housingId) housingId = defaultHousing + + if (socialId && housingId) { + hasBoth++ + } else { + if (!socialId) noSocial++ + if (!housingId) noHousing++ + console.log(`[缺失] ${emp.name} | social=${socialId ? '有' : '无'} housing=${housingId ? '有' : '无'} | city=${emp.city || '无'} | dept=${dept?.name || '无'}`) + } + } + + console.log(`\n=== 验证结果 ===`) + console.log(`有社保+公积金: ${hasBoth}`) + console.log(`缺社保: ${noSocial}`) + console.log(`缺公积金: ${noHousing}`) +} + +main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/backend/scripts/check-test-accounts.ts b/backend/scripts/check-test-accounts.ts new file mode 100644 index 0000000..18e7c22 --- /dev/null +++ b/backend/scripts/check-test-accounts.ts @@ -0,0 +1,80 @@ +/** + * 检查测试账户是否有关联数据(员工/部门),然后删除无关联的测试账户 + */ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +const TEST_IDS = [ + 'cmsve0ucj000h5kfta5te1qli', // 几何社保账户 + 'cmsve0uea001x5kftbvf9e6zq', // 北公积金账户 + 'cmsve0uec001z5kft5sfl4ika', // s公积金账户 + 'cmsve0ued00215kft32gmeug5', // sh公积金账户 + 'cmsve0uef00235kftz4clk842', // sha公积金账户 + 'cmsve0ueg00255kfttwtfeikj', // shan公积金账户 + 'cmsve0uei00275kftbwdrkf2n', // shang公积金账户 + 'cmsve0uej00295kftc157ts8b', // 上公积金账户 + 'cmsve0uek002b5kftnmmsqpfl', // shi公积金账户 + 'cmsve0uen002d5kftnzstcqa3', // shi'j公积金账户 + 'cmsve0ueo002f5kftxhh9r5lw', // shi'ji公积金账户 + 'cmsve0ueq002h5kft2fdu2tlk', // shi'jia公积金账户 + 'cmsve0uer002j5kftsi37wcuo', // shi'jia'z公积金账户 + 'cmsve0uet002l5kftev27qy7o', // shi'jia'zh公积金账户 + 'cmsve0uf5002x5kft16esy662', // 唐山12+6公积金账户 + 'cmsve0uf9002z5kft3lx7yl1x', // 几何公积金账户 + 'cmsve0ufb00315kftp9mnovag', // 几何5+5公积金账户 +] + +async function main() { + console.log('=== 检查关联 ===') + let canDelete: string[] = [] + let blocked: string[] = [] + + for (const id of TEST_IDS) { + const account = await prisma.socialAccount.findUnique({ where: { id }, select: { name: true, city: true, type: true } }) + if (!account) { + console.log(`[不存在] ${id}`) + continue + } + + const d1 = await prisma.department.count({ where: { socialAccountId: id } }) + const d2 = await prisma.department.count({ where: { housingAccountId: id } }) + + if (d1 + d2 > 0) { + console.log(`[阻止] ${account.type} ${account.name} (${account.city}) — 部门S/H:${d1}/${d2}`) + blocked.push(id) + } else { + console.log(`[可删] ${account.type} ${account.name} (${account.city})`) + canDelete.push(id) + } + } + + console.log(`\n可删除 ${canDelete.length} 个,阻止 ${blocked.length} 个`) + + if (canDelete.length === 0) { + console.log('无可删除账户') + return + } + + // 删除:先删关联的年度标准、社保记录、公积金记录,再删账户 + console.log('\n=== 开始删除 ===') + for (const id of canDelete) { + const account = await prisma.socialAccount.findUnique({ where: { id }, select: { name: true } }) + + // 删年度标准 + const stdDeleted = await prisma.socialYearStandard.deleteMany({ where: { accountId: id } }) + // 删员工社保记录 + const srDeleted = await prisma.employeeSocialInsRecord.deleteMany({ where: { accountId: id } }) + // 删员工公积金记录 + const hrDeleted = await prisma.employeeHousingFundRecord.deleteMany({ where: { accountId: id } }) + // 删月度处理记录 + const mpDeleted = await prisma.socialMonthlyProcess.deleteMany({ where: { accountId: id } }) + // 删账户 + await prisma.socialAccount.delete({ where: { id } }) + + console.log(`[已删] ${account?.name} — 标准:${stdDeleted.count} 社保记录:${srDeleted.count} 公积金记录:${hrDeleted.count} 月度:${mpDeleted.count}`) + } + + console.log('\n删除完成') +} + +main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/backend/scripts/fix-housing-base-limits.ts b/backend/scripts/fix-housing-base-limits.ts new file mode 100644 index 0000000..d073ca8 --- /dev/null +++ b/backend/scripts/fix-housing-base-limits.ts @@ -0,0 +1,47 @@ +/** + * 批量更新公积金账户的 housingBaseMin/housingBaseMax + * 数据来源:各城市2025年度公积金缴存基数上下限 + */ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +// 2025年度各城市公积金基数上下限 +const CITY_LIMITS: Record = { + '北京': { min: 2540, max: 35811 }, + '上海': { min: 2690, max: 37302 }, + '深圳': { min: 2360, max: 44265 }, + '杭州': { min: 2490, max: 40694 }, + '石家庄': { min: 2200, max: 26420 }, + '天津': { min: 2320, max: 27861 }, + '唐山': { min: 2200, max: 26420 }, // 河北省标准 +} + +async function main() { + const accounts = await prisma.socialAccount.findMany({ where: { type: 'HOUSING' } }) + let updated = 0 + let skipped = 0 + + for (const account of accounts) { + const limits = CITY_LIMITS[account.city] + if (!limits) { + console.log(`[跳过] ${account.name} (${account.city}) — 无该城市的公积金上下限数据`) + skipped++ + continue + } + + // 更新该账户下所有年度标准的 housingBaseMin/housingBaseMax + const result = await prisma.socialYearStandard.updateMany({ + where: { accountId: account.id }, + data: { + housingBaseMin: limits.min, + housingBaseMax: limits.max, + }, + }) + console.log(`[更新] ${account.name} (${account.city}) → 下限 ${limits.min} / 上限 ${limits.max},影响 ${result.count} 条标准`) + updated++ + } + + console.log(`\n完成:更新 ${updated} 个账户,跳过 ${skipped} 个`) +} + +main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/backend/scripts/fix-missing-standards.ts b/backend/scripts/fix-missing-standards.ts new file mode 100644 index 0000000..a2194e2 --- /dev/null +++ b/backend/scripts/fix-missing-standards.ts @@ -0,0 +1,99 @@ +/** + * 批量补齐无年度标准的账户:从旧 SocialInsuranceConfig / HousingFundConfig 继承数据 + */ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() + +async function main() { + const accounts = await prisma.socialAccount.findMany() + let created = 0 + let skipped = 0 + + for (const account of accounts) { + // 检查是否已有标准 + const existing = await prisma.socialYearStandard.findFirst({ + where: { accountId: account.id, isCurrent: true }, + }) + if (existing) { + skipped++ + continue + } + + // 查旧配置 + let oldConfig: any = null + if (account.type === 'HOUSING') { + oldConfig = await prisma.housingFundConfig.findFirst({ + where: { orgId: account.orgId, city: account.city }, + orderBy: { effectiveFrom: 'desc' }, + }) + } else { + oldConfig = await prisma.socialInsuranceConfig.findFirst({ + where: { orgId: account.orgId, city: account.city }, + orderBy: { effectiveFrom: 'desc' }, + }) + } + + if (!oldConfig) { + console.log(`[跳过] ${account.type} ${account.name} (${account.city}) — 无旧配置可继承`) + continue + } + + // 检查是否已有同 effectiveFrom 的标准(含历史标准) + const dupCheck = await prisma.socialYearStandard.findFirst({ + where: { accountId: account.id, effectiveFrom: oldConfig.effectiveFrom }, + }) + if (dupCheck) { + // 把已有的设为 current + await prisma.socialYearStandard.updateMany({ + where: { accountId: account.id, effectiveFrom: oldConfig.effectiveFrom }, + data: { isCurrent: true }, + }) + console.log(`[修复] ${account.type} ${account.name} (${account.city}) — 已有标准设为 current`) + created++ + continue + } + + // 创建年度标准 + const std = await prisma.socialYearStandard.create({ + data: { + orgId: account.orgId, + accountId: account.id, + // 社保比例 + pensionOrg: oldConfig.pensionOrg || 16, + pensionEmp: oldConfig.pensionEmp || 8, + medicalOrg: oldConfig.medicalOrg || 9.8, + medicalEmp: oldConfig.medicalEmp || 2, + medicalOrgExtra: (oldConfig as any).medicalOrgExtra || 0, + medicalEmpExtra: (oldConfig as any).medicalEmpExtra || 0, + unemploymentOrg: oldConfig.unemploymentOrg || 0.5, + unemploymentEmp: oldConfig.unemploymentEmp || 0.5, + injuryOrg: oldConfig.injuryOrg || 0.2, + maternityOrg: oldConfig.maternityOrg || 0.8, + baseMin: oldConfig.baseMin || 6326, + baseMax: oldConfig.baseMax || 33891, + medicalBaseMin: oldConfig.medicalBaseMin || 0, + medicalBaseMax: oldConfig.medicalBaseMax || 0, + extraInsurances: oldConfig.extraInsurances || null, + // 公积金比例 + housingOrg: (oldConfig as any).housingOrg || 12, + housingEmp: (oldConfig as any).housingEmp || 12, + housingBaseMin: 0, + housingBaseMax: 0, + // 最低工资 + minWage: (oldConfig as any).minWage || 0, + // 生效信息 + effectiveFrom: oldConfig.effectiveFrom, + effectiveTo: null, + isCurrent: true, + adjustmentDone: false, + createdBy: account.createdBy, + }, + }) + console.log(`[创建] ${account.type} ${account.name} (${account.city}) ← ${oldConfig.effectiveFrom}`) + created++ + } + + console.log(`\n完成:创建 ${created} 个标准,跳过 ${skipped} 个已有标准`) +} + +main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) })