文件
hyb-oa/server/prisma/schema.prisma
T
wangsli 5faf843cee feat(server): 站内通知基础设施与关键事件触发(F14,F30)
- 新增 notifications 表与 API(list/mark-read)
- submit/approve/reject/return/transfer/pay/forgot-password 写入站内通知
- 通知失败降级,不阻塞主流程
2026-02-09 17:45:17 +08:00

637 行
20 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum RoleCode {
employee
manager
ceo
finance
finance_mgr
administration
hr
admin
}
enum EmployeeStatus {
active
archived
}
enum DelegationGrantScope {
reimbursement_submit
all
}
enum ReimbursementStatus {
draft
reviewing
approved
paid
rejected
cancelled
}
enum InvoicePoolStatus {
pending_ocr
ocr_failed
available
bound
archived
}
enum InvoiceArchivedReason {
used_complete
unused_timeout
manual
}
enum OcrProvider {
tencent
}
enum OcrType {
vat
train
flight
taxi
general
}
enum DocumentResetPeriod {
none
daily
monthly
yearly
}
enum WorkflowInstanceStatus {
pending
approved
rejected
cancelled
terminated
}
enum WorkflowTaskStatus {
pending
completed
rejected
cancelled
skipped
}
enum WorkflowTaskOutcome {
approved
rejected
returned
start
transfer
skipped
}
enum ApprovalTransferStatus {
active
cancelled
}
enum ExpenseCategory {
travel
rd
production
office
business
assets
other
}
model Department {
id String @id @default(uuid()) @db.Uuid
name String
parent_id String? @db.Uuid
archived_at DateTime?
archived_by String? @db.Uuid
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
parent Department? @relation("DepartmentToParent", fields: [parent_id], references: [id], onDelete: Restrict)
children Department[] @relation("DepartmentToParent")
employees Employee[]
reimbursement_forms ReimbursementForm[]
workflow_instances WorkflowInstance[]
@@index([parent_id])
@@map("departments")
}
model Employee {
id String @id @default(uuid()) @db.Uuid
employee_no String @unique
name String
phone String @unique
email String @unique
department_id String @db.Uuid
title String
direct_manager_id String? @db.Uuid
hire_date DateTime @db.Date
status EmployeeStatus @default(active)
bank_account_encrypted String
bank_name String
id_number_encrypted String?
password_hash String
token_version Int @default(0)
must_change_password Boolean @default(false)
login_failed_count Int @default(0)
login_failed_first_at DateTime?
login_locked_until DateTime?
archived_at DateTime?
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
department Department @relation(fields: [department_id], references: [id], onDelete: Restrict)
direct_manager Employee? @relation("EmployeeToManager", fields: [direct_manager_id], references: [id], onDelete: Restrict)
subordinates Employee[] @relation("EmployeeToManager")
roles EmployeeRole[]
delegation_grants_as_principal DelegationGrant[] @relation("DelegationPrincipal")
delegation_grants_as_delegate DelegationGrant[] @relation("DelegationDelegate")
auth_sessions AuthSession[]
audit_logs AuditLog[] @relation("AuditActor")
notifications Notification[]
submitted_reimbursements ReimbursementForm[] @relation("ReimbursementSubmitter")
owned_reimbursements ReimbursementForm[] @relation("ReimbursementOwner")
paid_reimbursements ReimbursementForm[] @relation("ReimbursementPaidBy")
owned_invoices InvoicePool[] @relation("InvoiceOwner")
uploaded_invoices InvoicePool[] @relation("InvoiceUploader")
overridden_invoices InvoicePool[] @relation("InvoiceOverrideBy")
workflow_instances_started WorkflowInstance[] @relation("WorkflowInstanceSubmitter")
workflow_history_actions WorkflowHistory[] @relation("WorkflowHistoryActor")
workflow_tasks_assigned WorkflowTask[] @relation("WorkflowTaskAssignee")
workflow_tasks_completed WorkflowTask[] @relation("WorkflowTaskCompleter")
approval_transfers_from ApprovalTransfer[] @relation("TransferFrom")
approval_transfers_to ApprovalTransfer[] @relation("TransferTo")
password_reset_codes PasswordResetCode[]
@@index([department_id])
@@index([direct_manager_id])
@@map("employees")
}
model PasswordResetCode {
id String @id @default(uuid()) @db.Uuid
employee_id String @db.Uuid
code_hash String
expires_at DateTime
used_at DateTime?
created_at DateTime @default(now())
created_ip String?
created_user_agent String?
employee Employee @relation(fields: [employee_id], references: [id], onDelete: Cascade)
@@index([employee_id, created_at])
@@index([expires_at])
@@map("password_reset_codes")
}
model EmployeeRole {
employee_id String @db.Uuid
role_code RoleCode
created_at DateTime @default(now())
created_by String? @db.Uuid
employee Employee @relation(fields: [employee_id], references: [id], onDelete: Cascade)
@@id([employee_id, role_code])
@@map("employee_roles")
}
model DelegationGrant {
id String @id @default(uuid()) @db.Uuid
principal_employee_id String @db.Uuid
delegate_employee_id String @db.Uuid
scope DelegationGrantScope
valid_from DateTime @default(now())
valid_to DateTime?
revoked_at DateTime?
created_at DateTime @default(now())
created_by String? @db.Uuid
principal_employee Employee @relation("DelegationPrincipal", fields: [principal_employee_id], references: [id], onDelete: Restrict)
delegate_employee Employee @relation("DelegationDelegate", fields: [delegate_employee_id], references: [id], onDelete: Restrict)
@@index([principal_employee_id])
@@index([delegate_employee_id])
@@map("delegation_grants")
}
model AuthSession {
id String @id @default(uuid()) @db.Uuid
employee_id String @db.Uuid
refresh_token_hash String
device_fingerprint String
ip_address String
user_agent String
created_at DateTime @default(now())
last_seen_at DateTime @default(now())
expires_at DateTime
revoked_at DateTime?
employee Employee @relation(fields: [employee_id], references: [id], onDelete: Cascade)
@@index([employee_id])
@@index([expires_at])
@@index([revoked_at])
@@map("auth_sessions")
}
model AuditLog {
id String @id @default(uuid()) @db.Uuid
request_id String
actor_id String @db.Uuid
action String
target_type String
target_id String? @db.Uuid
ip_address String?
user_agent String?
extra Json
created_at DateTime @default(now())
actor Employee @relation("AuditActor", fields: [actor_id], references: [id], onDelete: Restrict)
@@index([request_id])
@@index([actor_id, created_at])
@@index([created_at, actor_id, action], map: "idx_audit_logs_query")
@@index([target_type, created_at], map: "idx_audit_logs_target_type_created_at")
@@map("audit_logs")
}
model Notification {
id String @id @default(uuid()) @db.Uuid
recipient_employee_id String @db.Uuid
type String @db.VarChar(64)
title String @db.VarChar(200)
body String?
target_type String? @db.VarChar(64)
target_id String? @db.Uuid
read_at DateTime?
created_at DateTime @default(now())
recipient Employee @relation(fields: [recipient_employee_id], references: [id], onDelete: Cascade)
@@index([recipient_employee_id, created_at])
@@index([recipient_employee_id, read_at, created_at], map: "idx_notifications_unread_query")
@@map("notifications")
}
model ExpenseType {
id String @id @default(uuid()) @db.Uuid
code String @unique
name String
description String?
category ExpenseCategory
sort_order Int
account_code_default String?
is_system_default Boolean @default(false)
enabled Boolean @default(true)
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
reimbursement_items ReimbursementItem[]
@@index([enabled, sort_order])
@@map("expense_types")
}
model DocumentSequence {
document_type String @id
prefix String
date_format String
separator String
number_length Int @default(4)
reset_period DocumentResetPeriod
current_number Int @default(0)
last_reset_date DateTime?
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
@@map("document_sequences")
}
model ReimbursementForm {
id String @id @default(uuid()) @db.Uuid
form_number String? @unique
submitter_id String @db.Uuid
owner_id String @db.Uuid
department_id String @db.Uuid
reimbursement_date DateTime @db.Date
total_amount Decimal @default(0) @db.Decimal(14, 2)
status ReimbursementStatus @default(draft)
description String?
payment_voucher_no String? @db.VarChar(50)
payment_date DateTime? @db.Date
payment_channel String? @db.VarChar(20)
paid_at DateTime?
paid_by String? @db.Uuid
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
submitter Employee @relation("ReimbursementSubmitter", fields: [submitter_id], references: [id], onDelete: Restrict)
owner Employee @relation("ReimbursementOwner", fields: [owner_id], references: [id], onDelete: Restrict)
paid_by_employee Employee? @relation("ReimbursementPaidBy", fields: [paid_by], references: [id], onDelete: SetNull)
department Department @relation(fields: [department_id], references: [id], onDelete: Restrict)
items ReimbursementItem[]
invoice_pool_items InvoicePool[]
workflow_instances WorkflowInstance[]
approval_transfers ApprovalTransfer[]
@@index([owner_id])
@@index([submitter_id])
@@index([department_id])
@@index([paid_by])
@@index([status])
@@index([reimbursement_date])
@@index([owner_id, status, updated_at], map: "idx_reimbursement_owner_status_updated_at")
@@map("reimbursement_forms")
}
model ReimbursementItem {
id String @id @default(uuid()) @db.Uuid
reimbursement_id String @db.Uuid
expense_type_id String @db.Uuid
expense_date DateTime @db.Date
amount Decimal @db.Decimal(14, 2)
description String
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
reimbursement_form ReimbursementForm @relation(fields: [reimbursement_id], references: [id], onDelete: Cascade)
expense_type ExpenseType @relation(fields: [expense_type_id], references: [id], onDelete: Restrict)
invoice_pool_items InvoicePool[]
@@index([reimbursement_id])
@@index([expense_type_id])
@@index([expense_date])
@@map("reimbursement_items")
}
model OcrResult {
id String @id @default(uuid()) @db.Uuid
file_sha256 String
ocr_provider OcrProvider @default(tencent)
ocr_type OcrType
result_json Json
confidence Int?
created_at DateTime @default(now())
invoice_pool_items InvoicePool[]
@@unique([file_sha256, ocr_provider, ocr_type])
@@index([file_sha256])
@@map("ocr_results")
}
model InvoicePool {
id String @id @default(uuid()) @db.Uuid
owner_employee_id String @db.Uuid
uploaded_by_employee_id String @db.Uuid
storage_path String
original_name String
mime_type String
file_size Int
file_sha256 String
status InvoicePoolStatus @default(pending_ocr)
archived_reason InvoiceArchivedReason?
archived_at DateTime?
linked_reimbursement_id String? @db.Uuid
linked_item_id String? @db.Uuid
remarks String?
ocr_provider OcrProvider @default(tencent)
ocr_type OcrType @default(general)
ocr_result_id String? @db.Uuid
ocr_attempts Int @default(0)
ocr_last_requested_at DateTime?
ocr_last_error String?
ocr_completed_at DateTime?
ocr_locked_at DateTime?
ocr_lock_owner String? @db.VarChar(128)
invoice_code String?
invoice_number String?
invoice_date DateTime? @db.Date
seller_name String?
buyer_name String?
amount Decimal? @db.Decimal(14, 2)
tax_amount Decimal? @db.Decimal(14, 2)
total_amount Decimal? @db.Decimal(14, 2)
is_duplicate_override Boolean @default(false)
override_ref_form_number String?
override_reason String?
overridden_by String? @db.Uuid
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
owner_employee Employee @relation("InvoiceOwner", fields: [owner_employee_id], references: [id], onDelete: Restrict)
uploaded_by_employee Employee @relation("InvoiceUploader", fields: [uploaded_by_employee_id], references: [id], onDelete: Restrict)
overridden_by_employee Employee? @relation("InvoiceOverrideBy", fields: [overridden_by], references: [id], onDelete: Restrict)
ocr_result OcrResult? @relation(fields: [ocr_result_id], references: [id], onDelete: SetNull)
linked_reimbursement ReimbursementForm? @relation(fields: [linked_reimbursement_id], references: [id], onDelete: SetNull)
linked_item ReimbursementItem? @relation(fields: [linked_item_id], references: [id], onDelete: SetNull)
@@index([owner_employee_id])
@@index([uploaded_by_employee_id])
@@index([file_sha256])
@@index([invoice_code, invoice_number])
@@index([status])
@@index([status, ocr_locked_at, created_at], map: "idx_invoice_pool_ocr_queue")
@@index([owner_employee_id, status, created_at], map: "idx_invoice_pool_owner_status_created_at")
@@index([linked_reimbursement_id])
@@map("invoice_pool")
}
model WorkflowDefinition {
id String @id @default(uuid()) @db.Uuid
name String
entity_type String
definition_json Json
version Int
is_active Boolean @default(true)
created_at DateTime @default(now())
created_by String? @db.Uuid
workflow_instances WorkflowInstance[]
@@unique([entity_type, version])
@@index([entity_type, is_active])
@@map("workflow_definitions")
}
model WorkflowInstance {
id String @id @default(uuid()) @db.Uuid
workflow_id String @db.Uuid
entity_type String
entity_id String @db.Uuid
submitter_id String @db.Uuid
current_status WorkflowInstanceStatus @default(pending)
current_step_id String
context_data Json
started_at DateTime @default(now())
completed_at DateTime?
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
workflow_definition WorkflowDefinition @relation(fields: [workflow_id], references: [id], onDelete: Restrict)
reimbursement_form ReimbursementForm @relation(fields: [entity_id], references: [id], onDelete: Cascade)
submitter Employee @relation("WorkflowInstanceSubmitter", fields: [submitter_id], references: [id], onDelete: Restrict)
department Department? @relation(fields: [context_department_id], references: [id], onDelete: SetNull)
workflow_tasks WorkflowTask[]
workflow_history WorkflowHistory[]
/// Denormalized for fast filtering (must match context_data.department_id)
context_department_id String? @db.Uuid
@@index([workflow_id])
@@index([entity_type, entity_id])
@@index([current_status])
@@index([submitter_id])
@@index([context_department_id])
@@map("workflow_instances")
}
model WorkflowTask {
id String @id @default(uuid()) @db.Uuid
instance_id String @db.Uuid
step_id String
step_name String
assignee_id String? @db.Uuid
assignee_role RoleCode?
status WorkflowTaskStatus @default(pending)
outcome WorkflowTaskOutcome?
comment String?
due_date DateTime?
created_at DateTime @default(now())
completed_at DateTime?
completed_by String? @db.Uuid
workflow_instance WorkflowInstance @relation(fields: [instance_id], references: [id], onDelete: Cascade)
assignee_employee Employee? @relation("WorkflowTaskAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
completer_employee Employee? @relation("WorkflowTaskCompleter", fields: [completed_by], references: [id], onDelete: SetNull)
workflow_history WorkflowHistory[]
@@index([instance_id])
@@index([status])
@@index([assignee_id])
@@index([assignee_role])
@@index([status, assignee_id, assignee_role], map: "idx_workflow_tasks_status_assignee")
@@index([status, assignee_id, created_at], map: "idx_workflow_tasks_status_assignee_id_created_at")
@@index([status, assignee_role, created_at], map: "idx_workflow_tasks_status_assignee_role_created_at")
@@map("workflow_tasks")
}
model WorkflowHistory {
id String @id @default(uuid()) @db.Uuid
instance_id String @db.Uuid
task_id String? @db.Uuid
actor_id String @db.Uuid
action String
comment String?
payload Json?
created_at DateTime @default(now())
workflow_instance WorkflowInstance @relation(fields: [instance_id], references: [id], onDelete: Cascade)
workflow_task WorkflowTask? @relation(fields: [task_id], references: [id], onDelete: SetNull)
actor Employee @relation("WorkflowHistoryActor", fields: [actor_id], references: [id], onDelete: Restrict)
@@index([instance_id, created_at])
@@index([task_id])
@@index([actor_id])
@@map("workflow_history")
}
model ApprovalRule {
id String @id @default(uuid()) @db.Uuid
name String
min_amount Decimal @db.Decimal(14, 2)
max_amount Decimal? @db.Decimal(14, 2)
level1_role RoleCode
level2_role RoleCode?
level3_role RoleCode?
enabled Boolean @default(true)
priority Int
created_at DateTime @default(now())
created_by String? @db.Uuid
updated_at DateTime @updatedAt
updated_by String? @db.Uuid
@@index([enabled, priority])
@@map("approval_rules")
}
model ApprovalTransfer {
id String @id @default(uuid()) @db.Uuid
reimbursement_id String @db.Uuid
from_approver_id String @db.Uuid
to_approver_id String @db.Uuid
reason String
status ApprovalTransferStatus @default(active)
created_at DateTime @default(now())
created_by String? @db.Uuid
reimbursement_form ReimbursementForm @relation(fields: [reimbursement_id], references: [id], onDelete: Cascade)
from_approver Employee @relation("TransferFrom", fields: [from_approver_id], references: [id], onDelete: Restrict)
to_approver Employee @relation("TransferTo", fields: [to_approver_id], references: [id], onDelete: Restrict)
@@index([reimbursement_id])
@@index([from_approver_id])
@@index([to_approver_id])
@@map("approval_transfers")
}