# 执行计划 - UI 自动化定时任务模块 + 系统配置-被测系统 URL

> **文档类型**: 计划执行文档
> **创建日期**: 2026-08-19
> **作者**: czj
> **状态**: 待实现
> **关联文档**: `_PRD_需求文档_UI自动化定时任务模块与系统配置被测系统URL.md`

---

## 一、执行概述

实现两大功能：

1. **UI 自动化定时任务模块**：新增 `scheduled_tasks` 表 + 后台 asyncio 调度循环 + 定时任务管理 API + 前端列表/编辑页 + 菜单/路由；执行完成后自动生成报告到报告中心，报告按模块分组展示。
2. **系统配置-被测系统 URL**：新增独立「被测系统」配置卡片 + `sut-config` API，保存即时生效，**启动时从 DB 加载**（修复重启回退问题）。

**设计原则**：
- 复用现有 `ExecutionService.create_execution()`（已支持 `trigger_type=scheduled` / `trigger_by`）与 `run_execution()`（串行锁），**不新增执行路径**；
- 调度器用 asyncio 后台任务（沿用 `message_consumer_loop` 模式），**不引入 APScheduler 依赖**（避免容器重装 + Windows 事件循环风险）；
- 被测系统 URL 复用 `dingtalk_configs.target_url`（单行表，字段已存在，**无数据库迁移**）。

---

## 二、任务分解与实施步骤

### Step 1：新增 `ScheduledTask` 模型

**文件**：`backend/app/models/scheduled_task.py`（新建）

```python
class ScheduledTask(Base):
    __tablename__ = "scheduled_tasks"
    id: str = String(64) PK          # scheduled_ 前缀
    name: str = String(100)          # 任务名（= trigger_by）
    enabled: bool = Boolean default True
    module_ids: list = JSON          # 选中模块 ID 列表
    schedule_type: str = String(20)  # interval / daily / weekly
    interval_value: int = Integer default 0
    interval_unit: str = String(10)  # minutes / hours / days
    run_time: str = String(5)        # HH:MM
    weekdays: list = JSON            # [0-6]
    auto_report: bool = Boolean default True
    last_run_at: datetime nullable
    next_run_at: datetime nullable
    run_count: int = Integer default 0
    created_at / updated_at
```

**同时**：`backend/app/database.py` 的 `_ensure_columns` 顶部依赖导入加入 `from app.models import scheduled_task  # noqa: F401`，确保 `init_db` 建表。

**验证**：本地起服务，确认表创建成功（SQLite），`SHOW CREATE` 无异常。

### Step 2：`SchedulerService` 调度引擎

**文件**：`backend/app/services/scheduler_service.py`（新建）

核心内容：
- `compute_next_run(task, now) -> datetime`：按 `schedule_type` 计算下次触发时间
  - `interval`：`now + interval_value × 单位`（分钟/小时/天）
  - `daily`：今天 `run_time`；若已过则明天
  - `weekly`：在选中 `weekdays` 中找下一个 ≥ now 的 `run_time`
- `async def run_task_once(task_id)`：单次触发流程（供后台循环与"立即执行"共用）
  1. 读任务 + 收集模块 active UI 用例（`module_id in module_ids AND status='active' AND case_type='ui'`，按 `module_id, order` 排序）
  2. 用例数=0 → 记日志，返回 `None`（跳过）
  3. `is_execution_running()` 为 True → 记日志「已有执行运行，跳过本次」，返回 `None`
  4. `create_execution(case_ids, name=f"{task.name}-{ts}", trigger_type="scheduled", trigger_by=task.name, case_type="ui")`
  5. `run_execution(exec_id)`
  6. 完成后：若 `auto_report` → `ReportService().generate_html()` + `save_report()`
  7. 更新 `last_run_at` / `run_count` / `next_run_at`，commit
- `async def scheduler_loop()`：`while True: 每 30s 轮询 enabled 且 (next_run_at<=now 或从未运行) 的任务 → asyncio.create_task(run_task_once(...))`

**注意**：触发用 `asyncio.create_task` 异步执行，调度循环不被单个长执行阻塞；`run_task_once` 内部已由 `run_execution` 的全局锁保证串行，且触发前先查 `is_execution_running()` 防重。

**验证**：单测/脚本创建一条 interval=1min 任务，观察调度触发产生 execution + 报告。

### Step 3：定时任务管理 API

**文件**：`backend/app/routers/scheduled_tasks.py`（新建）+ `backend/app/main.py`（注册，prefix `/api/scheduled-tasks`）

