package com.labelsys.backend.service; import java.nio.charset.StandardCharsets; import java.util.List; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.response.AnnotationResultCompareResponse; import com.labelsys.backend.dto.response.AnnotationResultResponse; import com.labelsys.backend.entity.AnnotationResult; import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.enums.AnnotationResultStatus; import com.labelsys.backend.enums.QaContentStorageMode; import com.labelsys.backend.mapper.AnnotationResultMapper; import com.labelsys.backend.mapper.SourceResourceMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Service @RequiredArgsConstructor public class AnnotationResultService { private final AnnotationResultMapper annotationResultMapper; private final SourceResourceMapper sourceResourceMapper; private final DataPermissionService dataPermissionService; private final ObjectStorageService objectStorageService; public PageResult pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { List allowedRoles = dataPermissionService.getAllowedRoles(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); LambdaQueryWrapper 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()); if (shouldFilterByUserId) { wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId()); } else if (!allowedRoles.isEmpty()) { wrapper.in(AnnotationResult::getCreatorRole, allowedRoles); } wrapper.orderByDesc(AnnotationResult::getCreatedAt); Page page = new Page<>(query.pageNo(), query.pageSize()); Page resultPage = annotationResultMapper.selectPage(page, wrapper); List 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, "结果不存在"); } return toResponse(result); } private AnnotationResultResponse toResponse(AnnotationResult result) { return new AnnotationResultResponse(result.getId(), result.getTaskId(), result.getResourceId(), deriveStatus(result), result.getRequiresManualReview(), result.getIsDeleted(), result.getQaContentStorageMode(), 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; } 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, "结果不存在"); } String qaContentJson = resolveQaContent(result); SourceResource resource = sourceResourceMapper.selectById(result.getResourceId()); return new AnnotationResultCompareResponse( result.getId(), result.getTaskId(), result.getResourceId(), qaContentJson, result.getDiffSummary(), result.getQaContentStorageMode(), result.getQaContentFilePath(), resource == null ? null : resource.getFilePath()); } private String resolveQaContent(AnnotationResult result) { if (QaContentStorageMode.EXTERNAL.name().equals(result.getQaContentStorageMode())) { if (result.getQaContentFilePath() == null || result.getQaContentFilePath().isBlank()) { log.warn("External storage mode but file path is empty, resultId={}", result.getId()); return "{}"; } try { String filePath = result.getQaContentFilePath(); String bucketName = extractBucketName(filePath); String objectKey = extractObjectKey(filePath); byte[] content = objectStorageService.download(bucketName, objectKey); return new String(content, StandardCharsets.UTF_8); } catch (Exception e) { log.error("Failed to download external qa content, resultId={}, filePath={}", result.getId(), result.getQaContentFilePath(), e); throw new BusinessException(ResultCode.ERROR, "下载问答内容失败"); } } else { return result.getQaContentJson() != null ? result.getQaContentJson() : "{}"; } } private String extractBucketName(String filePath) { if (filePath.startsWith("/")) { filePath = filePath.substring(1); } int firstSlash = filePath.indexOf("/"); if (firstSlash > 0) { return filePath.substring(0, firstSlash); } throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式"); } private String extractObjectKey(String filePath) { if (filePath.startsWith("/")) { filePath = filePath.substring(1); } int firstSlash = filePath.indexOf("/"); if (firstSlash > 0 && firstSlash < filePath.length() - 1) { return filePath.substring(firstSlash + 1); } throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式"); } }