2026-04-09 15:36:11 +08:00
|
|
|
|
package com.label.integration;
|
|
|
|
|
|
|
|
|
|
|
|
import com.label.AbstractIntegrationTest;
|
2026-04-14 13:31:50 +08:00
|
|
|
|
import com.label.module.user.dto.LoginRequest;
|
2026-04-09 15:36:11 +08:00
|
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
|
import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
|
|
|
|
|
import org.springframework.http.*;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 提取阶段审批集成测试(US4)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 测试场景:
|
|
|
|
|
|
* 1. 审批通过 → QA_GENERATION 任务自动创建,source_data 状态更新为 QA_REVIEW
|
|
|
|
|
|
* 2. 审批人与提交人相同(自审)→ 403 SELF_REVIEW_FORBIDDEN
|
|
|
|
|
|
* 3. 驳回后标注员可重领任务并再次提交
|
2026-04-09 15:36:11 +08:00
|
|
|
|
*/
|
|
|
|
|
|
public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private TestRestTemplate restTemplate;
|
|
|
|
|
|
|
|
|
|
|
|
private Long sourceId;
|
|
|
|
|
|
private Long taskId;
|
|
|
|
|
|
private Long annotatorUserId;
|
|
|
|
|
|
private Long reviewerUserId;
|
|
|
|
|
|
|
|
|
|
|
|
@BeforeEach
|
|
|
|
|
|
void setup() {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 获取种子用户 ID(init.sql 中已插入)
|
|
|
|
|
|
annotatorUserId = jdbcTemplate.queryForObject(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
"SELECT id FROM sys_user WHERE username = 'annotator01'", Long.class);
|
|
|
|
|
|
reviewerUserId = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT id FROM sys_user WHERE username = 'reviewer01'", Long.class);
|
|
|
|
|
|
Long companyId = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT id FROM sys_company WHERE company_code = 'DEMO'", Long.class);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 插入测试 source_data
|
2026-04-09 15:36:11 +08:00
|
|
|
|
jdbcTemplate.execute(
|
|
|
|
|
|
"INSERT INTO source_data (company_id, uploader_id, data_type, file_path, " +
|
|
|
|
|
|
"file_name, file_size, bucket_name, status) " +
|
|
|
|
|
|
"VALUES (" + companyId + ", " + annotatorUserId + ", 'TEXT', " +
|
|
|
|
|
|
"'test/approval-test/file.txt', 'file.txt', 100, 'test-bucket', 'PENDING')");
|
|
|
|
|
|
sourceId = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT id FROM source_data ORDER BY id DESC LIMIT 1", Long.class);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 插入 UNCLAIMED EXTRACTION 任务
|
2026-04-09 15:36:11 +08:00
|
|
|
|
jdbcTemplate.execute(
|
|
|
|
|
|
"INSERT INTO annotation_task (company_id, source_id, task_type, status) " +
|
|
|
|
|
|
"VALUES (" + companyId + ", " + sourceId + ", 'EXTRACTION', 'UNCLAIMED')");
|
|
|
|
|
|
taskId = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT id FROM annotation_task ORDER BY id DESC LIMIT 1", Long.class);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 测试 1: 审批通过 → QA 任务自动创建 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-04-14 13:31:50 +08:00
|
|
|
|
@DisplayName("审批通过后,QA_GENERATION 任务自动创建,source_data 状态变为 QA_REVIEW")
|
2026-04-09 15:36:11 +08:00
|
|
|
|
void approveTask_thenQaTaskAndSourceStatusUpdated() {
|
|
|
|
|
|
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
|
|
|
|
|
|
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 1. 标注员领取任务
|
|
|
|
|
|
ResponseEntity<Map> claimResp = restTemplate.exchange(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
baseUrl("/api/tasks/" + taskId + "/claim"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
assertThat(claimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 2. 标注员提交标注
|
|
|
|
|
|
ResponseEntity<Map> submitResp = restTemplate.exchange(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
baseUrl("/api/extraction/" + taskId + "/submit"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
assertThat(submitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 3. 审核员审批通过
|
|
|
|
|
|
// 注:ExtractionApprovedEventListener(@TransactionalEventListener AFTER_COMMIT)
|
|
|
|
|
|
// 在同一线程中同步执行,HTTP 响应返回前已完成后续处理
|
2026-04-09 15:36:11 +08:00
|
|
|
|
ResponseEntity<Map> approveResp = restTemplate.exchange(
|
|
|
|
|
|
baseUrl("/api/extraction/" + taskId + "/approve"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(reviewerToken), Map.class);
|
|
|
|
|
|
assertThat(approveResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:原任务状态变为 APPROVED,is_final=true
|
2026-04-09 15:36:11 +08:00
|
|
|
|
Map<String, Object> taskRow = jdbcTemplate.queryForMap(
|
|
|
|
|
|
"SELECT status, is_final FROM annotation_task WHERE id = ?", taskId);
|
|
|
|
|
|
assertThat(taskRow.get("status")).isEqualTo("APPROVED");
|
|
|
|
|
|
assertThat(taskRow.get("is_final")).isEqualTo(Boolean.TRUE);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:QA_GENERATION 任务已自动创建(UNCLAIMED 状态)
|
2026-04-09 15:36:11 +08:00
|
|
|
|
Integer qaTaskCount = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT COUNT(*) FROM annotation_task " +
|
|
|
|
|
|
"WHERE source_id = ? AND task_type = 'QA_GENERATION' AND status = 'UNCLAIMED'",
|
|
|
|
|
|
Integer.class, sourceId);
|
2026-04-14 13:31:50 +08:00
|
|
|
|
assertThat(qaTaskCount).as("QA_GENERATION 任务应已创建").isEqualTo(1);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:source_data 状态已更新为 QA_REVIEW
|
2026-04-09 15:36:11 +08:00
|
|
|
|
String sourceStatus = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT status FROM source_data WHERE id = ?", String.class, sourceId);
|
2026-04-14 13:31:50 +08:00
|
|
|
|
assertThat(sourceStatus).as("source_data 状态应为 QA_REVIEW").isEqualTo("QA_REVIEW");
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:training_dataset 已以 PENDING_REVIEW 状态创建
|
|
|
|
|
|
Integer datasetCount = jdbcTemplate.queryForObject(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
"SELECT COUNT(*) FROM training_dataset " +
|
|
|
|
|
|
"WHERE source_id = ? AND status = 'PENDING_REVIEW'",
|
|
|
|
|
|
Integer.class, sourceId);
|
2026-04-14 13:31:50 +08:00
|
|
|
|
assertThat(datasetCount).as("training_dataset 应已创建").isEqualTo(1);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 测试 2: 自审返回 403 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-04-14 13:31:50 +08:00
|
|
|
|
@DisplayName("审批人与任务领取人相同(自审)→ 403 SELF_REVIEW_FORBIDDEN")
|
2026-04-09 15:36:11 +08:00
|
|
|
|
void approveOwnSubmission_returnsForbidden() {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 直接将任务置为 SUBMITTED 并设 claimed_by = reviewer01(模拟自审场景)
|
2026-04-09 15:36:11 +08:00
|
|
|
|
jdbcTemplate.execute(
|
|
|
|
|
|
"UPDATE annotation_task " +
|
|
|
|
|
|
"SET status = 'SUBMITTED', claimed_by = " + reviewerUserId +
|
|
|
|
|
|
", claimed_at = NOW(), submitted_at = NOW() " +
|
|
|
|
|
|
"WHERE id = " + taskId);
|
|
|
|
|
|
|
|
|
|
|
|
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
|
|
|
|
|
|
|
|
|
|
|
|
ResponseEntity<Map> resp = restTemplate.exchange(
|
|
|
|
|
|
baseUrl("/api/extraction/" + taskId + "/approve"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(reviewerToken), Map.class);
|
|
|
|
|
|
|
|
|
|
|
|
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证任务状态未变
|
|
|
|
|
|
String status = jdbcTemplate.queryForObject(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
|
|
|
|
|
|
assertThat(status).isEqualTo("SUBMITTED");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 测试 3: 驳回 → 重领 → 再提交 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-04-14 13:31:50 +08:00
|
|
|
|
@DisplayName("驳回后标注员可重领任务并再次提交,任务状态恢复为 SUBMITTED")
|
2026-04-09 15:36:11 +08:00
|
|
|
|
void rejectThenReclaimAndResubmit_succeeds() {
|
|
|
|
|
|
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
|
|
|
|
|
|
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 1. 标注员领取并提交
|
2026-04-09 15:36:11 +08:00
|
|
|
|
restTemplate.exchange(baseUrl("/api/tasks/" + taskId + "/claim"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
restTemplate.exchange(baseUrl("/api/extraction/" + taskId + "/submit"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 2. 审核员驳回(驳回原因必填)
|
|
|
|
|
|
HttpHeaders rejectHeaders = new HttpHeaders();
|
2026-04-09 15:36:11 +08:00
|
|
|
|
rejectHeaders.set("Authorization", "Bearer " + reviewerToken);
|
|
|
|
|
|
rejectHeaders.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
|
HttpEntity<Map<String, String>> rejectReq = new HttpEntity<>(
|
2026-04-14 13:31:50 +08:00
|
|
|
|
Map.of("reason", "实体识别有误,请重新标注"), rejectHeaders);
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
ResponseEntity<Map> rejectResp = restTemplate.exchange(
|
|
|
|
|
|
baseUrl("/api/extraction/" + taskId + "/reject"),
|
|
|
|
|
|
HttpMethod.POST, rejectReq, Map.class);
|
|
|
|
|
|
assertThat(rejectResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:任务状态变为 REJECTED
|
2026-04-09 15:36:11 +08:00
|
|
|
|
String statusAfterReject = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
|
|
|
|
|
|
assertThat(statusAfterReject).isEqualTo("REJECTED");
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 3. 标注员重领任务(REJECTED → IN_PROGRESS)
|
|
|
|
|
|
ResponseEntity<Map> reclaimResp = restTemplate.exchange(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
baseUrl("/api/tasks/" + taskId + "/reclaim"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
assertThat(reclaimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:任务状态恢复为 IN_PROGRESS
|
2026-04-09 15:36:11 +08:00
|
|
|
|
String statusAfterReclaim = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
|
|
|
|
|
|
assertThat(statusAfterReclaim).isEqualTo("IN_PROGRESS");
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 4. 标注员再次提交(IN_PROGRESS → SUBMITTED)
|
|
|
|
|
|
ResponseEntity<Map> resubmitResp = restTemplate.exchange(
|
2026-04-09 15:36:11 +08:00
|
|
|
|
baseUrl("/api/extraction/" + taskId + "/submit"),
|
|
|
|
|
|
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
|
|
|
|
|
|
assertThat(resubmitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 验证:任务状态变为 SUBMITTED
|
2026-04-09 15:36:11 +08:00
|
|
|
|
String finalStatus = jdbcTemplate.queryForObject(
|
|
|
|
|
|
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
|
|
|
|
|
|
assertThat(finalStatus).isEqualTo("SUBMITTED");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// ------------------------------------------------------------------ 工具方法 --
|
2026-04-09 15:36:11 +08:00
|
|
|
|
|
|
|
|
|
|
private String loginAndGetToken(String companyCode, String username, String password) {
|
|
|
|
|
|
LoginRequest req = new LoginRequest();
|
|
|
|
|
|
req.setCompanyCode(companyCode);
|
|
|
|
|
|
req.setUsername(username);
|
|
|
|
|
|
req.setPassword(password);
|
|
|
|
|
|
ResponseEntity<Map> response = restTemplate.postForEntity(
|
|
|
|
|
|
baseUrl("/api/auth/login"), req, Map.class);
|
|
|
|
|
|
if (!response.getStatusCode().is2xxSuccessful()) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
|
|
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
|
|
|
|
|
|
return (String) data.get("token");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private HttpEntity<Void> bearerRequest(String token) {
|
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
|
headers.set("Authorization", "Bearer " + token);
|
|
|
|
|
|
return new HttpEntity<>(headers);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|