# HANDOFF — 定时任务执行状态展示 + 立即执行功能

> 最后更新：2026-07-21 16:30 ｜ 分支：troubleshoot-ai-assistant ｜ 负责人：czj

---

## 1. 我们在做什么

本轮解决用户反馈的"定时任务还是没执行"问题。经排查 `/api/health` 确认 `scheduler.running=true, job_count=3` —— **APScheduler 已启动、任务已注册**，问题不在"调度器没启动"，而在"到点没触发（疑似时区）或触发后执行失败"。

但页面**完全看不到执行情况**，用户无从判断。本次新增：

1. **「上次执行」列**：展示时间 + 成功/失败/运行中徽章 + 报告链接
2. **「立即执行」按钮**：手动触发一次，后台线程执行、立即返回、状态自动刷新
3. **异常路径状态统一**：`last_run_status` 与 `current_status` 保持一致

这套组合可定位"不执行"根因：点「立即执行」成功 → 执行链路没问题，问题在 cron 到点（对比"下次执行"时间，若晚 8 小时即确认时区问题）；失败 → 徽章直接暴露错误。

> 历史背景：上一轮已完成"定时任务弹窗优化（commit 28f55365）+ 通知配置（fdb0ee63）+ 定时任务不执行三根因修复（9d887980）"。本轮在其基础上补执行可见性。

---

## 2. 已经完成了什么

### ✅ 定时任务执行状态展示 + 立即执行（本轮）

**页面新增「上次执行」列**：
- 表格从 7 列扩展为 8 列：`任务名称 | 目标 | 套件 | 调度周期 | 启用 | 上次执行 | 下次执行 | 操作`
- 显示时间（`last_run_at`）、执行状态徽章（`current_status` 优先）、报告链接（`last_report_id`）
- 徽章样式：🔄 执行中（蓝）、✅ 成功（绿）、❌ 失败/⚠️ 异常（红）、⏸ 待执行（灰）
- 响应式：平板端隐藏"套件""上次执行""下次执行"

**新增「立即执行」按钮**：
- 操作列新增「执行」按钮（蓝色），点击后 POST `/api/service-monitor/schedules/<id>/run`
- 后台线程执行巡检（`run_now()` 函数），HTTP 立即返回，不阻塞
- 前端立即显示"执行中"徽章，启动 3 秒轮询直到完成
- 禁用状态下也可执行（手动触发是显式动作）

**后端改动**：
- `schedule_service.py`：抽 `_run_job_body()` 共享执行体、新增 `run_now()`、`create_schedule()` 补 `"current_status": "idle"`、异常路径状态统一为 `error`
- `routes.py`：`page_schedule` 注入 `run_badge`/`run_label`；新增 `POST .../schedules/<id>/run` 路由

**验证**：
- 本地测试：167 用例 + service_monitor 51 用例全绿 ✅
- 新路由已注册 ✅
- 待 5.60 部署后验证实际执行效果

---

### ✅ 定时任务弹窗交互优化（commit 28f55365）

- 删除 Cron 输入框，改为**重复周期单选组**（每天/工作日/每周）
- 新增**时:分下拉选择器**（0-23 时，每5分钟）
- 新增**星期多选按钮组**（仅每周模式，至少选1个）
- 新增**生效日期/失效日期**选择器
- 列表"Cron"列改为**"调度周期"**，显示自然语言描述（如"工作日 09:30"）
- 后端新增 `_build_cron()` / `_describe_schedule()` 自动转换
- 数据模型新增 `repeat_mode`/`hour`/`minute`/`weekdays`/`start_date`/`end_date`
- **验证**：5.60 浏览器验证通过，创建任务正确显示，编辑回填正确

### ✅ 通知配置功能（commit fdb0ee63）

