feat(plan): 生成 label_backend 完整实施规划文档
Phase 0:research.md(10项技术决策,无需澄清项) Phase 1:data-model.md(11张表+Redis结构),contracts/(8个模块API契约),quickstart.md(Docker Compose启动+流水线验证) plan.md:宪章11条全部通过,项目结构确认
This commit is contained in:
@@ -208,7 +208,7 @@ CREATE TABLE source_data (
|
||||
bucket_name VARCHAR(100) NOT NULL,
|
||||
parent_source_id BIGINT REFERENCES source_data(id), -- 视频转文本时指向原视频
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
-- PENDING / PREPROCESSING / EXTRACTING / QA_REVIEW / APPROVED / REJECTED
|
||||
-- PENDING / PREPROCESSING / EXTRACTING / QA_REVIEW / APPROVED(无 REJECTED 状态,QA 驳回作用于 annotation_task)
|
||||
reject_reason TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
@@ -961,10 +961,12 @@ public void unclaim(Long taskId) {
|
||||
| 方法 | 路径 | 最低权限 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| POST | `/api/tasks` | ADMIN | 为指定 source 创建 EXTRACTION 任务 |
|
||||
| GET | `/api/tasks/pool` | ANNOTATOR | 查看可领取任务列表(按角色过滤,分页) |
|
||||
| POST | `/api/tasks/{id}/claim` | ANNOTATOR | 领取任务(争抢式) |
|
||||
| GET | `/api/tasks/pool` | ANNOTATOR | 查看可领取任务池(UNCLAIMED 状态)。ANNOTATOR 只看到 EXTRACTION 类型;REVIEWER 只看到 SUBMITTED 状态(即审批队列,与 pending-review 等价);两者均分页,不可无界查询 |
|
||||
| POST | `/api/tasks/{id}/claim` | ANNOTATOR | 领取任务(争抢式,Redis SET NX + DB 乐观锁) |
|
||||
| POST | `/api/tasks/{id}/unclaim` | ANNOTATOR | 放弃任务,退回任务池 |
|
||||
| GET | `/api/tasks/mine` | ANNOTATOR | 查询我领取的任务列表(分页) |
|
||||
| GET | `/api/tasks/mine` | ANNOTATOR | 查询我领取的任务列表(包含 IN_PROGRESS、SUBMITTED、REJECTED 状态,分页) |
|
||||
| POST | `/api/tasks/{id}/reclaim` | ANNOTATOR | 重领被驳回的任务(task.status 必须为 REJECTED 且 claimedBy = 当前用户),状态流转 REJECTED → IN_PROGRESS |
|
||||
| GET | `/api/tasks/pending-review` | REVIEWER | 查看待我审批的任务列表(status = SUBMITTED,分页);REVIEWER 的专属审批入口 |
|
||||
| GET | `/api/tasks/{id}` | ANNOTATOR | 查看任务详情 |
|
||||
| GET | `/api/tasks` | ADMIN | 查询全部任务(支持过滤,分页) |
|
||||
| PUT | `/api/tasks/{id}/reassign` | ADMIN | 强制转移任务归属 |
|
||||
@@ -996,11 +998,16 @@ public void updateResult(Long taskId, String resultJsonStr) {
|
||||
annotationResultMapper.updateResultJson(taskId, resultJsonStr, CompanyContext.get());
|
||||
}
|
||||
|
||||
// 审批通过——级联触发,必须在同一事务内完成
|
||||
// 审批通过——两阶段:事务内完成同步步骤,事务提交后异步触发 QA 生成
|
||||
@Transactional
|
||||
@OperationLog(type = "EXTRACTION_APPROVE")
|
||||
public void approve(Long taskId) {
|
||||
AnnotationTask task = validateAndGetTask(taskId, "SUBMITTED");
|
||||
|
||||
// 自审校验:提交者不能审批自己的任务
|
||||
if (task.getClaimedBy().equals(getCurrentUserId()))
|
||||
throw new BusinessException("SELF_REVIEW_FORBIDDEN", "不允许审批自己提交的任务");
|
||||
|
||||
AnnotationResult result = annotationResultMapper.selectByTaskId(taskId);
|
||||
|
||||
// 1. annotation_result.is_final = true
|
||||
@@ -1016,22 +1023,55 @@ public void approve(Long taskId) {
|
||||
// 3. 写入任务历史
|
||||
insertHistory(taskId, "SUBMITTED", "APPROVED", getCurrentUserId(), null);
|
||||
|
||||
// 4. 调用 AI 生成候选问答对
|
||||
String promptKey = "IMAGE".equals(getSourceType(task)) ? "prompt_qa_gen_image" : "prompt_qa_gen_text";
|
||||
// 4. 发布领域事件,事务提交后异步执行 QA 生成(步骤 5-7)
|
||||
// 注:AI HTTP 调用禁止在 @Transactional 内同步执行——会占用数据库连接直至 AI 响应,
|
||||
// 且 AI 失败会错误地回滚已完成的审批。
|
||||
// 使用 @TransactionalEventListener(phase = AFTER_COMMIT) 保证先提交再触发。
|
||||
eventPublisher.publishEvent(new ExtractionApprovedEvent(taskId, task.getSourceId(),
|
||||
getSourceType(task), CompanyContext.get()));
|
||||
}
|
||||
|
||||
// 驳回——状态回退,标注员可重领
|
||||
@Transactional
|
||||
@OperationLog(type = "EXTRACTION_REJECT")
|
||||
public void reject(Long taskId, String reason) {
|
||||
AnnotationTask task = validateAndGetTask(taskId, "SUBMITTED");
|
||||
|
||||
// 自审校验
|
||||
if (task.getClaimedBy().equals(getCurrentUserId()))
|
||||
throw new BusinessException("SELF_REVIEW_FORBIDDEN", "不允许驳回自己提交的任务");
|
||||
|
||||
StateValidator.assertTransition(TaskStatus.SUBMITTED, TaskStatus.REJECTED, TaskStatus.TRANSITIONS);
|
||||
task.setStatus("REJECTED");
|
||||
taskMapper.updateById(task);
|
||||
insertHistory(taskId, "SUBMITTED", "REJECTED", getCurrentUserId(), reason);
|
||||
// source_data.status 保持 EXTRACTING 不变,待标注员重新提交后再推进
|
||||
}
|
||||
|
||||
// ExtractionApprovedEventListener(@TransactionalEventListener,独立事务)
|
||||
// 负责 5-7 步:AI 调用 → 写 training_dataset → 创建 QA 任务 → 更新 source_data
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void onExtractionApproved(ExtractionApprovedEvent event) {
|
||||
AnnotationTask task = taskMapper.selectById(event.getTaskId());
|
||||
AnnotationResult result = annotationResultMapper.selectByTaskId(event.getTaskId());
|
||||
|
||||
// 5. 调用 AI 生成候选问答对(在事务外执行,失败不影响审批结果)
|
||||
String promptKey = "IMAGE".equals(event.getSourceType()) ? "prompt_qa_gen_image" : "prompt_qa_gen_text";
|
||||
String promptTemplate = sysConfigService.get(promptKey);
|
||||
QaGenResponse qaResponse = generateQa(task, result, promptTemplate);
|
||||
|
||||
// 5. 将候选问答对写入 training_dataset(PENDING_REVIEW)
|
||||
// 6. 将候选问答对写入 training_dataset(PENDING_REVIEW)
|
||||
List<TrainingDataset> samples = buildTrainingSamples(task, result, qaResponse);
|
||||
trainingDatasetMapper.batchInsert(samples);
|
||||
|
||||
// 6. 创建 QA_GENERATION 阶段任务(UNCLAIMED)
|
||||
// 7. 创建 QA_GENERATION 阶段任务(UNCLAIMED)
|
||||
AnnotationTask qaTask = buildQaTask(task);
|
||||
taskMapper.insert(qaTask);
|
||||
insertHistory(qaTask.getId(), null, "UNCLAIMED", getCurrentUserId(), null);
|
||||
insertHistory(qaTask.getId(), null, "UNCLAIMED", task.getClaimedBy(), null);
|
||||
|
||||
// 7. source_data.status → QA_REVIEW
|
||||
sourceDataMapper.updateStatus(task.getSourceId(), "QA_REVIEW", CompanyContext.get());
|
||||
// 8. source_data.status → QA_REVIEW
|
||||
sourceDataMapper.updateStatus(event.getSourceId(), "QA_REVIEW", event.getCompanyId());
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1057,21 +1097,48 @@ public void approve(Long taskId) {
|
||||
@Transactional
|
||||
@OperationLog(type = "QA_APPROVE")
|
||||
public void approve(Long taskId) {
|
||||
// 1. training_dataset.status → APPROVED
|
||||
// 1. 先校验任务合法性(必须在任何 DB 写入之前执行,避免校验失败时数据已被修改)
|
||||
AnnotationTask task = validateAndGetTask(taskId, "SUBMITTED");
|
||||
|
||||
// 自审校验:提交者不能审批自己的任务
|
||||
if (task.getClaimedBy().equals(getCurrentUserId()))
|
||||
throw new BusinessException("SELF_REVIEW_FORBIDDEN", "不允许审批自己提交的任务");
|
||||
|
||||
// 2. training_dataset.status → APPROVED
|
||||
trainingDatasetMapper.approveByTaskId(taskId, getCurrentUserId(), CompanyContext.get());
|
||||
|
||||
// 2. annotation_task.status → APPROVED
|
||||
AnnotationTask task = validateAndGetTask(taskId, "SUBMITTED");
|
||||
// 3. annotation_task.status → APPROVED
|
||||
StateValidator.assertTransition(TaskStatus.SUBMITTED, TaskStatus.APPROVED, TaskStatus.TRANSITIONS);
|
||||
task.setStatus("APPROVED");
|
||||
task.setCompletedAt(LocalDateTime.now());
|
||||
taskMapper.updateById(task);
|
||||
|
||||
// 3. source_data.status → APPROVED(整条流水线完成)
|
||||
// 4. source_data.status → APPROVED(整条流水线完成)
|
||||
sourceDataMapper.updateStatus(task.getSourceId(), "APPROVED", CompanyContext.get());
|
||||
|
||||
// 4. 写入任务历史
|
||||
// 5. 写入任务历史
|
||||
insertHistory(taskId, "SUBMITTED", "APPROVED", getCurrentUserId(), null);
|
||||
}
|
||||
|
||||
// 驳回问答对——任务退回 IN_PROGRESS,training_dataset 删除候选记录
|
||||
@Transactional
|
||||
@OperationLog(type = "QA_REJECT")
|
||||
public void reject(Long taskId, String reason) {
|
||||
AnnotationTask task = validateAndGetTask(taskId, "SUBMITTED");
|
||||
|
||||
// 自审校验
|
||||
if (task.getClaimedBy().equals(getCurrentUserId()))
|
||||
throw new BusinessException("SELF_REVIEW_FORBIDDEN", "不允许驳回自己提交的任务");
|
||||
|
||||
// 删除本次生成的候选问答对(PENDING_REVIEW 状态),待标注员修改后重新提交
|
||||
trainingDatasetMapper.deleteByTaskId(taskId, CompanyContext.get());
|
||||
|
||||
StateValidator.assertTransition(TaskStatus.SUBMITTED, TaskStatus.REJECTED, TaskStatus.TRANSITIONS);
|
||||
task.setStatus("REJECTED");
|
||||
taskMapper.updateById(task);
|
||||
insertHistory(taskId, "SUBMITTED", "REJECTED", getCurrentUserId(), reason);
|
||||
// source_data.status 保持 QA_REVIEW 不变
|
||||
}
|
||||
```
|
||||
|
||||
**接口清单:**
|
||||
@@ -1241,14 +1308,15 @@ public final class StateValidator {
|
||||
|
||||
```java
|
||||
public enum SourceStatus {
|
||||
PENDING, PREPROCESSING, EXTRACTING, QA_REVIEW, APPROVED, REJECTED;
|
||||
PENDING, PREPROCESSING, EXTRACTING, QA_REVIEW, APPROVED;
|
||||
// 注:source_data 无 REJECTED 状态。QA 阶段驳回的是 annotation_task(→ REJECTED),
|
||||
// 不改变 source_data.status(保持 QA_REVIEW);重新提交后 source_data 随任务推进。
|
||||
|
||||
public static final Map<SourceStatus, Set<SourceStatus>> TRANSITIONS = Map.of(
|
||||
PENDING, Set.of(EXTRACTING, PREPROCESSING),
|
||||
PREPROCESSING, Set.of(PENDING),
|
||||
EXTRACTING, Set.of(QA_REVIEW),
|
||||
QA_REVIEW, Set.of(APPROVED, REJECTED),
|
||||
REJECTED, Set.of(EXTRACTING) // 驳回后可重提
|
||||
QA_REVIEW, Set.of(APPROVED)
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1263,7 +1331,7 @@ public enum TaskStatus {
|
||||
UNCLAIMED, Set.of(IN_PROGRESS),
|
||||
IN_PROGRESS, Set.of(SUBMITTED, UNCLAIMED, IN_PROGRESS),
|
||||
// IN_PROGRESS → IN_PROGRESS 用于 ADMIN 强制转移(持有人变更,状态不变)
|
||||
SUBMITTED, Set.oAPPROVED, REJECTED),
|
||||
SUBMITTED, Set.of(APPROVED, REJECTED),
|
||||
REJECTED, Set.of(IN_PROGRESS) // 驳回后重拾
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user