# 执行计划 — 安全测试定时任务模块

> **文档类型**：计划执行文档  
> **创建日期**：2026-08-19  
> **作者**：czj  
> **状态**：待实现  
> **关联文档**：`_PRD_安全测试定时任务模块_需求文档.md`

---

## 一、执行概述

在安全测试模块中新增**定时任务**功能，复用 UI 定时任务的 `scheduled_tasks` 表 + 后台 asyncio 调度循环，通过 `case_type` 字段区分 UI/安全测试，实现安全测试定时执行。

**设计原则**：
- 复用 `ScheduledTask` 模型（新增 `case_type` / `config_id` 字段），不新增表
- 复用 `scheduler_service.py` 调度引擎（扩展 `run_task_once` 按 `case_type` 分流）
- 复用 `SecurityService` 创建执行 + 运行 + `SecurityReportService` 生成报告
- 复用前端 `ScheduledTasks.vue` 的 UI 模式，独立安全测试页面
- 菜单放在安全测试父菜单下，不干扰 UI 定时任务

---

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

### Step 1：ScheduledTask 模型新增字段

**文件**：`backend/app/models/scheduled_task.py`

```python
# 新增字段
case_type: str = String(20) default "ui"    # ui / security
config_id: str = String(64) nullable=True    # 安全测试配置ID（security 时必填）
```

- `to_dict()` 方法对应增加这两个字段
- `schedule_description()` 无需改动

**文件**：`backend/app/database.py`

- `_ensure_columns` 的 `columns_to_add` 列表新增：
  - `("scheduled_tasks", "case_type", "VARCHAR(20) DEFAULT 'ui'")`
  - `("scheduled_tasks", "config_id", "VARCHAR(64) DEFAULT NULL")`

### Step 2：Schema 扩字段

**文件**：`backend/app/schemas/scheduled_task.py`

- `ScheduledTaskCreate` 新增：`case_type: str = "ui"`、`config_id: Optional[str] = None`
- `ScheduledTaskUpdate` 新增：`case_type: Optional[str] = None`、`config_id: Optional[str] = None`

### Step 3：调度引擎扩展安全测试执行路径

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

修改 `collect_module_cases`：

```python
async def collect_module_cases(db, module_ids: List[str], case_type: str = "ui") -> List[TestCase]:
    """收集选中模块下所有启用的用例（按 case_type 筛选）"""
    if not module_ids:
        return []
    query = (
        select(TestCase)
        .where(
            TestCase.module_id.in_(module_ids),
            TestCase.status == "active",
            TestCase.case_type == case_type,
        )
        .order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
    )
    result = await db.execute(query)
    return list(result.scalars().all())
```

修改 `run_task_once` 主流程，按 `case_type` 分流：

```python
if task.case_type == "security":
    return await _run_security_task_once(task, db)
else:
    # 原有 UI 逻辑
    ...
```

新增 `_run_security_task_once` 异步函数：

1. 收集模块安全用例（`collect_module_cases(db, task.module_ids, "security")`）
2. 用例数为 0 → 跳过，返回 None
3. 延迟导入 `SecurityService`、`SecurityReportService`
4. `security_service = SecurityService(db)`
5. `execution = await security_service.create_execution(config_id=task.config_id, module_ids=task.module_ids, name=f"{task.name}-{ts}")`
6. `await security_service.run_execution(execution.id)`
7. 若 `auto_report` → `SecurityReportService().generate_report(execution_id, db)`
8. 更新 `last_run_at` / `run_count` / `next_run_at`

### Step 4：安全测试定时任务管理 API

**文件**：`backend/app/routers/security_scheduled_tasks.py`（新建）

参考 `scheduled_tasks.py` 实现，关键差异：

| 接口 | 差异 |
|------|------|
| GET `""` | 筛选 `case_type="security"`，join 安全测试配置表展示 `config_name` |
| POST `""` | 自动设 `case_type="security"`，校验 `config_id` 必填 |
| PUT `"/{id}"` | 不允许改 `case_type` |
| DELETE `"/{id}"` | 同 UI 定时任务 |
| POST `"/{id}/run"` | 调用 `run_task_once`（内部已按 case_type 分流） |
| POST `"/{id}/toggle"` | 同 UI 定时任务 |
| GET `"/{id}/runs"` | 查 executions where trigger_type='scheduled' AND trigger_by=任务名 |

**响应增强**：新增 `config_name` 字段（从安全测试配置表查询）

**文件**：`backend/app/main.py`

- 注册 `security_scheduled_tasks.router`，`prefix="/api/security/scheduled-tasks"`

### Step 5：前端 API 封装

**文件**：`frontend/src/api/securityScheduledTasks.ts`（新建）

