Files
smart_grid_iot/.trae/specs/add-scheduled-job-management/tasks.md
T
andyesfly f4454ad014 feat: 新增电工APP端与定时任务管理功能
- 新增电工APP独立鉴权中间件与登录注册接口
- 新增定时任务管理模块,支持任务配置、执行日志与手动触发
- 新增运维费定价与电工派单成本价目管理
- 新增电工资质证照管理与到期提醒定时任务
- 完善数据库实体与DTO校验,优化权限控制
2026-10-02 16:08:00 +08:00

11 KiB
Raw Blame History

Tasks

规格:.trae/specs/add-scheduled-job-management/spec.md 说明:任务清单中「内置任务注册表」是本规格的核心抽象,Task 4 依赖它;Task 5-7 可并行。

  • Task 1: 数据库迁移与种子数据(全部后端任务的前置)

    • 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)
    • 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)
    • 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
    • SubTask 1.4: 同步修订 database/db_seed.sql:在现有菜单段落之后追加 47-50 菜单与 ADMIN 绑定、2 条 sys_job 种子,更新 sys_menu_id_seq 为 50 与文件头部注释中的菜单计数
    • SubTask 1.5: 执行迁移脚本两次验证幂等(第二次无报错、无重复菜单/任务行)
  • Task 2: 实体层

    • SubTask 2.1: 新建 server/src/entity/sys_job.entity.ts(列与 spec 中 sys_job 定义一致,params 用 DataType.JSONB,timestamps: true,字段名用 field 显式声明 snake_case,参考 sysOperationLog.entity.ts 风格)
    • SubTask 2.2: 新建 server/src/entity/sys_job_log.entity.ts(timestamps: true 且仅 created_at,与 sysOperationLog 一致)
  • Task 3: 内置任务注册表与两个内置任务(依赖 Task 2)

    • SubTask 3.1: server/package.json 显式声明 "cron": "^3.5.0" 并执行 yarn install(该版本已在 node_modules 与 yarn.lock 中存在,来自 @midwayjs/cron 传递依赖)
    • 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
    • SubTask 3.3: 新建 server/src/task/demo_heartbeat.task.ts:jobKey = demo_heartbeat,默认 cron */5 * * * *,参数 message(string,可选),handler 写 info 日志并返回「心跳时间」摘要
    • 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 条」
    • SubTask 3.5: 在 job_registry.ts 中把两个任务注册进 BUILTIN_JOBS
  • Task 4: 调度器(依赖 Task 2、3)

    • 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):校验表达式合法性
    • 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 跳过日志
    • SubTask 4.3: 实现失败重试:attempt <= retry_times 时用 setTimeout(retry_interval * 1000) 以 trigger_type = 3、attempt + 1 再次执行,重试期间保持重入锁;重试链结束仍失败则以 last_run_status = 2 收尾
    • SubTask 4.4: 修改 server/src/configuration.ts:onReady 中获取并 start() 调度服务(捕获异常),onStop 中 stop();保持不变更其它已有逻辑
  • Task 5: 业务服务与接口(依赖 Task 4)

    • SubTask 5.1: 新建 server/src/dto/job.dto.ts:UpdateJobDTO(cronExpression、params、retryTimes、retryInterval、remark,class-validator 校验)、JobLogQueryDTO(page/pageSize/status/triggerType/startDate/endDate)
    • 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
    • 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
    • SubTask 5.4: 修改 server/src/middleware/rbac.middleware.ts:PERMISSION_MAP 新增 '/api/jobs': 'system:job'
  • Task 6: 前端 API 与页面(依赖 Task 5)

    • SubTask 6.1: 新建 client/src/api/job.ts(getJobList / updateJob / changeJobStatus / runJob / getJobLogs / clearJobLogs,含 TS 类型,参考 api/log.ts 风格)
    • SubTask 6.2: 新建 client/src/views/system/job/index.vue:搜索栏(任务名称、状态)+ 任务表格(任务名/键、cron、参数摘要、状态开关、上次执行时间与状态与耗时、下次执行时间、重试配置、操作)+ 编辑弹窗(cron、按 paramFields 动态渲染的参数项、重试次数、重试间隔、备注)+ 立即执行二次确认 + 执行日志抽屉(分页表格、状态筛选、清理日志按钮),保持与 views/log/operation/index.vue 一致的页面结构
    • SubTask 6.3: 修改 client/src/router/index.ts:在系统管理下新增路由 system/job(name: SystemJob,meta: { requiresAuth: true, title: '定时任务', permission: 'system:job' })
  • Task 7: 联调与验证(依赖 Task 1-6)

    • SubTask 7.1: 后端 yarn dev 启动无编译错误;curl 验证列表/更新/启停/立即执行/日志查询/日志清理接口
    • SubTask 7.2: 将 demo_heartbeat 改为 */1 * * * * 验证「改 cron 即时生效」与「下次执行时间」更新;停用后验证不再触发、恢复启用后重新调度(首轮验证失败 → 见 Task 8;修复后复测:每分钟恰好触发 1 次、停用后 0 次触发、手动执行正常、恢复后 nextRunTime 正确)
    • SubTask 7.3: 验证重试:临时构造失败场景(如 log_clean 传入非法参数不可行时,用 retry_times/retry_interval 观测日志 trigger_type = 3 与 attempt 递增)
    • SubTask 7.4: 前端 yarn dev 启动,人工走通「编辑 → 立即执行 → 查看日志 → 清理日志」全链路,并确认侧边栏「系统管理 → 定时任务」可见(实测:侧边栏菜单可见、/system/job 正常进入、两行任务渲染正确、编辑保存成功、立即执行二次确认与成功提示、日志抽屉含手动触发的记录并带分页、清理日志二次确认成功;新标签页控制台无 error)
    • SubTask 7.5: 确认迁移脚本与 db_seed.sql 在空库/存量库两端结果一致,且 sys_menu_id_seq 为 50
  • Task 8: 修复调度器作用域缺陷(Task 7.2 首轮验证发现的真实缺陷)

    • SubTask 8.1: 定位根因 —— @Provide() 在本项目 Midway 版本中默认解析为 Request 作用域(@midwayjs/core 的 container.bind() 对 class 目标亦执行 definition.scope = options?.scope || ScopeEnum.Request),导致「启动注册定时器」与「接口 reload」落在不同服务实例上:reload 所在实例的 timers 为空,既停不掉旧定时器,又新建一个定时器,每次保存/启停都泄漏一个常驻定时器(实测同一分钟内出现 2 次以上重复触发,且停用后仍持续触发)
    • SubTask 8.2: 为 JobSchedulerService 显式声明 @Scope(ScopeEnum.Singleton),使定时器注册表在启动注册与接口重载之间共享(@InjectRepository 由单例数据源管理器解析,无需请求上下文,可安全用于单例)
    • SubTask 8.3: LogService 同为请求作用域,单例中不可直接 @Inject;改为在 buildRuntime() 内通过 this.app.createAnonymousContext().requestContext.getAsync(LogService) 按需获取(对齐 @midwayjs/cron 官方执行任务的上下文用法),注入 @App() 替换原 @Inject() logService
    • 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 可在接口契约冻结后并行