--- 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