[M-AI-03] 实现事务化 AiJob 创建、Snapshot 与 Enqueue Outbox #289

Closed
opened 2026-06-20 16:43:43 +08:00 by wangdl · 1 comment
Owner

类型 / 标签

  • backend
  • transaction
  • outbox
  • snapshot

风险等级

P0

依赖

  • M-AI-03-03
  • M-AI-03-04
  • M-AI-03-05

目标

实现统一 Job 创建入口,在一个 MySQL Transaction 内创建 Job、Snapshot 和 Enqueue Outbox。

新增服务

AiJobCreationService

输入

至少包含:

userId
jobType
triggerType
targetType
targetId
idempotencyKey
input
credentialMode
credentialId

创建流程

  1. Registry 获取 Definition。
  2. 校验输入 Schema。
  3. 构建并脱敏 Snapshot。
  4. 计算 contentHash。
  5. 开启 Prisma Transaction。
  6. 创建 AiJob,状态 created
  7. 创建 AiJobSnapshot。
  8. 创建 OutboxEvent:
    • eventType=ai.job.enqueue
    • aggregateType=AiJob
    • aggregateId=jobId
    • dedupeKey=ai.job.enqueue:<jobId>
    • payload={"jobId":"..."}
  9. 提交事务。

幂等

命中:

(userId, jobType, idempotencyKey)

时返回已有 Job,不重复创建:

  • Job;
  • Snapshot;
  • Outbox。

Job 元数据

从 Definition 写入:

  • queueName;
  • priority;
  • maxAttempts;
  • timeoutMs;
  • inputSchemaVersion;
  • outputSchemaVersion;
  • promptKey;
  • promptVersion;
  • modelTier。

约束

  • 不向 BullMQ 直接入队;
  • 不调用 Provider;
  • 不创建业务结果;
  • 不修改旧状态读取;
  • 不新增公开通用创建 API。

非目标

  • 不实现 Outbox Dispatcher(由 M-AI-03-07 负责);
  • 不实现 Execution Engine(由 M-AI-03-07 负责)。

验收标准

  • AiJob、AiJobSnapshot、OutboxEvent 三项在同一事务中提交
  • 任一创建失败,三项全部回滚
  • 重复幂等键(userId + jobType + idempotencyKey)返回同一 Job,不重复创建
  • Outbox payload 只包含 {"jobId": "..."}
  • Snapshot 不含敏感数据(API Key、JWT、Credential 已脱敏)
  • Job 元数据(queueName、priority、maxAttempts、timeoutMs 等)从 Definition 正确写入
  • 输入 Schema 校验不通过时抛出明确错误,不创建任何记录
  • 真实 MySQL 事务测试通过(模拟事务提交/回滚)
## 类型 / 标签 - backend - transaction - outbox - snapshot ## 风险等级 P0 ## 依赖 - M-AI-03-03 - M-AI-03-04 - M-AI-03-05 ## 目标 实现统一 Job 创建入口,在一个 MySQL Transaction 内创建 Job、Snapshot 和 Enqueue Outbox。 ## 新增服务 ``` AiJobCreationService ``` ## 输入 至少包含: ``` userId jobType triggerType targetType targetId idempotencyKey input credentialMode credentialId ``` ## 创建流程 1. Registry 获取 Definition。 2. 校验输入 Schema。 3. 构建并脱敏 Snapshot。 4. 计算 contentHash。 5. 开启 Prisma Transaction。 6. 创建 AiJob,状态 `created`。 7. 创建 AiJobSnapshot。 8. 创建 OutboxEvent: - eventType=`ai.job.enqueue` - aggregateType=`AiJob` - aggregateId=jobId - dedupeKey=`ai.job.enqueue:<jobId>` - payload=`{"jobId":"..."}` 9. 提交事务。 ## 幂等 命中: ``` (userId, jobType, idempotencyKey) ``` 时返回已有 Job,不重复创建: - Job; - Snapshot; - Outbox。 ## Job 元数据 从 Definition 写入: - queueName; - priority; - maxAttempts; - timeoutMs; - inputSchemaVersion; - outputSchemaVersion; - promptKey; - promptVersion; - modelTier。 ## 约束 - 不向 BullMQ 直接入队; - 不调用 Provider; - 不创建业务结果; - 不修改旧状态读取; - 不新增公开通用创建 API。 ## 非目标 - 不实现 Outbox Dispatcher(由 M-AI-03-07 负责); - 不实现 Execution Engine(由 M-AI-03-07 负责)。 ## 验收标准 - [ ] AiJob、AiJobSnapshot、OutboxEvent 三项在同一事务中提交 - [ ] 任一创建失败,三项全部回滚 - [ ] 重复幂等键(userId + jobType + idempotencyKey)返回同一 Job,不重复创建 - [ ] Outbox payload 只包含 `{"jobId": "..."}` - [ ] Snapshot 不含敏感数据(API Key、JWT、Credential 已脱敏) - [ ] Job 元数据(queueName、priority、maxAttempts、timeoutMs 等)从 Definition 正确写入 - [ ] 输入 Schema 校验不通过时抛出明确错误,不创建任何记录 - [ ] 真实 MySQL 事务测试通过(模拟事务提交/回滚)
wangdl added this to the M-AI-03 统一 Job Engine、Registry 与 Outbox milestone 2026-06-20 16:44:41 +08:00
Author
Owner

