后台作业控制面横跨三份事实:代码生成的 IBackgroundJobCatalog、SysBackgroundJobDefinition 持久化定义,以及 JobHost/Api fallback 的实际调度状态。Operations 已能修改持久化定义并立即执行,但查询和调度收敛仍不能简单视为一个强一致系统。
1. 三份事实
代码目录拥有 JobName、Source、Description、ModuleName、IsCritical 等声明;持久化定义拥有运行时 Cron/Interval/Enabled;调度宿主消费持久化状态。
2. 查询的当前事实源
OperationsService.GetJobScheduleAsync 直接遍历 _jobCatalog.Declarations,按 JobName 排序并投影。它不读取 IBackgroundJobDefinitionStore。
因此 PUT/stop 成功后,GET /api/operations/jobs 仍可能展示代码目录默认值。现有测试名和注释声称返回持久化值,但 Handler 只是代理 IOperationsService,没有证明真实服务读取 Store。
这是文档必须明确的源码/测试意图漂移。
3. 调度表示
DTO 的字段名仍是 CronExpression:
- Cron 作业返回原表达式;
- Interval 作业返回
interval:{seconds}s; - 无调度返回 null。
客户端必须按 interval: 前缀区分,而不能把所有非空字符串交给 Cron parser。
4. 持久化聚合不变量
BackgroundJobDefinition 直接承载编译期 ORM metadata,表名 SysBackgroundJobDefinition,JobName 唯一且支持软删。
运行时调度规则:
- Cron 与 Interval 不能同时设置;
- Interval 必须为正数;
- Enabled=true 时至少有一种调度;
- Disabled 时可无调度;
- Cron 当前只做 trim 和 160 长度限制,不验证语法。
5. 更新调度
var command = new UpdateBackgroundJobDefinition.Command( JobName: "export-execution", CronExpression: null, IntervalSeconds: 45, Enabled: true);
// ① Store 先按稳定 JobName 读取非软删定义。// ② 聚合一次性验证并替换三项运行时字段。// ③ SaveAsync 加入当前工作单元,返回 interval:45s 投影。Result<JobScheduleEntry> result = await mediator.Send(command, cancellationToken);Update 不允许创建任意 JobName。定义必须先由代码目录和 seed 进入数据库。
6. 启停不会清空调度
SetBackgroundJobEnabled 读取当前定义,保留 Cron/Interval,只修改 Enabled。
start 可能失败:一个 disabled 且没有任何调度的定义不能被直接启用,需先 PUT 一个有效调度。
7. HTTP 示例
POST /api/operations/jobs/workflow-timer/stopAuthorization: Bearer <token-with-operations.jobs.manage>PUT /api/operations/jobs/export-executionContent-Type: application/jsonAuthorization: Bearer <token-with-operations.jobs.manage>
{ "cronExpression": null, "intervalSeconds": 60, "enabled": true}手写端点组为这些写路由统一启用认证、userPolicy 限流和精确管理权限。
8. 立即执行路径
ExecuteBackgroundJob.Command 实现 INonTransactionalCommand,避免把长时间外部作业包进通用数据库事务。
Handler 先确认定义存在,再把稳定 JobName 交给 IJobExecutorDispatcher,并用 IBackgroundJobExecutionAuditor 包裹执行。
9. 分派器的启动期闭包
JobExecutorDispatcher 构造时验证:
- 同一 JobName 没有重复 binding;
- Catalog 每个声明都有 binding;
- 没有 Catalog 之外的多余 binding。
不满足时抛 InvalidOperationException,使错误尽早暴露。执行期只访问已经验证的字典,不动态扫描 DI。
10. 执行审计语义
审计信封记录开始/结束、成功/失败、错误码与 correlation。业务 Result 失败原样返回;异常和取消先尝试写失败审计,再重新抛出。
审计 sink 失败只记录 warning,不改变作业结果。这是可用性优先的 best-effort 选择,合规环境需要额外的审计健康门禁。
11. 修改调度的审计缺口
Update/start/stop 没有显式活动审计;它们依赖通用命令审计是否捕获足够 before/after 信息。当前说明书不能承诺每次调度变化都保存旧值、新值、操作者和原因。
管理命令也没有审批、变更原因、版本或并发令牌。两个管理员最后写入者获胜。
12. 多实例传播
Store 更新成功只表示定义持久化。是否以及何时影响 Quartz 或 API fallback,取决于各宿主的 reload/poll 机制。
Operations 响应没有返回调度器实例确认、配置版本或生效时间。商业控制面需要“desired state → observed state”模型,而不只是一次数据库写入。
13. 手动执行与 Enabled
Handler 只确认定义存在,不检查 Enabled。因此停止自动调度不一定禁止管理员手动 execute。
这是合理但必须显式的产品语义:Enabled 表示自动调度开关,不是作业能力总开关。若需要熔断,应增加独立 suspension/kill-switch 状态。
14. 故障诊断
NotFound:检查代码 Catalog、seed step、软删状态和目标环境数据库。
ScheduleConflict:请求同时提供 Cron 与 Interval。
ScheduleRequired:尝试启用无调度定义。
立即执行失败:先看 BackgroundJob 审计记录,再定位 binding 的业务错误;审计缺失还要检查 sink 健康。
GET 与 PUT 响应不一致:当前 GET 投影 Catalog,这是已知边界,不应反复刷新掩盖。
15. 测试重点
已有测试覆盖调度类型切换、无效更新不写 Store、启停保留调度、Manage 授权、立即执行成功/业务失败/异常/取消审计、API 无权限 403 与管理路由。
仍需补:真实 Service 的 GET-after-PUT、Cron 语法、并发更新、调度器收敛、多实例 reload、审计 sink 故障告警、disabled 手动执行策略、作业超时与取消。
16. GA 补全顺序
- GET 读取持久化 desired state,并同时展示 Catalog default;
- 增加 observed scheduler state 与实例心跳;
- 定义版本/ETag,拒绝丢失更新;
- 结构化 Cron 验证与下一次执行时间预览;
- 调度变更 before/after 审计、原因与可选审批;
- 发布 reload 事件并等待实例确认;
- 手动执行幂等键、超时、取消与并发策略;
- 建立 JobHost 和 API fallback 的同一合同套件。
17. 测试命令
# Application 行为。dotnet test tests/BitzOrcas.Application.Tests \ --filter FullyQualifiedName~BackgroundJobManagementTests
# HTTP 管理表面。dotnet test tests/BitzOrcas.Integration.Tests \ --filter FullyQualifiedName~BackgroundJobManagementApiTests
# Catalog/binding/Host 完整性。dotnet test tests/BitzOrcas.Architecture.Tests \ --filter 'FullyQualifiedName~BackgroundJobIntakeTests|FullyQualifiedName~OperationsRuntimeSurfaceTests'