Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9.5 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7804 | 定时任务健康自检 | admin | jw(GIT) | 新增接口 | deployed | verified | not_required | 后端已部署 TEST 并经网关实测;运维排查接口,管理后台无需接入页面 | 2026-09-24 | dev-v3 |
定时任务健康自检(#7804)
服务: hl-user-service(经网关调用,无需关心服务端口) PR: #8337 Issue: #7804 日期: 2026-09-24 影响范围: 定时任务运维排查(超级管理员),无前端页面
⚠️ 关键变化
新增只读端点,逐个定时任务并列返回三方状态:期望状态、sys_job.status(意图)、Quartz 触发器实况。三方不一致的条目 healthy=false,并附告警原文。
三条会直接影响你怎么用的点:
- 🔴 判断任务是否真在跑,要看
quartzState,不能看dbStatus。sys_job.status只是意图:用裸 SQL 把它置 PAUSED,任务照跑(TEST 上 1044 就这样连跑了 4 天)。原有的GET /admin/job列表只有sys_job.status,不能用来判断启停。 - 只有 SUPER_ADMIN 能调;其他角色返回业务码
210301。 - 成功码是
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> |
管理端 token,角色须为 SUPER_ADMIN |
出参
Result<List<SysJobHealthVO>>,按 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 |
请求示例
GET /admin/job/health
Authorization: Bearer <SUPER_ADMIN token>
响应示例
{
"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 为空数组:
{ "code": 200, "message": "成功", "data": [] }
读某个任务的 Quartz 状态失败时不降级为「健康」,而是返回一条 healthy=false、quartzState=UNKNOWN、message 含「Quartz 状态读取失败」的条目;孤儿扫描失败时同样返回一条「Quartz 孤儿触发器扫描失败」。
错误响应
非超级管理员:
{ "code": 210301, "message": "仅超级管理员可管理定时任务", "data": null, "success": false }
未带 token:
{ "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_<id>)不算孤儿。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误请求对照
| 做法 | 结论 |
|---|---|
✅ 用 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
- 关联 PR: wx/HL#8337
- 运维须知:
hl-user-service/docs/deployment.md(定时任务启停)
关联 / 联系人
链接
联系人
- 后端负责人: @jw