generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ========== 枚举 ========== enum Plan { FREE PRO ENTERPRISE } enum Role { ADMIN HR VIEWER } enum EmployeeStatus { ACTIVE RESIGNED } enum ContractType { FIXED UNFIXED UNSIGNED } enum SignMethod { PAPER ELECTRONIC } enum RiskType { CONTRACT SALARY TERMINATION MONTHLY } enum RiskLevel { HIGH MEDIUM LOW } enum PayrollBatchType { REGULAR // 常规发薪 TERMINATION // 离职结算 BONUS // 年终奖/奖金 } enum PayrollBatchStatus { DRAFT // 草稿(可编辑) ARCHIVED // 归档(已发薪,锁定) } enum PayslipItemType { INPUT // 手工输入项(计算依据) CALCULATED // 计算项(公式自动计算) } enum PayslipStatus { PENDING // 待发布 PUBLISHED // 已发布到员工端 } enum RiskStatus { PENDING RESOLVED IGNORED } enum TerminationReason { NEGOTIATED FAULT NONFAULT LAYOFF EXPIRED } enum RiskAssessment { SAFE WARNING DANGER } enum OnboardingStatus { PENDING APPROVED REJECTED } enum ContractConfirmStatus { UNCONFIRMED CONFIRMED EXPIRED } // ========== 核心表 ========== model Organization { id String @id @default(cuid()) name String plan Plan @default(FREE) maxEmployees Int @default(20) city String? payrollFrequency Int @default(1) // 每月发薪次数(1=一次一批) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] employees Employee[] contracts LaborContract[] overtimeRecords OvertimeRecord[] terminations TerminationRecord[] riskItems RiskItem[] auditLogs AuditLog[] payslips Payslip[] payrollBatches PayrollBatch[] payslipItems PayslipItem[] salaryChangeRecords SalaryChangeRecord[] onboardingLinks OnboardingLink[] confirmLinks ContractConfirmLink[] socialInsuranceConfig SocialInsuranceConfig? notificationSetting NotificationSetting? notificationLogs NotificationLog[] employeeAttachments EmployeeAttachment[] disciplinaryRecords DisciplinaryRecord[] attendanceRecords AttendanceRecord[] trainingRecords TrainingRecord[] performanceRecords PerformanceRecord[] } model User { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) phone String @unique email String? passwordHash String name String role Role @default(ADMIN) createdAt DateTime @default(now()) lastLoginAt DateTime? } // ========== 业务表 ========== model Employee { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) name String department String hireDate DateTime monthlySalary String // AES-256 加密存储 status EmployeeStatus @default(ACTIVE) gender String? phone String? idCardNumber String? // AES-256 加密存储 emergencyContact String? emergencyPhone String? address String? bankAccount String? // AES-256 加密存储 bankName String? passwordHash String? // 员工端登录密码 isPregnant Boolean @default(false) isInMedicalPeriod Boolean @default(false) isWorkInjured Boolean @default(false) // 薪税扩展 socialInsBase Float? // 社保缴费基数(按人核定) housingFundBase Float? // 公积金缴费基数(按人核定) specialDeduction Float @default(0) // 专项附加扣除(子女教育、赡养老人等,员工portal端填报) createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt contracts LaborContract[] overtimeRecords OvertimeRecord[] terminations TerminationRecord[] riskItems RiskItem[] payslips Payslip[] salaryChanges SalaryChangeRecord[] batchEntries BatchEntry[] attachments EmployeeAttachment[] disciplinaryRecords DisciplinaryRecord[] attendanceRecords AttendanceRecord[] trainingRecords TrainingRecord[] performanceRecords PerformanceRecord[] } model LaborContract { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) signDate DateTime? startDate DateTime endDate DateTime? contractType ContractType signMethod SignMethod @default(PAPER) contractYears Int @default(3) probationMonths Int @default(0) probationSalary Int @default(0) renewalCount Int @default(0) attachmentName String? attachmentUrl String? electronicContractNo String? electronicContractUrl String? createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt confirmLinks ContractConfirmLink[] } model OvertimeRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) month String // YYYY-MM weekdayHours Float @default(0) weekendHours Float @default(0) holidayHours Float @default(0) weekdayPay Float @default(0) weekendPay Float @default(0) holidayPay Float @default(0) totalPay Float @default(0) createdAt DateTime @default(now()) @@unique([employeeId, month]) } model TerminationRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) reason TerminationReason terminationDate DateTime compensation Float @default(0) riskLevel RiskAssessment @default(SAFE) checklist Json remark String? createdBy String createdAt DateTime @default(now()) } model RiskItem { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String? employee Employee? @relation(fields: [employeeId], references: [id], onDelete: SetNull) type RiskType level RiskLevel status RiskStatus @default(PENDING) title String description String actionUrl String? resolvedAt DateTime? resolvedBy String? remark String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([orgId, status]) @@index([orgId, type]) } model AuditLog { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) userId String action String entity String entityId String? detail Json? ip String? createdAt DateTime @default(now()) @@index([orgId, createdAt]) } // ========== 社保 & 通知 & 附件 ========== model SocialInsuranceConfig { id String @id @default(cuid()) orgId String @unique org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) city String @default("北京") pensionOrg Float @default(16) // 养老保险 企业比例 % pensionEmp Float @default(8) // 养老保险 个人比例 % medicalOrg Float @default(9.8) // 医疗保险 企业比例 % medicalEmp Float @default(2) // 医疗保险 个人比例 % unemploymentOrg Float @default(0.5) // 失业保险 企业比例 % unemploymentEmp Float @default(0.5) // 失业保险 个人比例 % injuryOrg Float @default(0.2) // 工伤保险 企业比例 % maternityOrg Float @default(0.8) // 生育保险 企业比例 % housingOrg Float @default(12) // 公积金 企业比例 % housingEmp Float @default(12) // 公积金 个人比例 % baseMin Float @default(6326) // 缴费基数下限 baseMax Float @default(33891) // 缴费基数上限 updatedAt DateTime @updatedAt } model NotificationSetting { id String @id @default(cuid()) orgId String @unique org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) contractExpiry Boolean @default(true) expiryDays Int @default(30) contractUnsigned Boolean @default(true) overtimeAlert Boolean @default(true) payslipReady Boolean @default(true) // 月度事务提醒日(每月几号) payrollDay Int @default(10) // 发薪日 socialInsDay Int @default(15) // 社保缴纳日 housingFundDay Int @default(15) // 公积金缴纳日 taxDay Int @default(15) // 个税申报日 wechatWebhook String? emailNotify Boolean @default(false) email String? updatedAt DateTime @updatedAt } model NotificationLog { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) type String // CONTRACT_EXPIRY / CONTRACT_UNSIGNED / OVERTIME / PAYSLIP title String content String channel String // WECHAT / EMAIL / IN_APP status String @default("SENT") // SENT / FAILED employeeId String? createdAt DateTime @default(now()) @@index([orgId, createdAt]) } model EmployeeAttachment { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) fileName String fileType String // ID_CARD / BANK_CARD / CONTRACT_SCAN / EDUCATION / OTHER fileUrl String fileSize Int @default(0) uploadedBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) } // ========== 仲裁证据链 ========== model DisciplinaryRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) violationDate DateTime violationType String // LATE/ABSENT/INSUBORDINATION/MISCONDUCT/VIOLATE_POLICY/OTHER description String severity String @default("WARNING") // WARNING/SERIOUS/SEVERE action String @default("ORAL_WARNING") // ORAL_WARNING/WRITTEN_WARNING/DEDUCTION/DEMOTION/TERMINATION actionDetail String? employeeAck Boolean @default(false) // 员工是否签字确认 ackDate DateTime? ackMethod String? // SIGN/ELECTRONIC/REFUSED witness String? // 见证人 attachmentUrl String? createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([orgId, employeeId]) } model AttendanceRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) date DateTime checkInTime String? // HH:mm checkOutTime String? // HH:mm status String @default("NORMAL") // NORMAL/LATE/EARLY_LEAVE/ABSENT/LEAVE/BUSINESS_TRIP lateMinutes Int @default(0) earlyMinutes Int @default(0) workHours Float @default(0) overtimeHours Float @default(0) remark String? createdBy String createdAt DateTime @default(now()) @@unique([employeeId, date]) @@index([orgId, employeeId]) } model TrainingRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) trainingDate DateTime topic String // 培训主题/制度名称 content String? // 培训内容摘要 trainer String? duration Float @default(0) // 培训时长(小时) ackStatus String @default("PENDING") // PENDING/SIGNED/REFUSED ackDate DateTime? attachmentUrl String? // 签收单扫描件 remark String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) } model PerformanceRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) period String // 考核周期 YYYY-MM 或 YYYY-Q1 score Float @default(0) // 考核得分 grade String @default("B") // A/B/C/D result String @default("QUALIFIED") // EXCELLENT/QUALIFIED/NEED_IMPROVE/UNQUALIFIED summary String? // 考核评语 improvementPlan String? // 改进计划(不胜任时) employeeAck Boolean @default(false) ackDate DateTime? reviewer String? createdBy String createdAt DateTime @default(now()) @@unique([employeeId, period]) @@index([orgId, employeeId]) } // ========== 员工端表 ========== model Payslip { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) month String // YYYY-MM // 薪酬构成 baseSalary Float @default(0) overtimePay Float @default(0) weekdayOvertimePay Float @default(0) weekendOvertimePay Float @default(0) holidayOvertimePay Float @default(0) allowance Float @default(0) deduction Float @default(0) bonus Float @default(0) // 奖金/年终奖 totalPay Float @default(0) // 应发合计 // 扣除项 socialEmp Float @default(0) // 个人社保 housingEmp Float @default(0) // 个人公积金 tax Float @default(0) // 个人所得税 netPay Float @default(0) // 实发工资 = totalPay - socialEmp - housingEmp - tax // 累计预扣法 ytdIncome Float @default(0) // 当年累计收入 ytdTaxDeducted Float @default(0) // 当年累计已扣税 ytdSocialEmp Float @default(0) // 当年累计个人社保 ytdHousingEmp Float @default(0) // 当年累计个人公积金 // 状态 status PayslipStatus @default(PENDING) // PENDING → PUBLISHED confirmedAt DateTime? confirmedIp String? publishedAt DateTime? // 工资条发布到员工端的时间 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([employeeId, month]) @@index([orgId, month]) @@index([orgId, status]) } model PayrollBatch { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) month String // YYYY-MM batchNo Int // 批次序号(1, 2, 3...) name String // 批次名称 type PayrollBatchType @default(REGULAR) status PayrollBatchStatus @default(DRAFT) employeeCount Int @default(0) totalPay Float @default(0) totalNetPay Float @default(0) totalSocialOrg Float @default(0) totalSocialEmp Float @default(0) totalHousingOrg Float @default(0) totalHousingEmp Float @default(0) totalTax Float @default(0) remark String? createdBy String createdAt DateTime @default(now()) archivedAt DateTime? updatedAt DateTime @updatedAt entries BatchEntry[] @@unique([orgId, month, batchNo]) @@index([orgId, month]) @@index([orgId, status]) } model BatchEntry { id String @id @default(cuid()) batchId String batch PayrollBatch @relation(fields: [batchId], references: [id], onDelete: Cascade) orgId String employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) // 薪酬项(可编辑的输入项) baseSalary Float @default(0) overtimePay Float @default(0) allowance Float @default(0) deduction Float @default(0) bonus Float @default(0) // 自动计算项 socialEmp Float @default(0) socialOrg Float @default(0) housingEmp Float @default(0) housingOrg Float @default(0) tax Float @default(0) totalPay Float @default(0) // 应发合计 netPay Float @default(0) // 实发工资 // 风险提示 riskWarnings Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([batchId, employeeId]) @@index([orgId, employeeId]) } model PayslipItem { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) name String // 显示名称 code String // 字段代码 type PayslipItemType @default(INPUT) formula String? // 计算公式(CALCULATED 类型),如 "baseSalary + overtimePay + allowance - deduction" order Int @default(0) isDefault Boolean @default(true) // 系统预置项不可删除 isEditable Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([orgId, code]) } model SalaryChangeRecord { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) oldSalary Float newSalary Float effectiveDate DateTime // 生效日期 reason String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) } model OnboardingLink { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) token String @unique employeeName String? phone String? status OnboardingStatus @default(PENDING) formData Json? expiresAt DateTime usedAt DateTime? createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([orgId, status]) } model ContractConfirmLink { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) contractId String contract LaborContract @relation(fields: [contractId], references: [id], onDelete: Cascade) token String @unique status ContractConfirmStatus @default(UNCONFIRMED) confirmedAt DateTime? confirmedIp String? expiresAt DateTime createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([orgId, status]) }