```typescript
export interface SecurityScheduledTask {
  id: string
  name: string
  enabled: boolean
  module_ids: string[]
  module_names: string[]
  config_id: string
  config_name: string
  schedule_type: string
  interval_value: number
  interval_unit: string
  run_time: string
  weekdays: number[]
  auto_report: boolean
  schedule_description: string
  last_run_at: string | null
  next_run_at: string | null
  run_count: number
  created_at: string | null
  updated_at: string | null
}

export interface SecurityScheduledTaskPayload {
  name: string
  module_ids: string[]
  config_id: string
  schedule_type: 'interval' | 'daily' | 'weekly'
  interval_value?: number
  interval_unit?: 'minutes' | 'hours' | 'days'
  run_time?: string
  weekdays?: number[]
  auto_report?: boolean
  enabled?: boolean
}

export const securityScheduledTaskApi = {
  async list(): Promise<{ total: number; items: SecurityScheduledTask[] }> { ... },
  async create(data: SecurityScheduledTaskPayload): Promise<SecurityScheduledTask> { ... },
  async update(id: string, data: Partial<SecurityScheduledTaskPayload>): Promise<SecurityScheduledTask> { ... },
  async remove(id: string): Promise<void> { ... },
  async run(id: string): Promise<{ message: string; task_id: string }> { ... },
  async toggle(id: string, enabled: boolean): Promise<SecurityScheduledTask> { ... },
  async getRuns(id: string, params?: { skip?: number; limit?: number }): Promise<{ total: number; items: any[] }> { ... },
}
```

### Step 6：安全测试定时任务前端页面

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

参考 `ScheduledTasks.vue` 实现，关键差异：

| 功能 | UI 定时任务 | 安全测试定时任务 |
|------|------------|----------------|
| 模块列表 | `/api/modules?case_type=ui` | `/api/modules?case_type=security` |
| 配置选择 | 无 | 新增「测试配置」下拉选择框（`/api/security/config`） |
| 创建请求 | 不传 `case_type` / `config_id` | 传 `case_type="security"` + `config_id` |
| 表格列 | 不含「测试配置」列 | 新增「测试配置」列 |
| 运行记录跳转 | 跳转到 `/reports/ui` | 跳转到 `/reports/security` |
| 提示信息 | UI 执行相关内容 | 安全测试执行相关内容 |

### Step 7：路由 / 菜单注册

**文件**：`frontend/src/router/index.ts`

- 新增路由：
```typescript
{
  path: '/security/scheduled-tasks',
  name: 'SecurityScheduledTasks',
  component: () => import('@/views/security/ScheduledTasks.vue'),
  meta: { title: '定时任务' }
}
```

**文件**：`frontend/src/App.vue`

- 安全测试父菜单新增子菜单「定时任务」：
```
<el-menu-item index="/security/scheduled-tasks">定时任务</el-menu-item>
```
- `SECURITY_SUB_LABELS` 映射新增 `'scheduled-tasks': '定时任务'`
- `pageTitle` 计算属性中 `/security` 路由支持 `scheduled-tasks` 类型

### Step 8：验证

1. 后端：`cd backend && python -m pytest tests/ -x -q`（确认无回归）
2. 手动功能验证（安全测试定时任务）：
   - 建一条「每 1 分钟」任务选 1-2 个安全模块 + 选默认配置 → 等待调度触发 → 安全测试执行记录产生 → 报告出现在安全测试报告中心
   - 立即执行按钮 → 立即产生安全执行 + 报告
   - 运行历史展示正确
   - 停用 → 不再触发；重启服务 → 任务自动恢复调度
3. UI 定时任务回归验证：
   - 确认 UI 定时任务列表页不受影响（只显示 `case_type="ui"` 的任务）
   - 确认 UI 定时任务调度正常
4. 前端：`cd frontend && npm run build` 通过

---

## 三、涉及文件清单

### 新建

| 文件 | 说明 |
|------|------|
| `backend/app/routers/security_scheduled_tasks.py` | 安全测试定时任务 API 路由 |
| `frontend/src/api/securityScheduledTasks.ts` | 前端 API 封装 |
| `frontend/src/views/security/ScheduledTasks.vue` | 安全测试定时任务页面 |

### 修改

| 文件 | 改动 |
|------|------|
| `backend/app/models/scheduled_task.py` | 新增 `case_type` / `config_id` 字段+ to_dict 扩展 |
| `backend/app/schemas/scheduled_task.py` | Create/Update 新增对应字段 |
| `backend/app/services/scheduler_service.py` | `collect_module_cases` 加 `case_type` 参数 + 新增 `_run_security_task_once` 分流 |
| `backend/app/database.py` | `_ensure_columns` 补齐新字段 |
| `backend/app/main.py` | 注册 security_scheduled_tasks 路由 |
| `frontend/src/router/index.ts` | 新增 `/security/scheduled-tasks` 路由 |
| `frontend/src/App.vue` | 安全测试父菜单新增「定时任务」子菜单 + 标签映射 |

