audit_logs 按月 RANGE 分区、复合主键与 6 组索引;ensure_partitions 幂等建分区;UPDATE/DELETE/TRUNCATE 触发器与 lingxi_app 最小权限;audit_export_jobs;种子新增 3 项配置与 6 条演示审计。
521 lines
22 KiB
Plaintext
521 lines
22 KiB
Plaintext
// 灵犀 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)
|
||
|
||
versions SystemConfigVersion[]
|
||
|
||
@@map("system_configs")
|
||
}
|
||
|
||
/// 系统配置版本历史(append-only):每次变更留前后值、原因、操作人(A4-BE-02)
|
||
model SystemConfigVersion {
|
||
id String @id @db.Char(26)
|
||
configKey String @map("config_key") @db.VarChar(128)
|
||
version Int
|
||
oldValue Json? @map("old_value") @db.JsonB
|
||
newValue Json @map("new_value") @db.JsonB
|
||
reason String? @db.VarChar(255)
|
||
changedBy String? @map("changed_by") @db.Char(26)
|
||
changedAt DateTime @default(now()) @map("changed_at") @db.Timestamptz(3)
|
||
|
||
config SystemConfig @relation(fields: [configKey], references: [configKey], onDelete: Cascade)
|
||
|
||
@@unique([configKey, version])
|
||
@@index([changedAt])
|
||
@@map("system_config_versions")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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[]
|
||
dictItems DictItem[]
|
||
|
||
@@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")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// A4 数据字典(catalog 模块):一张通用枚举表同时表达二级树与词表
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum DictStatus {
|
||
ACTIVE
|
||
DISABLED
|
||
}
|
||
|
||
enum DictKind {
|
||
TREE
|
||
LIST
|
||
}
|
||
|
||
/// 字典类型:编码唯一且创建后不可改;内置类型不可删除
|
||
model DictType {
|
||
id String @id @db.Char(26)
|
||
code String @unique @db.VarChar(64)
|
||
name String @db.VarChar(64)
|
||
groupKey String @map("group_key") @db.VarChar(32)
|
||
kind DictKind @default(LIST)
|
||
description String @default("") @db.VarChar(255)
|
||
industryScoped Boolean @default(false) @map("industry_scoped")
|
||
builtin Boolean @default(false)
|
||
status DictStatus @default(ACTIVE)
|
||
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)
|
||
updatedBy String? @map("updated_by") @db.VarChar(64)
|
||
|
||
items DictItem[]
|
||
|
||
@@index([groupKey, sortOrder])
|
||
@@map("dict_types")
|
||
}
|
||
|
||
/// 字典条目:industry_id 为空 = 通用层;parent_code 表达二级树;
|
||
/// 同 type + code + industry 唯一由迁移中的部分唯一索引保证(NULL 也唯一)。
|
||
model DictItem {
|
||
id String @id @db.Char(26)
|
||
typeId String @map("type_id") @db.Char(26)
|
||
code String @db.VarChar(64)
|
||
name String @db.VarChar(64)
|
||
parentCode String? @map("parent_code") @db.VarChar(64)
|
||
industryId String? @map("industry_id") @db.Char(26)
|
||
sortOrder Int @default(0) @map("sort_order")
|
||
status DictStatus @default(ACTIVE)
|
||
remark String @default("") @db.VarChar(255)
|
||
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.VarChar(64)
|
||
|
||
type DictType @relation(fields: [typeId], references: [id], onDelete: Cascade)
|
||
industry Industry? @relation(fields: [industryId], references: [id], onDelete: Restrict)
|
||
|
||
@@index([typeId, industryId, sortOrder])
|
||
@@index([typeId, parentCode])
|
||
@@map("dict_items")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// A5 审计与日志(platform 模块):append-only + 月分区 + 异步导出
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// 审计日志(A5-BE-01 / 工程规范 08):append-only,按月 RANGE 分区。
|
||
///
|
||
/// - 复合主键 `(id, occurred_at)`:分区键必须包含在主键 / 唯一约束中;
|
||
/// - 运行时按 id 查询请用 `findFirst`(单传 id 无法定位唯一行);
|
||
/// - `action_code` / `category` 的英文 code 存表,中文名与分类映射唯一来源是
|
||
/// `@lingxi/shared` 的 `AUDIT_ACTION_CATALOG`(避免字典漂移);
|
||
/// - 分区、索引、append-only 触发器与 `lingxi_app` 授权由迁移 SQL 手写(Prisma 不支持分区 DDL);
|
||
/// - 分区由 `audit_logs_ensure_partitions()` 保证,应用启动与每日定时调用。
|
||
model AuditLog {
|
||
id String @db.Char(26)
|
||
occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(3)
|
||
actorId String? @map("actor_id") @db.Char(26)
|
||
actorName String? @map("actor_name") @db.VarChar(64)
|
||
actorAccount String? @map("actor_account") @db.VarChar(64)
|
||
actionCode String @map("action_code") @db.VarChar(64)
|
||
category String @db.VarChar(32)
|
||
targetType String? @map("target_type") @db.VarChar(64)
|
||
targetId String? @map("target_id") @db.VarChar(128)
|
||
targetName String? @map("target_name") @db.VarChar(160)
|
||
customerId String? @map("customer_id") @db.Char(26)
|
||
customerName String? @map("customer_name") @db.VarChar(128)
|
||
result String @db.VarChar(16)
|
||
httpMethod String? @map("http_method") @db.VarChar(8)
|
||
httpPath String? @map("http_path") @db.VarChar(255)
|
||
traceId String? @map("trace_id") @db.VarChar(64)
|
||
ip String? @db.VarChar(64)
|
||
userAgent String? @map("user_agent") @db.VarChar(255)
|
||
beforeJson Json? @map("before_json") @db.JsonB
|
||
afterJson Json? @map("after_json") @db.JsonB
|
||
changesJson Json? @map("changes_json") @db.JsonB
|
||
reason String? @db.VarChar(255)
|
||
errorCode String? @map("error_code") @db.VarChar(32)
|
||
errorMessage String? @map("error_message") @db.VarChar(500)
|
||
durationMs Int? @map("duration_ms")
|
||
|
||
@@id([id, occurredAt])
|
||
@@index([occurredAt(sort: Desc), id(sort: Desc)])
|
||
@@index([actorId, occurredAt(sort: Desc)])
|
||
@@index([customerId, occurredAt(sort: Desc)])
|
||
@@index([actionCode, occurredAt(sort: Desc)])
|
||
@@index([targetType, targetId])
|
||
@@index([result, occurredAt(sort: Desc)])
|
||
@@map("audit_logs")
|
||
}
|
||
|
||
/// 审计异步导出任务(A5-BE-03):普通表(可更新进度),文件落本地临时目录,
|
||
/// 通过短时效 HMAC 签名链接下载;A6 任务协议就绪后替换 `setImmediate` 执行方式。
|
||
model AuditExportJob {
|
||
id String @id @db.Char(26)
|
||
status String @default("queued") @db.VarChar(16)
|
||
format String @default("csv") @db.VarChar(16)
|
||
filtersJson Json @default("{}") @map("filters_json") @db.JsonB
|
||
requestedBy String @map("requested_by") @db.Char(26)
|
||
requestedAt DateTime @default(now()) @map("requested_at") @db.Timestamptz(3)
|
||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||
totalRows Int? @map("total_rows")
|
||
rowCount Int? @map("row_count")
|
||
processedRows Int? @map("processed_rows")
|
||
truncated Boolean @default(false)
|
||
filePath String? @map("file_path") @db.VarChar(500)
|
||
fileSize Int? @map("file_size")
|
||
errorMessage String? @map("error_message") @db.VarChar(500)
|
||
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
|
||
downloadCount Int @default(0) @map("download_count")
|
||
lastDownloadedAt DateTime? @map("last_downloaded_at") @db.Timestamptz(3)
|
||
|
||
@@index([requestedBy, requestedAt(sort: Desc)])
|
||
@@index([status])
|
||
@@map("audit_export_jobs")
|
||
}
|