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 org.springframework.util.StringUtils; 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 pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { List allowedRoles = dataPermissionService.getAllowedRoles(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); var wrapper = new LambdaQueryWrapper() .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(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 = StringUtils.hasText(result.getDiffSummaryFilePath()) ? loadDiffSummary(result) : null; SourceResource resource = sourceResourceMapper.selectById(result.getResourceId()); // 转换 QA 记录 List qaRecords = qaContent.records().stream() .map(qa -> new AnnotationResultCompareResponse.QaRecord( qa.id(), qa.question(), qa.answer(), qa.requiresReview() )).toList(); // 转换差异记录 List 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 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); } 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() ); } 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() { }); } 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() { }); } 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, "无权访问该标注结果"); } } 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, "归档失败"); } } 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 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 records, Metadata metadata ) { private record DiffRecord(String qaId, String question, String extractAnswer, String verifyAnswer, String diffReason, String mergedAnswer) { } private record Metadata(String createdAt) { } } }