# 技术设计文档：admin-web 后台重构优化

## 概述

本次重构将 admin-web 从 18 个页面 / 11 个一级菜单重组为 7 个一级菜单模块，核心目标：

1. **菜单精简**：11 → 7 个一级菜单，按功能域聚合（ETL、触发器、AI 等）
2. **新增仪表盘**：登录后默认展示运行状态 Dashboard，聚合 OpsPanel + DB 健康 + AI 概览
3. **触发器统一管理**：聚合三套触发器数据源（biz.trigger_jobs / biz.ai_trigger_jobs / scheduled_tasks）
4. **新增后端 API**：PATCH /api/trigger-jobs/{id}/config（编辑触发器配置）、GET /api/admin/db-health（数据库健康）、GET /api/admin/triggers/unified（触发器统一列表）
5. **渐进式迁移**：新页面用目标路由开发，老页面测试通过后移入 `_archived/`

### 设计决策

- **Tab 视图而非子路由**：ETL 任务管理和触发器管理使用 Ant Design Tabs 组件 + URL 查询参数（`?tab=xxx`），而非嵌套路由。理由：Tab 切换不触发组件卸载，状态保持更自然。
- **复用而非重写**：Dashboard 通过 import 现有 OpsPanel / AIDashboard 组件的子模块实现聚合，不重写已有逻辑。
- **后端最小改动**：仅新增 3 个 API 端点，不修改现有业务逻辑。

## 架构

### 整体架构

```mermaid
graph TB
    subgraph "admin-web 前端"
        Router["React Router 7<br/>路由层"]
        Layout["AppLayout<br/>Sider + Content"]
        
        subgraph "7 个一级模块"
            Dashboard["运行状态<br/>/dashboard"]
            ETL["ETL 任务管理<br/>/etl-tasks"]
            MiniApp["小程序任务管理<br/>/task-engine/*"]
            Triggers["触发器管理<br/>/triggers"]
            Tenant["租户管理员<br/>/tenant-admins"]
            Settings["系统设置<br/>/settings"]
            Logs["日志调试<br/>/logs/*"]
        end
    end
    
    subgraph "后端 API（FastAPI）"
        ExistingAPI["现有 API 端点"]
        NewPATCH["PATCH /api/trigger-jobs/{id}/config"]
        NewDBHealth["GET /api/admin/db-health"]
        NewUnified["GET /api/admin/triggers/unified"]
    end
    
    subgraph "数据库"
        TriggerJobs["biz.trigger_jobs"]
        AITriggerJobs["biz.ai_trigger_jobs"]
        ScheduledTasks["scheduled_tasks"]
        FourDBs["4 个数据库健康检测"]
    end
    
    Router --> Layout
    Layout --> Dashboard & ETL & MiniApp & Triggers & Tenant & Settings & Logs
    Dashboard --> ExistingAPI & NewDBHealth
    Triggers --> ExistingAPI & NewPATCH & NewUnified
    NewUnified --> TriggerJobs & AITriggerJobs & ScheduledTasks
    NewDBHealth --> FourDBs
```

### 路由结构

```
/login                          → Login（不变）
/dashboard                      → Dashboard（新，默认首页）
/                               → Redirect → /dashboard
/etl-tasks?tab=config|manager|status  → ETLTasks（新，合并 3 页面）
/task-engine/trigger-jobs       → TriggerJobs（移动路由）
/task-engine/transfer-log       → TransferLog（不变）
/task-engine/pending-review     → PendingReview（不变）
/task-engine/config             → TaskEngineConfig（不变）
/triggers?tab=all|biz|ai|etl    → TriggerManager（新）
/tenant-admins                  → TenantAdmins（不变）
/settings/env-config            → EnvConfig（移动路由）
/logs/dev-trace                 → DevTrace（移动路由）
/logs/ai-run-logs               → AIRunLogs（移动路由）
/logs/db-viewer                 → DBViewer（移动路由）
/log-viewer                     → Redirect → /etl-tasks?tab=manager
```