| 方法 | 路径 | 行为 |
|------|------|------|
| GET | `""` | 任务列表（排序 enabled desc, next_run_at asc） |
| POST | `""` | 校验 name/module_ids/周期 → 创建 → 计算 next_run_at |
| PUT | `/{id}` | 更新 → 重算 next_run_at |
| DELETE | `/{id}` | 删除任务定义 |
| POST | `/{id}/run` | 立即执行：`asyncio.create_task(run_task_once(id))`，返回「已触发」 |
| POST | `/{id}/toggle` | body `{enabled: bool}` → 更新 enabled + 重算 next_run_at |
| GET | `/{id}/runs` | 查 executions where trigger_type='scheduled' AND trigger_by=任务名，返回列表 |

**schemas**：`backend/app/schemas/scheduled_task.py`（新建）定义 Create/Update 请求模型。

### Step 4：执行列表/报告列表小改

- `backend/app/routers/executions.py`：`list_executions` 增加可选 `trigger_type` / `trigger_by` 查询参数（透传给 `ExecutionService.list_executions`，追加 where 条件）。
- `backend/app/routers/reports.py`：`/api/reports/list` 响应 items 补充 `trigger_type` / `trigger_by` 字段。
- `backend/app/services/execution_service.py`：`list_executions` 方法签名加 `trigger_type` / `trigger_by` 可选参数。

### Step 5：报告按模块分组

**文件**：`backend/app/services/report_service.py`

- `generate_html`：关联 `TestCase.module_id` → `Module.name`，将 `case_results` 按模块分组；
- 新模板 `MODULE_SECTION_TEMPLATE`：模块名 + 该模块 passed/failed 统计 + 组内用例结果（复用现有 `CASE_RESULT_TEMPLATE`）；
- 报告头部已有执行时间/耗时/触发方式/触发人，保持。
- 增加可选的 `SUT_URL` 展示（报告页脚或头部 meta 显示被测系统地址，取自 `settings.TARGET_URL`）。

### Step 6：被测系统配置（后端）

**文件**：`backend/app/routers/system.py`

- `GET /api/system/sut-config`：读 `dingtalk_configs` id=1 的 `target_url`（DB 优先，回退 `settings.TARGET_URL`）；
- `PUT /api/system/sut-config`：body `{url}` → 持久化 `target_url` + `settings.TARGET_URL = url`；
- `backend/app/main.py` lifespan：`init_db` 后加载 `dingtalk_configs.target_url` → `settings.TARGET_URL`（修复重启回退）。
- `backend/app/schemas/dingtalk_config.py`：新增 `SutConfigUpdateRequest {url}` 模型（或复用）。

### Step 7：前端 API 封装

- `frontend/src/api/scheduledTasks.ts`（新建）：list/create/update/delete/run/toggle/runs；
- `frontend/src/api/system.ts`：新增 `getSutConfig()` / `updateSutConfig(url)`；
- `frontend/src/api/reports.ts`：`list` 参数/类型无需改（返回 items 含新字段，TS any）。

### Step 8：定时任务前端页面

**文件**：`frontend/src/views/ScheduledTasks.vue`（新建）

- 列表表格（见 PRD 3.1.2）；
- 创建/编辑对话框（模块多选：调 `/api/modules?case_type=ui`；周期表单联动：interval/daily/weekly 显示对应控件；auto_report 开关）；
- 「立即执行」按钮 + 执行中 loading；
- 启用/停用 Switch → toggle API；
- 「运行记录」抽屉：调 `/api/scheduled-tasks/{id}/runs` 展示历史执行（时间/状态/通过率/耗时），行内"查看报告"链接到 `/reports/ui`；
- 删除确认。

### Step 9：菜单 / 路由 / 系统配置卡片

- `frontend/src/router/index.ts`：新增 `/scheduled-tasks` → ScheduledTasks.vue（title 定时任务）；
- `frontend/src/App.vue`：自动化测试父菜单中，「执行中心」与「报告中心」之间加 `<el-menu-item index="/scheduled-tasks">定时任务</el-menu-item>`；`currentRoute` 回退逻辑无需改（一级 path 直达）；
- `frontend/src/views/system/Settings.vue`：新增「被测系统」卡片（独立 form，URL 校验），调 sut-config API；「钉钉对接配置」卡片移除 target_url 字段。

### Step 10：验证

