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:
Stev_Wang committed 2026-11-04 21:52:12 +08:00
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;
+87
View File
@@ -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")
}