### 菜单结构映射

| 序号 | 一级菜单 | 子项/Tab | 路由 |
|------|----------|----------|------|
| 1 | 运行状态 | — | /dashboard |
| 2 | ETL 任务管理 | 任务配置 / 任务管理 / ETL 状态（Tab） | /etl-tasks?tab=config\|manager\|status |
| 3 | 小程序任务管理 | 定时任务 / 转移日志 / 待审核任务 / 参数管理 | /task-engine/* |
| 4 | 触发器管理 | 全部 / 业务 / AI / ETL（Tab） | /triggers?tab=all\|biz\|ai\|etl |
| 5 | 租户管理员 | — | /tenant-admins |
| 6 | 系统设置 | 环境配置 / 触发器配置（跳转） | /settings/* |
| 7 | 日志调试 | DevTrace / AI 调用明细 / 数据库查看器 | /logs/* |


## 组件与接口

### 新增前端页面组件

#### 1. Dashboard（运行状态仪表盘）

```
src/pages/Dashboard.tsx
```

聚合展示系统运行状况，由 4 个区块组成：

- **OpsPanel 区块**：复用现有 OpsPanel 组件中的服务状态、Git 状态、系统资源子模块（需将 OpsPanel 拆分为可独立使用的子组件）
- **DB 健康监控区块**：新组件 `DbHealthCard`，调用 `GET /api/admin/db-health`，展示 4 个数据库的连接池 / 大小 / 慢查询
- **AI 运行总览区块**：复用现有 AIDashboard 组件的统计卡片、趋势图、预算进度
- **AI 调度摘要区块**：展示今日触发数、成功率、最近错误，提供"查看详情"跳转到 `/triggers?tab=ai`

跳转链接：
- "ETL 状态详情" → `/etl-tasks?tab=status`
- "触发器详情" → `/triggers?tab=all`
- "AI 调度详情" → `/triggers?tab=ai`

#### 2. ETLTasks（ETL 任务管理）

```
src/pages/ETLTasks.tsx
```

使用 Ant Design `Tabs` 组件合并 3 个现有页面：

- Tab "config"：渲染 `<TaskConfig />`（现有组件）
- Tab "manager"：渲染 `<TaskManager />`（现有组件）
- Tab "status"：渲染 `<ETLStatus />`（现有组件）

Tab 切换通过 URL 查询参数 `?tab=config|manager|status` 同步，支持浏览器前进/后退。使用 `useSearchParams` 读写当前 Tab。各 Tab 内容使用 `destroyInactiveTabPane={false}` 保持状态不丢失。

#### 3. TriggerManager（触发器统一管理）

```
src/pages/TriggerManager.tsx
```

4 个 Tab：

- **全部 Tab**（只读）：调用 `GET /api/admin/triggers/unified`，展示统一字段表格（名称、类型标签、触发条件、状态、上次/下次执行、最近错误）
- **业务 Tab**：复用现有 TriggerJobs 组件 + 新增编辑功能（调用 `PATCH /api/trigger-jobs/{id}/config`）
- **AI Tab**：复用现有 AIOperations 组件的手动操作功能 + AITriggerJobs 的调度状态
- **ETL Tab**：展示 scheduled_tasks 数据（调用现有调度 API）

#### 4. DbHealthCard（数据库健康卡片）

```
src/components/DbHealthCard.tsx
```

独立组件，接收 `GET /api/admin/db-health` 返回数据，为每个数据库渲染一张卡片：
- 连接池状态（活跃/空闲连接数，进度条）
- 数据库大小（MB）
- 慢查询数量（最近 1 小时）
- 连接失败时显示"未连接"状态标签

### 新增后端 API

#### 1. PATCH /api/trigger-jobs/{id}/config

```python
# apps/backend/app/routers/trigger_jobs.py 新增端点

@router.patch("/{job_id}/config", response_model=TriggerJobItem)
async def update_trigger_config(
    job_id: int,
    body: UpdateTriggerConfigRequest,
    user: CurrentUser = Depends(get_current_user),
) -> TriggerJobItem:
    """编辑触发器的 cron_expression 或 interval_seconds。"""
```

请求体 Schema：
```python
class UpdateTriggerConfigRequest(BaseModel):
    cron_expression: str | None = None
    interval_seconds: int | None = None
```

逻辑：
1. 查询 trigger_job 是否存在
2. 校验 cron_expression 格式（5 字段 cron 语法）
3. 校验 interval_seconds >= 1
4. 更新 trigger_config JSON 中对应字段
5. 调用 `_calculate_next_run()` 重新计算 next_run_at
6. 返回更新后的完整 trigger_job

#### 2. GET /api/admin/db-health

```python
# apps/backend/app/routers/admin_db_health.py 新增路由模块

@router.get("", response_model=list[DbHealthItem])
async def get_db_health(
    user: CurrentUser = Depends(get_current_user),
) -> list[DbHealthItem]:
    """返回 4 个数据库的健康状态。"""
```

响应 Schema：
```python
class DbHealthItem(BaseModel):
    db_name: str                          # etl_feiqiu / test_etl_feiqiu / zqyy_app / test_zqyy_app
    status: str                           # connected / disconnected
    active_connections: int | None = None
    idle_connections: int | None = None
    db_size_mb: float | None = None
    slow_query_count: int | None = None   # 最近 1 小时内 > 1s 的查询数
```

逻辑：
1. 遍历 4 个数据库 DSN 配置
2. 对每个库尝试连接并执行诊断 SQL：
   - `pg_stat_activity` 查连接池状态
   - `pg_database_size()` 查数据库大小
   - `pg_stat_statements` 或 `pg_stat_activity` 查慢查询
3. 连接失败时返回 `status: "disconnected"`，其余字段为 null

#### 3. GET /api/admin/triggers/unified

```python
# apps/backend/app/routers/admin_triggers.py 新增路由模块

@router.get("", response_model=list[UnifiedTriggerItem])
async def get_unified_triggers(
    user: CurrentUser = Depends(get_current_user),
) -> list[UnifiedTriggerItem]:
    """聚合三张表的触发器数据。"""
```

响应 Schema：
```python
class UnifiedTriggerItem(BaseModel):
    id: int
    name: str
    source: str                           # biz / ai / etl
    trigger_condition: str                # cron / interval / event
    status: str                           # running / idle / error / disabled
    last_run_at: str | None = None
    next_run_at: str | None = None
    last_error: str | None = None
```

逻辑：
1. 查询 `biz.trigger_jobs`，映射 source="biz"
2. 查询 `biz.ai_trigger_jobs`（最近 100 条），映射 source="ai"
3. 查询 `scheduled_tasks`，映射 source="etl"
4. 统一字段格式后合并返回

### 新增前端 API 模块

```typescript
// src/api/dbHealth.ts
export interface DbHealthItem {
  db_name: string;
  status: 'connected' | 'disconnected';
  active_connections: number | null;
  idle_connections: number | null;
  db_size_mb: number | null;
  slow_query_count: number | null;
}

export async function fetchDbHealth(): Promise<DbHealthItem[]> {
  const { data } = await apiClient.get<DbHealthItem[]>('/admin/db-health');
  return data;
}

// src/api/triggers.ts
export interface UnifiedTriggerItem {
  id: number;
  name: string;
  source: 'biz' | 'ai' | 'etl';
  trigger_condition: string;
  status: string;
  last_run_at: string | null;
  next_run_at: string | null;
  last_error: string | null;
}

export async function fetchUnifiedTriggers(): Promise<UnifiedTriggerItem[]> {
  const { data } = await apiClient.get<UnifiedTriggerItem[]>('/admin/triggers/unified');
  return data;
}

// src/api/triggerJobs.ts 新增
export interface UpdateTriggerConfigReq {
  cron_expression?: string;
  interval_seconds?: number;
}

export async function updateTriggerConfig(
  jobId: number, body: UpdateTriggerConfigReq
): Promise<TriggerJob> {
  const { data } = await apiClient.patch<TriggerJob>(`/trigger-jobs/${jobId}/config`, body);
  return data;
}
```

### 修改的现有组件

#### App.tsx 路由重构

- NAV_ITEMS 从 11 个一级项重组为 7 个
- Routes 更新为新路由结构
- 添加 `/` → `/dashboard` 重定向
- 添加 `/log-viewer` → `/etl-tasks?tab=manager` 重定向
- 登录成功后导航到 `/dashboard`

#### OpsPanel 组件拆分

将 OpsPanel.tsx 拆分为可独立使用的子组件：
- `ServiceStatusSection`：服务状态卡片
- `GitStatusSection`：Git 状态卡片
- `SystemResourceSection`：系统资源卡片

Dashboard 页面 import 这些子组件进行聚合展示。


## 数据模型

### 新增后端 Pydantic 模型

#### UpdateTriggerConfigRequest

```python
class UpdateTriggerConfigRequest(BaseModel):
    """触发器配置编辑请求（部分更新）"""
    cron_expression: str | None = None      # 5 字段 cron 表达式
    interval_seconds: int | None = None     # 间隔秒数，>= 1

    @model_validator(mode='after')
    def at_least_one_field(self) -> Self:
        if self.cron_expression is None and self.interval_seconds is None:
            raise ValueError('至少提供 cron_expression 或 interval_seconds 之一')
        return self
```

#### DbHealthItem

```python
class DbHealthItem(BaseModel):
    """单个数据库健康状态"""
    db_name: str
    status: Literal['connected', 'disconnected']
    active_connections: int | None = None
    idle_connections: int | None = None
    db_size_mb: float | None = None
    slow_query_count: int | None = None
```

#### UnifiedTriggerItem

```python
class UnifiedTriggerItem(BaseModel):
    """统一触发器视图项"""
    id: int
    name: str
    source: Literal['biz', 'ai', 'etl']
    trigger_condition: str
    status: str
    last_run_at: str | None = None
    next_run_at: str | None = None
    last_error: str | None = None
```

### 新增前端 TypeScript 类型

```typescript
// 数据库健康
interface DbHealthItem {
  db_name: string;
  status: 'connected' | 'disconnected';
  active_connections: number | null;
  idle_connections: number | null;
  db_size_mb: number | null;
  slow_query_count: number | null;
}

// 统一触发器
interface UnifiedTriggerItem {
  id: number;
  name: string;
  source: 'biz' | 'ai' | 'etl';
  trigger_condition: string;
  status: string;
  last_run_at: string | null;
  next_run_at: string | null;
  last_error: string | null;
}

// 触发器配置编辑
interface UpdateTriggerConfigReq {
  cron_expression?: string;
  interval_seconds?: number;
}
```

### 数据库变更

本次重构不新增数据库表。所有数据来自现有表：

| 表 | 库 | 用途 |
|----|-----|------|
| biz.trigger_jobs | zqyy_app | 4 个业务触发器 |
| biz.ai_trigger_jobs | zqyy_app | AI 事件链触发器 |
| scheduled_tasks | zqyy_app | ETL 调度任务 |
| pg_stat_activity | 各库 | 连接池状态 |
| pg_database | 各库 | 数据库大小 |

### Cron 表达式校验规则

PATCH API 中的 cron_expression 校验逻辑：

```python
import re

CRON_FIELD_PATTERNS = [
    r'(\*|[0-5]?\d)',                    # minute: 0-59
    r'(\*|[01]?\d|2[0-3])',              # hour: 0-23
    r'(\*|[1-9]|[12]\d|3[01])',          # day of month: 1-31
    r'(\*|[1-9]|1[0-2])',               # month: 1-12
    r'(\*|[0-6])',                        # day of week: 0-6
]

def validate_cron_expression(expr: str) -> bool:
    """校验 5 字段 cron 表达式基本格式。"""
    parts = expr.strip().split()
    if len(parts) != 5:
        return False
    for part, pattern in zip(parts, CRON_FIELD_PATTERNS):
        if not re.fullmatch(pattern, part):
            return False
    return True
```


## 正确性属性

*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*

### 属性 1：DB 健康 API 已连接数据库返回完整指标

*对于任意* 处于 `connected` 状态的数据库健康响应项，其 `active_connections`、`idle_connections`、`db_size_mb`、`slow_query_count` 字段均不为 null 且类型正确（连接数为非负整数，大小为非负浮点数）。

**验证需求：2.4, 6.2**

### 属性 2：Tab 切换状态保持 round-trip

*对于任意* ETL 任务管理或触发器管理的 Tab 视图，在某个 Tab 中设置筛选条件后切换到另一个 Tab 再切回来，原 Tab 的筛选条件应保持不变。

**验证需求：3.5**

### 属性 3：触发器统一视图数据完整性与字段完整性

*对于任意* 一组来自 biz.trigger_jobs、biz.ai_trigger_jobs、scheduled_tasks 三个数据源的触发器数据，统一视图 API 返回的记录总数应等于三个数据源记录数之和，且每条记录都包含 name、source、trigger_condition、status、last_run_at、next_run_at、last_error 字段。

**验证需求：4.1, 4.2**

### 属性 4：触发器配置编辑不变量

*对于任意* 通过 PATCH /api/trigger-jobs/{id}/config 的成功更新请求，trigger_job 中除 trigger_config 内的 cron_expression / interval_seconds 和 next_run_at 外的所有字段（id、job_type、job_name、trigger_condition、status、description、created_at）应保持不变。

**验证需求：4.7**

### 属性 5：触发器配置更新正确性

*对于任意* 有效的 cron_expression（符合 5 字段 cron 语法）或有效的 interval_seconds（>= 1），通过 PATCH API 更新后，返回的 trigger_job 中 trigger_config 对应字段值应等于请求值，且 next_run_at 应被重新计算为非 null 值。

**验证需求：5.2, 5.3, 5.7**

### 属性 6：触发器配置校验拒绝无效输入

*对于任意* 不符合 5 字段 cron 语法的字符串作为 cron_expression，或任意小于 1 的整数作为 interval_seconds，PATCH API 应返回 HTTP 422 状态码，且响应体包含描述性错误信息。

**验证需求：5.4, 5.5**

### 属性 7：侧边栏高亮与当前路由一致

*对于任意* 有效的应用路由路径，侧边栏中被高亮（selectedKeys）的菜单项应对应该路由所属的一级模块。

**验证需求：10.3**

### 属性 8：Tab 切换与 URL 查询参数同步 round-trip

*对于任意* Tab 视图页面（ETL 任务管理、触发器管理），设置 URL 查询参数 `?tab=X` 后渲染页面，当前激活的 Tab 应为 X；反之，点击 Tab Y 后，URL 查询参数应更新为 `?tab=Y`。

**验证需求：10.4**


## 错误处理

### 前端错误处理

| 场景 | 处理方式 |
|------|----------|
| API 请求失败（网络错误） | Ant Design `message.error()` 提示，保持当前页面状态 |
| API 返回 401 | 现有 axios 拦截器自动处理：尝试 refresh token → 失败则跳转 /login |
| API 返回 422（校验错误） | 在编辑表单中展示具体错误信息（如"cron 表达式格式无效"） |
| API 返回 500 | `message.error()` 提示通用错误信息 |
| 组件渲染异常 | 现有 ErrorBoundary 组件捕获，展示降级 UI |
| DB 健康 API 超时 | DbHealthCard 展示"加载超时"状态，提供重试按钮 |
| 触发器统一 API 部分数据源失败 | 后端返回可用数据源的数据，前端正常展示已获取的数据 |

### 后端错误处理

| 场景 | 处理方式 |
|------|----------|
| PATCH API：trigger_job 不存在 | 返回 HTTP 404 + `{"detail": "任务 {id} 不存在"}` |
| PATCH API：cron_expression 格式无效 | 返回 HTTP 422 + `{"detail": "cron 表达式格式无效，需要 5 字段格式"}` |
| PATCH API：interval_seconds < 1 | 返回 HTTP 422 + `{"detail": "interval_seconds 必须 >= 1"}` |
| PATCH API：请求体为空 | 返回 HTTP 422 + `{"detail": "至少提供 cron_expression 或 interval_seconds 之一"}` |
| DB 健康 API：某库连接失败 | 该库返回 `status: "disconnected"`，其余指标为 null，不影响其他库 |
| DB 健康 API：所有库连接失败 | 返回 4 个 disconnected 项，HTTP 200（不返回 500） |
| 统一触发器 API：某数据源查询失败 | 记录日志，返回其他数据源的数据，失败的数据源不包含在结果中 |
| JWT 认证失败 | 返回 HTTP 401（现有中间件处理） |

## 测试策略

### 双重测试方法

本项目采用单元测试 + 属性测试的双重策略：

- **单元测试（Vitest）**：验证具体示例、边界条件、错误场景
- **属性测试（fast-check）**：验证跨所有输入的通用属性
- 两者互补：单元测试捕获具体 bug，属性测试验证通用正确性

### 前端测试

**工具**：Vitest + @testing-library/react + fast-check

**属性测试配置**：
- 每个属性测试最少运行 100 次迭代
- 每个属性测试必须用注释引用设计文档中的属性编号
- 标签格式：`Feature: admin-web-restructure, Property {number}: {property_text}`

**单元测试覆盖**：
- 菜单结构验证（7 个一级菜单项、子项正确性）
- 路由重定向（/ → /dashboard、/log-viewer → /etl-tasks）
- Dashboard 区块渲染
- Tab 组件默认 Tab 选择
- DbHealthCard 各状态渲染（connected / disconnected）

**属性测试覆盖**：
- 属性 2：Tab 状态保持 round-trip
- 属性 7：侧边栏高亮与路由一致
- 属性 8：Tab-URL 参数同步 round-trip

### 后端测试

**工具**：pytest + hypothesis

**属性测试配置**：
- 每个属性测试使用 `@settings(max_examples=100)`
- 标签格式同上

**单元测试覆盖**：
- PATCH API 端点存在性
- JWT 认证拦截
- DB 健康 API 端点存在性
- 统一触发器 API 端点存在性

**属性测试覆盖**：
- 属性 1：DB 健康 API 已连接数据库返回完整指标
- 属性 3：触发器统一视图数据完整性
- 属性 4：触发器配置编辑不变量
- 属性 5：触发器配置更新正确性
- 属性 6：触发器配置校验拒绝无效输入

### E2E 测试

**工具**：Playwright

**覆盖范围**（每个新页面一个测试文件）：
- `dashboard.spec.ts`：Dashboard 页面渲染、4 个区块存在、跳转链接正确
- `etl-tasks.spec.ts`：ETL 任务管理 Tab 切换、各 Tab 内容渲染
- `trigger-manager.spec.ts`：触发器管理 4 个 Tab、统一视图数据、业务 Tab 编辑功能
- `navigation.spec.ts`：默认路由、重定向、菜单高亮、Tab-URL 同步

**测试批次**：每个测试文件控制在 Playwright 默认超时（30s）范围内。