1. 后端：`cd backend && python -m pytest tests/ -x -q`（新增 scheduled_task 相关最小单测：compute_next_run 三种模式）；确认原有 171 passed 不回归；
2. 手动功能验证（本地起 8001 + 前端 3000）：
   - 建一条「每 1 分钟」任务选 1-2 个模块 → 等待调度触发 → 执行记录 trigger_type=scheduled、trigger_by=任务名 → 报告出现在报告中心且按模块分组；
   - 立即执行按钮 → 立即产生执行 + 报告；
   - 执行中到点 → 任务跳过（日志可见）→ 下次恢复；
   - 停用 → 不再触发；重启服务 → 任务自动恢复调度、被测系统 URL 保持保存值；
   - 被测系统 URL 改为无效地址 → 执行失败（验证 URL 真正被使用），改回后恢复。
3. 前端：`cd frontend && npm run build` 通过。
4. 部署 5.60：scp 后端文件 + 前端 dist 到容器，`docker restart plat-auto-test-app`，验证调度 + 报告 + URL 配置。

---

## 三、涉及文件清单

### 新建
| 文件 | 说明 |
|------|------|
| `backend/app/models/scheduled_task.py` | ScheduledTask 模型 |
| `backend/app/schemas/scheduled_task.py` | 定时任务请求/响应 schema |
| `backend/app/services/scheduler_service.py` | 调度引擎 + 触发流程 |
| `backend/app/routers/scheduled_tasks.py` | 定时任务 API |
| `frontend/src/api/scheduledTasks.ts` | 前端 API |
| `frontend/src/views/ScheduledTasks.vue` | 定时任务页面 |

### 修改
| 文件 | 改动 |
|------|------|
| `backend/app/database.py` | 注册 scheduled_task 模型导入 |
| `backend/app/main.py` | 注册路由 + lifespan 加载被测系统 URL + 启动调度循环 |
| `backend/app/routers/executions.py` | 列表增加 trigger_type/trigger_by 过滤 |
| `backend/app/services/execution_service.py` | list_executions 支持新过滤 |
| `backend/app/routers/reports.py` | 列表响应补 trigger_type/trigger_by |
| `backend/app/services/report_service.py` | 报告按模块分组 + 被测系统地址 |
| `backend/app/routers/system.py` | sut-config 接口 |
| `backend/app/schemas/dingtalk_config.py` | SutConfig 请求模型 |
| `frontend/src/router/index.ts` | 定时任务路由 |
| `frontend/src/App.vue` | 侧边栏菜单项 |
| `frontend/src/views/system/Settings.vue` | 被测系统卡片 |
| `frontend/src/views/Reports.vue` | 触发方式列 |
| `frontend/src/api/system.ts` | sut-config API |

---

## 四、验收标准（对照 PRD 第七节）

| 验收项 | 预期 | 检查方式 |
|--------|------|---------|
| 定时任务自动触发 | 到点自动产生 scheduled 执行记录 | 本地观察日志 + 执行列表 |
| 报告自动生成且按模块分组 | 报告中心出现新报告，含模块分组统计 | 查看报告 |
| 立即执行 | 手动触发产生执行 + 报告 | 页面操作 |
| 执行冲突 | 到点时有执行在跑 → 跳过，无失败记录 | 日志 |
| 启停/删除 | 行为符合预期 | 页面操作 |
| 被测系统 URL | 保存即时生效 + 重启持久 | 改 URL → 执行；重启 → 仍生效 |
| 兼容 | 手动执行不回归、pytest 通过、前端构建通过 | 回归 |

---

## 五、风险与依赖

| 风险 | 缓解 |
|------|------|
| 调度循环在 Windows/容器内的事件循环兼容 | 仅使用 `asyncio` + `asyncio.create_task`，与 `message_consumer_loop` 同模式，已由该模式验证可行 |
| `create_task` 触发多任务并发 | 触发前 `is_execution_running()` 预检 + 执行器全局锁双保险 |
| `scheduled_tasks` 表在 MySQL 生产库自动建表 | `init_db` 的 `create_all` 会自动创建新表（MySQL 兼容），无需手动 DDL |
| 定时任务长时间执行时调度循环阻塞 | 触发用 `asyncio.create_task` 异步，循环 30s 粒度继续轮询 |
| 前端多窗口并行开发 | 本功能涉及 backend + frontend 多处文件，避免与他窗口同时改 `main.py` / `App.vue` / `router/index.ts` |

---

*本文档由 Claude Code 生成，遵循项目计划执行文档规范。*
