2026-04-27 10:27:57 +08:00
|
|
|
package com.labelsys.backend.service;
|
|
|
|
|
|
2026-04-29 14:02:01 +08:00
|
|
|
import java.nio.charset.StandardCharsets;
|
2026-04-28 12:15:10 +08:00
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
2026-04-27 10:27:57 +08:00
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
2026-04-27 16:25:39 +08:00
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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;
|
2026-04-29 14:02:01 +08:00
|
|
|
import com.labelsys.backend.enums.AnnotationResultStatus;
|
|
|
|
|
import com.labelsys.backend.enums.QaContentStorageMode;
|
2026-04-27 10:27:57 +08:00
|
|
|
import com.labelsys.backend.mapper.AnnotationResultMapper;
|
|
|
|
|
import com.labelsys.backend.mapper.SourceResourceMapper;
|
2026-04-28 12:15:10 +08:00
|
|
|
|
2026-04-27 10:27:57 +08:00
|
|
|
import lombok.RequiredArgsConstructor;
|
2026-04-27 16:25:39 +08:00
|
|
|
import lombok.extern.slf4j.Slf4j;
|
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 SourceResourceMapper sourceResourceMapper;
|
2026-04-27 16:25:39 +08:00
|
|
|
private final DataPermissionService dataPermissionService;
|
2026-04-29 14:02:01 +08:00
|
|
|
private final ObjectStorageService objectStorageService;
|
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
|
|
|
|
2026-04-29 14:02:01 +08:00
|
|
|
LambdaQueryWrapper<AnnotationResult> wrapper = new LambdaQueryWrapper<AnnotationResult>()
|
|
|
|
|
.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,
|
2026-04-29 14:02:01 +08:00
|
|
|
query.requiresManualReview());
|
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
|
|
|
|
2026-04-27 16:25:39 +08:00
|
|
|
wrapper.orderByDesc(AnnotationResult::getCreatedAt);
|
|
|
|
|
|
|
|
|
|
Page<AnnotationResult> page = new Page<>(query.pageNo(), query.pageSize());
|
|
|
|
|
Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper);
|
|
|
|
|
|
2026-04-28 12:15:10 +08:00
|
|
|
List<AnnotationResultResponse> 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, "结果不存在");
|
|
|
|
|
}
|
|
|
|
|
return toResponse(result);
|
|
|
|
|
}
|
|
|
|
|
|
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.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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 10:27:57 +08:00
|
|
|
public AnnotationResultCompareResponse compareResult(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, "结果不存在");
|
|
|
|
|
}
|
2026-04-29 14:02:01 +08:00
|
|
|
|
|
|
|
|
String qaContentJson = resolveQaContent(result);
|
|
|
|
|
|
2026-04-27 10:27:57 +08:00
|
|
|
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
|
2026-04-29 14:02:01 +08:00
|
|
|
return new AnnotationResultCompareResponse(
|
|
|
|
|
result.getId(),
|
|
|
|
|
result.getTaskId(),
|
|
|
|
|
result.getResourceId(),
|
|
|
|
|
qaContentJson,
|
|
|
|
|
result.getDiffSummary(),
|
|
|
|
|
result.getQaContentStorageMode(),
|
|
|
|
|
result.getQaContentFilePath(),
|
|
|
|
|
resource == null ? null : resource.getFilePath());
|
2026-04-27 10:27:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:02:01 +08:00
|
|
|
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() : "{}";
|
|
|
|
|
}
|
2026-04-27 10:27:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:02:01 +08:00
|
|
|
private String extractBucketName(String filePath) {
|
|
|
|
|
if (filePath.startsWith("/")) {
|
|
|
|
|
filePath = filePath.substring(1);
|
2026-04-27 10:27:57 +08:00
|
|
|
}
|
2026-04-29 14:02:01 +08:00
|
|
|
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);
|
2026-04-27 10:27:57 +08:00
|
|
|
}
|
2026-04-29 14:02:01 +08:00
|
|
|
throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式");
|
2026-04-27 10:27:57 +08:00
|
|
|
}
|
2026-04-27 16:25:39 +08:00
|
|
|
}
|