refactor: flatten dto entity and mapper packages

This commit is contained in:
wh
2026-04-14 13:28:10 +08:00
parent 0af19cf1b5
commit 29766ebd28
64 changed files with 1524 additions and 1780 deletions

View File

@@ -2,7 +2,7 @@ package com.label.integration;
import com.label.AbstractIntegrationTest;
import com.label.common.result.Result;
import com.label.module.user.dto.LoginRequest;
import com.label.dto.LoginRequest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,26 +15,22 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 认证流程集成测试US1
* 璁よ瘉娴佺▼闆嗘垚娴嬭瘯锛圲S1锛夈€? *
* 娴嬭瘯鍦烘櫙锛? * 1. 姝g‘瀵嗙爜鐧诲綍 鈫?杩斿洖 token
* 2. 閿欒瀵嗙爜鐧诲綍 鈫?401
* 3. 涓嶅瓨鍦ㄧ殑鍏徃浠g爜 鈫?401
* 4. 鏈夋晥 Token 璁块棶 /api/auth/me 鈫?200锛岃繑鍥炵敤鎴蜂俊鎭? * 5. 涓诲姩閫€鍑哄悗锛屽師 Token 璁块棶 /api/auth/me 鈫?401
*
* 测试场景:
* 1. 正确密码登录 → 返回 token
* 2. 错误密码登录 → 401
* 3. 不存在的公司代码 → 401
* 4. 有效 Token 访问 /api/auth/me → 200返回用户信息
* 5. 主动退出后,原 Token 访问 /api/auth/me → 401
*
* 测试数据来自 init.sql 种子DEMO 公司 / admin / admin123
*/
* 娴嬭瘯鏁版嵁鏉ヨ嚜 init.sql 绉嶅瓙锛圖EMO 鍏徃 / admin / admin123锛? */
public class AuthIntegrationTest extends AbstractIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
// ------------------------------------------------------------------ 登录测试 --
// ------------------------------------------------------------------ 鐧诲綍娴嬭瘯 --
@Test
@DisplayName("正确密码登录 → 返回 token")
@DisplayName("姝g‘瀵嗙爜鐧诲綍 鈫?杩斿洖 token")
void login_withCorrectCredentials_returnsToken() {
ResponseEntity<Map> response = doLogin("DEMO", "admin", "admin123");
@@ -52,23 +48,23 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
}
@Test
@DisplayName("错误密码登录 → 401 Unauthorized")
@DisplayName("閿欒瀵嗙爜鐧诲綍 鈫?401 Unauthorized")
void login_withWrongPassword_returns401() {
ResponseEntity<Map> response = doLogin("DEMO", "admin", "wrong_password");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@DisplayName("不存在的公司代码 → 401 Unauthorized")
@DisplayName("涓嶅瓨鍦ㄧ殑鍏徃浠g爜 鈫?401 Unauthorized")
void login_withUnknownCompany_returns401() {
ResponseEntity<Map> response = doLogin("NONEXIST", "admin", "admin123");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
// ------------------------------------------------------------------ /me 测试 --
// ------------------------------------------------------------------ /me 娴嬭瘯 --
@Test
@DisplayName("有效 Token 访问 /api/auth/me 200,返回用户信息")
@DisplayName("鏈夋晥 Token 璁块棶 /api/auth/me 鈫?200锛岃繑鍥炵敤鎴蜂俊鎭?)
void me_withValidToken_returns200WithUserInfo() {
String token = loginAndGetToken("DEMO", "admin", "admin123");
assertThat(token).isNotBlank();
@@ -89,22 +85,22 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
}
@Test
@DisplayName("无 Token 访问 /api/auth/me → 401")
@DisplayName("?Token 璁块棶 /api/auth/me ?401")
void me_withNoToken_returns401() {
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl("/api/auth/me"), String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
// ------------------------------------------------------------------ 退出测试 --
// ------------------------------------------------------------------ 閫€鍑烘祴璇?--
@Test
@DisplayName("主动退出后,原 Token 访问 /api/auth/me → 401")
@DisplayName("涓诲姩閫鍑哄悗锛屽師 Token 璁块棶 /api/auth/me ?401")
void logout_thenMe_returns401() {
String token = loginAndGetToken("DEMO", "admin", "admin123");
assertThat(token).isNotBlank();
// 确认登录有效
// 纭鐧诲綍鏈夋晥
ResponseEntity<Map> meResponse = restTemplate.exchange(
baseUrl("/api/auth/me"),
HttpMethod.GET,
@@ -112,15 +108,14 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
Map.class);
assertThat(meResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
// 退出
ResponseEntity<Map> logoutResponse = restTemplate.exchange(
// 閫€鍑? ResponseEntity<Map> logoutResponse = restTemplate.exchange(
baseUrl("/api/auth/logout"),
HttpMethod.POST,
bearerRequest(token),
Map.class);
assertThat(logoutResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
// 退出后再访问 /me 401
// 閫€鍑哄悗鍐嶈闂?/me 鈫?401
ResponseEntity<Map> meAfterLogout = restTemplate.exchange(
baseUrl("/api/auth/me"),
HttpMethod.GET,
@@ -129,9 +124,9 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
assertThat(meAfterLogout.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
// ------------------------------------------------------------------ 工具方法 --
// ------------------------------------------------------------------ 宸ュ叿鏂规硶 --
/** 发起登录请求,返回原始 ResponseEntity */
/** 鍙戣捣鐧诲綍璇锋眰锛岃繑鍥炲師濮?ResponseEntity */
private ResponseEntity<Map> doLogin(String companyCode, String username, String password) {
LoginRequest req = new LoginRequest();
req.setCompanyCode(companyCode);
@@ -140,7 +135,7 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
return restTemplate.postForEntity(baseUrl("/api/auth/login"), req, Map.class);
}
/** 登录并提取 token 字符串;失败时返回 null */
/** 鐧诲綍骞舵彁鍙?token 瀛楃涓诧紱澶辫触鏃惰繑鍥?null */
private String loginAndGetToken(String companyCode, String username, String password) {
ResponseEntity<Map> response = doLogin(companyCode, username, password);
if (!response.getStatusCode().is2xxSuccessful()) {
@@ -151,7 +146,7 @@ public class AuthIntegrationTest extends AbstractIntegrationTest {
return (String) data.get("token");
}
/** 构造带 Bearer Token 的请求实体(无 body */
/** 鏋勯€犲甫 Bearer Token 鐨勮姹傚疄浣擄紙鏃?body锛?*/
private HttpEntity<Void> bearerRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);

View File

@@ -1,7 +1,7 @@
package com.label.integration;
import com.label.AbstractIntegrationTest;
import com.label.module.user.dto.LoginRequest;
import com.label.dto.LoginRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -14,12 +14,10 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 提取阶段审批集成测试US4
*
* 测试场景:
* 1. 审批通过 → QA_GENERATION 任务自动创建source_data 状态更新为 QA_REVIEW
* 2. 审批人与提交人相同(自审)→ 403 SELF_REVIEW_FORBIDDEN
* 3. 驳回后标注员可重领任务并再次提交
* 鎻愬彇闃舵瀹℃壒闆嗘垚娴嬭瘯锛圲S4锛夈€? *
* 娴嬭瘯鍦烘櫙锛? * 1. 瀹℃壒閫氳繃 鈫?QA_GENERATION 浠诲姟鑷姩鍒涘缓锛宻ource_data 鐘舵€佹洿鏂颁负 QA_REVIEW
* 2. 瀹℃壒浜轰笌鎻愪氦浜虹浉鍚岋紙鑷锛夆啋 403 SELF_REVIEW_FORBIDDEN
* 3. 椹冲洖鍚庢爣娉ㄥ憳鍙噸棰嗕换鍔″苟鍐嶆鎻愪氦
*/
public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
@@ -33,15 +31,14 @@ public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
@BeforeEach
void setup() {
// 获取种子用户 IDinit.sql 中已插入)
annotatorUserId = jdbcTemplate.queryForObject(
// 鑾峰彇绉嶅瓙鐢ㄦ埛 ID锛坕nit.sql 涓凡鎻掑叆锛? annotatorUserId = jdbcTemplate.queryForObject(
"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);
// 插入测试 source_data
// 鎻掑叆娴嬭瘯 source_data
jdbcTemplate.execute(
"INSERT INTO source_data (company_id, uploader_id, data_type, file_path, " +
"file_name, file_size, bucket_name, status) " +
@@ -50,7 +47,7 @@ public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
sourceId = jdbcTemplate.queryForObject(
"SELECT id FROM source_data ORDER BY id DESC LIMIT 1", Long.class);
// 插入 UNCLAIMED EXTRACTION 任务
// 鎻掑叆 UNCLAIMED EXTRACTION 浠诲姟
jdbcTemplate.execute(
"INSERT INTO annotation_task (company_id, source_id, task_type, status) " +
"VALUES (" + companyId + ", " + sourceId + ", 'EXTRACTION', 'UNCLAIMED')");
@@ -58,66 +55,63 @@ public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
"SELECT id FROM annotation_task ORDER BY id DESC LIMIT 1", Long.class);
}
// ------------------------------------------------------------------ 测试 1: 审批通过 → QA 任务自动创建 --
// ------------------------------------------------------------------ 娴嬭瘯 1: 瀹℃壒閫氳繃 鈫?QA 浠诲姟鑷姩鍒涘缓 --
@Test
@DisplayName("审批通过后,QA_GENERATION 任务自动创建source_data 状态变为 QA_REVIEW")
@DisplayName("瀹℃壒閫氳繃鍚庯紝QA_GENERATION 浠诲姟鑷姩鍒涘缓锛宻ource_data 鐘舵€佸彉涓?QA_REVIEW")
void approveTask_thenQaTaskAndSourceStatusUpdated() {
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
// 1. 标注员领取任务
ResponseEntity<Map> claimResp = restTemplate.exchange(
// 1. 鏍囨敞鍛橀鍙栦换鍔? ResponseEntity<Map> claimResp = restTemplate.exchange(
baseUrl("/api/tasks/" + taskId + "/claim"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(claimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 2. 标注员提交标注
ResponseEntity<Map> submitResp = restTemplate.exchange(
// 2. 鏍囨敞鍛樻彁浜ゆ爣娉? ResponseEntity<Map> submitResp = restTemplate.exchange(
baseUrl("/api/extraction/" + taskId + "/submit"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(submitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 3. 审核员审批通过
// 注:ExtractionApprovedEventListener(@TransactionalEventListener AFTER_COMMIT)
// 在同一线程中同步执行HTTP 响应返回前已完成后续处理
// 3. 瀹℃牳鍛樺鎵归€氳繃
// 娉細ExtractionApprovedEventListener(@TransactionalEventListener AFTER_COMMIT)
// 鍦ㄥ悓涓€绾跨▼涓悓姝ユ墽琛岋紝HTTP 鍝嶅簲杩斿洖鍓嶅凡瀹屾垚鍚庣画澶勭悊
ResponseEntity<Map> approveResp = restTemplate.exchange(
baseUrl("/api/extraction/" + taskId + "/approve"),
HttpMethod.POST, bearerRequest(reviewerToken), Map.class);
assertThat(approveResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:原任务状态变为 APPROVEDis_final=true
// 楠岃瘉锛氬師浠诲姟鐘舵€佸彉涓?APPROVED锛宨s_final=true
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);
// 验证QA_GENERATION 任务已自动创建(UNCLAIMED 状态)
// 楠岃瘉锛歈A_GENERATION 浠诲姟宸茶嚜鍔ㄥ垱寤猴紙UNCLAIMED 鐘舵€侊級
Integer qaTaskCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM annotation_task " +
"WHERE source_id = ? AND task_type = 'QA_GENERATION' AND status = 'UNCLAIMED'",
Integer.class, sourceId);
assertThat(qaTaskCount).as("QA_GENERATION 任务应已创建").isEqualTo(1);
assertThat(qaTaskCount).as("QA_GENERATION 浠诲姟搴斿凡鍒涘缓").isEqualTo(1);
// 验证source_data 状态已更新为 QA_REVIEW
// 楠岃瘉锛歴ource_data 鐘舵€佸凡鏇存柊涓?QA_REVIEW
String sourceStatus = jdbcTemplate.queryForObject(
"SELECT status FROM source_data WHERE id = ?", String.class, sourceId);
assertThat(sourceStatus).as("source_data 状态应为 QA_REVIEW").isEqualTo("QA_REVIEW");
assertThat(sourceStatus).as("source_data 鐘舵€佸簲涓?QA_REVIEW").isEqualTo("QA_REVIEW");
// 验证training_dataset 已以 PENDING_REVIEW 状态创建
Integer datasetCount = jdbcTemplate.queryForObject(
// 楠岃瘉锛歵raining_dataset 宸蹭互 PENDING_REVIEW 鐘舵€佸垱寤? Integer datasetCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM training_dataset " +
"WHERE source_id = ? AND status = 'PENDING_REVIEW'",
Integer.class, sourceId);
assertThat(datasetCount).as("training_dataset 应已创建").isEqualTo(1);
assertThat(datasetCount).as("training_dataset 搴斿凡鍒涘缓").isEqualTo(1);
}
// ------------------------------------------------------------------ 测试 2: 自审返回 403 --
// ------------------------------------------------------------------ 娴嬭瘯 2: 鑷杩斿洖 403 --
@Test
@DisplayName("审批人与任务领取人相同(自审)→ 403 SELF_REVIEW_FORBIDDEN")
@DisplayName("瀹℃壒浜轰笌浠诲姟棰嗗彇浜虹浉鍚岋紙鑷锛夆啋 403 SELF_REVIEW_FORBIDDEN")
void approveOwnSubmission_returnsForbidden() {
// 直接将任务置为 SUBMITTED 并设 claimed_by = reviewer01(模拟自审场景)
// 鐩存帴灏嗕换鍔$疆涓?SUBMITTED 骞惰 claimed_by = reviewer01锛堟ā鎷熻嚜瀹″満鏅級
jdbcTemplate.execute(
"UPDATE annotation_task " +
"SET status = 'SUBMITTED', claimed_by = " + reviewerUserId +
@@ -132,67 +126,63 @@ public class ExtractionApprovalIntegrationTest extends AbstractIntegrationTest {
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
// 验证任务状态未变
String status = jdbcTemplate.queryForObject(
// 楠岃瘉浠诲姟鐘舵€佹湭鍙? String status = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(status).isEqualTo("SUBMITTED");
}
// ------------------------------------------------------------------ 测试 3: 驳回 → 重领 → 再提交 --
// ------------------------------------------------------------------ 娴嬭瘯 3: 椹冲洖 鈫?閲嶉 鈫?鍐嶆彁浜?--
@Test
@DisplayName("驳回后标注员可重领任务并再次提交,任务状态恢复为 SUBMITTED")
@DisplayName("椹冲洖鍚庢爣娉ㄥ憳鍙噸棰嗕换鍔″苟鍐嶆鎻愪氦锛屼换鍔$姸鎬佹仮澶嶄负 SUBMITTED")
void rejectThenReclaimAndResubmit_succeeds() {
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
// 1. 标注员领取并提交
// 1. 鏍囨敞鍛橀鍙栧苟鎻愪氦
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);
// 2. 审核员驳回(驳回原因必填)
HttpHeaders rejectHeaders = new HttpHeaders();
// 2. 瀹℃牳鍛橀┏鍥烇紙椹冲洖鍘熷洜蹇呭~锛? HttpHeaders rejectHeaders = new HttpHeaders();
rejectHeaders.set("Authorization", "Bearer " + reviewerToken);
rejectHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> rejectReq = new HttpEntity<>(
Map.of("reason", "实体识别有误,请重新标注"), rejectHeaders);
Map.of("reason", "瀹炰綋璇嗗埆鏈夎锛岃閲嶆柊鏍囨敞"), rejectHeaders);
ResponseEntity<Map> rejectResp = restTemplate.exchange(
baseUrl("/api/extraction/" + taskId + "/reject"),
HttpMethod.POST, rejectReq, Map.class);
assertThat(rejectResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:任务状态变为 REJECTED
// 楠岃瘉锛氫换鍔$姸鎬佸彉涓?REJECTED
String statusAfterReject = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(statusAfterReject).isEqualTo("REJECTED");
// 3. 标注员重领任务(REJECTED IN_PROGRESS
ResponseEntity<Map> reclaimResp = restTemplate.exchange(
// 3. 鏍囨敞鍛橀噸棰嗕换鍔★紙REJECTED 鈫?IN_PROGRESS锛? ResponseEntity<Map> reclaimResp = restTemplate.exchange(
baseUrl("/api/tasks/" + taskId + "/reclaim"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(reclaimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:任务状态恢复为 IN_PROGRESS
// 楠岃瘉锛氫换鍔$姸鎬佹仮澶嶄负 IN_PROGRESS
String statusAfterReclaim = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(statusAfterReclaim).isEqualTo("IN_PROGRESS");
// 4. 标注员再次提交(IN_PROGRESS SUBMITTED
ResponseEntity<Map> resubmitResp = restTemplate.exchange(
// 4. 鏍囨敞鍛樺啀娆℃彁浜わ紙IN_PROGRESS 鈫?SUBMITTED锛? ResponseEntity<Map> resubmitResp = restTemplate.exchange(
baseUrl("/api/extraction/" + taskId + "/submit"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(resubmitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:任务状态变为 SUBMITTED
// 楠岃瘉锛氫换鍔$姸鎬佸彉涓?SUBMITTED
String finalStatus = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(finalStatus).isEqualTo("SUBMITTED");
}
// ------------------------------------------------------------------ 工具方法 --
// ------------------------------------------------------------------ 宸ュ叿鏂规硶 --
private String loginAndGetToken(String companyCode, String username, String password) {
LoginRequest req = new LoginRequest();

View File

@@ -1,7 +1,7 @@
package com.label.integration;
import com.label.AbstractIntegrationTest;
import com.label.module.user.dto.LoginRequest;
import com.label.dto.LoginRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -14,11 +14,9 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* QA 问答生成阶段审批集成测试US5
*
* 测试场景:
* 1. QA 审批通过 → training_dataset.status = APPROVEDsource_data.status = APPROVED
* 2. QA 驳回 → 候选问答对被删除,标注员可重领
* QA 闂瓟鐢熸垚闃舵瀹℃壒闆嗘垚娴嬭瘯锛圲S5锛夈€? *
* 娴嬭瘯鍦烘櫙锛? * 1. QA 瀹℃壒閫氳繃 鈫?training_dataset.status = APPROVED锛宻ource_data.status = APPROVED
* 2. QA 椹冲洖 鈫?鍊欓€夐棶绛斿琚垹闄わ紝鏍囨敞鍛樺彲閲嶉
*/
public class QaApprovalIntegrationTest extends AbstractIntegrationTest {
@@ -40,7 +38,7 @@ public class QaApprovalIntegrationTest extends AbstractIntegrationTest {
Long companyId = jdbcTemplate.queryForObject(
"SELECT id FROM sys_company WHERE company_code = 'DEMO'", Long.class);
// 插入 source_dataQA_REVIEW 状态,模拟提取审批已完成)
// 鎻掑叆 source_data锛圦A_REVIEW 鐘舵€侊紝妯℃嫙鎻愬彇瀹℃壒宸插畬鎴愶級
jdbcTemplate.execute(
"INSERT INTO source_data (company_id, uploader_id, data_type, file_path, " +
"file_name, file_size, bucket_name, status) " +
@@ -49,129 +47,123 @@ public class QaApprovalIntegrationTest extends AbstractIntegrationTest {
sourceId = jdbcTemplate.queryForObject(
"SELECT id FROM source_data ORDER BY id DESC LIMIT 1", Long.class);
// 插入 QA_GENERATION 任务UNCLAIMED 状态,模拟提取审批通过后自动创建的 QA 任务)
jdbcTemplate.execute(
// 鎻掑叆 QA_GENERATION 浠诲姟锛圲NCLAIMED 鐘舵€侊紝妯℃嫙鎻愬彇瀹℃壒閫氳繃鍚庤嚜鍔ㄥ垱寤虹殑 QA 浠诲姟锛? jdbcTemplate.execute(
"INSERT INTO annotation_task (company_id, source_id, task_type, status) " +
"VALUES (" + companyId + ", " + sourceId + ", 'QA_GENERATION', 'UNCLAIMED')");
taskId = jdbcTemplate.queryForObject(
"SELECT id FROM annotation_task ORDER BY id DESC LIMIT 1", Long.class);
// 插入候选问答对(模拟 ExtractionApprovedEventListener 创建)
jdbcTemplate.execute(
// 鎻掑叆鍊欓€夐棶绛斿锛堟ā鎷?ExtractionApprovedEventListener 鍒涘缓锛? jdbcTemplate.execute(
"INSERT INTO training_dataset (company_id, task_id, source_id, sample_type, " +
"glm_format_json, status) VALUES (" + companyId + ", " + taskId + ", " + sourceId +
", 'TEXT', '{\"conversations\":[{\"question\":\"北京是哪个国家的首都?\",\"answer\":\"中国\"}]}'::jsonb, " +
", 'TEXT', '{\"conversations\":[{\"question\":\"鍖椾含鏄摢涓浗瀹剁殑棣栭兘锛焅",\"answer\":\"涓浗\"}]}'::jsonb, " +
"'PENDING_REVIEW')");
datasetId = jdbcTemplate.queryForObject(
"SELECT id FROM training_dataset ORDER BY id DESC LIMIT 1", Long.class);
}
// ------------------------------------------------------------------ 测试 1: 审批通过 → 终态 --
// ------------------------------------------------------------------ 娴嬭瘯 1: 瀹℃壒閫氳繃 鈫?缁堟€?--
@Test
@DisplayName("QA 审批通过 → training_dataset.status=APPROVEDsource_data.status=APPROVED")
@DisplayName("QA 瀹℃壒閫氳繃 鈫?training_dataset.status=APPROVED锛宻ource_data.status=APPROVED")
void approveQaTask_thenDatasetAndSourceApproved() {
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
// 注意QA 任务 claim 端点为 POST /api/tasks/{id}/claimANNOTATOR 角色)
// 但 TaskController.getPool 只给 ANNOTATOR 显示 EXTRACTION/UNCLAIMED
// QA 任务由 ANNOTATOR 直接领取(不经过任务池)
// 娉ㄦ剰锛歈A 浠诲姟 claim 绔偣涓?POST /api/tasks/{id}/claim锛圓NNOTATOR 瑙掕壊锛? // 浣?TaskController.getPool 鍙粰 ANNOTATOR 鏄剧ず EXTRACTION/UNCLAIMED
// QA 浠诲姟鐢?ANNOTATOR 鐩存帴棰嗗彇锛堜笉缁忚繃浠诲姟姹狅級
ResponseEntity<Map> claimResp = restTemplate.exchange(
baseUrl("/api/tasks/" + taskId + "/claim"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(claimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 提交 QA 结果
// 鎻愪氦 QA 缁撴灉
ResponseEntity<Map> submitResp = restTemplate.exchange(
baseUrl("/api/qa/" + taskId + "/submit"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(submitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 审批通过
// 瀹℃壒閫氳繃
ResponseEntity<Map> approveResp = restTemplate.exchange(
baseUrl("/api/qa/" + taskId + "/approve"),
HttpMethod.POST, bearerRequest(reviewerToken), Map.class);
assertThat(approveResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证training_dataset APPROVED
// 楠岃瘉锛歵raining_dataset 鈫?APPROVED
String datasetStatus = jdbcTemplate.queryForObject(
"SELECT status FROM training_dataset WHERE id = ?", String.class, datasetId);
assertThat(datasetStatus).as("training_dataset 状态应为 APPROVED").isEqualTo("APPROVED");
assertThat(datasetStatus).as("training_dataset 鐘舵€佸簲涓?APPROVED").isEqualTo("APPROVED");
// 验证annotation_task APPROVEDis_final=true
// 楠岃瘉锛歛nnotation_task 鈫?APPROVED锛宨s_final=true
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);
// 验证source_data APPROVED(整条流水线完成)
String sourceStatus = jdbcTemplate.queryForObject(
// 楠岃瘉锛歴ource_data 鈫?APPROVED锛堟暣鏉℃祦姘寸嚎瀹屾垚锛? String sourceStatus = jdbcTemplate.queryForObject(
"SELECT status FROM source_data WHERE id = ?", String.class, sourceId);
assertThat(sourceStatus).as("source_data 状态应为 APPROVED(流水线终态)").isEqualTo("APPROVED");
assertThat(sourceStatus).as("source_data 鐘舵€佸簲涓?APPROVED锛堟祦姘寸嚎缁堟€侊級").isEqualTo("APPROVED");
}
// ------------------------------------------------------------------ 测试 2: 驳回 → 候选记录删除 → 可重领 --
// ------------------------------------------------------------------ 娴嬭瘯 2: 椹冲洖 鈫?鍊欓€夎褰曞垹闄?鈫?鍙噸棰?--
@Test
@DisplayName("QA 驳回 → 候选问答对被删除,标注员可重领并再次提交")
@DisplayName("QA 椹冲洖 鈫?鍊欓€夐棶绛斿琚垹闄わ紝鏍囨敞鍛樺彲閲嶉骞跺啀娆℃彁浜?)
void rejectQaTask_thenDatasetDeletedAndReclaimable() {
String annotatorToken = loginAndGetToken("DEMO", "annotator01", "annot123");
String reviewerToken = loginAndGetToken("DEMO", "reviewer01", "review123");
// 领取并提交
restTemplate.exchange(baseUrl("/api/tasks/" + taskId + "/claim"),
// 棰嗗彇骞舵彁浜? restTemplate.exchange(baseUrl("/api/tasks/" + taskId + "/claim"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
restTemplate.exchange(baseUrl("/api/qa/" + taskId + "/submit"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
// 驳回(驳回原因必填)
// 椹冲洖锛堥┏鍥炲師鍥犲繀濉級
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + reviewerToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> rejectReq = new HttpEntity<>(
Map.of("reason", "问题描述不准确,请修改"), headers);
Map.of("reason", "鎻忚堪涓嶅噯纭紝璇蜂慨鏀?), headers);
ResponseEntity<Map> rejectResp = restTemplate.exchange(
baseUrl("/api/qa/" + taskId + "/reject"),
HttpMethod.POST, rejectReq, Map.class);
assertThat(rejectResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:任务状态变为 REJECTED
// 楠岃瘉锛氫换鍔$姸鎬佸彉涓?REJECTED
String statusAfterReject = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(statusAfterReject).isEqualTo("REJECTED");
// 验证:候选问答对已被删除
// 楠岃瘉锛氬€欓€夐棶绛斿宸茶鍒犻櫎
Integer datasetCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM training_dataset WHERE task_id = ?",
Integer.class, taskId);
assertThat(datasetCount).as("驳回后候选问答对应被删除").isEqualTo(0);
assertThat(datasetCount).as("椹冲洖鍚庡€欓€夐棶绛斿搴旇鍒犻櫎").isEqualTo(0);
// 验证source_data 保持 QA_REVIEW(不变)
// 楠岃瘉锛歴ource_data 淇濇寔 QA_REVIEW锛堜笉鍙橈級
String sourceStatus = jdbcTemplate.queryForObject(
"SELECT status FROM source_data WHERE id = ?", String.class, sourceId);
assertThat(sourceStatus).as("驳回后 source_data 应保持 QA_REVIEW").isEqualTo("QA_REVIEW");
assertThat(sourceStatus).as("椹冲洖鍚?source_data 搴斾繚鎸?QA_REVIEW").isEqualTo("QA_REVIEW");
// 标注员重领任务
ResponseEntity<Map> reclaimResp = restTemplate.exchange(
// 鏍囨敞鍛橀噸棰嗕换鍔? ResponseEntity<Map> reclaimResp = restTemplate.exchange(
baseUrl("/api/tasks/" + taskId + "/reclaim"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(reclaimResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 再次提交
// 鍐嶆鎻愪氦
ResponseEntity<Map> resubmitResp = restTemplate.exchange(
baseUrl("/api/qa/" + taskId + "/submit"),
HttpMethod.POST, bearerRequest(annotatorToken), Map.class);
assertThat(resubmitResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 验证:任务状态变为 SUBMITTED
// 楠岃瘉锛氫换鍔$姸鎬佸彉涓?SUBMITTED
String finalStatus = jdbcTemplate.queryForObject(
"SELECT status FROM annotation_task WHERE id = ?", String.class, taskId);
assertThat(finalStatus).isEqualTo("SUBMITTED");
}
// ------------------------------------------------------------------ 工具方法 --
// ------------------------------------------------------------------ 宸ュ叿鏂规硶 --
private String loginAndGetToken(String companyCode, String username, String password) {
LoginRequest req = new LoginRequest();

View File

@@ -1,7 +1,7 @@
package com.label.integration;
import com.label.AbstractIntegrationTest;
import com.label.module.user.dto.LoginRequest;
import com.label.dto.LoginRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -15,11 +15,8 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 用户管理集成测试US7
*
* 测试场景:
* 1. 变更角色后权限下一次请求立即生效(无需重新登录)
* 2. 禁用账号后现有 Token 下一次请求立即返回 401
* 鐢ㄦ埛绠悊闆嗘垚娴嬭瘯锛圲S7锛夈€? *
* 娴嬭瘯鍦烘櫙锛? * 1. 鍙樻洿瑙掕壊鍚庢潈闄愪笅涓€娆¤姹傜珛鍗崇敓鏁堬紙鏃犻渶閲嶆柊鐧诲綍锛? * 2. 绂佺敤璐﹀彿鍚庣幇鏈?Token 涓嬩竴娆¤姹傜珛鍗宠繑鍥?401
*/
public class UserManagementIntegrationTest extends AbstractIntegrationTest {
@@ -34,14 +31,14 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
assertThat(adminToken).isNotBlank();
}
// ------------------------------------------------------------------ 测试 1: 角色变更立即生效 --
// ------------------------------------------------------------------ 娴嬭瘯 1: 瑙掕壊鍙樻洿绔嬪嵆鐢熸晥 --
@Test
@DisplayName("创建用户为 ANNOTATOR,变更为 REVIEWER 后同一 Token 立即可访问审批接口")
@DisplayName("鍒涘缓鐢ㄦ埛涓?ANNOTATOR锛屽彉鏇翠负 REVIEWER 鍚庡悓涓€ Token 绔嬪嵆鍙闂鎵规帴鍙?)
void updateRole_takesEffectImmediately() {
String uniqueUsername = "testuser-" + UUID.randomUUID().toString().substring(0, 8);
// 1. 创建 ANNOTATOR 用户
// 1. 鍒涘缓 ANNOTATOR 鐢ㄦ埛
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + adminToken);
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -52,7 +49,7 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
new HttpEntity<>(Map.of(
"username", uniqueUsername,
"password", "test1234",
"realName", "测试用户",
"realName", "娴嬭瘯鐢ㄦ埛",
"role", "ANNOTATOR"
), headers),
Map.class);
@@ -62,11 +59,11 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map<String, Object> userData = (Map<String, Object>) createResp.getBody().get("data");
Long newUserId = ((Number) userData.get("id")).longValue();
// 2. 新用户登录获取 Token
// 2. 鏂扮敤鎴风櫥褰曡幏鍙?Token
String userToken = loginAndGetToken("DEMO", uniqueUsername, "test1234");
assertThat(userToken).isNotBlank();
// 3. 验证ANNOTATOR 无法访问待审批队列(REVIEWER 专属)→ 403
// 3. 楠岃瘉锛欰NNOTATOR 鏃犳硶璁块棶寰呭鎵归槦鍒楋紙REVIEWER 涓撳睘锛夆啋 403
ResponseEntity<Map> beforeRoleChange = restTemplate.exchange(
baseUrl("/api/tasks/pending-review"),
HttpMethod.GET,
@@ -74,7 +71,7 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map.class);
assertThat(beforeRoleChange.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
// 4. ADMIN 变更角色为 REVIEWER
// 4. ADMIN 鍙樻洿瑙掕壊涓?REVIEWER
ResponseEntity<Map> roleResp = restTemplate.exchange(
baseUrl("/api/users/" + newUserId + "/role"),
HttpMethod.PUT,
@@ -82,25 +79,25 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map.class);
assertThat(roleResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 5. 验证:同一 Token 下次请求立即具有 REVIEWER 权限 → 200
// 5. 楠岃瘉锛氬悓涓€ Token 涓嬫璇锋眰绔嬪嵆鍏锋湁 REVIEWER 鏉冮檺 鈫?200
ResponseEntity<Map> afterRoleChange = restTemplate.exchange(
baseUrl("/api/tasks/pending-review"),
HttpMethod.GET,
bearerRequest(userToken),
Map.class);
assertThat(afterRoleChange.getStatusCode())
.as("角色变更后同一 Token 应立即具有 REVIEWER 权限")
.as("瑙掕壊鍙樻洿鍚庡悓涓 Token 搴旂珛鍗冲叿鏈?REVIEWER 鏉冮檺")
.isEqualTo(HttpStatus.OK);
}
// ------------------------------------------------------------------ 测试 2: 禁用账号 Token 立即失效 --
// ------------------------------------------------------------------ 娴嬭瘯 2: 绂佺敤璐﹀彿 Token 绔嬪嵆澶辨晥 --
@Test
@DisplayName("禁用账号后,现有 Token 下一次请求立即返回 401")
@DisplayName("绂佺敤璐彿鍚庯紝鐜版湁 Token 涓嬩竴娆¤姹傜珛鍗宠繑鍥?401")
void disableAccount_tokenInvalidatedImmediately() {
String uniqueUsername = "testuser-" + UUID.randomUUID().toString().substring(0, 8);
// 1. 创建用户
// 1. 鍒涘缓鐢ㄦ埛
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + adminToken);
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -111,7 +108,7 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
new HttpEntity<>(Map.of(
"username", uniqueUsername,
"password", "test1234",
"realName", "测试用户",
"realName", "娴嬭瘯鐢ㄦ埛",
"role", "ANNOTATOR"
), headers),
Map.class);
@@ -121,11 +118,11 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map<String, Object> userData = (Map<String, Object>) createResp.getBody().get("data");
Long newUserId = ((Number) userData.get("id")).longValue();
// 2. 新用户登录,获取 Token
// 2. 鏂扮敤鎴风櫥褰曪紝鑾峰彇 Token
String userToken = loginAndGetToken("DEMO", uniqueUsername, "test1234");
assertThat(userToken).isNotBlank();
// 3. 验证 Token 有效
// 3. 楠岃瘉 Token 鏈夋晥
ResponseEntity<Map> meResp = restTemplate.exchange(
baseUrl("/api/auth/me"),
HttpMethod.GET,
@@ -133,7 +130,7 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map.class);
assertThat(meResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 4. ADMIN 禁用账号
// 4. ADMIN 绂佺敤璐﹀彿
ResponseEntity<Map> disableResp = restTemplate.exchange(
baseUrl("/api/users/" + newUserId + "/status"),
HttpMethod.PUT,
@@ -141,18 +138,18 @@ public class UserManagementIntegrationTest extends AbstractIntegrationTest {
Map.class);
assertThat(disableResp.getStatusCode()).isEqualTo(HttpStatus.OK);
// 5. 验证:禁用后,现有 Token 立即失效 → 401
// 5. 楠岃瘉锛氱鐢ㄥ悗锛岀幇鏈?Token 绔嬪嵆澶辨晥 鈫?401
ResponseEntity<Map> meAfterDisable = restTemplate.exchange(
baseUrl("/api/auth/me"),
HttpMethod.GET,
bearerRequest(userToken),
Map.class);
assertThat(meAfterDisable.getStatusCode())
.as("禁用账号后现有 Token 应立即失效")
.as("绂佺敤璐彿鍚庣幇鏈?Token 搴旂珛鍗冲け鏁?)
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
// ------------------------------------------------------------------ 工具方法 --
// ------------------------------------------------------------------ 宸ュ叿鏂规硶 --
private String loginAndGetToken(String companyCode, String username, String password) {
LoginRequest req = new LoginRequest();

View File

@@ -5,14 +5,14 @@ import com.label.module.annotation.controller.QaController;
import com.label.module.config.controller.SysConfigController;
import com.label.module.export.controller.ExportController;
import com.label.module.source.controller.SourceController;
import com.label.module.source.dto.SourceResponse;
import com.label.dto.SourceResponse;
import com.label.module.task.controller.TaskController;
import com.label.module.task.dto.TaskResponse;
import com.label.dto.TaskResponse;
import com.label.module.user.controller.AuthController;
import com.label.module.user.controller.UserController;
import com.label.module.user.dto.LoginRequest;
import com.label.module.user.dto.LoginResponse;
import com.label.module.user.dto.UserInfoResponse;
import com.label.dto.LoginRequest;
import com.label.dto.LoginResponse;
import com.label.dto.UserInfoResponse;
import com.label.module.video.controller.VideoController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -32,7 +32,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("OpenAPI 注解覆盖测试")
@DisplayName("OpenAPI 娉ㄨВ瑕嗙洊娴嬭瘯")
class OpenApiAnnotationTest {
private static final List<Class<?>> CONTROLLERS = List.of(
@@ -56,7 +56,7 @@ class OpenApiAnnotationTest {
);
@Test
@DisplayName("所有 REST Controller 都声明 @Tag")
@DisplayName("鎵€鏈?REST Controller 閮藉0鏄?@Tag")
void allControllersHaveTag() {
assertThat(CONTROLLERS)
.allSatisfy(controller ->
@@ -66,7 +66,7 @@ class OpenApiAnnotationTest {
}
@Test
@DisplayName("所有 REST endpoint 方法都声明 @Operation")
@DisplayName("鎵€鏈?REST endpoint 鏂规硶閮藉0鏄?@Operation")
void allEndpointMethodsHaveOperation() {
for (Class<?> controller : CONTROLLERS) {
Arrays.stream(controller.getDeclaredMethods())
@@ -79,7 +79,7 @@ class OpenApiAnnotationTest {
}
@Test
@DisplayName("核心 DTO 都声明 @Schema")
@DisplayName("鏍稿績 DTO 閮藉0鏄?@Schema")
void coreDtosHaveSchema() {
assertThat(DTOS)
.allSatisfy(dto ->

View File

@@ -29,6 +29,39 @@ class PackageStructureMigrationTest {
assertClassMissing("com.label.module.annotation.service.ExtractionApprovedEventListener");
}
@Test
@DisplayName("DTO、实体、Mapper 已迁移到扁平数据层")
void dataTypesMoved() {
for (String fqcn : java.util.List.of(
"com.label.dto.LoginRequest",
"com.label.dto.LoginResponse",
"com.label.dto.UserInfoResponse",
"com.label.dto.TaskResponse",
"com.label.dto.SourceResponse",
"com.label.entity.AnnotationResult",
"com.label.entity.TrainingDataset",
"com.label.entity.SysConfig",
"com.label.entity.ExportBatch",
"com.label.entity.SourceData",
"com.label.entity.AnnotationTask",
"com.label.entity.AnnotationTaskHistory",
"com.label.entity.SysCompany",
"com.label.entity.SysUser",
"com.label.entity.VideoProcessJob",
"com.label.mapper.AnnotationResultMapper",
"com.label.mapper.TrainingDatasetMapper",
"com.label.mapper.SysConfigMapper",
"com.label.mapper.ExportBatchMapper",
"com.label.mapper.SourceDataMapper",
"com.label.mapper.AnnotationTaskMapper",
"com.label.mapper.TaskHistoryMapper",
"com.label.mapper.SysCompanyMapper",
"com.label.mapper.SysUserMapper",
"com.label.mapper.VideoProcessJobMapper")) {
assertClassExists(fqcn);
}
}
private static void assertClassExists(String fqcn) {
assertThatCode(() -> Class.forName(fqcn)).doesNotThrowAnyException();
}