背景: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
+303
@@ -4,6 +4,26 @@
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
'/api/v1/health/queues': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* 队列健康检查
|
||||
* @description 返回 9 个队列的计数、队首等待时长与 worker 心跳;Redis 不可用时返回 503(错误码 50002)。
|
||||
*/
|
||||
get: operations['QueueHealthController_queues'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/auth/login': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -716,6 +736,92 @@ export interface paths {
|
||||
patch: operations['DictsController_updateItem'];
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/media-tasks': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 任务列表(类型 / 状态 / 客户 / 时间 / 我的 / 关键字筛选;稳定排序 createdAt DESC, id DESC) */
|
||||
get: operations['MediaTaskController_list'];
|
||||
put?: never;
|
||||
/** 提交任务(异步 202;M0 仅演练任务;必带 Idempotency-Key,同 key 同请求体 24h 内返回同一任务) */
|
||||
post: operations['MediaTaskController_submit'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/media-tasks/{id}/events': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 任务 SSE 进度(id 用 task_events.seq;支持 Last-Event-ID / last_event_id 续推,15s 心跳) */
|
||||
get: operations['MediaTaskController_events'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/media-tasks/{id}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 任务详情(首 100 条任务项 + itemsNextCursor;范围外 404) */
|
||||
get: operations['MediaTaskController_detail'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/media-tasks/{id}/cancel': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** 取消任务(仅排队 / 生成中;只把未开始项置 skipped,已成功项不受影响) */
|
||||
post: operations['MediaTaskController_cancel'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/media-tasks/{id}/retry': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** 重试任务(仅部分失败;只重置失败项,成功项不动) */
|
||||
post: operations['MediaTaskController_retry'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/health': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -911,6 +1017,74 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
QueueCountsDto: {
|
||||
/**
|
||||
* @description 等待中任务数
|
||||
* @example 0
|
||||
*/
|
||||
waiting: number;
|
||||
/**
|
||||
* @description 执行中任务数
|
||||
* @example 0
|
||||
*/
|
||||
active: number;
|
||||
/**
|
||||
* @description 已完成任务数(保留窗口内的历史计数)
|
||||
* @example 12
|
||||
*/
|
||||
completed: number;
|
||||
/**
|
||||
* @description 失败任务数(保留窗口内的历史计数)
|
||||
* @example 1
|
||||
*/
|
||||
failed: number;
|
||||
/**
|
||||
* @description 延迟任务数
|
||||
* @example 0
|
||||
*/
|
||||
delayed: number;
|
||||
};
|
||||
WorkerHeartbeatDto: {
|
||||
/** @description 最近 30s 内是否有 worker 心跳 */
|
||||
present: boolean;
|
||||
/**
|
||||
* @description 心跳年龄(秒);心跳缺失或值非法时为 null
|
||||
* @example 3
|
||||
*/
|
||||
ageSeconds: number | null;
|
||||
};
|
||||
QueueHealthEntryDto: {
|
||||
/**
|
||||
* @description 队列名
|
||||
* @example media:verify
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description 队列组
|
||||
* @example verify
|
||||
*/
|
||||
group: string;
|
||||
counts: components['schemas']['QueueCountsDto'];
|
||||
/**
|
||||
* @description 队首任务等待秒数
|
||||
* @example 0
|
||||
*/
|
||||
oldestWaitingSeconds: number;
|
||||
workerHeartbeat: components['schemas']['WorkerHeartbeatDto'];
|
||||
};
|
||||
QueueHealthReportDto: {
|
||||
/**
|
||||
* @example ok
|
||||
* @enum {string}
|
||||
*/
|
||||
status: 'ok' | 'degraded';
|
||||
/**
|
||||
* @example ok
|
||||
* @enum {string}
|
||||
*/
|
||||
redis: 'ok';
|
||||
queues: components['schemas']['QueueHealthEntryDto'][];
|
||||
};
|
||||
ComponentHealthDto: {
|
||||
/**
|
||||
* @example up
|
||||
@@ -1018,6 +1192,33 @@ export interface components {
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
QueueHealthController_queues: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description 队列状态快照(可能为 degraded) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['QueueHealthReportDto'];
|
||||
};
|
||||
};
|
||||
/** @description Redis 不可用(错误码 50002) */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
AuthController_login: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1972,6 +2173,108 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_submit: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
202: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_events: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_detail: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_cancel: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
MediaTaskController_retry: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
HealthController_check: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
Reference in new issue
Block a user