- 左侧菜单新增 🔔 通知配置（仅管理员可见，位于定时任务与目标管理之间）
- 新增 `notification_service.py`：邮件/钉钉/企业微信配置 CRUD、测试发送、巡检后通知
- 新增 `notification.html`：三渠道配置卡片 + 触发条件 + 测试按钮
- `runner_service.py` 的 `run_inspection_sync()` 完成后调用通知服务
- 敏感字段用 Fernet 加密存储（复用 `crypto.py` 的 `encrypt_password`）
- **验证**：5.60 浏览器验证页面功能正常

### ✅ 定时任务不执行修复（commit 9d90d646）

**根因 1：Flask debug reloader**（核心）
- `server.py` 的 `FLASK_DEBUG` 默认值从 `'1'` 改为 `'0'`
- `init_scheduler()` 增加 `WERKZEUG_RUN_MAIN` 环境变量检查
- `upload_to_server.py` 启动命令显式 `FLASK_DEBUG=0`

**根因 2：start_date 类型错误**
- `_add_job()` 中字符串转 `datetime` 并附加 `timezone.utc`，end_date 设为 `23:59:59`

**根因 3：croniter 依赖缺失**
- `requirements.txt` 增加 `apscheduler==3.11.0` + `croniter==6.2.4`
- `skill/code/requirements.txt` 增加对应依赖
- `upload_to_server.py` 的 `_REQUIRED_PACKAGES` 增加 `'croniter'`

**验证**：`/api/health` 返回 `scheduler: {'running': true, 'job_count': 3}` ✅

### ✅ 其他改进

- **编辑保存提示**：`schedule.html` 成功后 `alert('保存成功')`
- **执行状态显示**：`schedule_service.py` 新增 `_update_current_status()`，`current_status` 字段（idle/running/success/failed）
- **批量删除报告**：`reports.html` checkbox + 全选 + 批量删除按钮；`routes.py` 新增 `DELETE /api/service-monitor/reports/batch`
- **health 接口增强**：`routes/troubleshoot.py` 增加 `scheduler` 状态字段

### ✅ 提交并推送（commit 9d887980 / 633da268）

5 个提交已推送到 `origin troubleshoot-ai-assistant`：
```
28f55365 feat(service-monitor): 定时任务弹窗交互优化
fdb0ee63 feat(service-monitor): 通知配置功能（邮件/钉钉/企业微信）
9d90d646 fix(service-monitor): 定时任务不执行修复 + 依赖补充 + 报告批量删除
9d887980 docs: 更新 HANDOFF 交接文档
633da268 docs(service-monitor): 定时任务弹窗优化 PRD 文档
```

---

## 3. 当前卡在哪

**无卡点**。代码改动完成、测试通过、待部署到 5.60。

**待验证项**：
1. **本地验证**：起服务 → 定时任务页点「执行」→ 徽章刷新 → 报告链接跳转
2. **5.60 部署验证**：点「立即执行」若成功 → 对比"下次执行"时间判断是否时区问题

---

## 4. 下一步计划

1. **部署到 5.60**（P0）
   - 执行 `! cd deploy && SSH_PASSWORD='***' python upload_to_server.py`
   - 刷新页面确认「上次执行」列显示、「执行」按钮可见

2. **验证立即执行功能**（P0）
   - 点「执行」→ 徽章变"执行中"→ 数秒后自动刷新为"成功/失败"
   - 点「报告」链接跳转正常

3. **定位不执行根因**（P1）
   - 若「立即执行」成功 → 问题在 cron 到点；对比"下次执行"时间，晚 8 小时即确认时区问题（需后续修 CronTrigger 时区）
   - 若「立即执行」失败 → 徽章直接暴露错误，看日志定位

4. **创建 Merge Request**（P2）
   - 访问 http://git.ubainsyun.com/bing/ubains-module-test/merge_requests/new?merge_request%5Bsource_branch%5D=troubleshoot-ai-assistant
   - 将 troubleshoot-ai-assistant 合并到 master

---

## 5. 踩过的坑（绝对不要再踩）

### 坑 0：定时任务"看起来没执行"，实际是页面没展示执行状态（本轮）

