feat: 新增电工APP端与定时任务管理功能
- 新增电工APP独立鉴权中间件与登录注册接口 - 新增定时任务管理模块,支持任务配置、执行日志与手动触发 - 新增运维费定价与电工派单成本价目管理 - 新增电工资质证照管理与到期提醒定时任务 - 完善数据库实体与DTO校验,优化权限控制
This commit is contained in:
1 parent
1803ad679c
commit
f4454ad014
57 files changed
+9388
-200
No files matched your search
@@ -0,0 +1,76 @@
|
||||
# Tasks
|
||||
|
||||
> 规格:`.trae/specs/add-scheduled-job-management/spec.md`
|
||||
> 说明:任务清单中「内置任务注册表」是本规格的核心抽象,Task 4 依赖它;Task 5-7 可并行。
|
||||
|
||||
- [x] Task 1: 数据库迁移与种子数据(全部后端任务的前置)
|
||||
- [x] SubTask 1.1: 新建 `database/migrations/20261002_job_management.sql`:创建 `sys_job`(含 `job_key` 唯一索引、`status` 索引)与 `sys_job_log`(含 `(job_id, created_at DESC)`、`created_at` 索引)两张表,风格对齐现有迁移(注释、幂等 `IF NOT EXISTS`)
|
||||
- [x] SubTask 1.2: 同一迁移内写入菜单:`sys_menu` 新增 id 47(定时任务,parent_id=6,menu_path `/system/job`,component `system/job/index`,permission `system:job`,menu_type=2,sort_order=6)、48/49/50(按钮权限 `system:job:update` / `system:job:run` / `system:job:log:clear`,parent_id=47,menu_type=3),使用 `ON CONFLICT (id) DO UPDATE` 幂等;为 ADMIN 角色(role_id=1)插入 `sys_role_menu` 绑定;`setval('sys_menu_id_seq', 50, true)`
|
||||
- [x] SubTask 1.3: 同一迁移内写入 `sys_job` 种子:`demo_heartbeat`(`*/5 * * * *`,params `{"message":"heartbeat"}`,status=1)与 `log_clean`(`0 3 * * *`,params `{"keepDays":90}`,status=1,retry_times=2、retry_interval=60),使用 `ON CONFLICT (job_key) DO NOTHING`
|
||||
- [x] SubTask 1.4: 同步修订 `database/db_seed.sql`:在现有菜单段落之后追加 47-50 菜单与 ADMIN 绑定、2 条 `sys_job` 种子,更新 `sys_menu_id_seq` 为 50 与文件头部注释中的菜单计数
|
||||
- [x] SubTask 1.5: 执行迁移脚本两次验证幂等(第二次无报错、无重复菜单/任务行)
|
||||
|
||||
- [x] Task 2: 实体层
|
||||
- [x] SubTask 2.1: 新建 `server/src/entity/sys_job.entity.ts`(列与 spec 中 `sys_job` 定义一致,`params` 用 `DataType.JSONB`,`timestamps: true`,字段名用 `field` 显式声明 snake_case,参考 `sysOperationLog.entity.ts` 风格)
|
||||
- [x] SubTask 2.2: 新建 `server/src/entity/sys_job_log.entity.ts`(`timestamps: true` 且仅 `created_at`,与 `sysOperationLog` 一致)
|
||||
|
||||
- [x] Task 3: 内置任务注册表与两个内置任务(依赖 Task 2)
|
||||
- [x] SubTask 3.1: `server/package.json` 显式声明 `"cron": "^3.5.0"` 并执行 `yarn install`(该版本已在 `node_modules` 与 `yarn.lock` 中存在,来自 `@midwayjs/cron` 传递依赖)
|
||||
- [x] SubTask 3.2: 新建 `server/src/task/job_registry.ts`:定义 `BuiltinJob` 类型(jobKey / jobName / description / defaultCron / defaultParams / paramFields / handler)与 `BUILTIN_JOBS` 数组、`getBuiltinJob(jobKey)`;`paramFields` 支持 `number` 类型与 min/max
|
||||
- [x] SubTask 3.3: 新建 `server/src/task/demo_heartbeat.task.ts`:`jobKey = demo_heartbeat`,默认 cron `*/5 * * * *`,参数 `message`(string,可选),handler 写 info 日志并返回「心跳时间」摘要
|
||||
- [x] SubTask 3.4: 新建 `server/src/task/log_clean.task.ts`:`jobKey = log_clean`,默认 cron `0 3 * * *`,参数 `keepDays`(number,1-3650,默认 90),handler 调用 `LogService.cleanExpiredLogs(keepDays)` 清理 `sys_operation_log`,再删除 `sys_job_log` 中早于 `keepDays` 的记录,返回「清理操作日志 X 条、任务日志 Y 条」
|
||||
- [x] SubTask 3.5: 在 `job_registry.ts` 中把两个任务注册进 `BUILTIN_JOBS`
|
||||
|
||||
- [x] Task 4: 调度器(依赖 Task 2、3)
|
||||
- [x] SubTask 4.1: 新建 `server/src/service/job_scheduler.service.ts`:
|
||||
- `start()`:加载 `status=1` 的 `sys_job` → 按 `job_key` 匹配注册表 → 用 `cron` 包 `CronJob`(timezone `Asia/Shanghai`)注册并 `start()`;内部 try/catch + 最多 3 次重试(间隔 5s),失败仅记录日志不阻塞启动
|
||||
- `stop()`:停止全部定时器(应用停止时调用)
|
||||
- `reload(jobKey)`:停旧 → 按最新配置重新注册(停用则不注册)
|
||||
- `runOnce(jobKey, triggerType)`:导出给 controller 的立即执行入口,返回执行结果
|
||||
- `nextRunTime(cronExpression)`:用 cron 表达式计算下次执行时间,非法或空返回 `null`
|
||||
- `validateCron(expression)`:校验表达式合法性
|
||||
- [x] SubTask 4.2: 实现执行内核:进程内 `Set<string>` 重入锁 → 未注册任务返回「任务实现不存在」→ 写入 `sys_job_log`(`trigger_type`、`attempt`、`started_at`)→ 执行 handler → 回写日志 `status`/`finished_at`/`cost_ms`/`message`/`error_stack`(截断 4000 字符)→ 回写 `sys_job` 的 `last_run_at`/`last_run_status`/`last_run_cost_ms`/`last_error`;并发触发时写 `status = 3` 跳过日志
|
||||
- [x] SubTask 4.3: 实现失败重试:`attempt <= retry_times` 时用 `setTimeout(retry_interval * 1000)` 以 `trigger_type = 3`、`attempt + 1` 再次执行,重试期间保持重入锁;重试链结束仍失败则以 `last_run_status = 2` 收尾
|
||||
- [x] SubTask 4.4: 修改 `server/src/configuration.ts`:`onReady` 中获取并 `start()` 调度服务(捕获异常),`onStop` 中 `stop()`;保持不变更其它已有逻辑
|
||||
|
||||
- [x] Task 5: 业务服务与接口(依赖 Task 4)
|
||||
- [x] SubTask 5.1: 新建 `server/src/dto/job.dto.ts`:`UpdateJobDTO`(cronExpression、params、retryTimes、retryInterval、remark,`class-validator` 校验)、`JobLogQueryDTO`(page/pageSize/status/triggerType/startDate/endDate)
|
||||
- [x] SubTask 5.2: 新建 `server/src/service/job.service.ts`:
|
||||
- `list(query)`:按任务名称模糊 + 状态筛选,返回 `list/total/page/pageSize`,每行附带注册表信息(`jobName`、`description`、`paramFields`、`registered`)、`nextRunTime`(由调度器计算)、上次执行信息
|
||||
- `update(id, dto)`:校验存在性与已注册 → 校验 cron 合法性 → 按 `paramFields` 白名单与范围校验 params(越界返回 400)→ 保存 → `scheduler.reload(jobKey)`
|
||||
- `changeStatus(id, status)`:切换启停 → `scheduler.reload(jobKey)`
|
||||
- `run(id)`:调用 `scheduler.runOnce(jobKey, 2)`
|
||||
- `listLogs(id, query)`:分页 + 状态/触发方式/时间范围筛选,`created_at DESC`
|
||||
- `clearLogs(days = 90)`:删除早于 `days` 天的 `sys_job_log`
|
||||
- [x] SubTask 5.3: 新建 `server/src/controller/job.controller.ts`(`/api/jobs`,注意静态路径写在动态路径之前):
|
||||
- `GET /api/jobs`、`PUT /api/jobs/:id`、`PUT /api/jobs/:id/status`、`POST /api/jobs/:id/run`
|
||||
- `GET /api/jobs/:id/logs`、`DELETE /api/jobs/logs/clear`
|
||||
- 返回统一 `{ code, message, data }`,400/404 场景给出明确 message
|
||||
- [x] SubTask 5.4: 修改 `server/src/middleware/rbac.middleware.ts`:`PERMISSION_MAP` 新增 `'/api/jobs': 'system:job'`
|
||||
|
||||
- [x] Task 6: 前端 API 与页面(依赖 Task 5)
|
||||
- [x] SubTask 6.1: 新建 `client/src/api/job.ts`(`getJobList` / `updateJob` / `changeJobStatus` / `runJob` / `getJobLogs` / `clearJobLogs`,含 TS 类型,参考 `api/log.ts` 风格)
|
||||
- [x] SubTask 6.2: 新建 `client/src/views/system/job/index.vue`:搜索栏(任务名称、状态)+ 任务表格(任务名/键、cron、参数摘要、状态开关、上次执行时间与状态与耗时、下次执行时间、重试配置、操作)+ 编辑弹窗(cron、按 `paramFields` 动态渲染的参数项、重试次数、重试间隔、备注)+ 立即执行二次确认 + 执行日志抽屉(分页表格、状态筛选、清理日志按钮),保持与 `views/log/operation/index.vue` 一致的页面结构
|
||||
- [x] SubTask 6.3: 修改 `client/src/router/index.ts`:在系统管理下新增路由 `system/job`(`name: SystemJob`,`meta: { requiresAuth: true, title: '定时任务', permission: 'system:job' }`)
|
||||
|
||||
- [x] Task 7: 联调与验证(依赖 Task 1-6)
|
||||
- [x] SubTask 7.1: 后端 `yarn dev` 启动无编译错误;`curl` 验证列表/更新/启停/立即执行/日志查询/日志清理接口
|
||||
- [x] SubTask 7.2: 将 `demo_heartbeat` 改为 `*/1 * * * *` 验证「改 cron 即时生效」与「下次执行时间」更新;停用后验证不再触发、恢复启用后重新调度(首轮验证失败 → 见 Task 8;修复后复测:每分钟恰好触发 1 次、停用后 0 次触发、手动执行正常、恢复后 `nextRunTime` 正确)
|
||||
- [x] SubTask 7.3: 验证重试:临时构造失败场景(如 `log_clean` 传入非法参数不可行时,用 `retry_times`/`retry_interval` 观测日志 `trigger_type = 3` 与 `attempt` 递增)
|
||||
- [x] SubTask 7.4: 前端 `yarn dev` 启动,人工走通「编辑 → 立即执行 → 查看日志 → 清理日志」全链路,并确认侧边栏「系统管理 → 定时任务」可见(实测:侧边栏菜单可见、`/system/job` 正常进入、两行任务渲染正确、编辑保存成功、立即执行二次确认与成功提示、日志抽屉含手动触发的记录并带分页、清理日志二次确认成功;新标签页控制台无 error)
|
||||
- [x] SubTask 7.5: 确认迁移脚本与 `db_seed.sql` 在空库/存量库两端结果一致,且 `sys_menu_id_seq` 为 50
|
||||
|
||||
- [x] Task 8: 修复调度器作用域缺陷(Task 7.2 首轮验证发现的真实缺陷)
|
||||
- [x] SubTask 8.1: 定位根因 —— `@Provide()` 在本项目 Midway 版本中默认解析为 **Request 作用域**(`@midwayjs/core` 的 `container.bind()` 对 class 目标亦执行 `definition.scope = options?.scope || ScopeEnum.Request`),导致「启动注册定时器」与「接口 `reload`」落在不同服务实例上:`reload` 所在实例的 `timers` 为空,既停不掉旧定时器,又新建一个定时器,每次保存/启停都泄漏一个常驻定时器(实测同一分钟内出现 2 次以上重复触发,且停用后仍持续触发)
|
||||
- [x] SubTask 8.2: 为 `JobSchedulerService` 显式声明 `@Scope(ScopeEnum.Singleton)`,使定时器注册表在启动注册与接口重载之间共享(`@InjectRepository` 由单例数据源管理器解析,无需请求上下文,可安全用于单例)
|
||||
- [x] SubTask 8.3: `LogService` 同为请求作用域,单例中不可直接 `@Inject`;改为在 `buildRuntime()` 内通过 `this.app.createAnonymousContext().requestContext.getAsync(LogService)` 按需获取(对齐 `@midwayjs/cron` 官方执行任务的上下文用法),注入 `@App()` 替换原 `@Inject() logService`
|
||||
- [x] SubTask 8.4: 清理定位期间加入的临时诊断日志,`npx tsc --noEmit` 通过,并在隔离单实例(7009)下复测 7.2 全部通过
|
||||
|
||||
# Task Dependencies
|
||||
|
||||
- Task 2 依赖 Task 1(表结构存在后才能通过实体读写)
|
||||
- Task 3 依赖 Task 2;Task 4 依赖 Task 2、3
|
||||
- Task 5 依赖 Task 4;Task 6 依赖 Task 5
|
||||
- Task 7 依赖 Task 1-6
|
||||
- Task 8 依赖 Task 7.2 的首轮失败验证;Task 8 完成后需回归 Task 7.2
|
||||
- 并行建议:Task 1 完成后,Task 2/3 与前端 Task 6.1(API 层)可并行;Task 5 与 Task 6.2/6.3 可在接口契约冻结后并行
|
||||
Reference in new issue
Block a user