Files
lablesys_backend/src/main/java/com/labelsys/backend/service/AnnotationResultService.java

365 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.labelsys.backend.common.ResultCode;
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.AnnotationResultPageQuery;
import com.labelsys.backend.dto.request.MergeReviewResultRequest;
import com.labelsys.backend.dto.response.AnnotationResultCompareResponse;
import com.labelsys.backend.dto.response.AnnotationResultResponse;
import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.AnnotationResultHistory;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.AnnotationResultStatus;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.AnnotationResultHistoryMapper;
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper;
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
private final SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
private final ObjectMapper objectMapper;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
var wrapper = new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getIsDeleted, Boolean.FALSE)
.eq(AnnotationResult::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
.eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
query.requiresManualReview())
.orderByDesc(AnnotationResult::getCreatedAt);
if (shouldFilterByUserId) {
wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
}
var page = new Page<AnnotationResult>(query.pageNo(), query.pageSize());
var resultPage = annotationResultMapper.selectPage(page, wrapper);
var records = resultPage.getRecords().stream()
.map(this::toResponse)
.filter(response -> query.runtimeStatus() == null
|| query.runtimeStatus().equals(response.runtimeStatus()))
.toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
}
public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId,
currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
//assertResultPermission(currentUser, result);
return toResponse(result);
}
public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId,
currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
//assertResultPermission(currentUser, result);
QaContent qaContent = loadQaContent(result);
DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
loadDiffSummary(result) : null;
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
// 转换 QA 记录
List<AnnotationResultCompareResponse.QaRecord> qaRecords = qaContent.records().stream()
.map(qa -> new AnnotationResultCompareResponse.QaRecord(
qa.id(),
qa.question(),
qa.answer(),
qa.requiresReview()
)).toList();
// 转换差异记录
List<AnnotationResultCompareResponse.DiffRecord> diffRecords = diffContent != null ?
diffContent.records().stream()
.map(diff -> new AnnotationResultCompareResponse.DiffRecord(
diff.qaId(),
diff.question(),
diff.extractAnswer(),
diff.verifyAnswer(),
diff.diffReason(),
diff.mergedAnswer()
)).toList() : List.of();
return new AnnotationResultCompareResponse(
result.getId(),
result.getTaskId(),
result.getResourceId(),
qaRecords,
diffRecords,
resource == null ? null : resource.getFilePath()
);
}
@Transactional
public void mergeReviewResult(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
//assertResultPermission(currentUser, result);
// 读取当前 qa.json
QaContent qaContent = loadQaContent(result);
// 更新 qa.json 的 answer 字段
List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream()
.map(record -> {
String mergedAnswer = request.mergedAnswers().get(record.id());
if (mergedAnswer != null) {
return new QaContent.QaRecord(
record.id(),
record.question(),
mergedAnswer,
false
);
}
return record;
})
.toList();
QaContent updatedQaContent = new QaContent(
qaContent.taskId(),
qaContent.resourceId(),
updatedQaRecords,
new QaContent.Metadata(
qaContent.metadata().createdAt(),
LocalDateTime.now().toString()
)
);
saveQaContent(result, updatedQaContent);
// 更新数据库记录
result.setIsDeleted(Boolean.TRUE);
result.setReviewerId(currentUser.userId());
result.setReviewComment(request.reviewComment());
result.setReviewedAt(LocalDateTime.now());
result.setRequiresManualReview(false);
annotationResultMapper.updateById(result);
// 归档到历史表(人工审核后归档)
archiveToHistory(result, currentUser, "审核通过后归档", false);
log.info("merged review result, companyId={}, userId={}, resultId={}",
currentUser.companyId(), currentUser.userId(), resultId);
}
private AnnotationResultResponse toResponse(AnnotationResult result) {
return new AnnotationResultResponse(
result.getId(),
result.getTaskId(),
result.getTaskName(), // 新增
result.getResourceId(),
result.getResourceName(), // 新增
deriveStatus(result),
result.getRequiresManualReview(),
result.getIsDeleted(),
result.getQaContentFilePath(),
result.getDiffSummaryFilePath(),
result.getReviewComment(),
result.getReviewedAt(),
result.getCreatedAt()
);
}
private AnnotationResultStatus deriveStatus(AnnotationResult result) {
if (Boolean.TRUE.equals(result.getIsDeleted())) {
return AnnotationResultStatus.ARCHIVED;
}
if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
return AnnotationResultStatus.MANUAL_REVIEW_PENDING;
}
return AnnotationResultStatus.AUTO_ARCHIVE_PENDING;
}
private QaContent loadQaContent(AnnotationResult result) {
try {
String filePath = result.getQaContentFilePath();
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 TypeReference<QaContent>() {
});
} catch (Exception e) {
log.error("Failed to load qa content, resultId={}, filePath={}", result.getId(),
result.getQaContentFilePath(), e);
throw new BusinessException(ResultCode.ERROR, "加载问答内容失败");
}
}
private DiffContent loadDiffSummary(AnnotationResult result) {
try {
String filePath = result.getDiffSummaryFilePath();
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 TypeReference<DiffContent>() {
});
} catch (Exception e) {
log.error("Failed to load diff summary, resultId={}, filePath={}", result.getId(),
result.getDiffSummaryFilePath(), e);
throw new BusinessException(ResultCode.ERROR, "加载差异摘要失败");
}
}
private void saveQaContent(AnnotationResult result, QaContent qaContent) {
try {
String jsonContent = objectMapper.writeValueAsString(qaContent);
String filePath = result.getQaContentFilePath();
String bucketName = extractBucketName(filePath);
String objectKey = extractObjectKey(filePath);
objectStorageService.upload(bucketName, objectKey, jsonContent.getBytes(StandardCharsets.UTF_8),
"application/json");
} catch (Exception e) {
log.error("Failed to save qa content, resultId={}", result.getId(), e);
throw new BusinessException(ResultCode.ERROR, "保存问答内容失败");
}
}
private void assertResultPermission(LoginUser currentUser, AnnotationResult result) {
if (!dataPermissionService.canAccessCreator(currentUser, result.getCreatorId(),
UserRole.valueOf(result.getCreatorRole()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问该标注结果");
}
}
/**
* 归档到历史表
* @param result 标注结果
* @param currentUser 当前用户
* @param archiveReason 归档原因
* @param isAutoArchive 是否自动归档true=自动归档false=人工审核后归档)
*/
private void archiveToHistory(AnnotationResult result, LoginUser currentUser, String archiveReason, boolean isAutoArchive) {
try {
// 读取 qa.json 内容用于归档
QaContent qaContent = loadQaContent(result);
// 构建归档记录
AnnotationResultHistory.AnnotationResultHistoryBuilder historyBuilder = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.taskName(result.getTaskName())
.resourceId(result.getResourceId())
.resourceName(result.getResourceName())
//.qaContentJson(objectMapper.writeValueAsString(qaContent))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(archiveReason)
.archivedBy(currentUser.userId())
.archivedAt(LocalDateTime.now())
.createdAt(LocalDateTime.now());
// 根据归档类型设置审核人信息
if (isAutoArchive) {
// 自动归档reviewer_id为NULLname和comment为"auto"
historyBuilder
.reviewerId(null)
.reviewerName("auto")
.reviewerComment("auto");
} else {
// 人工审核后归档:使用审核人信息
historyBuilder
.reviewerId(result.getReviewerId())
.reviewerName(currentUser.realName())
.reviewerComment(result.getReviewComment());
}
annotationResultHistoryMapper.insert(historyBuilder.build());
log.info("archived result to history, resultId={}, historyId={}, isAutoArchive={}",
result.getId(), historyBuilder.build().getId(), isAutoArchive);
} catch (Exception e) {
log.error("Failed to archive result to history, resultId={}", result.getId(), e);
throw new BusinessException(ResultCode.ERROR, "归档失败");
}
}
private String extractBucketName(String filePath) {
// 从文件路径中提取 bucket 名称
// 例如annotation-results/2/qa/801.json -> annotation-results
int firstSlash = filePath.indexOf('/');
return firstSlash > 0 ? filePath.substring(0, firstSlash) : filePath;
}
private String extractObjectKey(String filePath) {
// 从文件路径中提取 object key
// 例如annotation-results/2/qa/801.json -> 2/qa/801.json
int firstSlash = filePath.indexOf('/');
return firstSlash > 0 ? filePath.substring(firstSlash + 1) : "";
}
// 内部类qa.json 结构
private record QaContent(
Long taskId,
Long resourceId,
List<QaRecord> records,
Metadata metadata
) {
private record QaRecord(String id, String question, String answer, Boolean requiresReview) {
}
private record Metadata(String createdAt, String updatedAt) {
}
}
// 内部类diff.json 结构
private record DiffContent(
Long taskId,
Long resourceId,
List<DiffRecord> records,
Metadata metadata
) {
private record DiffRecord(String qaId, String question, String extractAnswer,
String verifyAnswer, String diffReason, String mergedAnswer) {
}
private record Metadata(String createdAt) {
}
}
}