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

337 lines
15 KiB
Java
Raw Normal View History

2026-04-27 10:27:57 +08:00
package com.labelsys.backend.service;
2026-04-27 16:25:39 +08:00
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
2026-04-27 10:27:57 +08:00
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.enums.UserRole;
2026-04-27 10:27:57 +08:00
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultPageQuery;
import com.labelsys.backend.dto.request.MergeReviewResultRequest;
2026-04-27 10:27:57 +08:00
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;
2026-04-27 10:27:57 +08:00
import com.labelsys.backend.entity.SourceResource;
2026-04-29 14:02:01 +08:00
import com.labelsys.backend.enums.AnnotationResultStatus;
import com.labelsys.backend.mapper.AnnotationResultHistoryMapper;
2026-04-27 10:27:57 +08:00
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.util.IdGenerator;
2026-04-27 10:27:57 +08:00
import lombok.RequiredArgsConstructor;
2026-04-27 16:25:39 +08:00
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
2026-04-27 10:27:57 +08:00
2026-04-27 16:25:39 +08:00
@Slf4j
2026-04-27 10:27:57 +08:00
@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;
2026-04-27 10:27:57 +08:00
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
2026-04-27 16:25:39 +08:00
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
2026-04-28 12:15:10 +08:00
var wrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AnnotationResult>()
2026-04-29 14:02:01 +08:00
.eq(AnnotationResult::getCompanyId, currentUser.companyId())
2026-04-28 12:15:10 +08:00
.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);
2026-04-28 12:15:10 +08:00
2026-04-27 16:25:39 +08:00
if (shouldFilterByUserId) {
wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
}
2026-04-28 12:15:10 +08:00
var page = new Page<AnnotationResult>(query.pageNo(), query.pageSize());
var resultPage = annotationResultMapper.selectPage(page, wrapper);
2026-04-27 16:25:39 +08:00
var records = resultPage.getRecords().stream()
.map(this::toResponse)
2026-04-29 14:02:01 +08:00
.filter(response -> query.runtimeStatus() == null
|| query.runtimeStatus().equals(response.runtimeStatus()))
.toList();
2026-04-27 16:25:39 +08:00
2026-04-29 14:02:01 +08:00
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
2026-04-27 10:27:57 +08:00
}
public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) {
2026-04-27 16:25:39 +08:00
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
2026-04-28 12:15:10 +08:00
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId,
2026-04-29 14:02:01 +08:00
currentUser.companyId(), currentUser.userId());
2026-04-27 10:27:57 +08:00
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
}
assertResultPermission(currentUser, result);
2026-04-27 10:27:57 +08:00
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 = StringUtils.hasText(result.getDiffSummaryFilePath()) ?
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.setReviewerId(currentUser.userId());
result.setReviewComment(request.reviewComment());
result.setReviewedAt(LocalDateTime.now());
result.setRequiresManualReview(false);
annotationResultMapper.updateById(result);
// 归档到历史表
archiveToHistory(result, currentUser, "审核通过后归档");
log.info("merged review result, companyId={}, userId={}, resultId={}",
currentUser.companyId(), currentUser.userId(), resultId);
}
2026-04-29 14:02:01 +08:00
private AnnotationResultResponse toResponse(AnnotationResult result) {
return new AnnotationResultResponse(
result.getId(),
result.getTaskId(),
result.getResourceId(),
deriveStatus(result),
result.getRequiresManualReview(),
result.getIsDeleted(),
result.getQaContentFilePath(),
result.getDiffSummaryFilePath(),
result.getReviewComment(),
result.getReviewedAt(),
result.getCreatedAt()
);
2026-04-29 14:02:01 +08:00
}
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, "加载问答内容失败");
2026-04-27 10:27:57 +08:00
}
}
2026-04-29 14:02:01 +08:00
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, "加载差异摘要失败");
}
}
2026-04-29 14:02:01 +08:00
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, "保存问答内容失败");
}
2026-04-27 10:27:57 +08:00
}
private void assertResultPermission(LoginUser currentUser, AnnotationResult result) {
if (!dataPermissionService.canAccessCreator(currentUser, result.getCreatorId(),
UserRole.valueOf(result.getCreatorRole()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问该标注结果");
}
}
private void archiveToHistory(AnnotationResult result, LoginUser currentUser, String archiveReason) {
try {
// 读取 qa.json 内容用于归档
QaContent qaContent = loadQaContent(result);
// 构建归档记录
AnnotationResultHistory history = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role().name())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
//.qaContentJson(objectMapper.writeValueAsString(qaContent))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(archiveReason)
.archivedBy(currentUser.userId())
.archivedAt(LocalDateTime.now())
.createdAt(LocalDateTime.now())
.build();
annotationResultHistoryMapper.insert(history);
log.info("archived result to history, resultId={}, historyId={}", result.getId(), history.getId());
} catch (Exception e) {
log.error("Failed to archive result to history, resultId={}", result.getId(), e);
throw new BusinessException(ResultCode.ERROR, "归档失败");
2026-04-29 14:02:01 +08:00
}
2026-04-27 10:27:57 +08:00
}
2026-04-29 14:02:01 +08:00
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) {
2026-04-27 10:27:57 +08:00
}
private record Metadata(String createdAt, String updatedAt) {
2026-04-29 14:02:01 +08:00
}
}
// 内部类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) {
2026-04-29 14:02:01 +08:00
}
private record Metadata(String createdAt) {
2026-04-27 10:27:57 +08:00
}
}
2026-04-27 16:25:39 +08:00
}