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

750 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计文档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<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_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 秒检查一次)"""
...
```
#### CLIBuilderCLI 命令构建器)
从现有 `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 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 类型(前端)
```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 子进程超时:记录错误日志,任务状态标记为 `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 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 状态查询