# 问题处理 — 定时任务与巡检报告交互优化

> 版本：1.0 ｜ 日期：2026-07-21 ｜ 作者：czj

---

## 问题描述

| # | 问题 | 严重度 |
|---|------|--------|
| 1 | 编辑定时任务后无"保存成功"提示 | P2 体验 |
| 2 | 定时任务执行时页面无执行状态感知 | P2 体验 |
| 3 | 巡检报告列表缺少批量删除功能 | P2 功能 |
| 4 | 定时任务到时间后不执行、不输出报告 | P0 功能 |

---

## 问题 4 根因分析（P0）

### 现象

设置了定时任务（如 5.44 全量巡检），到了指定时间后：
- 巡检报告未生成
- `schedules.json` 中 `last_run_at` 始终为 `null`
- APScheduler 在服务进程中为 `None`

### 根因

**根因 1：Flask debug 模式的 reloader 机制**

`server.py` 中 `debug = os.environ.get('FLASK_DEBUG', '1') == '1'` 默认为 `True`。

Flask debug 模式使用 Werkzeug reloader，会启动两个进程：
- **父进程（reloader 监控进程）**：`init_scheduler()` 在此执行，APScheduler 正常初始化
- **子进程（实际服务进程）**：reloader fork 出的新进程，`init_scheduler()` 中的 `_scheduler is not None` 检查通过，但实际是新进程的全新全局变量 `_scheduler = None`

结果：APScheduler 运行在即将被丢弃的父进程中，实际处理请求的子进程中 scheduler 为 None。

**根因 2：start_date/end_date 类型错误**（已修复）

APScheduler 的 `CronTrigger.start_date` 需要 `datetime` 对象，不能直接赋值字符串。传字符串会报 `'str' object has no attribute 'astimezone'`，传无时区 datetime 会报 `can't compare offset-naive and offset-aware datetimes`。

**根因 3：croniter 依赖缺失**（已修复）

### 修复方案

| 根因 | 修复 |
|------|------|
| Flask debug reloader | 1. systemd 服务加 `Environment=FLASK_DEBUG=0`；2. `init_scheduler()` 检查 `WERKZEUG_RUN_MAIN` 环境变量，仅在 reloader 子进程中初始化 |
| start_date 类型错误 | 转为 `datetime` + `timezone.utc`（已修复） |
| croniter 缺失 | requirements.txt 补充（已修复） |

---

## 问题 1：编辑保存后无提示

### 现象

编辑定时任务，点击保存，弹窗关闭、页面刷新，但没有"保存成功"提示。

### 原因

`saveSchedule()` 中成功时只执行 `closeModal() + location.reload()`，无用户提示。

### 修复

保存成功后先 `alert('保存成功')` 再刷新，或使用 toast 提示。

---

## 问题 2：定时任务执行状态不可见

### 现象

定时任务到了执行时间后，页面上无法看到是否正在执行，只能等几分钟后查看巡检报告是否产生。

### 修复方案

在定时任务列表中增加**执行状态**列：
- `idle`（空闲）：从未执行或上次已完成
- `running`（执行中）：正在执行巡检
- `success`（成功）：上次执行成功，显示报告链接
- `failed`（失败）：上次执行失败

后端 `schedule_service` 在 `_execute_scheduled_job` 开始/结束时更新状态。

前端定时轮询刷新列表（每 30 秒），或在列表页显示 `last_run_status`。

---

## 问题 3：批量删除报告

### 现象

巡检报告列表只能逐条删除，无法批量选择删除。

### 修复方案

1. 报告列表每行增加 checkbox
2. 表头增加全选 checkbox
3. 底部增加"删除选中"按钮
4. 新增 API `DELETE /api/service-monitor/reports/batch` 接收 `{ids: []}`
