Files
Neo-ZQYY/.kiro/specs/admin-web-console/design.md

28 KiB
Raw Blame History

设计文档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 迁移至 PostgreSQLzqyy_app 库)
  3. 前后端通过 JSON API 通信,实时日志通过 WebSocket 推送
  4. 数据库查询限制为只读,防止误操作
  5. 多门店隔离:通过 site_id 贯穿全链路Operator 登录后绑定门店,所有 API 请求自动携带 site_id
  6. 执行流程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

请求流程

  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_idCLI 参数名为 --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_queuescheduled_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任务执行器

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 秒检查一次)"""
        ...

CLIBuilderCLI 命令构建器)

从现有 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 ID7 种之一),对应 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 有效的 TaskConfigSchemaCLIBuilder 生成的命令参数列表应包含 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 子进程超时:记录错误日志,任务状态标记为 failederror_message 记录超时信息
  • ETL_CLI 子进程异常退出:记录 exit_code 和 stderr 输出,任务状态标记为 failed
  • 取消不存在或已完成的任务:返回 404 Not Found409 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 1TaskConfig 序列化往返(核心数据模型)
  • Property 6时间窗口验证输入验证
  • Property 7TaskConfig 到 CLI 命令转换(关键业务逻辑)
  • Property 8-10队列 CRUD 不变量(状态管理)
  • Property 15-16.env 解析与写入往返(配置管理)
  • Property 17SQL 写操作拦截(安全关键)
  • Property 19日志过滤数据过滤逻辑

单元测试

单元测试覆盖具体示例和边界条件:

  • JWT 令牌生成/验证/过期
  • 调度器 next_run 计算(各种调度类型)
  • CLI 命令构建的具体场景
  • API 端点的请求/响应格式
  • 前端组件渲染和交互

集成测试

需要数据库环境的测试:

  • 任务队列的完整生命周期
  • 调度任务的创建/触发/执行
  • 数据库查看器的 Schema 浏览和查询执行
  • ETL 状态查询