28 KiB
设计文档:Web 管理后台(admin-web-console)
概述
将现有 PySide6 桌面 GUI 替换为 BS 架构的 Web 管理后台。系统分为两部分:
- 后端:在现有
apps/backend/FastAPI 骨架上扩展,新增 ETL 管理相关的 RESTful API 和 WebSocket 端点 - 前端:在
apps/admin-web/下使用 React + Vite + Ant Design 构建 SPA 应用
核心设计原则:
- 后端通过子进程调用现有 ETL_CLI,不重写 ETL 逻辑
- 调度任务从本地 JSON 迁移至 PostgreSQL(
zqyy_app库) - 前后端通过 JSON API 通信,实时日志通过 WebSocket 推送
- 数据库查询限制为只读,防止误操作
- 多门店隔离:通过
site_id贯穿全链路,Operator 登录后绑定门店,所有 API 请求自动携带 site_id - 执行流程(Flow)分离:完整保留现有 7 种 Flow 和 3 种处理模式,前端按 Flow 动态展示可选层和任务
架构
graph TB
subgraph "前端 (apps/admin-web/)"
FE[React SPA<br/>Vite + Ant Design]
end
subgraph "后端 (apps/backend/)"
API[FastAPI 应用]
AUTH[JWT 认证中间件]
WS[WebSocket 端点<br/>实时日志推送]
EXEC[TaskExecutor<br/>子进程管理]
QUEUE[TaskQueue<br/>队列管理]
SCHED[Scheduler<br/>定时调度]
end
subgraph "数据层"
PG_APP[(zqyy_app<br/>用户/队列/调度/历史)]
PG_ETL[(etl_feiqiu<br/>ODS/DWD/DWS)]
ENV[.env 文件]
end
subgraph "ETL Connector"
CLI[ETL CLI<br/>子进程]
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
请求流程
- 前端发起 HTTP 请求 → JWT 中间件验证令牌(提取 site_id)→ 路由处理
- 任务执行:API 接收 TaskConfig(含 site_id)→ TaskQueue 入队 → TaskExecutor 取出执行 → 子进程调用 ETL_CLI(
--store-id {site_id})→ stdout/stderr 通过 WebSocket 推送 - 调度触发:Scheduler 定时检查到期任务 → 自动入队 → 同上执行流程
多门店隔离设计
现有系统通过 site_id(即 store_id,CLI 参数名为 --store-id)实现多门店数据隔离:
- ETL 数据库层:所有业务表包含
site_id字段,appschema 通过 RLS 按current_setting('app.current_site_id')自动过滤 - ETL CLI 层:通过
--store-id参数指定门店
Web 管理后台的隔离策略:
- 用户-门店绑定:
admin_users表增加site_id字段,每个 Operator 绑定一个门店 - JWT 令牌携带 site_id:登录时将
site_id写入 JWT payload,后续请求自动提取 - API 层自动注入:所有涉及 ETL 操作的 API,从 JWT 中提取
site_id,自动注入到 TaskConfig 和数据库查询中 - 数据库查看器隔离:查询 ETL 数据库时,设置
SET LOCAL app.current_site_id = '{site_id}',利用 RLS 自动过滤 - 队列和调度隔离:
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天)
前端交互逻辑:
- Operator 选择 Flow → 前端根据 Flow 包含的层,动态显示/隐藏 ODS 任务选择、DWD 表选择、DWS 任务选择
- Operator 选择处理模式 → 前端根据模式显示/隐藏校验相关选项
- 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(任务执行器)
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(任务队列)
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(调度器)
class Scheduler:
"""基于 PostgreSQL 的定时调度器"""
async def check_and_enqueue(self) -> None:
"""检查到期的调度任务,将其 TaskConfig 加入队列"""
...
async def start(self) -> None:
"""启动后台定时检查循环(每 30 秒检查一次)"""
...
CLIBuilder(CLI 命令构建器)
从现有 gui/utils/cli_builder.py 迁移,核心逻辑不变:
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 表
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 表
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 表
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 表
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)
# 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 类型(前端)
// 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<string, unknown>;
}
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<string, unknown> | 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 次迭代。
每个属性测试必须用注释标注对应的设计文档属性编号:
# 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 状态查询