docs(changelog): #7804 定时任务健康自检 GET /admin/job/health(新增接口)
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -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>` | 管理端 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 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /admin/job/health
|
||||
Authorization: Bearer <SUPER_ADMIN token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```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_<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](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
|
||||
在新工单中引用
屏蔽一个用户