适配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

@@ -3,8 +3,8 @@ package com.labelsys.backend.controller;
import com.labelsys.backend.context.UserContext;
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.service.AnnotationResultArchiveService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -34,17 +34,18 @@ public class AnnotationResultArchiveController {
@Operation(summary = "查询归档历史详情")
@GetMapping("/{id}")
public ResponseEntity<AnnotationResultHistoryResponse> getHistory(
public ResponseEntity<AnnotationResultHistoryDetailResponse> getHistory(
@Parameter(description = "历史记录ID", example = "901")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultArchiveService.getHistory(UserContext.requireUser(), id));
}
}
@Operation(summary = "加载归档文件内容")
@GetMapping("/{id}/content")
public ResponseEntity<FileContentResponse> loadFileContent(
@Parameter(description = "历史记录ID", example = "901")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultArchiveService.loadFileContent(UserContext.requireUser(), id));
}
}
// @Operation(summary = "加载归档文件内容")
// @GetMapping("/{id}/content")
// public ResponseEntity<FileContentResponse> loadFileContent(
// @Parameter(description = "历史记录ID", example = "901")
// @PathVariable Long id) {
// return ResponseEntity.ok(annotationResultArchiveService.loadFileContent(UserContext.requireUser(), id));
// }
//}

View File

@@ -9,7 +9,7 @@ public record MergeReviewResultRequest(
@Schema(description = "合并后的答案映射key为qa记录IDvalue为合并后的答案")
Map<String, String> mergedAnswers,
@Schema(description = "审核备注")
String reviewComment
@Schema(description = "每条QA记录的审核评论映射key为qa记录IDvalue为审核评论")
Map<String, String> reviewComments
) {
}

View File

