feat(server): 完成 P0+P1 后端能力(工作流、RBAC、付款归档)
P0 MVP: - 认证:login/refresh/logout/forgot/reset + tokenVersion - RBAC:角色到权限映射 + requirePermission - 发票池:upload/bind/signed-url + 判重(VALID_003/VALID_005) - 报销:create/view/submit + 单号生成 - 工作流:approve + department_manager 解析 P1 扩展: - 动作:reject/return/withdraw(含 comment) - transfer:任务转审 + 链路上限 3 - pay:approved -> paid + 发票归档 used_complete - 动态审批链:approval_rules 驱动 回归脚本: - smoke-test.sh / regression-dedup.sh / regression-workflow-actions.sh
这个提交包含在:
@@ -0,0 +1,695 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RoleCode" AS ENUM ('employee', 'manager', 'ceo', 'finance', 'finance_mgr', 'administration', 'hr', 'admin');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EmployeeStatus" AS ENUM ('active', 'archived');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DelegationGrantScope" AS ENUM ('reimbursement_submit', 'all');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReimbursementStatus" AS ENUM ('draft', 'reviewing', 'approved', 'paid', 'rejected', 'cancelled');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "InvoicePoolStatus" AS ENUM ('pending_ocr', 'ocr_failed', 'available', 'bound', 'archived');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "InvoiceArchivedReason" AS ENUM ('used_complete', 'unused_timeout', 'manual');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OcrProvider" AS ENUM ('tencent');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OcrType" AS ENUM ('vat', 'train', 'flight', 'taxi', 'general');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentResetPeriod" AS ENUM ('none', 'daily', 'monthly', 'yearly');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkflowInstanceStatus" AS ENUM ('pending', 'approved', 'rejected', 'cancelled', 'terminated');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkflowTaskStatus" AS ENUM ('pending', 'completed', 'rejected', 'cancelled', 'skipped');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkflowTaskOutcome" AS ENUM ('approved', 'rejected', 'returned', 'start', 'transfer', 'skipped');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ApprovalTransferStatus" AS ENUM ('active', 'cancelled');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExpenseCategory" AS ENUM ('travel', 'rd', 'production', 'office', 'business', 'assets', 'other');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "departments" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"parent_id" UUID,
|
||||
"archived_at" TIMESTAMP(3),
|
||||
"archived_by" UUID,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "departments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "employees" (
|
||||
"id" UUID NOT NULL,
|
||||
"employee_no" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"department_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"direct_manager_id" UUID NOT NULL,
|
||||
"hire_date" DATE NOT NULL,
|
||||
"status" "EmployeeStatus" NOT NULL DEFAULT 'active',
|
||||
"bank_account_encrypted" TEXT NOT NULL,
|
||||
"bank_name" TEXT NOT NULL,
|
||||
"id_number_encrypted" TEXT,
|
||||
"password_hash" TEXT NOT NULL,
|
||||
"token_version" INTEGER NOT NULL DEFAULT 0,
|
||||
"archived_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "employees_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "employee_roles" (
|
||||
"employee_id" UUID NOT NULL,
|
||||
"role_code" "RoleCode" NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
|
||||
CONSTRAINT "employee_roles_pkey" PRIMARY KEY ("employee_id","role_code")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "delegation_grants" (
|
||||
"id" UUID NOT NULL,
|
||||
"principal_employee_id" UUID NOT NULL,
|
||||
"delegate_employee_id" UUID NOT NULL,
|
||||
"scope" "DelegationGrantScope" NOT NULL,
|
||||
"valid_from" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"valid_to" TIMESTAMP(3),
|
||||
"revoked_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
|
||||
CONSTRAINT "delegation_grants_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "auth_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"employee_id" UUID NOT NULL,
|
||||
"refresh_token_hash" TEXT NOT NULL,
|
||||
"device_fingerprint" TEXT NOT NULL,
|
||||
"ip_address" TEXT NOT NULL,
|
||||
"user_agent" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_seen_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||
"revoked_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "auth_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "audit_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"request_id" TEXT NOT NULL,
|
||||
"actor_id" UUID NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"target_type" TEXT NOT NULL,
|
||||
"target_id" UUID,
|
||||
"ip_address" TEXT,
|
||||
"user_agent" TEXT,
|
||||
"extra" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "expense_types" (
|
||||
"id" UUID NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"category" "ExpenseCategory" NOT NULL,
|
||||
"sort_order" INTEGER NOT NULL,
|
||||
"account_code_default" TEXT,
|
||||
"is_system_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "expense_types_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "document_sequences" (
|
||||
"document_type" TEXT NOT NULL,
|
||||
"prefix" TEXT NOT NULL,
|
||||
"date_format" TEXT NOT NULL,
|
||||
"separator" TEXT NOT NULL,
|
||||
"number_length" INTEGER NOT NULL DEFAULT 4,
|
||||
"reset_period" "DocumentResetPeriod" NOT NULL,
|
||||
"current_number" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_reset_date" TIMESTAMP(3),
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "document_sequences_pkey" PRIMARY KEY ("document_type")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "reimbursement_forms" (
|
||||
"id" UUID NOT NULL,
|
||||
"form_number" TEXT,
|
||||
"submitter_id" UUID NOT NULL,
|
||||
"owner_id" UUID NOT NULL,
|
||||
"department_id" UUID NOT NULL,
|
||||
"reimbursement_date" DATE NOT NULL,
|
||||
"total_amount" DECIMAL(14,2) NOT NULL DEFAULT 0,
|
||||
"status" "ReimbursementStatus" NOT NULL DEFAULT 'draft',
|
||||
"description" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "reimbursement_forms_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "reimbursement_items" (
|
||||
"id" UUID NOT NULL,
|
||||
"reimbursement_id" UUID NOT NULL,
|
||||
"expense_type_id" UUID NOT NULL,
|
||||
"expense_date" DATE NOT NULL,
|
||||
"amount" DECIMAL(14,2) NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "reimbursement_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ocr_results" (
|
||||
"id" UUID NOT NULL,
|
||||
"file_sha256" TEXT NOT NULL,
|
||||
"ocr_provider" "OcrProvider" NOT NULL DEFAULT 'tencent',
|
||||
"ocr_type" "OcrType" NOT NULL,
|
||||
"result_json" JSONB NOT NULL,
|
||||
"confidence" INTEGER,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ocr_results_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "invoice_pool" (
|
||||
"id" UUID NOT NULL,
|
||||
"owner_employee_id" UUID NOT NULL,
|
||||
"uploaded_by_employee_id" UUID NOT NULL,
|
||||
"storage_path" TEXT NOT NULL,
|
||||
"original_name" TEXT NOT NULL,
|
||||
"mime_type" TEXT NOT NULL,
|
||||
"file_size" INTEGER NOT NULL,
|
||||
"file_sha256" TEXT NOT NULL,
|
||||
"status" "InvoicePoolStatus" NOT NULL DEFAULT 'pending_ocr',
|
||||
"archived_reason" "InvoiceArchivedReason",
|
||||
"archived_at" TIMESTAMP(3),
|
||||
"linked_reimbursement_id" UUID,
|
||||
"linked_item_id" UUID,
|
||||
"remarks" TEXT,
|
||||
"ocr_provider" "OcrProvider" NOT NULL DEFAULT 'tencent',
|
||||
"ocr_type" "OcrType" NOT NULL DEFAULT 'general',
|
||||
"ocr_result_id" UUID,
|
||||
"ocr_attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"ocr_last_error" TEXT,
|
||||
"ocr_completed_at" TIMESTAMP(3),
|
||||
"invoice_code" TEXT,
|
||||
"invoice_number" TEXT,
|
||||
"invoice_date" DATE,
|
||||
"seller_name" TEXT,
|
||||
"buyer_name" TEXT,
|
||||
"amount" DECIMAL(14,2),
|
||||
"tax_amount" DECIMAL(14,2),
|
||||
"total_amount" DECIMAL(14,2),
|
||||
"is_duplicate_override" BOOLEAN NOT NULL DEFAULT false,
|
||||
"override_ref_form_number" TEXT,
|
||||
"override_reason" TEXT,
|
||||
"overridden_by" UUID,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "invoice_pool_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workflow_definitions" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"entity_type" TEXT NOT NULL,
|
||||
"definition_json" JSONB NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
|
||||
CONSTRAINT "workflow_definitions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workflow_instances" (
|
||||
"id" UUID NOT NULL,
|
||||
"workflow_id" UUID NOT NULL,
|
||||
"entity_type" TEXT NOT NULL,
|
||||
"entity_id" UUID NOT NULL,
|
||||
"submitter_id" UUID NOT NULL,
|
||||
"current_status" "WorkflowInstanceStatus" NOT NULL DEFAULT 'pending',
|
||||
"current_step_id" TEXT NOT NULL,
|
||||
"context_data" JSONB NOT NULL,
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
"context_department_id" UUID,
|
||||
|
||||
CONSTRAINT "workflow_instances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workflow_tasks" (
|
||||
"id" UUID NOT NULL,
|
||||
"instance_id" UUID NOT NULL,
|
||||
"step_id" TEXT NOT NULL,
|
||||
"step_name" TEXT NOT NULL,
|
||||
"assignee_id" UUID,
|
||||
"assignee_role" "RoleCode",
|
||||
"status" "WorkflowTaskStatus" NOT NULL DEFAULT 'pending',
|
||||
"outcome" "WorkflowTaskOutcome",
|
||||
"comment" TEXT,
|
||||
"due_date" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"completed_by" UUID,
|
||||
|
||||
CONSTRAINT "workflow_tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workflow_history" (
|
||||
"id" UUID NOT NULL,
|
||||
"instance_id" UUID NOT NULL,
|
||||
"task_id" UUID,
|
||||
"actor_id" UUID NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"comment" TEXT,
|
||||
"payload" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "workflow_history_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "approval_rules" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"min_amount" DECIMAL(14,2) NOT NULL,
|
||||
"max_amount" DECIMAL(14,2),
|
||||
"level1_role" "RoleCode" NOT NULL,
|
||||
"level2_role" "RoleCode",
|
||||
"level3_role" "RoleCode",
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"priority" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" UUID,
|
||||
|
||||
CONSTRAINT "approval_rules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "approval_transfers" (
|
||||
"id" UUID NOT NULL,
|
||||
"reimbursement_id" UUID NOT NULL,
|
||||
"from_approver_id" UUID NOT NULL,
|
||||
"to_approver_id" UUID NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
"status" "ApprovalTransferStatus" NOT NULL DEFAULT 'active',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID,
|
||||
|
||||
CONSTRAINT "approval_transfers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "departments_parent_id_idx" ON "departments"("parent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "employees_employee_no_key" ON "employees"("employee_no");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "employees_phone_key" ON "employees"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "employees_email_key" ON "employees"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "employees_department_id_idx" ON "employees"("department_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "employees_direct_manager_id_idx" ON "employees"("direct_manager_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "delegation_grants_principal_employee_id_idx" ON "delegation_grants"("principal_employee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "delegation_grants_delegate_employee_id_idx" ON "delegation_grants"("delegate_employee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_sessions_employee_id_idx" ON "auth_sessions"("employee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_sessions_expires_at_idx" ON "auth_sessions"("expires_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "auth_sessions_revoked_at_idx" ON "auth_sessions"("revoked_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_logs_request_id_idx" ON "audit_logs"("request_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_logs_actor_id_created_at_idx" ON "audit_logs"("actor_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "expense_types_code_key" ON "expense_types"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "expense_types_enabled_sort_order_idx" ON "expense_types"("enabled", "sort_order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "reimbursement_forms_form_number_key" ON "reimbursement_forms"("form_number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_forms_owner_id_idx" ON "reimbursement_forms"("owner_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_forms_submitter_id_idx" ON "reimbursement_forms"("submitter_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_forms_department_id_idx" ON "reimbursement_forms"("department_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_forms_status_idx" ON "reimbursement_forms"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_forms_reimbursement_date_idx" ON "reimbursement_forms"("reimbursement_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_items_reimbursement_id_idx" ON "reimbursement_items"("reimbursement_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_items_expense_type_id_idx" ON "reimbursement_items"("expense_type_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reimbursement_items_expense_date_idx" ON "reimbursement_items"("expense_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ocr_results_file_sha256_idx" ON "ocr_results"("file_sha256");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ocr_results_file_sha256_ocr_provider_ocr_type_key" ON "ocr_results"("file_sha256", "ocr_provider", "ocr_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_owner_employee_id_idx" ON "invoice_pool"("owner_employee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_uploaded_by_employee_id_idx" ON "invoice_pool"("uploaded_by_employee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_file_sha256_idx" ON "invoice_pool"("file_sha256");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_invoice_code_invoice_number_idx" ON "invoice_pool"("invoice_code", "invoice_number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_status_idx" ON "invoice_pool"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invoice_pool_linked_reimbursement_id_idx" ON "invoice_pool"("linked_reimbursement_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_definitions_entity_type_is_active_idx" ON "workflow_definitions"("entity_type", "is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "workflow_definitions_entity_type_version_key" ON "workflow_definitions"("entity_type", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_instances_workflow_id_idx" ON "workflow_instances"("workflow_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_instances_entity_type_entity_id_idx" ON "workflow_instances"("entity_type", "entity_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_instances_current_status_idx" ON "workflow_instances"("current_status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_instances_submitter_id_idx" ON "workflow_instances"("submitter_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_instances_context_department_id_idx" ON "workflow_instances"("context_department_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_tasks_instance_id_idx" ON "workflow_tasks"("instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_tasks_status_idx" ON "workflow_tasks"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_tasks_assignee_id_idx" ON "workflow_tasks"("assignee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_tasks_assignee_role_idx" ON "workflow_tasks"("assignee_role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_history_instance_id_created_at_idx" ON "workflow_history"("instance_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_history_task_id_idx" ON "workflow_history"("task_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workflow_history_actor_id_idx" ON "workflow_history"("actor_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "approval_rules_enabled_priority_idx" ON "approval_rules"("enabled", "priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "approval_transfers_reimbursement_id_idx" ON "approval_transfers"("reimbursement_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "approval_transfers_from_approver_id_idx" ON "approval_transfers"("from_approver_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "approval_transfers_to_approver_id_idx" ON "approval_transfers"("to_approver_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "departments" ADD CONSTRAINT "departments_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "employees" ADD CONSTRAINT "employees_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "employees" ADD CONSTRAINT "employees_direct_manager_id_fkey" FOREIGN KEY ("direct_manager_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "employee_roles" ADD CONSTRAINT "employee_roles_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "delegation_grants" ADD CONSTRAINT "delegation_grants_principal_employee_id_fkey" FOREIGN KEY ("principal_employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "delegation_grants" ADD CONSTRAINT "delegation_grants_delegate_employee_id_fkey" FOREIGN KEY ("delegate_employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reimbursement_forms" ADD CONSTRAINT "reimbursement_forms_submitter_id_fkey" FOREIGN KEY ("submitter_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reimbursement_forms" ADD CONSTRAINT "reimbursement_forms_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reimbursement_forms" ADD CONSTRAINT "reimbursement_forms_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reimbursement_items" ADD CONSTRAINT "reimbursement_items_reimbursement_id_fkey" FOREIGN KEY ("reimbursement_id") REFERENCES "reimbursement_forms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reimbursement_items" ADD CONSTRAINT "reimbursement_items_expense_type_id_fkey" FOREIGN KEY ("expense_type_id") REFERENCES "expense_types"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_owner_employee_id_fkey" FOREIGN KEY ("owner_employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_uploaded_by_employee_id_fkey" FOREIGN KEY ("uploaded_by_employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_overridden_by_fkey" FOREIGN KEY ("overridden_by") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_ocr_result_id_fkey" FOREIGN KEY ("ocr_result_id") REFERENCES "ocr_results"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_linked_reimbursement_id_fkey" FOREIGN KEY ("linked_reimbursement_id") REFERENCES "reimbursement_forms"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invoice_pool" ADD CONSTRAINT "invoice_pool_linked_item_id_fkey" FOREIGN KEY ("linked_item_id") REFERENCES "reimbursement_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_instances" ADD CONSTRAINT "workflow_instances_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "workflow_definitions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_instances" ADD CONSTRAINT "workflow_instances_entity_id_fkey" FOREIGN KEY ("entity_id") REFERENCES "reimbursement_forms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_instances" ADD CONSTRAINT "workflow_instances_submitter_id_fkey" FOREIGN KEY ("submitter_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_instances" ADD CONSTRAINT "workflow_instances_context_department_id_fkey" FOREIGN KEY ("context_department_id") REFERENCES "departments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_tasks" ADD CONSTRAINT "workflow_tasks_instance_id_fkey" FOREIGN KEY ("instance_id") REFERENCES "workflow_instances"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_tasks" ADD CONSTRAINT "workflow_tasks_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_tasks" ADD CONSTRAINT "workflow_tasks_completed_by_fkey" FOREIGN KEY ("completed_by") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_history" ADD CONSTRAINT "workflow_history_instance_id_fkey" FOREIGN KEY ("instance_id") REFERENCES "workflow_instances"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_history" ADD CONSTRAINT "workflow_history_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "workflow_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workflow_history" ADD CONSTRAINT "workflow_history_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "approval_transfers" ADD CONSTRAINT "approval_transfers_reimbursement_id_fkey" FOREIGN KEY ("reimbursement_id") REFERENCES "reimbursement_forms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "approval_transfers" ADD CONSTRAINT "approval_transfers_from_approver_id_fkey" FOREIGN KEY ("from_approver_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "approval_transfers" ADD CONSTRAINT "approval_transfers_to_approver_id_fkey" FOREIGN KEY ("to_approver_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Custom constraints & partial unique indexes (P0 requirements)
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
-- approval_transfers: 转审 reason≥10(P0)
|
||||
ALTER TABLE "approval_transfers"
|
||||
ADD CONSTRAINT "approval_transfers_reason_len_chk"
|
||||
CHECK (char_length("reason") >= 10);
|
||||
|
||||
-- invoice_pool: 判重例外字段完整性(P0)
|
||||
ALTER TABLE "invoice_pool"
|
||||
ADD CONSTRAINT "invoice_pool_duplicate_override_chk"
|
||||
CHECK (
|
||||
"is_duplicate_override" = false
|
||||
OR (
|
||||
"override_ref_form_number" IS NOT NULL
|
||||
AND "override_reason" IS NOT NULL
|
||||
AND char_length("override_reason") >= 10
|
||||
AND "overridden_by" IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
-- invoice_pool: linked_item_id 只能在存在 linked_reimbursement_id 时出现(P0)
|
||||
ALTER TABLE "invoice_pool"
|
||||
ADD CONSTRAINT "invoice_pool_linked_item_requires_reimbursement_chk"
|
||||
CHECK ("linked_item_id" IS NULL OR "linked_reimbursement_id" IS NOT NULL);
|
||||
|
||||
-- invoice_pool: 状态机不变量(P0,见规格书 4.4.2/4.4.3)
|
||||
ALTER TABLE "invoice_pool"
|
||||
ADD CONSTRAINT "invoice_pool_status_invariants_chk"
|
||||
CHECK (
|
||||
(
|
||||
"status" IN ('pending_ocr', 'ocr_failed', 'available')
|
||||
AND "linked_reimbursement_id" IS NULL
|
||||
AND "linked_item_id" IS NULL
|
||||
AND "archived_reason" IS NULL
|
||||
AND "archived_at" IS NULL
|
||||
)
|
||||
OR (
|
||||
"status" = 'bound'
|
||||
AND "linked_reimbursement_id" IS NOT NULL
|
||||
AND "archived_reason" IS NULL
|
||||
AND "archived_at" IS NULL
|
||||
)
|
||||
OR (
|
||||
"status" = 'archived'
|
||||
AND "archived_reason" IS NOT NULL
|
||||
AND "archived_at" IS NOT NULL
|
||||
AND (
|
||||
(
|
||||
"archived_reason" = 'used_complete'
|
||||
AND "linked_reimbursement_id" IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
"archived_reason" IN ('unused_timeout', 'manual')
|
||||
AND "linked_reimbursement_id" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- invoice_pool: 全局双策略判重(只对已使用历史生效;P0 定案)
|
||||
CREATE UNIQUE INDEX "invoice_pool_file_sha256_dedup_ux"
|
||||
ON "invoice_pool" ("file_sha256")
|
||||
WHERE (
|
||||
(
|
||||
"status" = 'bound'
|
||||
OR ("status" = 'archived' AND "archived_reason" = 'used_complete')
|
||||
)
|
||||
AND "is_duplicate_override" = false
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "invoice_pool_invoice_code_number_dedup_ux"
|
||||
ON "invoice_pool" ("invoice_code", "invoice_number")
|
||||
WHERE (
|
||||
"invoice_code" IS NOT NULL
|
||||
AND "invoice_number" IS NOT NULL
|
||||
AND (
|
||||
"status" = 'bound'
|
||||
OR ("status" = 'archived' AND "archived_reason" = 'used_complete')
|
||||
)
|
||||
AND "is_duplicate_override" = false
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "employees" ALTER COLUMN "direct_manager_id" DROP NOT NULL;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "password_reset_codes" (
|
||||
"id" UUID NOT NULL,
|
||||
"employee_id" UUID NOT NULL,
|
||||
"code_hash" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||
"used_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_ip" TEXT,
|
||||
"created_user_agent" TEXT,
|
||||
|
||||
CONSTRAINT "password_reset_codes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "password_reset_codes_employee_id_created_at_idx" ON "password_reset_codes"("employee_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "password_reset_codes_expires_at_idx" ON "password_reset_codes"("expires_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "password_reset_codes" ADD CONSTRAINT "password_reset_codes_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add composite index for workflow task inbox query (status + assignee filters)
|
||||
CREATE INDEX "idx_workflow_tasks_status_assignee"
|
||||
ON "workflow_tasks" ("status", "assignee_id", "assignee_role");
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,595 @@
|
||||
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)
|
||||
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")
|
||||
|
||||
submitted_reimbursements ReimbursementForm[] @relation("ReimbursementSubmitter")
|
||||
owned_reimbursements ReimbursementForm[] @relation("ReimbursementOwner")
|
||||
|
||||
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])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
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)
|
||||
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([status])
|
||||
@@index([reimbursement_date])
|
||||
@@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_error String?
|
||||
ocr_completed_at DateTime?
|
||||
|
||||
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([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")
|
||||
@@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")
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户