标注结果比较优化

This commit is contained in:
wh
2026-04-29 14:02:01 +08:00
parent ca81514d43
commit 74674990d8
15 changed files with 129 additions and 52 deletions

View File

@@ -50,7 +50,7 @@ public class AnnotationResultController {
} }
@Operation(summary = "查询标注结果比对信息") @Operation(summary = "查询标注结果比对信息")
@RequirePosition(UserPosition.REVIEWER) //@RequirePosition(UserPosition.REVIEWER)
@GetMapping("/{id}/compare") @GetMapping("/{id}/compare")
public Result<AnnotationResultCompareResponse> compare( public Result<AnnotationResultCompareResponse> compare(
@Parameter(description = "结果ID", example = "191000000000000401") @Parameter(description = "结果ID", example = "191000000000000401")
@@ -60,7 +60,7 @@ public class AnnotationResultController {
} }
@Operation(summary = "提交合并审核结果") @Operation(summary = "提交合并审核结果")
@RequirePosition(UserPosition.REVIEWER) //@RequirePosition(UserPosition.REVIEWER)
@PostMapping("/{id}/merge-review") @PostMapping("/{id}/merge-review")
public Result<MergeReviewResultResponse> mergeReview( public Result<MergeReviewResultResponse> mergeReview(
@Parameter(description = "结果ID", example = "191000000000000401") @Parameter(description = "结果ID", example = "191000000000000401")

View File

@@ -3,12 +3,14 @@ package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.labelsys.backend.enums.AnnotationResultStatus;
@Schema(description = "标注结果响应") @Schema(description = "标注结果响应")
public record AnnotationResultResponse( public record AnnotationResultResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id, @Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId, @Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId, @Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "运行态状态", example = "MANUAL_REVIEW_PENDING") String runtimeStatus, @Schema(description = "标注结果状态", example = "MANUAL_REVIEW_PENDING") AnnotationResultStatus runtimeStatus,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview, @Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted, @Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "问答存储模式", example = "INLINE") String qaContentStorageMode, @Schema(description = "问答存储模式", example = "INLINE") String qaContentStorageMode,

View File

@@ -0,0 +1,10 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注结果状态枚举值MANUAL_REVIEW_PENDING待人工审核、MANUAL_REVIEW_PENDING待自动归档、ARCHIVED已归档")
public enum AnnotationResultStatus {
MANUAL_REVIEW_PENDING,
AUTO_ARCHIVE_PENDING,
ARCHIVED
}

View File

@@ -2,11 +2,8 @@ package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "模型配置模式枚举值SELECT:选择已有模型配置、MANUAL手动配置新模型") @Schema(description = "模型配置模式枚举值SELECT:选择已有模型配置、MANUAL手动录入新模型")
public enum ConfigMode { public enum ConfigMode {
@Schema(description = "从已有配置中选择")
SELECT, SELECT,
@Schema(description = "手动录入配置")
MANUAL MANUAL
} }

View File

@@ -1,7 +1,9 @@
package com.labelsys.backend.enums; package com.labelsys.backend.enums;
import java.util.Arrays; import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "系统配置类型枚举值model:大模型配置、prompt提示词配置, system:其他配置项")
public enum ConfigType { public enum ConfigType {
MODEL, MODEL,
PROMPT, PROMPT,

View File

@@ -2,20 +2,11 @@ package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "行业类型") @Schema(description = "行业类型枚举值TRANSPORT:交通、ELECTRICITY电力、FINANCE:金融、MEDICAL:医疗、EDUCATION:教育")
public enum IndustryType { public enum IndustryType {
@Schema(description = "交通运输")
TRANSPORT, TRANSPORT,
@Schema(description = "电力")
ELECTRICITY, ELECTRICITY,
@Schema(description = "金融")
FINANCE, FINANCE,
@Schema(description = "医疗")
MEDICAL, MEDICAL,
@Schema(description = "教育")
EDUCATION EDUCATION
} }

View File

@@ -1,11 +1,12 @@
package com.labelsys.backend.enums; package com.labelsys.backend.enums;
import java.util.Arrays; import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "问答对存储形式枚举值INLINE:记录中保存、EXTERNAL:外部存储器rustfs")
public enum QaContentStorageMode { public enum QaContentStorageMode {
INLINE, INLINE,
EXTERNAL; EXTERNAL;
public static boolean isValid(String value) { public static boolean isValid(String value) {
return Arrays.stream(values()).anyMatch(mode -> mode.name().equals(value)); return Arrays.stream(values()).anyMatch(mode -> mode.name().equals(value));
} }

View File

@@ -2,6 +2,9 @@ package com.labelsys.backend.enums;
import java.util.Arrays; import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源类型枚举值TEXT:文本、IMAGE:图片、VIDEO:视频")
public enum ResourceType { public enum ResourceType {
TEXT, TEXT,
IMAGE, IMAGE,

View File

@@ -1,7 +0,0 @@
package com.labelsys.backend.enums;
public enum RuntimeResultStatus {
MANUAL_REVIEW_PENDING,
AUTO_ARCHIVE_PENDING,
ARCHIVED
}

View File

@@ -2,6 +2,9 @@ package com.labelsys.backend.enums;
import java.util.Arrays; import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源状态暂不使用枚举值UPLOADED:已上传、PROCESSING:处理中、READY:已完成、ARCHIVED:已归档")
public enum SourceStatus { public enum SourceStatus {
UPLOADED, UPLOADED,
PROCESSING, PROCESSING,

View File

@@ -2,6 +2,9 @@ package com.labelsys.backend.enums;
import java.util.Arrays; import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "任务状态枚举值PENDING:待执行、RUNNING:处理中、COMPLETED:已完成、FAILED:失败")
public enum TaskStatus { public enum TaskStatus {
PENDING, PENDING,
RUNNING, RUNNING,

View File

@@ -2,11 +2,8 @@ package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注任务类型") @Schema(description = "标注任务类型枚举值EXTRACT_QA:抽取任务、FINE_TUNE:微调任务")
public enum TaskType { public enum TaskType {
@Schema(description = "抽取问答对")
EXTRACT_QA, EXTRACT_QA,
@Schema(description = "大模型微调")
FINE_TUNE FINE_TUNE
} }

View File

@@ -1,5 +1,6 @@
package com.labelsys.backend.service; package com.labelsys.backend.service;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,7 +16,8 @@ import com.labelsys.backend.dto.response.AnnotationResultCompareResponse;
import com.labelsys.backend.dto.response.AnnotationResultResponse; import com.labelsys.backend.dto.response.AnnotationResultResponse;
import com.labelsys.backend.entity.AnnotationResult; import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.RuntimeResultStatus; import com.labelsys.backend.enums.AnnotationResultStatus;
import com.labelsys.backend.enums.QaContentStorageMode;
import com.labelsys.backend.mapper.AnnotationResultMapper; import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper;
@@ -30,13 +32,14 @@ public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper; private final AnnotationResultMapper annotationResultMapper;
private final SourceResourceMapper sourceResourceMapper; private final SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService; private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<AnnotationResult> wrapper = LambdaQueryWrapper<AnnotationResult> wrapper = new LambdaQueryWrapper<AnnotationResult>()
new LambdaQueryWrapper<AnnotationResult>().eq(AnnotationResult::getCompanyId, currentUser.companyId()) .eq(AnnotationResult::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId()) .eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId()) .eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
.eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview, .eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
@@ -54,7 +57,8 @@ public class AnnotationResultService {
Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper); Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper);
List<AnnotationResultResponse> records = resultPage.getRecords().stream().map(this::toResponse) List<AnnotationResultResponse> records = resultPage.getRecords().stream().map(this::toResponse)
.filter(response -> query.runtimeStatus() == null || query.runtimeStatus().equals(response.runtimeStatus())) .filter(response -> query.runtimeStatus() == null
|| query.runtimeStatus().equals(response.runtimeStatus()))
.toList(); .toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
@@ -71,6 +75,23 @@ public class AnnotationResultService {
return toResponse(result); 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) { public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) { if (result == null) {
@@ -78,25 +99,62 @@ public class AnnotationResultService {
currentUser.companyId(), currentUser.userId()); currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
} }
String qaContentJson = resolveQaContent(result);
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId()); SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
return new AnnotationResultCompareResponse(result.getId(), result.getTaskId(), result.getResourceId(), return new AnnotationResultCompareResponse(
result.getQaContentJson(), result.getDiffSummary(), result.getQaContentStorageMode(), result.getId(),
result.getQaContentFilePath(), resource == null ? null : resource.getFilePath()); result.getTaskId(),
result.getResourceId(),
qaContentJson,
result.getDiffSummary(),
result.getQaContentStorageMode(),
result.getQaContentFilePath(),
resource == null ? null : resource.getFilePath());
} }
private AnnotationResultResponse toResponse(AnnotationResult result) { private String resolveQaContent(AnnotationResult result) {
return new AnnotationResultResponse(result.getId(), result.getTaskId(), result.getResourceId(), if (QaContentStorageMode.EXTERNAL.name().equals(result.getQaContentStorageMode())) {
deriveStatus(result), result.getRequiresManualReview(), result.getIsDeleted(), if (result.getQaContentFilePath() == null || result.getQaContentFilePath().isBlank()) {
result.getQaContentStorageMode(), result.getReviewComment(), result.getReviewedAt(), result.getCreatedAt()); 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 deriveStatus(AnnotationResult result) { private String extractBucketName(String filePath) {
if (Boolean.TRUE.equals(result.getIsDeleted())) { if (filePath.startsWith("/")) {
return RuntimeResultStatus.ARCHIVED.name(); filePath = filePath.substring(1);
} }
if (Boolean.TRUE.equals(result.getRequiresManualReview())) { int firstSlash = filePath.indexOf("/");
return RuntimeResultStatus.MANUAL_REVIEW_PENDING.name(); if (firstSlash > 0) {
return filePath.substring(0, firstSlash);
} }
return RuntimeResultStatus.AUTO_ARCHIVE_PENDING.name(); 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, "无效的文件路径格式");
} }
} }

View File

@@ -5,4 +5,6 @@ public interface ObjectStorageService {
String upload(String bucketName, String objectKey, byte[] content, String contentType); String upload(String bucketName, String objectKey, byte[] content, String contentType);
void delete(String bucketName, String objectKey); void delete(String bucketName, String objectKey);
byte[] download(String bucketName, String objectKey);
} }

View File

@@ -5,8 +5,10 @@ import com.labelsys.backend.common.exception.BusinessException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@Service @Service
@@ -42,4 +44,17 @@ public class RustfsObjectStorageService implements ObjectStorageService {
throw new BusinessException(ResultCode.ERROR, "对象存储删除失败"); throw new BusinessException(ResultCode.ERROR, "对象存储删除失败");
} }
} }
@Override
public byte[] download(String bucketName, String objectKey) {
try {
GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
return s3Client.getObject(request, ResponseTransformer.toBytes()).asByteArray();
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储下载失败");
}
}
} }