diff --git a/changelogs-v2/2026-09/24_7804_定时任务健康自检-新增接口-管理后台.md b/changelogs-v2/2026-09/24_7804_定时任务健康自检-新增接口-管理后台.md new file mode 100644 index 00000000..6caefe0a --- /dev/null +++ b/changelogs-v2/2026-09/24_7804_定时任务健康自检-新增接口-管理后台.md @@ -0,0 +1,250 @@ +--- +schema: "hl-changelog/v2" +ticket: "7804" +title: "定时任务健康自检" +consumer: "admin" +author: "jw(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端已部署 TEST 并经网关实测;运维排查接口,管理后台无需接入页面" +updated_at: "2026-09-24" +base: "dev-v3" +--- + +# 定时任务健康自检(#7804) + +> **服务**: hl-user-service(经网关调用,无需关心服务端口) +> **PR**: #8337 +> **Issue**: #7804 +> **日期**: 2026-09-24 +> **影响范围**: 定时任务运维排查(超级管理员),无前端页面 + +--- + +## ⚠️ 关键变化 + +新增只读端点,逐个定时任务并列返回三方状态:**期望状态**、**`sys_job.status`(意图)**、**Quartz 触发器实况**。三方不一致的条目 `healthy=false`,并附告警原文。 + +**三条会直接影响你怎么用的点**: + +1. 🔴 **判断任务是否真在跑,要看 `quartzState`,不能看 `dbStatus`**。`sys_job.status` 只是意图:用裸 SQL 把它置 PAUSED,任务照跑(TEST 上 1044 就这样连跑了 4 天)。原有的 `GET /admin/job` 列表只有 `sys_job.status`,不能用来判断启停。 +2. **只有 SUPER_ADMIN 能调**;其他角色返回业务码 `210301`。 +3. **成功码是 `code: 200`**;业务失败也返回 HTTP 200,**一律按 `body.code` 判**。 + +--- + +## 一、背景(选填) + +`SysJobService.initScheduledJobs()` 为了集群安全是纯增量的,只补注册 ACTIVE 任务,从不移除已有的 Quartz 触发器。所以「裸 SQL 置 PAUSED」停不掉任务,而原来的 `[JobHealth]` 自检只读 `sys_job`,这种漂移下自检是绿的。本单把自检改为以 Quartz 为准,并把结果开放成接口。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 定时任务健康自检 | GET | `/admin/job/health` | 新增接口 | 只读,逐个任务返回期望 / sys_job / Quartz 三方状态与告警 | + +--- + +## 三、接口详情 + +### 1. 定时任务健康自检 `GET /admin/job/health` + +**VO**: `SysJobHealthVO` + +#### 使用场景 + +运维或超级管理员排查「某个定时任务到底有没有在跑」「sys_job 与调度器是否一致」时调用。结果与服务日志里的 `[JobHealth]` WARN 同源同文:启动时和每天 09:30 自动跑一次,日志里只记不一致的条目;本接口返回全部条目,健康的也在内。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `Authorization` | Header | String | ✅ | `Bearer ` | 管理端 token,角色须为 SUPER_ADMIN | + +#### 出参 + +`Result>`,按 jobId 升序排列。另有两类条目追加在末尾:名单内但 sys_job 里不存在的 job,以及 Quartz 里有调度而 sys_job 没有这一行的 job。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `jobId` | Long | 任务 ID。🔴 部分任务是 19 位雪花 ID(如 `2067459972995731458`),前端按字符串处理 | +| `jobName` | String / null | 任务名;sys_job 中不存在该行时为 null | +| `expectedStatus` | String / null | 期望状态,来自配置 `hl.job.health-check.expected-status`;未列入名单的任务为 null。取值 `ACTIVE` / `PAUSED` | +| `dbStatus` | String / null | `sys_job.status`,即意图;sys_job 中不存在该行时为 null。取值 `ACTIVE` / `PAUSED` | +| `quartzState` | String | Quartz 触发器实况,取值见「六.5 枚举」 | +| `healthy` | Boolean | 期望、sys_job、Quartz 三方一致时为 true | +| `message` | String / null | 告警原文;健康时为 null | + +#### 请求示例 + +```http +GET /admin/job/health +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "jobId": 1039, + "jobName": "Fleet导入临时文件收敛", + "expectedStatus": null, + "dbStatus": "PAUSED", + "quartzState": "NORMAL", + "healthy": false, + "message": "jobId=1039 name=Fleet导入临时文件收敛 期望=未列入名单 DB=PAUSED Quartz=NORMAL:sys_job 与 Quartz 漂移(sys_job 只是意图,Quartz 才是实况)" + }, + { + "jobId": 1041, + "jobName": "团期出发推进", + "expectedStatus": "ACTIVE", + "dbStatus": "ACTIVE", + "quartzState": "NORMAL", + "healthy": true, + "message": null + } + ] +} +``` + +**示例说明**:两条取自测试环境 2026-09-24 14:54 的真实调用(当时 1039 是人为构造的漂移),实际返回全部任务(TEST 上 34 条)。 + +#### 空数据 / 降级响应 + +sys_job 为空且 Quartz 无孤儿调度时,`data` 为空数组: + +```json +{ "code": 200, "message": "成功", "data": [] } +``` + +读某个任务的 Quartz 状态失败时不降级为「健康」,而是返回一条 `healthy=false`、`quartzState=UNKNOWN`、`message` 含「Quartz 状态读取失败」的条目;孤儿扫描失败时同样返回一条「Quartz 孤儿触发器扫描失败」。 + +#### 错误响应 + +**非超级管理员**: + +```json +{ "code": 210301, "message": "仅超级管理员可管理定时任务", "data": null, "success": false } +``` + +**未带 token**: + +```json +{ "code": 401, "message": "缺少有效的 Authorization 头", "data": null, "success": false } +``` + +查 sys_job 表失败时不会返回「全绿」,而是走全局异常处理返回错误码。 + +#### 业务边界 + +- 只读,不改 sys_job,也不改 Quartz。 +- **全部** sys_job 行都会比对 sys_job 与 Quartz,不只期望名单内的。 +- 名单内的任务额外比对期望值:期望 ≠ Quartz 实况、期望 ≠ sys_job 都会报。 +- Quartz 实况折算口径:`NORMAL` / `BLOCKED` 会继续触发,算 ACTIVE;`PAUSED` / `COMPLETE` / `NONE` 不会再触发,算 PAUSED;`ERROR` / `UNKNOWN` 一律不健康。 +- `POST /admin/job/{id}/trigger` 在任务未注册时建的一次性调度(`trigger_`)不算孤儿。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误请求对照 + +| 做法 | 结论 | +|---|---| +| ✅ 用 `quartzState` / `healthy` 判断任务是否真在跑 | 权威源 | +| ❌ 用 `GET /admin/job` 列表里的 `status` 判断启停 | 那只是 sys_job 意图,会与实况永久漂移 | +| ✅ 停任务走 `POST /admin/job/{id}/pause` | 会同时摘掉 Quartz 触发器 | +| ❌ 裸 SQL `UPDATE sys_job SET status='PAUSED'`(+ 重启) | 触发器保留,任务照跑;本接口会报「漂移」 | + +--- + +## 六、边界行为 + +- 期望名单为空时仍会比对全部任务的 sys_job 与 Quartz 一致性。 +- 配置 `hl.job.health-check.enabled=false` 只关闭启动时和每日的自检日志,不影响本接口。 +- 经网关落到 primary / secondary 任一实例,结果相同:Quartz 是 JDBC 集群,状态读的是共享的 `QRTZ_*` 表。 + +--- + +## 六.5 枚举 / 数据字典 + +### quartzState + +**所属字段**: `quartzState` | **类型**: `String` + +| 值 | 含义 | 折算 | +|---|---|---| +| `NORMAL` | 调度中(库里显示 WAITING / ACQUIRED / EXECUTING) | ACTIVE | +| `BLOCKED` | 上一次执行尚未结束 | ACTIVE | +| `PAUSED` | 已暂停 | PAUSED | +| `COMPLETE` | 不再触发 | PAUSED | +| `NONE` | 未注册 | PAUSED | +| `ERROR` | 触发器出错 | 不健康 | +| `UNKNOWN` | 读取失败 | 不健康 | + +--- + +## 七、不影响范围 + +- 既有 `/admin/job/**` 端点(增删改查、pause / resume / trigger / logs)的入参、出参、行为均不变。 +- `initScheduledJobs()` 的启动注册逻辑不变,仍然只增不减,不会自动摘除漂移的触发器。 +- 无数据库结构变更,无网关路由变更(沿用 `Path=/admin/job/**`)。 + +--- + +## 八、测试环境已验证 + +真实网关调用(`https://api.test.1814.love`),hl-user-service 已部署 `dev-v3 @ 1ffd758db`(本单合并提交,两个实例在 14:38 完成滚动重启)。token 是自签的(adminId=1002 / test_admin),经 SSH 临时写入 Redis 令牌位,窗口为 14:54:10–14:54:13,用完即还原。 + +``` +构造:UPDATE sys_job SET status='PAUSED' WHERE job_id=1039 → sys_job=PAUSED,QRTZ_TRIGGERS 仍 WAITING +修复前代码重启 → [JobHealth] 关键定时任务状态自检通过,共核 5 项 (阴性对照:旧版测不出) +修复后代码重启 → [JobHealth] jobId=1039 … DB=PAUSED Quartz=NORMAL:sys_job 与 Quartz 漂移 ✓ +GET /admin/job/health(SUPER_ADMIN)→ 200,34 条中仅 1039 healthy=false;1041/1042 healthy=true ✓ +还原 1039 为 ACTIVE 后再调 → 200,34 条全部 healthy=true ✓ +role=ADMIN → code 210301「仅超级管理员可管理定时任务」 ✓ +不带 token → code 401「缺少有效的 Authorization 头」 ✓ +``` + +本地单测:`mvn -o -pl hl-user-service -am test` 共 5051 例,0 失败 / 0 错误;另做 4 个变异体验证,每个都被测试杀死。 + +--- + +## 九、相关历史 PR + +- #7538 / #7640:引入 `[JobHealth]` 自检 +- #8268:用 Flyway 下线 1044,同时清理了 `QRTZ_*` + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7804](https://git.1814.love/wx/HL/issues/7804) +- 关联 PR: [wx/HL#8337](https://git.1814.love/wx/HL/pulls/8337) +- 运维须知: `hl-user-service/docs/deployment.md`(定时任务启停) + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7804](https://git.1814.love/wx/HL/issues/7804) +- **PR**: [#8337](https://git.1814.love/wx/HL/pulls/8337) +- **Merge commit**: [1ffd758db](https://git.1814.love/wx/HL/commit/1ffd758db) + +### 联系人 + +- **后端负责人**: @jw