feat(media): A6 任务中心 API、SSE 进度与队列骨架
ci / 质量门禁 (push) Successful in 6m4s

背景:A6-BE-01..05 与 A6-INF-01,打通「提交 → 队列 → SSE 进度 → 完成/失败 → 取消 / 仅重试失败项」闭环,为 B3 / M1 的真实任务提供统一协议。
改动:
- API media 模块:POST/GET /media-tasks、详情、SSE(Last-Event-ID 续推 + 15s 心跳 + 800ms 落库轮询)、取消(只影响未开始项)、仅重试失败项;Idempotency-Key 24h(异步提交 202;同 key 同 body 返回同一任务且不重复审计,不同 body 40902);范围外统一 404;
- 队列骨架:BullMQ 5 九队列(5 组独立启停)、Outbox dispatcher(Redis 锁 + FOR UPDATE SKIP LOCKED + jobId 幂等 + 指数退避/死信)、worker 按 QUEUE 启停、demo 演练处理器(事务 + 行锁 + 取消/重试/幂等)、优雅退出与心跳;
- 监控:GET /api/v1/health/queues、GET /api/v1/metrics(Prometheus 0.0.4)、infra/monitoring 告警规则与接入说明;
- 通用:AuditInterceptor 支持幂等重放 skipAudit(默认行为不变)、AppModule 接线、.env.example 的 QUEUE 语义、bullmq 5.81.5 / worker @prisma/client / worker 覆盖率依赖。
影响面:OpenAPI 新增 6 个端点(openapi:diff 无破坏性变更);API 进程新增 outbox dispatcher 与 metrics/health 路由;worker 新增 BullMQ 消费;未改旧审计导出链路与既有接口。
回滚:回滚提交并停止 worker 新队列消费;task/outbox 新表数据不影响旧功能,可按需保留。
验证:api 单测 257、集成 92(连跑 2 次)、worker 27、shared 197;typecheck / lint / format:check / build / openapi:diff 全绿;本地真实 worker 启用 9 队列心跳,/health/queues、/metrics 实测,/media-tasks 未登录 401。
This commit is contained in:
Stev_Wang committed 2026-11-04 21:52:35 +08:00
1 parent 4a29cf815e
commit 633765f10f
70 files changed
+8674 -45

No files matched your search

+264
View File
@@ -1,6 +1,32 @@
{
"openapi": "3.1.0",
"paths": {
"/api/v1/health/queues": {
"get": {
"description": "返回 9 个队列的计数、队首等待时长与 worker 心跳;Redis 不可用时返回 503(错误码 50002)。",
"operationId": "QueueHealthController_queues",
"parameters": [],
"responses": {
"200": {
"description": "队列状态快照(可能为 degraded)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueHealthReportDto"
}
}
}
},
"503": {
"description": "Redis 不可用(错误码 50002)"
}
},
"summary": "队列健康检查",
"tags": [
"platform"
]
}
},
"/api/v1/auth/login": {
"post": {
"description": "返回 accessToken / refreshToken / expiresIn / me",
@@ -1094,6 +1120,124 @@
]
}
},
"/api/v1/media-tasks": {
"post": {
"operationId": "MediaTaskController_submit",
"parameters": [],
"responses": {
"202": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "提交任务(异步 202;M0 仅演练任务;必带 Idempotency-Key,同 key 同请求体 24h 内返回同一任务)",
"tags": [
"media-tasks"
]
},
"get": {
"operationId": "MediaTaskController_list",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "任务列表(类型 / 状态 / 客户 / 时间 / 我的 / 关键字筛选;稳定排序 createdAt DESC, id DESC)",
"tags": [
"media-tasks"
]
}
},
"/api/v1/media-tasks/{id}/events": {
"get": {
"operationId": "MediaTaskController_events",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "任务 SSE 进度(id 用 task_events.seq;支持 Last-Event-ID / last_event_id 续推,15s 心跳)",
"tags": [
"media-tasks"
]
}
},
"/api/v1/media-tasks/{id}": {
"get": {
"operationId": "MediaTaskController_detail",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "任务详情(首 100 条任务项 + itemsNextCursor;范围外 404)",
"tags": [
"media-tasks"
]
}
},
"/api/v1/media-tasks/{id}/cancel": {
"post": {
"operationId": "MediaTaskController_cancel",
"parameters": [],
"responses": {
"201": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "取消任务(仅排队 / 生成中;只把未开始项置 skipped,已成功项不受影响)",
"tags": [
"media-tasks"
]
}
},
"/api/v1/media-tasks/{id}/retry": {
"post": {
"operationId": "MediaTaskController_retry",
"parameters": [],
"responses": {
"201": {
"description": ""
}
},
"security": [
{
"bearer": []
}
],
"summary": "重试任务(仅部分失败;只重置失败项,成功项不动)",
"tags": [
"media-tasks"
]
}
},
"/health": {
"get": {
"description": "真实检查 PostgreSQL 与 Redis;任一不可用返回 503 与统一错误体。",
@@ -1361,6 +1505,126 @@
}
},
"schemas": {
"QueueCountsDto": {
"type": "object",
"properties": {
"waiting": {
"type": "number",
"description": "等待中任务数",
"example": 0
},
"active": {
"type": "number",
"description": "执行中任务数",
"example": 0
},
"completed": {
"type": "number",
"description": "已完成任务数(保留窗口内的历史计数)",
"example": 12
},
"failed": {
"type": "number",
"description": "失败任务数(保留窗口内的历史计数)",
"example": 1
},
"delayed": {
"type": "number",
"description": "延迟任务数",
"example": 0
}
},
"required": [
"waiting",
"active",
"completed",
"failed",
"delayed"
]
},
"WorkerHeartbeatDto": {
"type": "object",
"properties": {
"present": {
"type": "boolean",
"description": "最近 30s 内是否有 worker 心跳"
},
"ageSeconds": {
"type": "number",
"nullable": true,
"description": "心跳年龄(秒);心跳缺失或值非法时为 null",
"example": 3
}
},
"required": [
"present",
"ageSeconds"
]
},
"QueueHealthEntryDto": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "队列名",
"example": "media:verify"
},
"group": {
"type": "string",
"description": "队列组",
"example": "verify"
},
"counts": {
"$ref": "#/components/schemas/QueueCountsDto"
},
"oldestWaitingSeconds": {
"type": "number",
"description": "队首任务等待秒数",
"example": 0
},
"workerHeartbeat": {
"$ref": "#/components/schemas/WorkerHeartbeatDto"
}
},
"required": [
"name",
"group",
"counts",
"oldestWaitingSeconds",
"workerHeartbeat"
]
},
"QueueHealthReportDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ok",
"degraded"
],
"example": "ok"
},
"redis": {
"type": "string",
"enum": [
"ok"
],
"example": "ok"
},
"queues": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueueHealthEntryDto"
}
}
},
"required": [
"status",
"redis",
"queues"
]
},
"ComponentHealthDto": {
"type": "object",
"properties": {