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 LABOR INTERNSHIP } enum SignMethod { PAPER ELECTRONIC } enum RiskType { CONTRACT SALARY TERMINATION MONTHLY ONBOARDING } enum RiskLevel { HIGH MEDIUM LOW } enum PayrollBatchType { REGULAR // 常规发薪 TERMINATION // 离职结算 BONUS // 年终奖/奖金 SEVERANCE // 补偿金按月发放(无社保,个税按政策处理) } enum PayrollBatchStatus { DRAFT // 草稿(可编辑) ARCHIVED // 归档(已发薪,锁定) } enum PayslipItemType { INPUT // 手工输入项(计算依据) CALCULATED // 计算项(公式自动计算) } enum PayslipStatus { PENDING // 待发布 PUBLISHED // 已发布到员工端 } enum RiskStatus { PENDING RESOLVED IGNORED } enum TerminationReason { NEGOTIATED FAULT NONFAULT LAYOFF EXPIRED RESIGNATION } enum RiskAssessment { SAFE WARNING DANGER } enum OnboardingStatus { PENDING APPROVED REJECTED CANCELLED } 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[] aiConversations AIConversation[] aiReviewRecords AIReviewRecord[] socialInsuranceConfig SocialInsuranceConfig[] housingFundConfigs HousingFundConfig[] socialInsRecords EmployeeSocialInsRecord[] housingFundRecords EmployeeHousingFundRecord[] departmentRecords EmployeeDepartmentRecord[] notificationSetting NotificationSetting? overtimeConfig OvertimeConfig? 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) disabled Boolean @default(false) 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 加密存储 idCardHash String? // SHA-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? // 社保缴费基数(便捷字段,由Record同步) housingFundBase Float? // 公积金缴费基数(便捷字段,由Record同步) socialInsStartMonth String? // 当前社保开始年月(便捷字段) socialInsEndMonth String? // 当前社保截止年月(便捷字段,null=在保) housingFundStartMonth String? // 当前公积金开始年月(便捷字段) housingFundEndMonth String? // 当前公积金截止年月(便捷字段) specialDeduction Float @default(0) // 专项附加扣除(子女教育、赡养老人等,员工portal端填报) city String? // 员工社保参保城市 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[] socialInsRecords EmployeeSocialInsRecord[] housingFundRecords EmployeeHousingFundRecord[] departmentRecords EmployeeDepartmentRecord[] aiReviewRecords AIReviewRecord[] @@unique([orgId, idCardHash]) } 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) batchId String? // 关联的发薪批次(加入后锁定,不可重复加入) 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) type String @default("TERMINATION") // TERMINATION=公司解聘, RESIGNATION=员工主动离职 reason TerminationReason terminationDate DateTime resignationReason String? // 主动离职原因(type=RESIGNATION时使用) compensation Float @default(0) socialInsEndMonth String? // 社保截止缴费年月 YYYY-MM housingFundEndMonth String? // 公积金截止缴费年月 YYYY-MM riskLevel RiskAssessment @default(SAFE) checklist Json remark String? createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 流程状态机 status String @default("DRAFT") // DRAFT|PENDING_APPROVAL|APPROVED|EXECUTING|COMPLETED|REJECTED|CANCELLED currentStep Int @default(0) // 当前完成到第几步 // 补偿金分项明细 + 调整记录 compensationBreakdown Json? // { severance, noticePay, doublePay, other, adjustments: [{field, from, to, reason}] } // 合规检查覆盖记录 checklistOverrides Json? // { key: { checked: bool, overrideReason: string } } // 工作交接清单 handoverItems Json? // [{ key, label, done, remark }] // 审批信息 approvedBy String? approvedAt DateTime? approvalComment String? updatedBy String? @@index([orgId, status]) } 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 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) // 生育保险 企业比例 % baseMin Float @default(6326) // 社保缴费基数下限 baseMax Float @default(33891) // 社保缴费基数上限 effectiveFrom String // 生效月份 YYYY-MM effectiveTo String? // 失效月份 YYYY-MM(null=当前有效) isCurrent Boolean @default(true) // 是否当前生效版本 adjustmentDone Boolean @default(false) // 是否已执行过社保基数调整 createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([orgId, city, effectiveFrom]) @@index([orgId, isCurrent]) } model HousingFundConfig { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) city String @default("北京") housingOrg Float @default(12) // 公积金 企业比例 % housingEmp Float @default(12) // 公积金 个人比例 % baseMin Float @default(6326) // 公积金缴费基数下限 baseMax Float @default(33891) // 公积金缴费基数上限 effectiveFrom String // 生效月份 YYYY-MM effectiveTo String? // 失效月份 YYYY-MM(null=当前有效) isCurrent Boolean @default(true) // 是否当前生效版本 adjustmentDone Boolean @default(false) // 是否已执行过公积金基数调整 createdBy String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([orgId, city, effectiveFrom]) @@index([orgId, isCurrent]) } 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 OvertimeConfig { id String @id @default(cuid()) orgId String @unique org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) weekdayRate Float @default(1.5) // 工作日加班倍率 weekendRate Float @default(2.0) // 休息日加班倍率 holidayRate Float @default(3.0) // 法定节假日加班倍率 monthlyDays Float @default(21.75) // 月计薪天数 dailyHours Float @default(8) // 每日工时 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 // 生效日期 effectiveMonth String // 生效年月 YYYY-MM(从 effectiveDate 转换) endMonth String? // 失效年月 YYYY-MM(null=至今有效,被新版本覆盖时设置) changeType String @default("SALARY_CHANGE") // ONBOARDING=入职, REHIRE=重新入职, SALARY_CHANGE=调薪 reason String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, effectiveMonth, endMonth]) } model EmployeeSocialInsRecord { 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) city String @default("北京") // 参保城市 startMonth String // 开始缴费年月 YYYY-MM endMonth String? // 截止缴费年月 YYYY-MM(null=至今有效) base Float // 缴费基数 changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘 changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时) remark String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, startMonth, endMonth]) @@index([orgId, city]) } model EmployeeHousingFundRecord { 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) city String @default("北京") // 参保城市 startMonth String // 开始缴费年月 YYYY-MM endMonth String? // 截止缴费年月 YYYY-MM(null=至今有效) base Float // 缴费基数 changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘 changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时) remark String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, startMonth, endMonth]) } model EmployeeDepartmentRecord { 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) oldDepartment String // 调整前部门 newDepartment String // 调整后部门 effectiveMonth String // 生效年月 YYYY-MM endMonth String? // 失效年月 YYYY-MM(null=至今有效) reason String? // 调部门原因 changeType String // ONBOARDING=入职, REHIRE=重新入职, TRANSFER=调部门 createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, effectiveMonth, endMonth]) } 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]) } // ========== AI 会话 & 审查记录 ========== model AIConversation { id String @id @default(cuid()) orgId String org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) userId String title String @default("新对话") messages Json // [{ role, content }] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([orgId, userId]) } model AIReviewRecord { 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 String // REVIEW=合同审查, CASE=案例匹配 input String // 用户输入的合同文本或争议情形 result String // AI 返回的审查/分析结果 createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) } // ========== RAG 知识库 ========== model RagKnowledge { id String @id title String content String source String category String embedding Unsupported("vector(1536)")? createdAt DateTime @default(now()) @map("created_at") @@index([category]) @@map("rag_knowledge") }