适配QA问答对数据结构

This commit is contained in:
wh
2026-05-09 23:46:56 +08:00
parent e637e8a6a4
commit 9425ff3a1e
16 changed files with 359 additions and 101 deletions

View File

@@ -8,6 +8,7 @@ import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultHistoryPageQuery;
import com.labelsys.backend.dto.response.AnnotationResultHistoryDetailResponse;
import com.labelsys.backend.dto.response.AnnotationResultHistoryResponse;
import com.labelsys.backend.dto.response.FileContentResponse;
import com.labelsys.backend.dto.response.MergeReviewResultResponse;
@@ -29,6 +30,8 @@ import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import static org.springframework.util.StringUtils.hasText;
@Slf4j
@Service
@RequiredArgsConstructor
@@ -66,6 +69,7 @@ public class AnnotationResultArchiveService {
var page = new Page<AnnotationResultHistory>(query.pageNo(), query.pageSize());
var resultPage = annotationResultHistoryMapper.selectPage(page, wrapper);
// 分页查询不加载 qa 内容
var records = resultPage.getRecords().stream()
.map(this::toResponse)
.toList();
@@ -79,14 +83,17 @@ public class AnnotationResultArchiveService {
}
}
public AnnotationResultHistoryResponse getHistory(LoginUser currentUser, Long historyId) {
public AnnotationResultHistoryDetailResponse getHistory(LoginUser currentUser, Long historyId) {
try {
AnnotationResultHistory history = annotationResultHistoryMapper.selectById(historyId);
if (history == null || !history.getCompanyId().equals(currentUser.companyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "历史记录不存在");
}
assertHistoryPermission(currentUser, history);
return toResponse(history);
// 详情查询加载 QA 内容
QaContent qaContent = loadQaContent(history.getQaContentFilePath());
return toDetailResponse(history, qaContent);
} catch (Exception e) {
log.error("getHistory failed, companyId={}, userId={}, historyId={}, error={}",
currentUser.companyId(), currentUser.userId(), historyId, e.getMessage(), e);
@@ -104,19 +111,66 @@ public class AnnotationResultArchiveService {
private AnnotationResultHistoryResponse toResponse(AnnotationResultHistory history) {
return new AnnotationResultHistoryResponse(
history.getId(),
// history.getSourceResultId(),
history.getTaskId(),
history.getTaskName(), // 新增
history.getTaskName(),
history.getResourceId(),
history.getResourceName(), // 新增
history.getResourceName(),
history.getQaContentFilePath(),
history.getArchiveReason(),
history.getArchivedBy(),
history.getArchivedAt(),
history.getCreatedAt(),
history.getReviewerId(),
history.getReviewerName(),
history.getReviewerComment()
history.getReviewerName()
);
}
private AnnotationResultHistoryDetailResponse toDetailResponse(AnnotationResultHistory history,
QaContent qaContent) {
// 转换 QA 内容
AnnotationResultHistoryDetailResponse.QaContentDto qaContentDto = null;
if (qaContent != null && qaContent.records() != null) {
qaContentDto = new AnnotationResultHistoryDetailResponse.QaContentDto(
qaContent.records().stream()
.map(r -> new AnnotationResultHistoryDetailResponse.QaRecordDto(
r.id(),
r.batchId(),
r.question(),
r.answer(),
r.requiresReview(),
r.sourceSegments() != null ?
new AnnotationResultHistoryDetailResponse.SourceSegmentsDto(
r.sourceSegments().segment(),
r.sourceSegments().chunkIndex(),
r.sourceSegments().chunkTitle(),
r.sourceSegments().chunkContent()) :
null,
r.questionCategory(),
r.scores() != null ? new AnnotationResultHistoryDetailResponse.ScoresDto(
r.scores().similarity(),
r.scores().confidence1(),
r.scores().confidence2(),
r.scores().hallucination(),
r.scores().trust()) : null,
r.reviewComment()))
.toList()
);
}
return new AnnotationResultHistoryDetailResponse(
history.getId(),
history.getTaskId(),
history.getTaskName(),
history.getResourceId(),
history.getResourceName(),
history.getQaContentFilePath(),
qaContentDto,
history.getArchiveReason(),
history.getArchivedBy(),
history.getArchivedAt(),
history.getCreatedAt(),
history.getReviewerId(),
history.getReviewerName()
);
}
@@ -124,13 +178,14 @@ public class AnnotationResultArchiveService {
public int autoArchiveEligibleResults() {
try {
LocalDateTime cutoff = LocalDateTime.now().minus(autoArchiveTimeout);
List<AnnotationResult> results = annotationResultMapper.selectList(new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getIsDeleted, false)
.eq(AnnotationResult::getRequiresManualReview, false)
.lt(AnnotationResult::getCreatedAt, cutoff));
List<AnnotationResult> results = annotationResultMapper.selectList(
new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getIsDeleted, false)
.eq(AnnotationResult::getRequiresManualReview, false)
.lt(AnnotationResult::getCreatedAt, cutoff));
int archivedCount = 0;
for (AnnotationResult result : results) {
if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE", null) != null) {
if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE") != null) {
archivedCount++;
}
}
@@ -153,8 +208,7 @@ public class AnnotationResultArchiveService {
*/
private MergeReviewResultResponse archiveRuntimeResult(AnnotationResult result,
Long reviewerId,
String archiveReason,
String reviewComment) {
String archiveReason) {
LocalDateTime archivedAt = LocalDateTime.now();
// 从对象存储读取 qa.json 内容
@@ -177,22 +231,64 @@ public class AnnotationResultArchiveService {
.archivedAt(archivedAt)
.reviewerId(null)
.reviewerName("auto")
.reviewerComment("auto")
.build();
annotationResultHistoryMapper.insert(history);
int updated = annotationResultMapper.markArchived(
result.getId(),
result.getCompanyId(),
reviewerId,
reviewComment,
archivedAt);
reviewerId);
if (updated == 0) {
return null;
}
return new MergeReviewResultResponse(result.getId(), history.getId(), archiveReason, archivedAt);
}
/**
* 加载 QA 内容
*/
private QaContent loadQaContent(String filePath) {
try {
if (!hasText(filePath)) {
log.warn("QA content file path is empty");
return new QaContent(null, null, List.of(), null);
}
String bucketName = extractBucketName(filePath);
String objectKey = extractObjectKey(filePath);
byte[] content = objectStorageService.download(bucketName, objectKey);
String jsonContent = new String(content, StandardCharsets.UTF_8);
return objectMapper.readValue(jsonContent, new com.fasterxml.jackson.core.type.TypeReference<QaContent>() {
});
} catch (Exception e) {
log.warn("Failed to load QA content, returning empty content. filePath={}, error={}", filePath,
e.getMessage());
return new QaContent(null, null, List.of(), null);
}
}
// 内部类qa.json 结构
private record QaContent(
Long taskId,
Long resourceId,
List<QaRecord> records,
Metadata metadata
) {
private record QaRecord(String id, Long batchId, String question, String answer,
Boolean requiresReview, SourceSegments sourceSegments,
String questionCategory, Scores scores, String reviewComment) {
}
private record SourceSegments(String segment, Integer chunkIndex, String chunkTitle, String chunkContent) {
}
private record Scores(Double similarity, Double confidence1, Double confidence2,
Double hallucination, Double trust) {
}
private record Metadata(String createdAt, String updatedAt) {
}
}
/**
* 从对象存储读取 qa.json 内容
*/
@@ -239,8 +335,9 @@ public class AnnotationResultArchiveService {
/**
* 加载归档记录的文件内容
*
* @param currentUser 当前用户
* @param historyId 历史记录ID
* @param historyId 历史记录ID
* @return 文件内容响应
*/
public FileContentResponse loadFileContent(LoginUser currentUser, Long historyId) {
@@ -250,17 +347,17 @@ public class AnnotationResultArchiveService {
throw new BusinessException(ResultCode.NOT_FOUND, "历史记录不存在");
}
//assertHistoryPermission(currentUser, history);
String filePath = history.getQaContentFilePath();
if (filePath == null || filePath.isEmpty()) {
throw new BusinessException(ResultCode.ERROR, "文件路径为空");
}
String bucketName = extractBucketName(filePath);
String objectKey = extractObjectKey(filePath);
byte[] content = objectStorageService.download(bucketName, objectKey);
String contentStr = new String(content, StandardCharsets.UTF_8);
return new FileContentResponse(filePath, contentStr, content.length);
} catch (BusinessException e) {
throw e;
@@ -270,4 +367,5 @@ public class AnnotationResultArchiveService {
throw new BusinessException(ResultCode.ERROR, "加载文件内容失败");
}
}
}