import { PrismaClient } from '@prisma/client' import bcrypt from 'bcryptjs' import { encrypt } from '../src/lib/crypto' const prisma = new PrismaClient() // 社保计算(与 payroll.service.ts 一致) function calcSocial(base: number, config: any) { const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const socialEmp = actualBase * (config.pensionEmp + config.medicalEmp + config.unemploymentEmp) / 100 const socialOrg = actualBase * (config.pensionOrg + config.medicalOrg + config.unemploymentOrg + config.injuryOrg + config.maternityOrg) / 100 return { socialEmp: Math.round(socialEmp * 100) / 100, socialOrg: Math.round(socialOrg * 100) / 100 } } function calcHousing(base: number, config: any) { const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const housingEmp = actualBase * config.housingEmp / 100 const housingOrg = actualBase * config.housingOrg / 100 return { housingEmp: Math.round(housingEmp * 100) / 100, housingOrg: Math.round(housingOrg * 100) / 100 } } function calcTax(taxableIncome: number): number { if (taxableIncome <= 0) return 0 let tax = 0 if (taxableIncome <= 36000) tax = taxableIncome * 0.03 else if (taxableIncome <= 144000) tax = taxableIncome * 0.10 - 2520 else if (taxableIncome <= 300000) tax = taxableIncome * 0.20 - 16920 else if (taxableIncome <= 420000) tax = taxableIncome * 0.25 - 31920 else if (taxableIncome <= 660000) tax = taxableIncome * 0.30 - 52920 else if (taxableIncome <= 960000) tax = taxableIncome * 0.35 - 85920 else tax = taxableIncome * 0.45 - 181920 return Math.max(0, Math.round(tax * 100) / 100) } // 9名员工完整数据 const EMPLOYEES = [ { name: '张伟', gender: '男', dept: '技术部', phone: '13900000001', idCard: '310101199001011234', salary: 18000, hireDate: '2023-03-01', socialBase: 18000, housingBase: 18000, specialDeduction: 2000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 14400, bank: '工商银行', account: '6222021234567890001', emergency: '张父', emergencyPhone: '13800001001', address: '上海市浦东新区张江路100号' }, { name: '李娜', gender: '女', dept: '技术部', phone: '13900000002', idCard: '310102199203052345', salary: 15000, hireDate: '2023-06-15', socialBase: 15000, housingBase: 15000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 12000, bank: '建设银行', account: '6227001234567890002', emergency: '李母', emergencyPhone: '13800001002', address: '上海市徐汇区漕河泾50号', pregnant: true }, { name: '王强', gender: '男', dept: '销售部', phone: '13900000003', idCard: '310103198812103456', salary: 12000, hireDate: '2024-01-10', socialBase: 12000, housingBase: 12000, specialDeduction: 3000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 9600, bank: '招商银行', account: '6225881234567890003', emergency: '王妻', emergencyPhone: '13800001003', address: '上海市闵行区莘庄路200号' }, { name: '赵敏', gender: '女', dept: '人事部', phone: '13900000004', idCard: '310104199506154567', salary: 10000, hireDate: '2022-09-01', socialBase: 10000, housingBase: 10000, specialDeduction: 1500, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '农业银行', account: '6228481234567890004', emergency: '赵父', emergencyPhone: '13800001004', address: '上海市黄浦区南京东路300号' }, { name: '陈刚', gender: '男', dept: '销售部', phone: '13900000005', idCard: '310105199907205678', salary: 8000, hireDate: '2024-07-01', socialBase: 8000, housingBase: 8000, specialDeduction: 0, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 6400, bank: '中国银行', account: '6217001234567890005', emergency: '陈母', emergencyPhone: '13800001005', address: '上海市杨浦区五角场400号' }, { name: '刘洋', gender: '男', dept: '技术部', phone: '13900000006', idCard: '310106198504016789', salary: 22000, hireDate: '2021-04-01', socialBase: 33891, housingBase: 33891, specialDeduction: 4000, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '交通银行', account: '6222601234567890006', emergency: '刘妻', emergencyPhone: '13800001006', address: '上海市长宁区中山公园500号' }, { name: '周婷', gender: '女', dept: '财务部', phone: '13900000007', idCard: '310107199311157890', salary: 13000, hireDate: '2023-11-15', socialBase: 13000, housingBase: 13000, specialDeduction: 2500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 10400, bank: '浦发银行', account: '6225161234567890007', emergency: '周父', emergencyPhone: '13800001007', address: '上海市静安区南京西路600号' }, { name: '孙磊', gender: '男', dept: '技术部', phone: '13900000008', idCard: '310108199008018901', salary: 16000, hireDate: '2022-06-01', socialBase: 16000, housingBase: 16000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 12800, bank: '民生银行', account: '6226161234567890008', emergency: '孙母', emergencyPhone: '13800001008', address: '上海市虹口区四川北路700号' }, { name: '吴芳', gender: '女', dept: '销售部', phone: '13900000009', idCard: '310109199702159012', salary: 9000, hireDate: '2025-02-15', socialBase: 9000, housingBase: 9000, specialDeduction: 500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 7200, bank: '光大银行', account: '6226621234567890009', emergency: '吴夫', emergencyPhone: '13800001009', address: '上海市宝山区牡丹江路800号' }, ] async function main() { // 1. 清空所有数据(按依赖顺序删除) console.log('清空现有数据...') await prisma.notificationLog.deleteMany() await prisma.auditLog.deleteMany() await prisma.batchEntry.deleteMany() await prisma.payrollBatch.deleteMany() await prisma.payslipItem.deleteMany() await prisma.salaryChangeRecord.deleteMany() await prisma.payslip.deleteMany() await prisma.overtimeRecord.deleteMany() await prisma.terminationRecord.deleteMany() await prisma.riskItem.deleteMany() await prisma.employeeAttachment.deleteMany() await prisma.disciplinaryRecord.deleteMany() await prisma.attendanceRecord.deleteMany() await prisma.trainingRecord.deleteMany() await prisma.performanceRecord.deleteMany() await prisma.laborContract.deleteMany() await prisma.contractConfirmLink.deleteMany() await prisma.onboardingLink.deleteMany() await prisma.employee.deleteMany() await prisma.socialInsuranceConfig.deleteMany() await prisma.notificationSetting.deleteMany() await prisma.user.deleteMany() await prisma.organization.deleteMany() console.log('数据已清空') // 2. 创建企业 const org = await prisma.organization.create({ data: { name: '智云科技有限公司', plan: 'PRO', maxEmployees: 50, city: '上海', payrollFrequency: 1, }, }) console.log('企业已创建:', org.name) // 3. 创建管理员 const passwordHash = await bcrypt.hash('12345678', 10) const admin = await prisma.user.create({ data: { orgId: org.id, phone: '13800000001', name: '管理员', passwordHash, role: 'ADMIN', }, }) console.log('管理员已创建:', admin.phone) // 4. 创建社保配置(上海标准) await prisma.socialInsuranceConfig.create({ data: { orgId: org.id, city: '上海', pensionOrg: 16, pensionEmp: 8, medicalOrg: 9.8, medicalEmp: 2, unemploymentOrg: 0.5, unemploymentEmp: 0.5, injuryOrg: 0.2, maternityOrg: 0.8, housingOrg: 7, housingEmp: 7, baseMin: 7384, baseMax: 36921, effectiveFrom: '2025-07', createdBy: admin.id, }, }) console.log('社保配置已创建') // 5. 创建通知设置 await prisma.notificationSetting.create({ data: { orgId: org.id, contractExpiry: true, expiryDays: 30, contractUnsigned: true, overtimeAlert: true, payslipReady: true, payrollDay: 10, socialInsDay: 15, housingFundDay: 15, taxDay: 15, }, }) // 6. 创建薪酬模版(预置项) const defaultItems: { name: string; code: string; type: 'INPUT' | 'CALCULATED'; formula: string | null; order: number; isDefault: boolean; isEditable: boolean }[] = [ { name: '基本工资', code: 'baseSalary', type: 'INPUT', formula: null, order: 1, isDefault: true, isEditable: true }, { name: '加班费', code: 'overtimePay', type: 'CALCULATED', formula: 'weekdayOvertimePay + weekendOvertimePay + holidayOvertimePay', order: 2, isDefault: true, isEditable: false }, { name: '津贴补贴', code: 'allowance', type: 'INPUT', formula: null, order: 3, isDefault: true, isEditable: true }, { name: '奖金', code: 'bonus', type: 'INPUT', formula: null, order: 4, isDefault: true, isEditable: true }, { name: '扣款', code: 'deduction', type: 'INPUT', formula: null, order: 5, isDefault: true, isEditable: true }, { name: '应发合计', code: 'totalPay', type: 'CALCULATED', formula: 'baseSalary + overtimePay + allowance + bonus - deduction', order: 6, isDefault: true, isEditable: false }, { name: '个人社保', code: 'socialEmp', type: 'CALCULATED', formula: 'SOCIAL_EMP', order: 7, isDefault: true, isEditable: false }, { name: '个人公积金', code: 'housingEmp', type: 'CALCULATED', formula: 'HOUSING_EMP', order: 8, isDefault: true, isEditable: false }, { name: '个人所得税', code: 'tax', type: 'CALCULATED', formula: 'TAX', order: 9, isDefault: true, isEditable: false }, { name: '实发工资', code: 'netPay', type: 'CALCULATED', formula: 'totalPay - socialEmp - housingEmp - tax', order: 10, isDefault: true, isEditable: false }, ] for (const item of defaultItems) { await prisma.payslipItem.create({ data: { orgId: org.id, ...item }, }) } console.log('薪酬模版已创建') // 7. 创建9名员工 + 合同 for (let i = 0; i < EMPLOYEES.length; i++) { const e = EMPLOYEES[i] const emp = await prisma.employee.create({ data: { orgId: org.id, name: e.name, department: e.dept, hireDate: new Date(e.hireDate), monthlySalary: encrypt(String(e.salary)), phone: e.phone, idCardNumber: encrypt(e.idCard), gender: e.gender, socialInsBase: e.socialBase, housingFundBase: e.housingBase, specialDeduction: e.specialDeduction, bankName: e.bank, bankAccount: encrypt(e.account), emergencyContact: e.emergency, emergencyPhone: e.emergencyPhone, address: e.address, isPregnant: e.pregnant || false, createdBy: admin.id, }, }) // 创建合同 const startDate = new Date(e.hireDate) const endDate = e.contractType === 'FIXED' ? new Date(startDate.getFullYear() + e.years, startDate.getMonth(), startDate.getDate() - 1) : null await prisma.laborContract.create({ data: { orgId: org.id, employeeId: emp.id, signDate: new Date(e.hireDate), startDate, endDate, contractType: e.contractType as any, signMethod: 'PAPER', contractYears: e.years, probationMonths: e.probation, probationSalary: e.probationSalary, createdBy: admin.id, }, }) console.log(`员工 ${i + 1}/9 已创建: ${e.name} - ${e.dept} - ¥${e.salary}/月`) } // 8. 生成 1-6 月历史工资条(已发布),使 7 月累计预扣个税有 YTD 数据 console.log('\n生成 1-6 月历史工资条...') const socialConfig = await prisma.socialInsuranceConfig.findFirst({ where: { orgId: org.id, isCurrent: true } }) const allEmployees = await prisma.employee.findMany({ where: { orgId: org.id } }) const months = ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06'] for (const emp of allEmployees) { // 跳过 2026 年之后入职的员工 const hireYear = emp.hireDate.getFullYear() if (hireYear > 2026) continue const hireMonth = hireYear === 2026 ? emp.hireDate.getMonth() + 1 : 1 let ytdIncome = 0, ytdSocialEmp = 0, ytdHousingEmp = 0, ytdTaxDeducted = 0 for (let m = 1; m <= 6; m++) { if (m < hireMonth) continue const monthStr = `2026-${String(m).padStart(2, '0')}` const baseSalary = emp.socialInsBase || 0 // 用社保基数作为基本工资(简化) const social = calcSocial(emp.socialInsBase || baseSalary, socialConfig) const housing = calcHousing(emp.housingFundBase || baseSalary, socialConfig) const totalPay = baseSalary const specialDeduction = emp.specialDeduction * m ytdIncome += totalPay ytdSocialEmp += social.socialEmp ytdHousingEmp += housing.housingEmp const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * m - ytdSocialEmp - ytdHousingEmp - specialDeduction) const ytdTax = calcTax(ytdTaxableIncome) const monthTax = Math.max(0, Math.round((ytdTax - ytdTaxDeducted) * 100) / 100) ytdTaxDeducted += monthTax const netPay = Math.round((totalPay - social.socialEmp - housing.housingEmp - monthTax) * 100) / 100 await prisma.payslip.create({ data: { orgId: org.id, employeeId: emp.id, month: monthStr, baseSalary, overtimePay: 0, allowance: 0, deduction: 0, bonus: 0, totalPay, socialEmp: social.socialEmp, housingEmp: housing.housingEmp, tax: monthTax, netPay, ytdIncome, ytdTaxDeducted, ytdSocialEmp, ytdHousingEmp, status: 'PUBLISHED', publishedAt: new Date(`${monthStr}-10T10:00:00Z`), confirmedAt: new Date(`${monthStr}-12T10:00:00Z`), }, }) } console.log(` ${emp.name}: 1-6月工资条已生成`) } console.log('\n===== 示例数据创建完成 =====') console.log(`企业: ${org.name}`) console.log(`管理员: 13800000001 / 密码: 12345678`) console.log(`员工: ${EMPLOYEES.length} 人`) console.log('社保配置: 上海标准') console.log('薪酬模版: 10项预置') } main() .catch((e) => { console.error(e) process.exit(1) }) .finally(async () => { await prisma.$disconnect() })