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 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? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] employees Employee[] contracts LaborContract[] overtimeRecords OvertimeRecord[] terminations TerminationRecord[] riskItems RiskItem[] auditLogs AuditLog[] payslips Payslip[] 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) createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt contracts LaborContract[] overtimeRecords OvertimeRecord[] terminations TerminationRecord[] riskItems RiskItem[] payslips Payslip[] 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) totalPay Float @default(0) confirmedAt DateTime? confirmedIp String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([employeeId, month]) @@index([orgId, month]) } 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]) }