Files
lingxi/prisma/schema.prisma
T
Stev_Wang 4a29cf815e feat(shared): A6 任务协议契约、任务数据模型与系统配置
背景:A6 任务中心需要统一任务协议(九态状态机 / 队列映射 / SSE 事件)与任务落库模型,供 API、worker 与前端共用,避免各端自建枚举。
改动:
- 新增 packages/shared/src/tasks/task-protocol.ts:任务类型、架构 §3.6 九态与允许迁移、5 组 9 队列定义、Outbox/SSE 事件、进度与收敛纯函数;
- 新增 packages/shared/src/schemas/tasks.ts:提交/列表/详情/取消/重试/SSE 的 Zod DTO(body camelCase、query snake_case,列表 { items, next_cursor });
- system_configs 新增 task 分组(演练上限 20 项 / 单条成本 0.02 元 / 单条时长 3 秒),seed 按定义幂等;
- Prisma 新增 media_tasks / media_items / task_events 三表与迁移 20261104125003_a6_media_tasks(expand-only,不改旧表)。
影响面:仅新增 shared 导出、配置项与三张新表,旧接口与旧表不受影响。
回滚:撤销提交,并对三张新表执行 prisma migrate resolve/删除(无业务数据)。
验证:shared 197 tests 全绿;typecheck / lint / format:check 全绿;prisma migrate status up to date。
2026-11-04 21:52:12 +08:00

608 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 灵犀 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")
}
// ---------------------------------------------------------------------------
// A6 任务中心(media 模块):任务头 / 任务项 / 任务事件三层
// 状态机见 @lingxi/shared tasks/task-protocol.ts(架构 §3.6);
// 状态用 varchar 存英文 code(与 audit_logs 同风格),翻译只在前端,避免 enum 迁移成本。
// ---------------------------------------------------------------------------
/// 任务头:存提交时的参数快照与预估成本 / 时长;进度落库(重启不丢)。
/// `customer_id` / `customer_name` 为冗余快照(跨模块禁止 join customers)。
model MediaTask {
id String @id @db.Char(26)
type String @db.VarChar(16)
status String @default("queued") @db.VarChar(24)
title String @db.VarChar(200)
customerId String @map("customer_id") @db.Char(26)
customerName String @map("customer_name") @db.VarChar(128)
createdBy String @map("created_by") @db.Char(26)
createdByName String @map("created_by_name") @db.VarChar(64)
itemTotal Int @map("item_total")
itemSucceeded Int @default(0) @map("item_succeeded")
itemFailed Int @default(0) @map("item_failed")
itemSkipped Int @default(0) @map("item_skipped")
estimatedItems Int @map("estimated_items")
estimatedCost Decimal @map("estimated_cost") @db.Decimal(14, 4)
estimatedSeconds Int @map("estimated_seconds")
progress Int @default(0)
payloadJson Json @map("payload_json") @db.JsonB
cancelRequestedAt DateTime? @map("cancel_requested_at") @db.Timestamptz(3)
canceledAt DateTime? @map("canceled_at") @db.Timestamptz(3)
canceledBy String? @map("canceled_by") @db.Char(26)
lastErrorCode String? @map("last_error_code") @db.VarChar(32)
lastErrorMessage String? @map("last_error_message") @db.VarChar(500)
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
items MediaItem[]
events TaskEvent[]
@@index([customerId, createdAt(sort: Desc)])
@@index([status, createdAt(sort: Desc)])
@@index([type, createdAt(sort: Desc)])
@@index([createdBy, createdAt(sort: Desc)])
@@map("media_tasks")
}
/// 任务项:只记状态与失败原因,产物引用在 M1+ 的产物表 / 对象存储。
/// 外键选择 onDelete: Cascade —— 本批没有删除任务的对外入口,级联只用于测试夹具清理,
/// 避免残留孤儿任务项;审计留痕由 append-only 的 audit_logs 承担(见实施报告)。
model MediaItem {
id String @id @db.Char(26)
taskId String @map("task_id") @db.Char(26)
seq Int
status String @default("queued") @db.VarChar(24)
failureCode String? @map("failure_code") @db.VarChar(64)
failureMessage String? @map("failure_message") @db.VarChar(500)
attempts Int @default(0)
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
task MediaTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
/// (task_id, seq) 唯一约束同时提供按任务顺序扫描的索引,不再重复建普通索引
@@unique([taskId, seq])
@@index([taskId, status])
@@index([status])
@@map("media_items")
}
/// 任务事件(SSE 增量源):`seq` 为只增不改的 BigInt,SSE 的 `id:` 直接用它续推。
/// 事件类型 item / progress / done / error(架构 §6.4),payload 由产物方写入。
model TaskEvent {
id String @id @db.Char(26)
taskId String @map("task_id") @db.Char(26)
seq BigInt @unique @default(autoincrement())
eventType String @map("event_type") @db.VarChar(16)
payloadJson Json @map("payload_json") @db.JsonB
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
task MediaTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@index([taskId, seq])
@@map("task_events")
}