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。
This commit is contained in:
1 parent
b505dfe5e3
commit
4a29cf815e
8 files changed
+1267
No files matched your search
@@ -0,0 +1,93 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "media_tasks" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"type" VARCHAR(16) NOT NULL,
|
||||
"status" VARCHAR(24) NOT NULL DEFAULT 'queued',
|
||||
"title" VARCHAR(200) NOT NULL,
|
||||
"customer_id" CHAR(26) NOT NULL,
|
||||
"customer_name" VARCHAR(128) NOT NULL,
|
||||
"created_by" CHAR(26) NOT NULL,
|
||||
"created_by_name" VARCHAR(64) NOT NULL,
|
||||
"item_total" INTEGER NOT NULL,
|
||||
"item_succeeded" INTEGER NOT NULL DEFAULT 0,
|
||||
"item_failed" INTEGER NOT NULL DEFAULT 0,
|
||||
"item_skipped" INTEGER NOT NULL DEFAULT 0,
|
||||
"estimated_items" INTEGER NOT NULL,
|
||||
"estimated_cost" DECIMAL(14,4) NOT NULL,
|
||||
"estimated_seconds" INTEGER NOT NULL,
|
||||
"progress" INTEGER NOT NULL DEFAULT 0,
|
||||
"payload_json" JSONB NOT NULL,
|
||||
"cancel_requested_at" TIMESTAMPTZ(3),
|
||||
"canceled_at" TIMESTAMPTZ(3),
|
||||
"canceled_by" CHAR(26),
|
||||
"last_error_code" VARCHAR(32),
|
||||
"last_error_message" VARCHAR(500),
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "media_tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "media_items" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"task_id" CHAR(26) NOT NULL,
|
||||
"seq" INTEGER NOT NULL,
|
||||
"status" VARCHAR(24) NOT NULL DEFAULT 'queued',
|
||||
"failure_code" VARCHAR(64),
|
||||
"failure_message" VARCHAR(500),
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"started_at" TIMESTAMPTZ(3),
|
||||
"finished_at" TIMESTAMPTZ(3),
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "media_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "task_events" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"task_id" CHAR(26) NOT NULL,
|
||||
"seq" BIGSERIAL NOT NULL,
|
||||
"event_type" VARCHAR(16) NOT NULL,
|
||||
"payload_json" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "task_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_tasks_customer_id_created_at_idx" ON "media_tasks"("customer_id", "created_at" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_tasks_status_created_at_idx" ON "media_tasks"("status", "created_at" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_tasks_type_created_at_idx" ON "media_tasks"("type", "created_at" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_tasks_created_by_created_at_idx" ON "media_tasks"("created_by", "created_at" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_items_task_id_status_idx" ON "media_items"("task_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "media_items_status_idx" ON "media_items"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "media_items_task_id_seq_key" ON "media_items"("task_id", "seq");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "task_events_seq_key" ON "task_events"("seq");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "task_events_task_id_seq_idx" ON "task_events"("task_id", "seq");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_items" ADD CONSTRAINT "media_items_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "media_tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "task_events" ADD CONSTRAINT "task_events_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "media_tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -518,3 +518,90 @@ model AuditExportJob {
|
||||
@@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")
|
||||
}
|
||||
Reference in new issue
Block a user