**坑**：用户反馈"还是没执行"，但 `/api/health` 显示 `scheduler.running=true, job_count:3` —— 调度器在跑、任务已注册。真正问题是 `schedule.html` 的"状态"列只显示启用/禁用开关，`last_run_at`/`last_run_status`/`current_status`/`last_report_id` 这些字段数据都有但 UI 没渲染，用户无从判断。

**避免方法**：
- 定时任务功能必须有**执行可见性**：上次执行时间 + 成功/失败徽章 + 报告链接
- 加「立即执行」按钮可主动验证执行链路，区分"到点没触发"和"触发后失败"

### 坑 0.5：疑似时区问题（待验证）

**坑**：`_add_job()` 用 `CronTrigger.from_crontab(cron)`，默认按服务器本地时区解析 cron 字段。若 5.60 系统时区是 UTC，用户配"每天 09:30"实际会在北京时间 17:30 才触发，看起来像"没执行"。

**避免方法**：部署后对比"下次执行"时间与预期，晚 8 小时即确认。后续可显式锁定 `CronTrigger` 时区为 `Asia/Shanghai`。

### 坑 1：Flask debug 模式的 reloader 导致 APScheduler 失效（最严重）

**坑**：`server.py` 默认 `FLASK_DEBUG='1'`，Flask debug 模式用 Werkzeug reloader fork 出子进程后，父进程中初始化的 APScheduler 被丢弃，子进程 `_scheduler` 全局变量重置为 None。症状是定时任务"看起来注册成功但永远不执行"。

**为什么踩**：开发环境默认开 debug 方便热重载，但 reloader 双进程机制与 APScheduler 单进程初始化冲突。

**避免方法**：
- 生产环境**必须** `FLASK_DEBUG=0`
- `init_scheduler()` 检查 `WERKZEUG_RUN_MAIN` 环境变量，仅在子进程初始化
- health 接口暴露 scheduler 状态快速诊断

### 坑 2：APScheduler 的 start_date/end_date 需要带时区的 datetime

**坑**：直接把 `"2026-07-20"` 字符串赋给 `CronTrigger.start_date` 报 `'str' object has no attribute 'astimezone'`。改为 datetime 后又报 `can't compare offset-naive and offset-aware datetimes`。

**避免方法**：用 `datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)` 转换。end_date 当天也要执行，设为 `23:59:59`。

### 坑 3：croniter 依赖未声明

**坑**：`apscheduler` 在 `upload_to_server.py` 的 `_REQUIRED_PACKAGES` 中声明了，`croniter` 没有。缺失导致 `next_run_at` 为 null，任务无法注册。

**避免方法**：**所有 Python 依赖必须在三处同步声明**：
1. `requirements.txt`（容器化部署）
2. `skill/code/requirements.txt`（开发环境）
3. `deploy/upload_to_server.py` 的 `_REQUIRED_PACKAGES`（SSH 部署）

### 坑 4：notification_service 导入了不存在的函数名

**坑**：导入了 `encrypt_value`/`decrypt_value`，但 `crypto.py` 实际函数名是 `encrypt_password`/`decrypt_password`，导致 `ImportError` 服务启动崩溃。

**避免方法**：新增模块前检查依赖模块的实际函数签名，不要凭记忆写函数名。

### 坑 5：5.60 服务器 pip install 需要特殊参数

**坑**：5.60 是 Ubuntu + Python 3.14，`pip install` 报 `externally-managed-environment`，需加 `--break-system-packages`。

**避免方法**：`upload_to_server.py` 的依赖安装命令已加此参数；后续手动安装时也要加。

### 坑 6：APScheduler 验证要用 HTTP 接口而非新进程

**坑**：用 `python3 -c "from schedule_service import ..."` 验证 scheduler 状态时，这是**全新独立进程**，`_scheduler` 当然是 None，不能反映运行中服务进程的真实状态。

