# Checklist ## 数据库与种子 - [x] `database/migrations/20261002_job_management.sql` 已创建,建 `sys_job` 与 `sys_job_log` 两表及索引,可重复执行无报错 - [x] `sys_job` 含 `job_key` 唯一索引与 `status` 索引;`params` 为 JSONB 且默认 `{}` - [x] `sys_job_log` 含 `(job_id, created_at DESC)` 与 `created_at` 索引 - [x] 迁移写入菜单 id 47(定时任务,parent_id=6,`/system/job`,`system:job`,menu_type=2,sort_order=6)与 48/49/50 三个按钮权限(parent_id=47,menu_type=3) - [x] ADMIN 角色(role_id=1)已绑定菜单 47-50;`sys_menu_id_seq` 被设为 50 - [x] 迁移写入 `demo_heartbeat`(`*/5 * * * *`)与 `log_clean`(`0 3 * * *`,retry_times=2、retry_interval=60)两条 `sys_job` 种子,重复执行不产生重复行 - [x] `database/db_seed.sql` 已同步菜单 47-50、ADMIN 绑定、2 条 `sys_job` 种子与 `sys_menu_id_seq`=50;新库初始化与迁移后结构一致 - [x] 既有菜单 id 7-11 的名称、路径、排序未被改动 ## 后端 - [x] `server/package.json` 已显式声明 `cron` 依赖,`yarn dev` 可正常编译启动 - [x] 内置任务注册表 `server/src/task/job_registry.ts` 提供 `jobKey/jobName/description/defaultCron/defaultParams/paramFields/handler`,页面参数表单由 `paramFields` 驱动,无需前端硬编码 - [x] `demo_heartbeat` 与 `log_clean` 两个内置任务均已注册,`job_key` 与种子数据一致 - [x] 调度器启动加载全部启用任务,停用任务不被调度 - [x] 调度初始化失败时不阻塞应用启动(try/catch + 最多 3 次重试),`onStop` 停止全部定时器 - [x] 页面修改 cron 后无需重启即按新表达式触发,列表 `nextRunTime` 同步更新 - [x] 提交非法 cron 表达式返回 400 且提示明确,配置未落库 - [x] 停用任务后定时触发停止,`nextRunTime` 为 `null`/`-`,但仍可手动执行 - [x] 同一任务执行中再次触发时写 `status = 3` 跳过日志且不并发执行 - [x] 失败且 `attempt <= retry_times` 时按 `retry_interval` 秒后以 `trigger_type = 3` 重试,`attempt` 递增 - [x] `sys_job.last_run_at / last_run_status / last_run_cost_ms / last_error` 每次执行后正确回写(成功时清空 `last_error`) - [x] `log_clean` 同时清理 `sys_operation_log` 与 `sys_job_log`,摘要含两张表删除条数,默认 `keepDays = 90` - [x] `keepDays` 越界(如 0 或 5000)返回 400,配置未落库 - [x] `GET /api/jobs` 支持任务名称模糊与状态筛选,返回分页结构并携带 `paramFields`、`nextRunTime`、上次执行信息 - [x] `PUT /api/jobs/:id`、`PUT /api/jobs/:id/status`、`POST /api/jobs/:id/run`、`GET /api/jobs/:id/logs`、`DELETE /api/jobs/logs/clear` 均可用且返回统一结构 - [x] 静态路由 `/api/jobs/logs/clear` 未被 `:id` 动态路由吞掉 - [x] `error_stack` 落库被截断至 4000 字符,`message` 摘要可读 - [x] 未注册的 `job_key` 在页面标记为未注册,编辑/启停/执行返回 400「任务实现不存在」 - [x] `PERMISSION_MAP` 已新增 `'/api/jobs': 'system:job'`,非 ADMIN 无该权限时访问返回 403 - [x] 所有查询/更新走 Sequelize 参数化 API,无字符串拼接 SQL 与模板字符串 LIKE - [x] 控制器入参使用 DTO + `class-validator`,未滥用 `any`(除既有约定处) - [x] 列表接口均使用 `limit + offset` 分页,无全表扫描式查询 - [x] 调度器为进程内单例(`@Scope(ScopeEnum.Singleton)`),保证「启动注册」与「接口重载」操作同一个定时器注册表 ## 前端 - [x] `client/src/api/job.ts` 提供 6 个接口方法与 TS 类型 - [x] `client/src/views/system/job/index.vue` 含搜索栏 + 表格 + 编辑弹窗 + 立即执行二次确认 + 执行日志抽屉 - [x] 表格列包含任务名/键、cron、参数摘要、状态开关、上次执行时间与状态与耗时、下次执行时间、重试配置 - [x] 编辑弹窗中 cron、参数、重试次数、重试间隔可编辑,且保存前后端校验失败时弹窗保留并提示 message - [x] 执行日志抽屉支持分页、状态筛选与「清理日志」二次确认(默认清理 90 天前) - [x] 路由 `/system/job` 已注册,`meta.permission = 'system:job'` - [x] 侧边栏「系统管理」下可见「定时任务」(ADMIN),非 ADMIN 访问跳转 403 - [x] 页面未使用 `v-html`,cron 与参数输入含长度/范围校验 - [x] 页面结构与既有页面(操作日志 / 数据字典)保持一致,未引入多余 UI 元素 ## 联调 - [x] 后端 `yarn dev` 启动无编译错误,接口逐条通过 - [x] 前端 `yarn dev` 启动,编辑 → 立即执行 → 查看日志 → 清理日志 全链路可用 - [x] 改 cron 即时生效与启停即时生效已验证 - [x] 重试机制(`trigger_type = 3`、`attempt` 递增)已验证 - [x] 迁移脚本与 `db_seed.sql` 在空库与存量库两端结果一致 - [x] 隔离单实例复测:`*/1` 每分钟恰好触发 1 次、停用后 0 次触发、手动执行正常、恢复后 `nextRunTime` 正确(见 tasks.md Task 8) ## 遗留说明(非阻塞) - `sys_job.job_key` 同时存在列级 `UNIQUE` 自动生成的 `sys_job_job_key_key` 与显式 `uk_sys_job_key`,两个索引功能重复(冗余但无害,不影响验收) - 非法入参返回的是响应体 `code=400`,HTTP 状态保持 200,与仓库既有 26 个控制器的约定一致