// 灵犀 Prisma schema(A0 基线) // 依据:docs/工程规范/05-数据库规范.md、技术架构 §5.3 / §5.4.5 // 约定:表/列 snake_case;主键 char(26) ULID(应用侧生成);时间 timestamptz;JSON 用 JsonB。 // A0 只建平台侧骨架表(outbox_events / idempotency_keys / system_configs); // 身份、客户、行业、脚本、素材等业务表按里程碑在各自模块内新增,禁止跨模块表。 generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } /// 事务性发件箱:领域事件先落库,再由 worker 投递(技术架构 §5.3) model OutboxEvent { id String @id @db.Char(26) aggregateType String @map("aggregate_type") @db.VarChar(64) aggregateId String @map("aggregate_id") @db.Char(26) eventType String @map("event_type") @db.VarChar(128) payloadJson Json @map("payload_json") @db.JsonB traceId String? @map("trace_id") @db.VarChar(64) occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(3) publishedAt DateTime? @map("published_at") @db.Timestamptz(3) attempts Int @default(0) lastError String? @map("last_error") @db.Text @@index([publishedAt, occurredAt]) @@index([aggregateType, aggregateId]) @@map("outbox_events") } /// 请求幂等键:任务提交必须带 Idempotency-Key(TTL 24h) model IdempotencyKey { id String @id @db.Char(26) scope String @db.VarChar(64) key String @db.VarChar(128) requestHash String @map("request_hash") @db.VarChar(64) responseStatus Int? @map("response_status") responseBody Json? @map("response_body") @db.JsonB createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) expiresAt DateTime @map("expires_at") @db.Timestamptz(3) @@unique([scope, key]) @@index([expiresAt]) @@map("idempotency_keys") } /// 部门:两级组织(一级部门 → 小组),父子用 parent_id 关联(A1 identity) model Department { id String @id @db.Char(26) code String @unique @db.VarChar(64) name String @db.VarChar(64) parentId String? @map("parent_id") @db.Char(26) leaderUserId String? @map("leader_user_id") @db.Char(26) sortOrder Int @default(0) @map("sort_order") status String @default("ACTIVE") @db.VarChar(16) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) parent Department? @relation("DepartmentTree", fields: [parentId], references: [id]) children Department[] @relation("DepartmentTree") leader User? @relation("DepartmentLeader", fields: [leaderUserId], references: [id]) users User[] @relation("UserDepartment") @@unique([parentId, name]) @@index([parentId, sortOrder]) @@map("departments") } enum UserStatus { ACTIVE DISABLED } /// 用户:登录账号唯一、手机号唯一;生命周期只停用不删除(PRD R-02) model User { id String @id @db.Char(26) account String @unique @db.VarChar(64) email String? @unique @db.VarChar(128) name String @db.VarChar(64) phone String @unique @db.VarChar(20) passwordHash String @map("password_hash") @db.VarChar(255) departmentId String @map("department_id") @db.Char(26) jobTitle String? @map("job_title") @db.VarChar(64) status UserStatus @default(ACTIVE) mustChangePassword Boolean @default(true) @map("must_change_password") lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3) lastLoginIp String? @map("last_login_ip") @db.VarChar(64) disabledAt DateTime? @map("disabled_at") @db.Timestamptz(3) remark String? @db.VarChar(200) createdBy String? @map("created_by") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) department Department @relation("UserDepartment", fields: [departmentId], references: [id]) roles UserRole[] refreshTokens RefreshToken[] ledDepartments Department[] @relation("DepartmentLeader") ownedCustomers Customer[] @relation("CustomerOwner") createdCustomers Customer[] @relation("CustomerCreator") appliedCustomerFields CustomerField[] @relation("CustomerFieldApplicant") approvedCustomerFields CustomerField[] @relation("CustomerFieldApprover") @@index([departmentId, status]) @@index([status, lastLoginAt]) @@map("users") } enum RoleStatus { ACTIVE DISABLED } /// 角色:A1 只做绑定多选,权限点配置属 A2(此处先存角色-权限点) model Role { id String @id @db.Char(26) code String @unique @db.VarChar(64) name String @db.VarChar(64) description String @default("") @db.VarChar(255) isSystem Boolean @default(false) @map("is_system") status RoleStatus @default(ACTIVE) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) users UserRole[] permissions RolePermission[] dataScopes DataScope[] @@map("roles") } /// 用户-角色绑定(一人多角色) model UserRole { userId String @map("user_id") @db.Char(26) roleId String @map("role_id") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) @@id([userId, roleId]) @@index([roleId]) @@map("user_roles") } /// 角色-权限点:权限点取 @lingxi/shared PERMISSION_CODES,每请求实时解析(不缓存旧权限) model RolePermission { roleId String @map("role_id") @db.Char(26) permissionCode String @map("permission_code") @db.VarChar(64) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) @@id([roleId, permissionCode]) @@index([permissionCode]) @@map("role_permissions") } /// 权限点目录(A2-BE-02):唯一事实源在 @lingxi/shared PERMISSION_CATALOG; /// 启动时幂等 upsert 同步,只增不删;前端权限树由目录生成(不手写迁移)。 model Permission { code String @id @db.VarChar(64) module String @db.VarChar(32) moduleLabel String @map("module_label") @db.VarChar(32) name String @db.VarChar(64) description String @default("") @db.VarChar(255) isSpecial Boolean @default(false) @map("is_special") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@index([module, sortOrder]) @@map("permissions") } /// 角色数据范围(A2-BE-04):六类范围之一 + 可选授权对象(当前只有「指定客户组」用客户组 id)。 /// 一行 = 一个范围类型的一个授权对象;无对象类型(全部 / 本部门 / 负责客户 / 本人创建)只存一行 target_id = NULL。 /// 解析后的可见客户集合由 CustomerDirectory 端口计算,不落库(A3 前为空集)。 model DataScope { id String @id @db.Char(26) roleId String @map("role_id") @db.Char(26) scopeType String @map("scope_type") @db.VarChar(32) targetId String? @map("target_id") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) @@unique([roleId, scopeType, targetId]) @@index([roleId]) @@index([scopeType, targetId]) @@map("data_scopes") } /// 刷新令牌:jti 作主键、只存哈希;旋转时旧 token 立刻吊销并记录 replaced_by_id model RefreshToken { id String @id @db.Char(26) userId String @map("user_id") @db.Char(26) familyId String @map("family_id") @db.Char(26) tokenHash String @unique @map("token_hash") @db.VarChar(64) userAgent String? @map("user_agent") @db.VarChar(255) ip String? @db.VarChar(64) expiresAt DateTime @map("expires_at") @db.Timestamptz(3) revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3) replacedById String? @map("replaced_by_id") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, expiresAt]) @@index([familyId]) @@map("refresh_tokens") } /// 系统配置:阈值 / 上限类数字唯一存放处(PRD R-26),带版本与变更审计 model SystemConfig { id String @id @db.Char(26) configKey String @unique @map("config_key") @db.VarChar(128) configValue Json @map("config_value") @db.JsonB valueType String @map("value_type") @db.VarChar(16) description String @default("") @db.VarChar(255) version Int @default(1) isEditable Boolean @default(true) @map("is_editable") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) updatedBy String? @map("updated_by") @db.Char(26) @@map("system_configs") } // --------------------------------------------------------------------------- // A3 客户与行业(client 模块;ADR-002 两级行业) // --------------------------------------------------------------------------- enum IndustryStatus { ACTIVE DISABLED } /// 行业两级树:一级 parent_id 为空,二级 parent_id 指向一级;最多两级。 /// 名称唯一性由迁移中的部分唯一索引保证:一级全局唯一、二级同父唯一。 model Industry { id String @id @db.Char(26) parentId String? @map("parent_id") @db.Char(26) level Int @default(1) name String @db.VarChar(64) sortOrder Int @default(10) @map("sort_order") status IndustryStatus @default(ACTIVE) remark String @default("") @db.VarChar(200) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) parent Industry? @relation("IndustryTree", fields: [parentId], references: [id], onDelete: Restrict) children Industry[] @relation("IndustryTree") customers Customer[] @@index([parentId, sortOrder]) @@index([level, status]) @@map("industries") } enum CustomerStatus { ACTIVE PAUSED ENDED } /// 客户:隔离单元;industry_id 必须指向末级行业;软删除保留历史。 model Customer { id String @id @db.Char(26) industryId String @map("industry_id") @db.Char(26) name String @db.VarChar(128) shortName String @map("short_name") @db.VarChar(32) status CustomerStatus @default(ACTIVE) ownerUserId String @map("owner_user_id") @db.Char(26) contactName String? @map("contact_name") @db.VarChar(32) contactPhone String? @map("contact_phone") @db.VarChar(32) preferences Json @default("{}") @db.JsonB customFields Json @default("[]") @map("custom_fields") @db.JsonB remark String? @db.VarChar(500) createdBy String? @map("created_by") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) industry Industry @relation(fields: [industryId], references: [id], onDelete: Restrict) owner User @relation("CustomerOwner", fields: [ownerUserId], references: [id], onDelete: Restrict) creator User? @relation("CustomerCreator", fields: [createdBy], references: [id], onDelete: SetNull) groupMembers CustomerGroupMember[] fields CustomerField[] @@unique([industryId, name]) @@index([ownerUserId, status]) @@index([status, updatedAt]) @@index([deletedAt]) @@map("customers") } /// 客户组:授权与数据范围的最小单位。 model CustomerGroup { id String @id @db.Char(26) name String @unique @db.VarChar(64) description String @default("") @db.VarChar(255) createdBy String? @map("created_by") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) members CustomerGroupMember[] @@map("customer_groups") } /// 客户组 ↔ 客户(复合主键) model CustomerGroupMember { groupId String @map("group_id") @db.Char(26) customerId String @map("customer_id") @db.Char(26) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) group CustomerGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) @@id([groupId, customerId]) @@index([customerId]) @@map("customer_group_members") } enum CustomerFieldStatus { PENDING ACTIVE REJECTED DISABLED } /// 客户级字段:只有 enters_generation=true 的字段需要审批; /// 基础字段存 customers.custom_fields(JSONB),不进入本表。 model CustomerField { id String @id @db.Char(26) customerId String @map("customer_id") @db.Char(26) name String @db.VarChar(32) code String @db.VarChar(32) type String @db.VarChar(16) options Json @default("[]") @db.JsonB purpose String @db.VarChar(200) entersGeneration Boolean @default(false) @map("enters_generation") status CustomerFieldStatus @default(PENDING) applicantUserId String @map("applicant_user_id") @db.Char(26) approverUserId String? @map("approver_user_id") @db.Char(26) appliedAt DateTime @default(now()) @map("applied_at") @db.Timestamptz(3) approvedAt DateTime? @map("approved_at") @db.Timestamptz(3) rejectReason String? @map("reject_reason") @db.VarChar(200) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) applicant User @relation("CustomerFieldApplicant", fields: [applicantUserId], references: [id], onDelete: Restrict) approver User? @relation("CustomerFieldApprover", fields: [approverUserId], references: [id], onDelete: SetNull) @@unique([customerId, code]) @@index([status, appliedAt]) @@index([customerId, status]) @@map("customer_fields") }