750 lines
28 KiB
Markdown
750 lines
28 KiB
Markdown
# 设计文档: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 秒检查一次)"""
|
||
...
|
||
```
|
||
|
||
#### 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<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 次迭代。
|
||
|
||
每个属性测试必须用注释标注对应的设计文档属性编号:
|
||
|
||
```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 状态查询
|