背景: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:
1 parent
4a29cf815e
commit
633765f10f
70 files changed
+8674
-45
No files matched your search
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user