---

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

| 验收项 | 预期 | 检查方式 |
|--------|------|---------|
| 安全测试定时任务自动触发 | 到点自动产生 scheduled 执行记录 | 观察日志 + 执行列表 |
| 报告自动生成 | 安全测试报告中心出现新报告 | 查看报告 |
| 立即执行 | 手动触发产生执行 + 报告 | 页面操作 |
| 执行冲突 | 到点时有执行在跑 → 跳过，无失败记录 | 日志 |
| 启停/删除 | 行为符合预期 | 页面操作 |
| UI 定时任务不受影响 | UI 定时任务列表不显示安全任务、调度正常 | 页面操作 + 日志 |
| 前端构建 | `npm run build` 通过 | 构建命令 |

---

## 五、关键代码结构参考

### 后端路由（参考 `scheduled_tasks.py`）

```
security_scheduled_tasks.py
├── list_tasks()       → GET    ""       筛选 case_type="security"
├── create_task()      → POST   ""       自动设 case_type="security"
├── update_task()      → PUT    "/{id}"
├── delete_task()      → DELETE "/{id}"
├── toggle_task()      → POST   "/{id}/toggle"
├── run_task_now()     → POST   "/{id}/run"
└── list_task_runs()   → GET    "/{id}/runs"
```

### 调度引擎扩展（参考 `scheduler_service.py` 现有结构）

```
scheduler_service.py
├── compute_next_run()        ← 无需改动
├── collect_module_cases()    ← 加 case_type 参数 (默认 "ui")
├── run_task_once()           ← 按 task.case_type 分流
│   ├── case_type="ui"        ← 现有逻辑
│   └── case_type="security"  ← 新增 _run_security_task_once()
├── _run_security_task_once() ← 新增
├── check_and_trigger()       ← 无需改动
├── scheduler_loop()          ← 无需改动
└── start_scheduler()         ← 无需改动
```

### 安全测试执行流程（`_run_security_task_once`）

```python
async def _run_security_task_once(task_id: str) -> Optional[str]:
    """安全测试定时任务触发流程"""
    from app.services.security_service import SecurityService
    from app.services.security_report_service import SecurityReportService
    
    if task_id in _running_tasks:
        return None
    _running_tasks.add(task_id)
    try:
        async with async_session_maker() as db:
            task = await db.get(ScheduledTask, task_id)
            if not task:
                return None
            
            # 1. 收集安全测试用例
            cases = await collect_module_cases(db, task.module_ids, "security")
            if not cases:
                task.next_run_at = compute_next_run(task)
                await db.flush()
                return None
            
            # 2. 创建执行记录（通过 SecurityService）
            security_service = SecurityService(db)
            case_ids = [c.id for c in cases]
            run_name = f"{task.name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
            execution = await security_service.create_execution(
                config_id=task.config_id,
                module_ids=task.module_ids,
                case_ids=case_ids,
                name=run_name,
            )
            # 修改 trigger_type / trigger_by
            execution.trigger_type = "scheduled"
            execution.trigger_by = task.name
            await db.commit()
            
            # 3. 执行安全测试
            await security_service.run_execution(execution.id)
            
            # 4. 自动生成报告
            if task.auto_report:
                report_service = SecurityReportService()
                await report_service.generate_report(execution.id, db)
            
            # 5. 更新调度状态
            task.last_run_at = datetime.now()
            task.run_count = (task.run_count or 0) + 1
            task.next_run_at = compute_next_run(task)
            await db.commit()
            return execution.id
    finally:
        _running_tasks.discard(task_id)
```

---

## 六、风险与依赖

| 风险 | 缓解 |
|------|------|
| `scheduled_tasks` 表已有数据（UI 定时任务），新增字段需兼容旧数据 | `case_type` 默认 `"ui"`，旧数据自动兼容；`config_id` 默认 `NULL` |
| 调度引擎分流逻辑修改影响现有 UI 定时任务 | `run_task_once` 分流前检查 `task.case_type`，UI 任务走原逻辑不变 |
| `SecurityService.create_execution` 内部逻辑与定时任务不完全匹配 | 创建后手动修改 `trigger_type`/`trigger_by` 字段 |
| 前后端多窗口并行开发 | 本功能涉及多处文件，避免与他窗口同时改 `main.py` / `App.vue` / `router/index.ts` |

---

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