feat: 新增电工APP端与定时任务管理功能

- 新增电工APP独立鉴权中间件与登录注册接口
- 新增定时任务管理模块,支持任务配置、执行日志与手动触发
- 新增运维费定价与电工派单成本价目管理
- 新增电工资质证照管理与到期提醒定时任务
- 完善数据库实体与DTO校验,优化权限控制
This commit is contained in:
andyesfly committed 2026-10-02 16:08:00 +08:00
1 parent 1803ad679c
commit f4454ad014
57 files changed
+9388 -200

No files matched your search

@@ -0,0 +1,64 @@
# 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 个控制器的约定一致