@@ -18,10 +18,25 @@ public record AnnotationResultCompareResponse(
@Schema(description = "问答记录")
public record QaRecord(
@Schema(description = "记录ID", example = "qa_001") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "运输时效是多久?") String question,
@Schema(description = "答案", example = "3天") String answer,
@Schema(description = "是否需要审核", example = "true") Boolean requiresReview
) {}
@Schema(description = "是否需要审核", example = "true") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegments sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") Scores scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegments(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@Schema(description = "差异记录")
public record DiffRecord(
@@ -30,6 +45,19 @@ public record AnnotationResultCompareResponse(
@Schema(description = "提取模型答案", example = "3天") String extractAnswer,
@Schema(description = "校验模型答案", example = "72小时") String verifyAnswer,
@Schema(description = "差异原因", example = "时间单位不一致") String diffReason,
@Schema(description = "合并后的最终答案", example = "72小时3天") String mergedAnswer
) {}
@Schema(description = "合并后的最终答案", example = "72小时3天") String mergedAnswer,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") Scores scores
) {
}
@Schema(description = "评分结构")
public record Scores(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -25,8 +25,6 @@ public record AnnotationResultDetailResponse(
@Schema(description = "问答内容") QaContentDto qaContent,
@Schema(description = "差异摘要(需要审核时有值)") DiffContentDto diffSummary,
@Schema(description = "审核备注", example = "需统一时间字段口径。") String reviewComment,
@Schema(description = "审核时间", example = "2026-04-27T11:00:00") LocalDateTime reviewedAt,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
) {
@Schema(description = "问答内容结构")
@@ -38,9 +36,23 @@ public record AnnotationResultDetailResponse(
@Schema(description = "问答记录")
public record QaRecordDto(
@Schema(description = "记录ID", example = "q1") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "产品重量是多少?") String question,
@Schema(description = "答案", example = "5kg") String answer,
@Schema(description = "是否需要审核", example = "false") Boolean requiresReview
@Schema(description = "是否需要审核", example = "false") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegmentsDto sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegmentsDto(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@@ -57,7 +69,19 @@ public record AnnotationResultDetailResponse(
@Schema(description = "抽取答案", example = "2年") String extractAnswer,
@Schema(description = "验证答案", example = "3年") String verifyAnswer,
@Schema(description = "差异原因", example = "抽取与验证结果不一致") String diffReason,
@Schema(description = "合并后答案") String mergedAnswer
@Schema(description = "合并后答案") String mergedAnswer,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores
) {
}
@Schema(description = "评分结构")
public record ScoresDto(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -0,0 +1,63 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "归档历史详情响应")
public record AnnotationResultHistoryDetailResponse(
@Schema(description = "历史记录ID", example = "901") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "任务名称", example = "产品说明书标注") String taskName,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "资源名称", example = "产品A说明书.pdf") String resourceName,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/802.json") String qaContentFilePath,
@Schema(description = "问答内容") QaContentDto qaContent,
@Schema(description = "归档原因", example = "审核通过后归档") String archiveReason,
@Schema(description = "归档操作人ID", example = "5") Long archivedBy,
@Schema(description = "归档时间", example = "2026-05-06T10:30:00") LocalDateTime archivedAt,
@Schema(description = "创建时间", example = "2026-05-06T10:30:00") LocalDateTime createdAt,
@Schema(description = "审核人ID自动归档时为null", example = "5") Long reviewerId,
@Schema(description = "审核人姓名自动归档时为auto", example = "张三") String reviewerName
) {
@Schema(description = "问答内容结构")
public record QaContentDto(
@Schema(description = "问答记录列表") List<QaRecordDto> records
) {
}
@Schema(description = "问答记录")
public record QaRecordDto(
@Schema(description = "记录ID", example = "q1") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "产品重量是多少?") String question,
@Schema(description = "答案", example = "5kg") String answer,
@Schema(description = "是否需要审核", example = "false") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegmentsDto sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegmentsDto(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@Schema(description = "评分结构")
public record ScoresDto(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -20,7 +20,6 @@ public record AnnotationResultHistoryResponse(
@Schema(description = "归档时间", example = "2026-05-06T10:30:00") LocalDateTime archivedAt,
@Schema(description = "创建时间", example = "2026-05-06T10:30:00") LocalDateTime createdAt,
@Schema(description = "审核人ID自动归档时为null", example = "5") Long reviewerId,
@Schema(description = "审核人姓名自动归档时为auto", example = "张三") String reviewerName,
@Schema(description = "审核意见自动归档时为auto", example = "内容符合要求") String reviewerComment
@Schema(description = "审核人姓名自动归档时为auto", example = "张三") String reviewerName
) {
}

View File

@@ -17,8 +17,6 @@ public record AnnotationResultResponse(
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/801.json") String qaContentFilePath,
@Schema(description = "差异摘要文件路径", example = "annotation-results/2/diff/801.json") String diffSummaryFilePath,
@Schema(description = "审核备注", example = "需统一时间字段口径。") String reviewComment,
@Schema(description = "审核时间", example = "2026-04-27T11:00:00") LocalDateTime reviewedAt,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
) {
}

View File

@@ -56,12 +56,6 @@ public class AnnotationResult {
@TableField("reviewer_id")
private Long reviewerId;
@TableField("review_comment")
private String reviewComment;
@TableField("reviewed_at")
private LocalDateTime reviewedAt;
@TableField("created_at")
private LocalDateTime createdAt;

View File

@@ -37,5 +37,4 @@ public class AnnotationResultHistory {
// 新增审核人相关字段
private Long reviewerId;
private String reviewerName;
private String reviewerComment;
}

View File

@@ -11,7 +11,5 @@ public interface AnnotationResultMapper extends BaseMapper<AnnotationResult> {
int markArchived(@Param("id") Long id,
@Param("companyId") Long companyId,
@Param("reviewerId") Long reviewerId,
@Param("reviewComment") String reviewComment,
@Param("reviewedAt") LocalDateTime reviewedAt);
@Param("reviewerId") Long reviewerId);
}

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, "加载文件内容失败");
}
}
}

View File

@@ -116,7 +116,24 @@ public class AnnotationResultService {
AnnotationResultDetailResponse.QaContentDto qaContentDto = new AnnotationResultDetailResponse.QaContentDto(
qaContent.records().stream()
.map(r -> new AnnotationResultDetailResponse.QaRecordDto(
r.id(), r.question(), r.answer(), r.requiresReview()))
r.id(),
r.batchId(),
r.question(),
r.answer(),
r.requiresReview(),
r.sourceSegments() != null ? new AnnotationResultDetailResponse.SourceSegmentsDto(
r.sourceSegments().segment(),
r.sourceSegments().chunkIndex(),
r.sourceSegments().chunkTitle(),
r.sourceSegments().chunkContent()) : null,
r.questionCategory(),
r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
r.scores().similarity(),
r.scores().confidence1(),
r.scores().confidence2(),
r.scores().hallucination(),
r.scores().trust()) : null,
r.reviewComment()))
.toList()
);
@@ -127,7 +144,14 @@ public class AnnotationResultService {
diffContent.records().stream()
.map(r -> new AnnotationResultDetailResponse.DiffRecordDto(
r.qaId(), r.question(), r.extractAnswer(),
r.verifyAnswer(), r.diffReason(), r.mergedAnswer()))
r.verifyAnswer(), r.diffReason(), r.mergedAnswer(),
r.questionCategory(),
r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
r.scores().similarity(),
r.scores().confidence1(),
r.scores().confidence2(),
r.scores().hallucination(),
r.scores().trust()) : null))
.toList()
);
}
@@ -145,8 +169,6 @@ public class AnnotationResultService {
result.getDiffSummaryFilePath(),
qaContentDto,
diffContentDto,
result.getReviewComment(),
result.getReviewedAt(),
result.getCreatedAt()
);
}
@@ -173,9 +195,23 @@ public class AnnotationResultService {
List<AnnotationResultCompareResponse.QaRecord> qaRecords = qaContent.records().stream()
.map(qa -> new AnnotationResultCompareResponse.QaRecord(
qa.id(),
qa.batchId(),
qa.question(),
qa.answer(),
qa.requiresReview()
qa.requiresReview(),
qa.sourceSegments() != null ? new AnnotationResultCompareResponse.SourceSegments(
qa.sourceSegments().segment(),
qa.sourceSegments().chunkIndex(),
qa.sourceSegments().chunkTitle(),
qa.sourceSegments().chunkContent()) : null,
qa.questionCategory(),
qa.scores() != null ? new AnnotationResultCompareResponse.Scores(
qa.scores().similarity(),
qa.scores().confidence1(),
qa.scores().confidence2(),
qa.scores().hallucination(),
qa.scores().trust()) : null,
qa.reviewComment()
)).toList();
// 转换差异记录
@@ -187,7 +223,14 @@ public class AnnotationResultService {
diff.extractAnswer(),
diff.verifyAnswer(),
diff.diffReason(),
diff.mergedAnswer()
diff.mergedAnswer(),
diff.questionCategory(),
diff.scores() != null ? new AnnotationResultCompareResponse.Scores(
diff.scores().similarity(),
diff.scores().confidence1(),
diff.scores().confidence2(),
diff.scores().hallucination(),
diff.scores().trust()) : null
)).toList() : List.of();
return new AnnotationResultCompareResponse(
@@ -220,16 +263,22 @@ public class AnnotationResultService {
// 读取当前 qa.json
QaContent qaContent = loadQaContent(result);
// 更新 qa.json 的 answer 字段
// 更新 qa.json 的 answer 字段和 reviewComment
List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream()
.map(record -> {
String mergedAnswer = request.mergedAnswers().get(record.id());
if (mergedAnswer != null) {
String reviewComment = request.reviewComments() != null ? request.reviewComments().get(record.id()) : null;
if (mergedAnswer != null || reviewComment != null) {
return new QaContent.QaRecord(
record.id(),
record.batchId(),
record.question(),
mergedAnswer,
false
mergedAnswer != null ? mergedAnswer : record.answer(),
false,
record.sourceSegments(),
record.questionCategory(),
record.scores(),
reviewComment != null ? reviewComment : record.reviewComment()
);
}
return record;
@@ -247,11 +296,18 @@ public class AnnotationResultService {
);
saveQaContent(result, updatedQaContent);
// 更新数据库记录
result.setIsDeleted(Boolean.TRUE);
result.setReviewerId(currentUser.userId());
result.setReviewComment(request.reviewComment());
result.setReviewedAt(LocalDateTime.now());
// 更新数据库记录(使用 markArchived 保证幂等性,防止并发重复归档)
int updated = annotationResultMapper.markArchived(
result.getId(),
currentUser.companyId(),
currentUser.userId());
if (updated == 0) {
// 记录已被其他进程归档
throw new BusinessException(ResultCode.CONFLICT, "记录已被归档");
}
// 更新 requires_manual_review 字段
result.setRequiresManualReview(false);
annotationResultMapper.updateById(result);
@@ -281,8 +337,6 @@ public class AnnotationResultService {
result.getIsDeleted(),
result.getQaContentFilePath(),
result.getDiffSummaryFilePath(),
result.getReviewComment(),
result.getReviewedAt(),
result.getCreatedAt()
);
}
@@ -392,17 +446,15 @@ public class AnnotationResultService {
// 根据归档类型设置审核人信息
if (isAutoArchive) {
// 自动归档reviewer_id为NULLname和comment为"auto"
// 自动归档reviewer_id为NULLname为"auto"
historyBuilder
.reviewerId(null)
.reviewerName("auto")
.reviewerComment("auto");
.reviewerName("auto");
} else {
// 人工审核后归档:使用审核人信息
historyBuilder
.reviewerId(result.getReviewerId())
.reviewerName(currentUser.realName())
.reviewerComment(result.getReviewComment());
.reviewerName(currentUser.realName());
}
annotationResultHistoryMapper.insert(historyBuilder.build());
@@ -436,7 +488,16 @@ public class AnnotationResultService {
List<QaRecord> records,
Metadata metadata
) {
private record QaRecord(String id, String question, String answer, Boolean requiresReview) {
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) {
@@ -451,7 +512,12 @@ public class AnnotationResultService {
Metadata metadata
) {
private record DiffRecord(String qaId, String question, String extractAnswer,
String verifyAnswer, String diffReason, String mergedAnswer) {
String verifyAnswer, String diffReason, String mergedAnswer,
String questionCategory, Scores scores) {
}
private record Scores(Double similarity, Double confidence1, Double confidence2,
Double hallucination, Double trust) {
}
private record Metadata(String createdAt) {

View File

@@ -13,16 +13,14 @@
<result column="requires_manual_review" property="requiresManualReview"/>
<result column="is_deleted" property="isDeleted"/>
<result column="reviewer_id" property="reviewerId"/>
<result column="review_comment" property="reviewComment"/>
<result column="reviewed_at" property="reviewedAt"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<sql id="AnnotationResultColumns">
id, company_id, creator_id, creator_role, task_id, resource_id, qa_content_file_path,
diff_summary_file_path, requires_manual_review, is_deleted, reviewer_id, review_comment,
reviewed_at, created_at, updated_at
diff_summary_file_path, requires_manual_review, is_deleted, reviewer_id,
created_at, updated_at
</sql>
<select id="findActiveByIdAndCompanyId" resultMap="AnnotationResultResultMap">
@@ -37,10 +35,7 @@
<update id="markArchived">
update annotation_result
set is_deleted = true,
reviewer_id = #{reviewerId},
review_comment = #{reviewComment},
reviewed_at = #{reviewedAt},
updated_at = #{reviewedAt}
reviewer_id = #{reviewerId}
where id = #{id}
and company_id = #{companyId}
and is_deleted = false

View File

@@ -86,25 +86,25 @@ ON CONFLICT DO NOTHING;
INSERT INTO annotation_result (
id, company_id, creator_id, creator_role, task_id, task_name, resource_id, resource_name,
qa_content_file_path, diff_summary_file_path,
requires_manual_review, is_deleted, reviewer_id, review_comment, reviewed_at
requires_manual_review, is_deleted, reviewer_id
) VALUES
(801, 2, 3, 'EMPLOYEE', 701, '多资源问答抽取任务', 601, '设备巡检规范.txt',
'annotation-artifacts/qa/qa1.json',
'annotation-artifacts/diff/diff1.json',
TRUE, FALSE, NULL, NULL, NULL),
TRUE, FALSE, NULL),
(802, 2, 3, 'EMPLOYEE', 702, '图片问答抽取任务', 602, '控制柜照片.jpg',
'annotation-artifacts/qa/qa2.json',
'annotation-artifacts/diff/diff2.json',
FALSE, FALSE, 5, '结果可通过。', CURRENT_TIMESTAMP)
FALSE, FALSE, 5)
ON CONFLICT DO NOTHING;
INSERT INTO annotation_result_history (
id, company_id, creator_id, creator_role, source_result_id, task_id, task_name, resource_id, resource_name,
qa_content_file_path, reviewer_id, reviewer_name, reviewer_comment, archive_reason, archived_by, archived_at
qa_content_file_path, reviewer_id, reviewer_name, archive_reason, archived_by, archived_at
) VALUES
(901, 2, 3, 'EMPLOYEE', 802, 702, '图片问答抽取任务', 602, '控制柜照片.jpg',
'annotation-results/2/qa/802.json',
5, '甲公司审核员', '结果可通过。', '审核通过后归档', 5, CURRENT_TIMESTAMP)
5, '甲公司审核员', '审核通过后归档', 5, CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;
INSERT INTO image_bbox_annotation (

View File

@@ -263,8 +263,6 @@ CREATE TABLE IF NOT EXISTS annotation_result
requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
reviewer_id BIGINT,
review_comment TEXT,
reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_annotation_result_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
@@ -286,8 +284,6 @@ COMMENT ON COLUMN annotation_result.diff_summary_file_path IS '差异摘要文
COMMENT ON COLUMN annotation_result.requires_manual_review IS '是否需要人工审核,默认 FALSE。当diff_summary_file_path不为空时为TRUE。';
COMMENT ON COLUMN annotation_result.is_deleted IS '软删除标记,默认 FALSE。';
COMMENT ON COLUMN annotation_result.reviewer_id IS '审核人用户ID。';
COMMENT ON COLUMN annotation_result.review_comment IS '审核意见。';
COMMENT ON COLUMN annotation_result.reviewed_at IS '审核时间。';
COMMENT ON COLUMN annotation_result.created_at IS '创建时间。';
COMMENT ON COLUMN annotation_result.updated_at IS '更新时间。';
COMMENT ON COLUMN annotation_result.task_name IS '任务名称(冗余字段)。';
@@ -307,7 +303,6 @@ CREATE TABLE IF NOT EXISTS annotation_result_history
qa_content_file_path VARCHAR(512) NOT NULL,
reviewer_id BIGINT,
reviewer_name VARCHAR(128),
reviewer_comment TEXT,
archive_reason VARCHAR(256),
archived_by BIGINT,
archived_at TIMESTAMP,
@@ -336,7 +331,6 @@ COMMENT ON COLUMN annotation_result_history.archived_at IS '归档时间。';
COMMENT ON COLUMN annotation_result_history.created_at IS '创建时间。';
COMMENT ON COLUMN annotation_result_history.reviewer_id IS '审核人用户ID。自动归档时为NULL。';
COMMENT ON COLUMN annotation_result_history.reviewer_name IS '审核人姓名。自动归档时为"auto"。';
COMMENT ON COLUMN annotation_result_history.reviewer_comment IS '审核意见。自动归档时为"auto"。';
COMMENT ON COLUMN annotation_result_history.task_name IS '任务名称(冗余字段)。';
COMMENT ON COLUMN annotation_result_history.resource_name IS '资源名称(冗余字段)。';