**避免方法**：通过 HTTP 接口（health 加 scheduler 字段）检查运行中进程的 scheduler 状态，不要新开 Python 进程验证。

---

## 6. 关键文件与命令速查

### 本次新增文件

| 文件 | 用途 |
|------|------|
| `skill/code/web/service_monitor/services/notification_service.py` | 通知服务（邮件/钉钉/企业微信） |
| `skill/code/web/templates/service_monitor/notification.html` | 通知配置页面 |
| `Docs/需求文档/服务监测/PRD_需求文档_定时任务弹窗优化.md` | 弹窗优化 PRD |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务弹窗优化.md` | 弹窗优化计划 |
| `Docs/需求文档/服务监测/PRD_需求文档_通知配置.md` | 通知配置 PRD |
| `Docs/需求文档/服务监测/PRD_计划执行_通知配置.md` | 通知配置计划 |
| `Docs/需求文档/服务监测/PRD_问题处理_定时任务与报告优化.md` | 问题处理文档 |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务与报告优化.md` | 问题修复计划 |

### 本次修改文件

| 文件 | 改动要点 |
|------|---------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 抽 `_run_job_body`、新增 `run_now`、`create_schedule` 补 `current_status:idle`、异常状态统一为 `error` |
| `skill/code/web/service_monitor/routes.py` | `page_schedule` 注入 `run_badge`/`run_label`；新增 `POST .../schedules/<id>/run` |
| `skill/code/web/templates/service_monitor/schedule.html` | 表格 7→8 列、执行状态徽章 CSS、执行按钮、3 秒轮询自动刷新、响应式适配 |

### 历史修改文件（前几轮）

| 文件 | 改动要点 |
|------|---------|
| `skill/code/web/server.py` | FLASK_DEBUG 默认值改为 `'0'` |
| `skill/code/web/service_monitor/services/schedule_service.py` | _build_cron/_describe_schedule/_update_current_status + reloader 防护 + start_date 类型修复 |
| `skill/code/web/service_monitor/services/runner_service.py` | run_inspection_sync 完成后调用通知 |
| `skill/code/web/service_monitor/utils/paths.py` | 增加 NOTIFICATIONS_FILE |
| `skill/code/web/service_monitor/routes.py` | 通知配置 6 个路由 + 批量删除 API |
| `skill/code/web/routes/troubleshoot.py` | health 接口增加 scheduler 状态 |
| `skill/code/web/templates/service_monitor/base.html` | 左侧菜单增加"通知配置" |
| `skill/code/web/templates/service_monitor/schedule.html` | 弹窗全面重写 + 保存提示 |
| `skill/code/web/templates/service_monitor/reports.html` | checkbox + 批量删除 |
| `requirements.txt` | +apscheduler==3.11.0 +croniter==6.2.4 |
| `skill/code/requirements.txt` | +apscheduler>=3.10.0 +croniter>=2.0.0 |
| `deploy/upload_to_server.py` | _REQUIRED_PACKAGES +croniter；启动命令 +FLASK_DEBUG=0 |

### 常用命令

```bash
# 本地启动（开发 debug 模式）
FLASK_DEBUG=1 python skill/code/web/server.py

# 本地启动（生产模式，定时任务可用）
python skill/code/web/server.py

# 部署到 5.60（用 ! 前缀在会话内执行，避免读不到 SSH_PASSWORD）
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py

# 健康检查（含 scheduler 状态）
curl -s http://192.168.5.60:8088/api/health

# 单元测试
cd skill/code && python -m pytest -v
```

### 数据文件

| 数据 | 文件 |
|------|------|
| 监测目标 | `service_monitor/data/targets.json` |
| 巡检报告 | `service_monitor/data/reports/*.json` |
| 定时任务 | `service_monitor/data/schedules.json` |
| 通知配置 | `service_monitor/data/notifications.json` |

### 未提交文件

- `skill/code/web/users.json`（仅登录统计更新，无需提交）
