# 设计文档:Web 管理后台(admin-web-console) ## 概述 将现有 PySide6 桌面 GUI 替换为 BS 架构的 Web 管理后台。系统分为两部分: - **后端**:在现有 `apps/backend/` FastAPI 骨架上扩展,新增 ETL 管理相关的 RESTful API 和 WebSocket 端点 - **前端**:在 `apps/admin-web/` 下使用 React + Vite + Ant Design 构建 SPA 应用 核心设计原则: 1. 后端通过子进程调用现有 ETL_CLI,不重写 ETL 逻辑 2. 调度任务从本地 JSON 迁移至 PostgreSQL(`zqyy_app` 库) 3. 前后端通过 JSON API 通信,实时日志通过 WebSocket 推送 4. 数据库查询限制为只读,防止误操作 5. **多门店隔离**:通过 `site_id` 贯穿全链路,Operator 登录后绑定门店,所有 API 请求自动携带 site_id 6. **执行流程(Flow)分离**:完整保留现有 7 种 Flow 和 3 种处理模式,前端按 Flow 动态展示可选层和任务 ## 架构 ```mermaid graph TB subgraph "前端 (apps/admin-web/)" FE[React SPA
Vite + Ant Design] end subgraph "后端 (apps/backend/)" API[FastAPI 应用] AUTH[JWT 认证中间件] WS[WebSocket 端点
实时日志推送] EXEC[TaskExecutor
子进程管理] QUEUE[TaskQueue
队列管理] SCHED[Scheduler
定时调度] end subgraph "数据层" PG_APP[(zqyy_app
用户/队列/调度/历史)] PG_ETL[(etl_feiqiu
ODS/DWD/DWS)] ENV[.env 文件] end subgraph "ETL Connector" CLI[ETL CLI
子进程] end FE -->|HTTP/WS| API API --> AUTH API --> WS API --> EXEC API --> QUEUE API --> SCHED EXEC -->|subprocess| CLI CLI --> PG_ETL QUEUE --> PG_APP SCHED --> PG_APP SCHED -->|到期触发| QUEUE API -->|只读查询| PG_ETL API -->|读写| PG_APP API -->|读写| ENV ``` ### 请求流程 1. 前端发起 HTTP 请求 → JWT 中间件验证令牌(提取 site_id)→ 路由处理 2. 任务执行:API 接收 TaskConfig(含 site_id)→ TaskQueue 入队 → TaskExecutor 取出执行 → 子进程调用 ETL_CLI(`--store-id {site_id}`)→ stdout/stderr 通过 WebSocket 推送 3. 调度触发:Scheduler 定时检查到期任务 → 自动入队 → 同上执行流程 ### 多门店隔离设计 现有系统通过 `site_id`(即 `store_id`,CLI 参数名为 `--store-id`)实现多门店数据隔离: - ETL 数据库层:所有业务表包含 `site_id` 字段,`app` schema 通过 RLS 按 `current_setting('app.current_site_id')` 自动过滤 - ETL CLI 层:通过 `--store-id` 参数指定门店 Web 管理后台的隔离策略: 1. **用户-门店绑定**:`admin_users` 表增加 `site_id` 字段,每个 Operator 绑定一个门店 2. **JWT 令牌携带 site_id**:登录时将 `site_id` 写入 JWT payload,后续请求自动提取 3. **API 层自动注入**:所有涉及 ETL 操作的 API,从 JWT 中提取 `site_id`,自动注入到 TaskConfig 和数据库查询中 4. **数据库查看器隔离**:查询 ETL 数据库时,设置 `SET LOCAL app.current_site_id = '{site_id}'`,利用 RLS 自动过滤 5. **队列和调度隔离**:`task_queue` 和 `scheduled_tasks` 表增加 `site_id` 字段,查询时按 site_id 过滤 ### 执行流程(Flow)配置设计 > 术语说明:**Connector**(数据源连接器)指对接的上游 SaaS 平台(如飞球),对应 `apps/etl/pipelines/{connector}/`;**Flow**(执行流程)指 ETL 任务的处理链路,描述数据从哪一层流到哪一层。CLI 参数 `--pipeline` 实际传递的是 Flow ID。 完整保留现有 7 种 Flow,前端根据选择动态展示: | Flow ID | 显示名称 | 包含层 | |---------|---------|--------| | `api_ods` | API → ODS | ODS | | `api_ods_dwd` | API → ODS → DWD | ODS, DWD | | `api_full` | API → ODS → DWD → DWS汇总 → DWS指数 | ODS, DWD, DWS, INDEX | | `ods_dwd` | ODS → DWD | DWD | | `dwd_dws` | DWD → DWS汇总 | DWS | | `dwd_dws_index` | DWD → DWS汇总 → DWS指数 | DWS, INDEX | | `dwd_index` | DWD → DWS指数 | INDEX | 3 种处理模式: - `increment_only`:仅增量处理 - `verify_only`:校验并修复(可选"校验前从 API 获取") - `increment_verify`:增量 + 校验并修复 时间窗口模式: - `lookback`:回溯 + 冗余(lookback_hours + overlap_seconds) - `custom`:自定义时间范围(window_start + window_end) - 窗口切分:不切分 / 按天(1天/10天/30天) 前端交互逻辑: 1. Operator 选择 Flow → 前端根据 Flow 包含的层,动态显示/隐藏 ODS 任务选择、DWD 表选择、DWS 任务选择 2. Operator 选择处理模式 → 前端根据模式显示/隐藏校验相关选项 3. Operator 选择时间窗口模式 → 前端切换回溯配置或自定义日期选择器 ## 组件与接口 ### 后端 API 路由 ``` apps/backend/app/ ├── main.py # FastAPI 入口(已有,扩展) ├── config.py # 配置加载(已有) ├── database.py # 数据库连接(已有,扩展) ├── auth/ │ ├── __init__.py │ ├── jwt.py # JWT 令牌生成/验证 │ └── dependencies.py # FastAPI 依赖注入(当前用户) ├── routers/ │ ├── auth.py # POST /api/auth/login, POST /api/auth/refresh │ ├── tasks.py # 任务注册表 & 配置 API │ ├── execution.py # 任务执行 & 队列 API │ ├── schedules.py # 调度任务 CRUD API │ ├── env_config.py # 环境配置 API │ ├── db_viewer.py # 数据库查看器 API │ └── etl_status.py # ETL 状态 API ├── schemas/ │ ├── auth.py # 认证相关 Pydantic 模型 │ ├── tasks.py # 任务配置 Pydantic 模型 │ ├── execution.py # 执行记录 Pydantic 模型 │ ├── schedules.py # 调度配置 Pydantic 模型 │ └── db_viewer.py # 数据库查看器 Pydantic 模型 ├── services/ │ ├── task_executor.py # 子进程管理,执行 ETL_CLI │ ├── task_queue.py # 任务队列管理 │ ├── scheduler.py # 定时调度器 │ └── cli_builder.py # CLI 命令构建(从 gui/utils/cli_builder.py 迁移) ├── middleware/ │ └── auth.py # JWT 认证中间件 └── ws/ └── logs.py # WebSocket 日志推送端点 ``` ### 前端结构 ``` apps/admin-web/ ├── package.json ├── vite.config.ts ├── tsconfig.json ├── index.html ├── src/ │ ├── main.tsx # 入口 │ ├── App.tsx # 根组件(Layout + Router) │ ├── api/ # API 客户端 │ │ ├── client.ts # axios 实例(JWT 拦截器) │ │ ├── auth.ts │ │ ├── tasks.ts │ │ ├── execution.ts │ │ ├── schedules.ts │ │ ├── envConfig.ts │ │ ├── dbViewer.ts │ │ └── etlStatus.ts │ ├── pages/ │ │ ├── Login.tsx │ │ ├── TaskConfig.tsx # 任务配置 │ │ ├── TaskManager.tsx # 任务管理(队列 + 调度) │ │ ├── EnvConfig.tsx # 环境配置 │ │ ├── DBViewer.tsx # 数据库查看器 │ │ ├── ETLStatus.tsx # ETL 状态 │ │ └── LogViewer.tsx # 日志查看器 │ ├── components/ # 可复用组件 │ │ ├── TaskSelector.tsx # 任务选择器(按业务域分组) │ │ ├── DwdTableSelector.tsx │ │ ├── TimeWindowForm.tsx │ │ └── LogStream.tsx # WebSocket 日志流组件 │ ├── hooks/ │ │ ├── useAuth.ts │ │ └── useWebSocket.ts │ ├── store/ # 状态管理(React Context 或 Zustand) │ │ └── authStore.ts │ └── types/ # TypeScript 类型定义 │ └── index.ts ``` ### 核心 API 端点 | 方法 | 路径 | 说明 | 需求 | |------|------|------|------| | POST | `/api/auth/login` | 用户登录,返回 JWT | 1 | | POST | `/api/auth/refresh` | 刷新访问令牌 | 1 | | GET | `/api/tasks/registry` | 获取任务注册表(按业务域分组) | 2 | | GET | `/api/tasks/dwd-tables` | 获取 DWD 表定义(按业务域分组) | 2 | | POST | `/api/tasks/validate` | 验证 TaskConfig | 2, 11 | | GET | `/api/tasks/flows` | 获取执行流程列表(7 种 Flow + 3 种处理模式) | 2 | | POST | `/api/execution/run` | 提交任务执行 | 3 | | GET | `/api/execution/queue` | 获取当前队列 | 4 | | POST | `/api/execution/queue` | 添加任务到队列 | 4 | | PUT | `/api/execution/queue/reorder` | 调整队列顺序 | 4 | | DELETE | `/api/execution/queue/{id}` | 从队列删除任务 | 4 | | POST | `/api/execution/{id}/cancel` | 取消执行中的任务 | 3 | | GET | `/api/execution/history` | 获取执行历史 | 4 | | GET | `/api/execution/{id}/logs` | 获取历史任务日志 | 9 | | GET | `/api/schedules` | 获取调度任务列表 | 5 | | POST | `/api/schedules` | 创建调度任务 | 5 | | PUT | `/api/schedules/{id}` | 更新调度任务 | 5 | | DELETE | `/api/schedules/{id}` | 删除调度任务 | 5 | | PATCH | `/api/schedules/{id}/toggle` | 启用/禁用调度任务 | 5 | | GET | `/api/env-config` | 获取环境配置 | 6 | | PUT | `/api/env-config` | 更新环境配置 | 6 | | GET | `/api/env-config/export` | 导出配置(去敏感值) | 6 | | GET | `/api/db/schemas` | 获取 Schema 列表 | 7 | | GET | `/api/db/schemas/{name}/tables` | 获取表列表和行数 | 7 | | GET | `/api/db/tables/{schema}/{table}/columns` | 获取表列定义 | 7 | | POST | `/api/db/query` | 执行只读 SQL 查询 | 7 | | GET | `/api/etl-status/cursors` | 获取 ETL 游标状态 | 8 | | GET | `/api/etl-status/recent-runs` | 获取最近执行记录 | 8 | | WS | `/ws/logs/{execution_id}` | 实时日志 WebSocket | 9 | ### 关键服务组件 #### TaskExecutor(任务执行器) ```python class TaskExecutor: """管理 ETL_CLI 子进程的生命周期""" async def execute(self, config: TaskConfig, execution_id: str) -> None: """ 以子进程方式调用 ETL_CLI。 - 使用 asyncio.create_subprocess_exec 启动子进程 - 逐行读取 stdout/stderr,广播到 WebSocket 连接 - 记录退出码和执行时长到数据库 """ ... async def cancel(self, execution_id: str) -> bool: """向子进程发送 SIGTERM,等待退出后标记为已取消""" ... ``` #### TaskQueue(任务队列) ```python class TaskQueue: """基于 PostgreSQL 的任务队列""" async def enqueue(self, config: TaskConfig) -> str: """入队,返回队列任务 ID""" ... async def dequeue(self) -> Optional[QueuedTask]: """取出队首待执行任务""" ... async def reorder(self, task_id: str, new_position: int) -> None: """调整任务在队列中的位置""" ... async def process_loop(self) -> None: """后台循环:队列非空且无运行中任务时,自动取出执行""" ... ``` #### Scheduler(调度器) ```python class Scheduler: """基于 PostgreSQL 的定时调度器""" async def check_and_enqueue(self) -> None: """检查到期的调度任务,将其 TaskConfig 加入队列""" ... async def start(self) -> None: """启动后台定时检查循环(每 30 秒检查一次)""" ... ``` #### CLIBuilder(CLI 命令构建器) 从现有 `gui/utils/cli_builder.py` 迁移,核心逻辑不变: ```python class CLIBuilder: """将 TaskConfig 转换为 ETL_CLI 命令行参数列表""" def build_command(self, config: TaskConfig, etl_project_path: str) -> list[str]: """ 构建完整命令: [python, -m, cli.main, --pipeline, {flow_id}, --tasks, ..., --store-id, {site_id}, ...] 注意:CLI 参数名 --pipeline 传递的是 Flow ID """ ... ``` ## 数据模型 ### 数据库表(zqyy_app) #### admin_users 表 ```sql CREATE TABLE admin_users ( id SERIAL PRIMARY KEY, username VARCHAR(64) UNIQUE NOT NULL, password_hash VARCHAR(256) NOT NULL, display_name VARCHAR(128), site_id BIGINT NOT NULL, -- 绑定的门店 ID is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_admin_users_site ON admin_users(site_id); ``` #### task_queue 表 ```sql CREATE TABLE task_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id BIGINT NOT NULL, -- 门店隔离 config JSONB NOT NULL, -- 序列化的 TaskConfig status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / running / success / failed / cancelled position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, exit_code INTEGER, error_message TEXT ); CREATE INDEX idx_task_queue_status ON task_queue(status); CREATE INDEX idx_task_queue_site_position ON task_queue(site_id, position) WHERE status = 'pending'; ``` #### task_execution_log 表 ```sql CREATE TABLE task_execution_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), queue_id UUID REFERENCES task_queue(id), site_id BIGINT NOT NULL, -- 门店隔离 task_codes TEXT[] NOT NULL, status VARCHAR(20) NOT NULL, started_at TIMESTAMPTZ NOT NULL, finished_at TIMESTAMPTZ, exit_code INTEGER, duration_ms INTEGER, command TEXT, -- 实际执行的 CLI 命令 output_log TEXT, -- stdout 完整日志 error_log TEXT, -- stderr 日志 summary JSONB, -- 执行摘要 created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_execution_log_site_started ON task_execution_log(site_id, started_at DESC); ``` #### scheduled_tasks 表 ```sql CREATE TABLE scheduled_tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id BIGINT NOT NULL, -- 门店隔离 name VARCHAR(256) NOT NULL, task_codes TEXT[] NOT NULL, task_config JSONB NOT NULL, -- 序列化的 TaskConfig schedule_config JSONB NOT NULL, -- 序列化的 ScheduleConfig enabled BOOLEAN DEFAULT TRUE, last_run_at TIMESTAMPTZ, next_run_at TIMESTAMPTZ, run_count INTEGER DEFAULT 0, last_status VARCHAR(20), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_scheduled_tasks_site ON scheduled_tasks(site_id); CREATE INDEX idx_scheduled_tasks_next_run ON scheduled_tasks(next_run_at) WHERE enabled = TRUE; ``` ### Pydantic 模型(后端 schemas) ```python # schemas/tasks.py class TaskConfigSchema(BaseModel): """任务配置 — 前后端传输格式""" tasks: list[str] pipeline: str = "api_ods_dwd" # 执行流程 Flow ID(7 种之一),对应 CLI --pipeline 参数 processing_mode: str = "increment_only" # 处理模式(3 种之一) pipeline_flow: str = "FULL" # 传统模式兼容(已弃用,保留向后兼容) dry_run: bool = False window_mode: str = "lookback" # lookback / custom window_start: str | None = None window_end: str | None = None window_split: str | None = None # none / day window_split_days: int | None = None # 1 / 10 / 30 lookback_hours: int = 24 overlap_seconds: int = 600 fetch_before_verify: bool = False skip_ods_when_fetch_before_verify: bool = False ods_use_local_json: bool = False store_id: int | None = None # 门店 ID(由后端从 JWT 注入,前端不传) dwd_only_tables: list[str] | None = None # DWD 表级选择 extra_args: dict[str, Any] = {} @model_validator(mode="after") def validate_window(self) -> "TaskConfigSchema": """验证时间窗口:结束日期不早于开始日期""" if self.window_start and self.window_end: if self.window_end < self.window_start: raise ValueError("window_end 不能早于 window_start") return self class PipelineDefinition(BaseModel): """执行流程(Flow)定义 — 注意:字段名保留 pipeline 以兼容 CLI 参数""" id: str name: str layers: list[str] class ProcessingModeDefinition(BaseModel): """处理模式定义""" id: str name: str description: str # schemas/schedules.py class ScheduleConfigSchema(BaseModel): """调度配置""" schedule_type: Literal["once", "interval", "daily", "weekly", "cron"] interval_value: int = 1 interval_unit: Literal["minutes", "hours", "days"] = "hours" daily_time: str = "04:00" weekly_days: list[int] = [1] weekly_time: str = "04:00" cron_expression: str = "0 4 * * *" enabled: bool = True start_date: str | None = None end_date: str | None = None ``` ### TypeScript 类型(前端) ```typescript // types/index.ts interface TaskConfig { tasks: string[]; pipeline: string; // 执行流程 Flow ID(对应 CLI --pipeline) processing_mode: string; // 处理模式 pipeline_flow: string; // 传统模式兼容(已弃用) dry_run: boolean; window_mode: string; // lookback / custom window_start: string | null; window_end: string | null; window_split: string | null; // none / day window_split_days: number | null; // 1 / 10 / 30 lookback_hours: number; overlap_seconds: number; fetch_before_verify: boolean; skip_ods_when_fetch_before_verify: boolean; ods_use_local_json: boolean; store_id: number | null; // 由后端注入 dwd_only_tables: string[] | null; // DWD 表级选择 extra_args: Record; } interface PipelineDefinition { id: string; // Flow ID(字段名保留 pipeline 兼容 CLI) name: string; layers: string[]; // 包含的层:ODS / DWD / DWS / INDEX } interface ProcessingModeDefinition { id: string; name: string; description: string; } interface TaskDefinition { code: string; name: string; description: string; domain: string; requires_window: boolean; is_ods: boolean; is_dimension: boolean; default_enabled: boolean; } interface ScheduleConfig { schedule_type: "once" | "interval" | "daily" | "weekly" | "cron"; interval_value: number; interval_unit: "minutes" | "hours" | "days"; daily_time: string; weekly_days: number[]; weekly_time: string; cron_expression: string; enabled: boolean; start_date: string | null; end_date: string | null; } interface QueuedTask { id: string; site_id: number; config: TaskConfig; status: "pending" | "running" | "success" | "failed" | "cancelled"; position: number; created_at: string; started_at: string | null; finished_at: string | null; exit_code: number | null; error_message: string | null; } interface ExecutionLog { id: string; site_id: number; task_codes: string[]; status: string; started_at: string; finished_at: string | null; exit_code: number | null; duration_ms: number | null; command: string; summary: Record | null; } ``` ## 正确性属性 *属性(Property)是指在系统所有有效执行中都应成立的特征或行为——本质上是对系统行为的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。* ### Property 1: TaskConfig 序列化往返一致性 *For any* 有效的 TaskConfigSchema 对象,将其序列化为 JSON 字符串后再反序列化,应产生与原始对象等价的结果。 **Validates: Requirements 11.1, 11.2, 11.3** ### Property 2: 无效凭据始终被拒绝 *For any* 不存在于 admin_users 表中的用户名/密码组合,登录接口应返回 401 状态码。 **Validates: Requirements 1.2** ### Property 3: 有效 JWT 令牌授权访问 *For any* 由系统签发且未过期的 JWT 令牌,携带该令牌访问受保护端点应返回非 401 状态码。 **Validates: Requirements 1.3** ### Property 4: 任务注册表按业务域正确分组 *For any* Task_Registry 中的任务集合,API 返回的分组结果中,每个任务应出现在且仅出现在其所属业务域的分组中。 **Validates: Requirements 2.1** ### Property 5: Flow 层级过滤正确性 *For any* Flow 选择和任务列表,过滤后的结果应只包含与所选 Flow 包含的层兼容的任务,且不遗漏任何兼容任务。 **Validates: Requirements 2.2** ### Property 6: 时间窗口验证 *For any* 两个日期字符串,当 window_end 早于 window_start 时,TaskConfigSchema 验证应失败;当 window_end 不早于 window_start 时,验证应通过。 **Validates: Requirements 2.3** ### Property 7: TaskConfig 到 CLI 命令转换完整性 *For any* 有效的 TaskConfigSchema,CLIBuilder 生成的命令参数列表应包含 TaskConfig 中所有非空字段对应的 CLI 参数。 **Validates: Requirements 2.5, 2.6** ### Property 8: 队列 CRUD 不变量 *For any* 任务队列状态,入队一个任务后队列长度增加 1 且新任务状态为 pending;删除一个 pending 任务后队列长度减少 1 且该任务不再出现在队列中。 **Validates: Requirements 4.1, 4.4** ### Property 9: 队列出队顺序 *For any* 包含多个 pending 任务的队列,dequeue 操作应返回 position 值最小的任务。 **Validates: Requirements 4.2** ### Property 10: 队列重排一致性 *For any* 队列和重排操作(将任务移动到新位置),重排后队列中任务的相对顺序应与请求一致。 **Validates: Requirements 4.3** ### Property 11: 执行历史排序与限制 *For any* 执行历史记录集合,API 返回的结果应按 started_at 降序排列,且结果数量不超过请求的 limit 值。 **Validates: Requirements 4.5, 8.2** ### Property 12: 调度任务 CRUD 往返 *For any* 有效的 ScheduleConfigSchema,创建调度任务后再查询该任务,返回的调度配置应与创建时提交的配置等价。 **Validates: Requirements 5.1, 5.4** ### Property 13: 到期调度任务自动入队 *For any* enabled 为 true 且 next_run_at 早于当前时间的调度任务,check_and_enqueue 执行后该任务的 TaskConfig 应出现在执行队列中。 **Validates: Requirements 5.2** ### Property 14: 调度任务启用/禁用状态 *For any* 调度任务,禁用后 next_run_at 应为 NULL;重新启用后 next_run_at 应被重新计算为非 NULL 值(对于非一次性调度)。 **Validates: Requirements 5.3** ### Property 15: .env 解析与敏感值掩码 *For any* 包含敏感键(PASSWORD、TOKEN、SECRET、DSN)的 .env 文件内容,API 返回的键值对列表中这些键的值应被掩码替换,不包含原始敏感值。 **Validates: Requirements 6.1, 6.3** ### Property 16: .env 写入往返一致性 *For any* 有效的键值对集合(不含注释和空行),写入 .env 文件后再读取解析,应得到与原始集合等价的键值对。 **Validates: Requirements 6.2** ### Property 17: SQL 写操作拦截 *For any* 包含 INSERT、UPDATE、DELETE、DROP 或 TRUNCATE 关键词(不区分大小写)的 SQL 语句,数据库查看器 API 应拒绝执行并返回错误。 **Validates: Requirements 7.5** ### Property 18: SQL 查询结果行数限制 *For any* SQL 查询执行结果,返回的行数应不超过 1000。 **Validates: Requirements 7.4** ### Property 19: 日志过滤正确性 *For any* 日志行集合和过滤关键词,过滤后的结果应只包含含有该关键词的日志行,且不遗漏任何匹配行。 **Validates: Requirements 9.2** ### Property 20: 门店隔离 — 队列和调度数据不跨站泄露 *For any* 两个不同 site_id 的 Operator,一个 Operator 查询队列/调度/执行历史时,结果中不应包含另一个 site_id 的数据。 **Validates: Requirements 1.3(隐含的多门店隔离要求)** ### Property 21: Flow 层级与任务兼容性 *For any* Flow 类型和任务定义,当 Flow 包含的层不包含该任务所属层时,该任务不应出现在可选列表中;当 Flow 包含该任务所属层时,该任务应出现在可选列表中。 **Validates: Requirements 2.2** ## 错误处理 ### 认证错误 - 无效凭据:返回 `401 Unauthorized`,响应体包含 `{"detail": "用户名或密码错误"}` - 令牌过期:返回 `401 Unauthorized`,响应体包含 `{"detail": "令牌已过期"}` - 令牌无效:返回 `401 Unauthorized`,响应体包含 `{"detail": "无效的令牌"}` ### 任务执行错误 - TaskConfig 验证失败:返回 `422 Unprocessable Entity`,响应体包含字段级错误详情 - ETL_CLI 子进程超时:记录错误日志,任务状态标记为 `failed`,error_message 记录超时信息 - ETL_CLI 子进程异常退出:记录 exit_code 和 stderr 输出,任务状态标记为 `failed` - 取消不存在或已完成的任务:返回 `404 Not Found` 或 `409 Conflict` ### 队列错误 - 删除非 pending 状态的任务:返回 `409 Conflict`,提示只能删除待执行任务 - 重排包含非 pending 任务:忽略非 pending 任务,只重排 pending 任务 ### 数据库查看器错误 - SQL 写操作拦截:返回 `400 Bad Request`,提示只允许只读查询 - SQL 查询超时(30 秒):终止查询,返回 `408 Request Timeout` - SQL 语法错误:返回 `400 Bad Request`,包含 PostgreSQL 原始错误信息 ### 环境配置错误 - .env 文件不存在:返回 `404 Not Found` - 配置格式错误:返回 `422 Unprocessable Entity`,包含错误行号和描述 - 文件写入权限不足:返回 `500 Internal Server Error`,记录详细错误日志 ### 通用错误 - 所有 API 错误响应统一格式:`{"detail": "错误描述", "code": "ERROR_CODE"}` - 未捕获异常:返回 `500 Internal Server Error`,日志记录完整堆栈 ## 测试策略 ### 测试框架 - **后端单元测试 & 属性测试**:pytest + hypothesis - 路径:`apps/backend/tests/` - 运行:`cd apps/backend && pytest tests/ -v` - **前端单元测试**:Vitest + React Testing Library - 路径:`apps/admin-web/src/__tests__/` - 运行:`cd apps/admin-web && pnpm test` ### 属性测试(Property-Based Testing) 使用 hypothesis 库,每个属性测试至少运行 100 次迭代。 每个属性测试必须用注释标注对应的设计文档属性编号: ```python # Feature: admin-web-console, Property 1: TaskConfig 序列化往返一致性 @given(config=st.builds(TaskConfigSchema, ...)) def test_task_config_round_trip(config): ... ``` 属性测试重点覆盖: - Property 1:TaskConfig 序列化往返(核心数据模型) - Property 6:时间窗口验证(输入验证) - Property 7:TaskConfig 到 CLI 命令转换(关键业务逻辑) - Property 8-10:队列 CRUD 不变量(状态管理) - Property 15-16:.env 解析与写入往返(配置管理) - Property 17:SQL 写操作拦截(安全关键) - Property 19:日志过滤(数据过滤逻辑) ### 单元测试 单元测试覆盖具体示例和边界条件: - JWT 令牌生成/验证/过期 - 调度器 next_run 计算(各种调度类型) - CLI 命令构建的具体场景 - API 端点的请求/响应格式 - 前端组件渲染和交互 ### 集成测试 需要数据库环境的测试: - 任务队列的完整生命周期 - 调度任务的创建/触发/执行 - 数据库查看器的 Schema 浏览和查询执行 - ETL 状态查询