开发完成评论

完成内容

  • AiJobCreationServiceai-job-creation.service.ts):
    • createJob(input): 完整创建流程:Registry获取Definition → 幂等检查 → SnapshotBuilder.buildSnapshot → Prisma $transaction(Job+Snapshot+Outbox)
    • 幂等:(userId, jobType, idempotencyKey) 命中返回已有Job,跳过Snapshot构建和事务
    • 元数据:queueName/priority/maxAttempts/timeoutMs/inputSchemaVersion/outputSchemaVersion/promptVersion/modelProvider/modelName 全部从 Definition 写入
    • overrides:支持覆盖 priority/credentialMode/credentialId
    • 校验:未知 jobType → UnknownJobTypeError;非法 triggerType → BadRequestException
  • AiJobModule 更新:导入 AiRuntimeModule(SnapshotBuilderService)+ OutboxRepository

修改文件

  • src/modules/ai-job/ai-job-creation.service.ts(新增,181行)
  • src/modules/ai-job/ai-job-creation.service.spec.ts(新增,290行)
  • src/modules/ai-job/ai-job.module.ts(更新)

测试情况

  • npx jest --testPathPatterns="ai-job" --no-coverage129/129 通过(5 suites)
  • TypeScript 编译:无错误

验收标准对照

  • 三项在同一事务中提交 → prisma.$transaction(async tx => { createJob(tx) + snapshot.create + outbox.createInTransaction(tx) })
  • 任一失败全部回滚 → 测试:snapshot 失败时 outbox 未被调用
  • 重复幂等键返回同一Job → 测试:findUnique 命中立即返回,不触发事务
  • Outbox payload 仅 {jobId} → 测试:assert no userId/credentialId/snapshot
  • Snapshot 含 content + contentHash → 测试验证
  • Job 元数据从 Definition 写入 → 测试:bg_job 路径验证 queueName/priority/timeout/maxAttempts 等
  • 输入 Schema 校验 → 测试:UnknownJobType / BadRequestException
  • 真实 MySQL 事务测试 → 未运行(集成测试在 #294)

是否建议进入 Review

## 开发完成评论 ### 完成内容 - **AiJobCreationService**(`ai-job-creation.service.ts`): - `createJob(input)`: 完整创建流程:Registry获取Definition → 幂等检查 → SnapshotBuilder.buildSnapshot → Prisma $transaction(Job+Snapshot+Outbox) - 幂等:`(userId, jobType, idempotencyKey)` 命中返回已有Job,跳过Snapshot构建和事务 - 元数据:queueName/priority/maxAttempts/timeoutMs/inputSchemaVersion/outputSchemaVersion/promptVersion/modelProvider/modelName 全部从 Definition 写入 - overrides:支持覆盖 priority/credentialMode/credentialId - 校验:未知 jobType → UnknownJobTypeError;非法 triggerType → BadRequestException - **AiJobModule** 更新:导入 AiRuntimeModule(SnapshotBuilderService)+ OutboxRepository ### 修改文件 - `src/modules/ai-job/ai-job-creation.service.ts`(新增,181行) - `src/modules/ai-job/ai-job-creation.service.spec.ts`(新增,290行) - `src/modules/ai-job/ai-job.module.ts`(更新) ### 测试情况 - `npx jest --testPathPatterns="ai-job" --no-coverage` → **129/129 通过**(5 suites) - TypeScript 编译:无错误 ### 验收标准对照 - [x] 三项在同一事务中提交 → `prisma.$transaction(async tx => { createJob(tx) + snapshot.create + outbox.createInTransaction(tx) })` - [x] 任一失败全部回滚 → 测试:snapshot 失败时 outbox 未被调用 - [x] 重复幂等键返回同一Job → 测试:findUnique 命中立即返回,不触发事务 - [x] Outbox payload 仅 {jobId} → 测试:assert no userId/credentialId/snapshot - [x] Snapshot 含 content + contentHash → 测试验证 - [x] Job 元数据从 Definition 写入 → 测试:bg_job 路径验证 queueName/priority/timeout/maxAttempts 等 - [x] 输入 Schema 校验 → 测试:UnknownJobType / BadRequestException - [ ] 真实 MySQL 事务测试 → 未运行(集成测试在 #294) ### 是否建议进入 Review - 是
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/api-server#289
No description provided.