Merge branch 'dev54'

This commit is contained in:
wh
2026-05-07 00:24:21 +08:00
21 changed files with 733 additions and 310 deletions

View File

@@ -0,0 +1,41 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultHistoryPageQuery;
import com.labelsys.backend.dto.response.AnnotationResultHistoryResponse;
import com.labelsys.backend.service.AnnotationResultArchiveService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/annotation-result-history")
@RequiredArgsConstructor
@Tag(name = "标注结果归档管理", description = "标注结果归档历史查询接口")
public class AnnotationResultArchiveController {
private final AnnotationResultArchiveService annotationResultArchiveService;
@Operation(summary = "分页查询归档历史")
@GetMapping
public ResponseEntity<PageResult<AnnotationResultHistoryResponse>> pageHistory(
@Valid AnnotationResultHistoryPageQuery query) {
return ResponseEntity.ok(annotationResultArchiveService.pageHistory(UserContext.requireUser(), query));
}
@Operation(summary = "查询归档历史详情")
@GetMapping("/{id}")
public ResponseEntity<AnnotationResultHistoryResponse> getHistory(
@Parameter(description = "历史记录ID", example = "901")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultArchiveService.getHistory(UserContext.requireUser(), id));
}
}

View File

@@ -1,23 +1,20 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
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.dto.response.MergeReviewResultResponse;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.AnnotationResultArchiveService;
import com.labelsys.backend.service.AnnotationResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -25,48 +22,46 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "标注结果管理")
@RestController
@RequestMapping("/api/annotation-results")
@RequiredArgsConstructor
@Tag(name = "标注结果管理", description = "标注结果相关接口")
public class AnnotationResultController {
private final AnnotationResultService annotationResultService;
private final AnnotationResultArchiveService annotationResultArchiveService;
@Operation(summary = "分页查询标注结果")
@GetMapping
public Result<PageResult<AnnotationResultResponse>> page(@ParameterObject AnnotationResultPageQuery query) {
return Result.success(annotationResultService.pageResults(UserContext.requireUser(), query));
public ResponseEntity<PageResult<AnnotationResultResponse>> pageResults(
@Valid AnnotationResultPageQuery query) {
return ResponseEntity.ok(annotationResultService.pageResults(UserContext.requireUser(), query));
}
@Operation(summary = "查询标注结果详情")
@GetMapping("/{id}")
public Result<AnnotationResultResponse> detail(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id
) {
return Result.success(annotationResultService.getResult(UserContext.requireUser(), id));
public ResponseEntity<AnnotationResultResponse> getResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultService.getResult(UserContext.requireUser(), id));
}
@Operation(summary = "查询标注结果比对信息")
//@RequirePosition(UserPosition.REVIEWER)
@Operation(summary = "查询结果比对信息REVIEWER岗位以上可操作")
@GetMapping("/{id}/compare")
public Result<AnnotationResultCompareResponse> compare(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id
) {
return Result.success(annotationResultService.compareResult(UserContext.requireUser(), id));
@RequirePosition(UserPosition.REVIEWER)
public ResponseEntity<AnnotationResultCompareResponse> compareResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultService.compareResult(UserContext.requireUser(), id));
}
@Operation(summary = "提交合并审核结果")
//@RequirePosition(UserPosition.REVIEWER)
@PostMapping("/{id}/merge-review")
public Result<MergeReviewResultResponse> mergeReview(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id,
@Valid @RequestBody MergeReviewResultRequest request
) {
return Result.success(annotationResultArchiveService.mergeReview(UserContext.requireUser(), id, request));
@Operation(summary = "提交合并审核结果,REVIEWER岗位以上可操作")
@PostMapping("/{id}/merge")
@RequirePosition(UserPosition.REVIEWER)
public ResponseEntity<Void> mergeReviewResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id,
@Valid @RequestBody MergeReviewResultRequest request) {
annotationResultService.mergeReviewResult(UserContext.requireUser(), id, request);
return ResponseEntity.ok().build();
}
}
}

View File

@@ -68,23 +68,24 @@ public class SourceResourceController {
return Result.success();
}
@Operation(summary = "下载图片资源")
@Operation(summary = "下载资源")
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadImage(
public ResponseEntity<byte[]> downloadResource(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
var currentUser = UserContext.requireUser();
byte[] imageData = sourceResourceService.downloadImage(currentUser, id);
byte[] resourceData = sourceResourceService.downloadResource(currentUser, id);
// 获取资源信息以确定Content-Type
SourceResource resource = sourceResourceService.getResourceEntity(id);
String contentType = sourceResourceService.getImageContentType(resource);
String contentType = sourceResourceService.getContentType(resource);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getResourceName() + "\"")
.body(imageData);
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getResourceName() + "\"")
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(resource.getFileSize()))
.body(resourceData);
}
// 添加新接口

View File

@@ -0,0 +1,13 @@
// AnnotationResultHistoryPageQuery.java
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "归档历史分页查询")
public record AnnotationResultHistoryPageQuery(
@Schema(description = "任务ID", example = "701") Long taskId,
@Schema(description = "资源ID", example = "601") Long resourceId,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
}

View File

@@ -1,12 +1,15 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.util.Map;
@Schema(description = "合并审核结果请求")
public record MergeReviewResultRequest(
@Schema(description = "差异摘要 JSON", example = "{\"changed\":[{\"field\":\"answer\",\"from\":\"3天\",\"to\":\"72小时\"}],\"summary\":\"统一时间表达\"}") @NotBlank(message = "差异摘要不能为空") String diffSummary,
@Schema(description = "最终问答内容 JSON", example = "[{\"question\":\"运输时效是多久?\",\"answer\":\"72小时\"}]") @NotBlank(message = "问答内容不能为空") String qaContentJson,
@Schema(description = "审核备注", example = "已按审核意见合并,统一为小时口径。") String reviewComment
@Schema(description = "合并后的答案映射key为qa记录IDvalue为合并后的答案")
Map<String, String> mergedAnswers,
@Schema(description = "审核备注")
String reviewComment
) {
}
}

View File

@@ -4,10 +4,17 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源分页查询请求")
public record SourceResourcePageQuery(
@Schema(description = "关键字", example = "运输") String keyword,
@Schema(description = "资源类型", example = "TEXT") String resourceType,
@Schema(description = "资源状态", example = "READY") String sourceStatus,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
@Schema(description = "关键字", example = "运输") String keyword,
@Schema(description = "资源类型", example = "TEXT") String resourceType,
@Schema(description = "资源状态", example = "READY") String sourceStatus,
@Schema(description = "页码可选与pageSize同时提供时启用分页", example = "1") Integer pageNo,
@Schema(description = "每页数量可选与pageNo同时提供时启用分页", example = "10") Integer pageSize
) {
}
/**
* 判断是否需要分页
* @return true表示需要分页false表示查询全部
*/
public boolean needPagination() {
return pageNo != null && pageSize != null;
}
}

View File

@@ -2,15 +2,34 @@ package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "标注结果比对响应")
public record AnnotationResultCompareResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "问答内容 JSON", example = "[{\"question\":\"运输时效是多久?\",\"answer\":\"3天\"}]") String qaContentJson,
@Schema(description = "差异摘要 JSON", example = "{\"changed\":[{\"field\":\"answer\",\"from\":\"3天\",\"to\":\"72小时\"}],\"summary\":\"统一时间表达\"}") String diffSummary,
@Schema(description = "问答存储模式", example = "EXTERNAL") String qaContentStorageMode,
@Schema(description = "外置问答文件路径", example = "review/191000000000000401/qa-content.json") String qaContentFilePath,
@Schema(description = "资源预览路径", example = "preview/191000000000000101/index.html") String sourcePreviewPath
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "问答对列表") List<QaRecord> qaRecords,
@Schema(description = "差异列表") List<DiffRecord> diffRecords,
@Schema(description = "资源预览路径", example = "preview/191000000000000101/index.html") String sourcePreviewPath
) {
}
@Schema(description = "问答记录")
public record QaRecord(
@Schema(description = "记录ID", example = "qa_001") String id,
@Schema(description = "问题", example = "运输时效是多久?") String question,
@Schema(description = "答案", example = "3天") String answer,
@Schema(description = "是否需要审核", example = "true") Boolean requiresReview
) {}
@Schema(description = "差异记录")
public record DiffRecord(
@Schema(description = "关联的问答记录ID", example = "qa_001") String qaId,
@Schema(description = "问题", example = "运输时效是多久?") String question,
@Schema(description = "提取模型答案", example = "3天") String extractAnswer,
@Schema(description = "校验模型答案", example = "72小时") String verifyAnswer,
@Schema(description = "差异原因", example = "时间单位不一致") String diffReason,
@Schema(description = "合并后的最终答案", example = "72小时3天") String mergedAnswer
) {}
}

View File

@@ -0,0 +1,20 @@
// AnnotationResultHistoryResponse.java
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "归档历史响应")
public record AnnotationResultHistoryResponse(
@Schema(description = "历史记录ID", example = "901") Long id,
@Schema(description = "来源结果ID", example = "802") Long sourceResultId,
@Schema(description = "任务ID", example = "701") Long taskId,
@Schema(description = "资源ID", example = "601") Long resourceId,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/802.json") String qaContentFilePath,
@Schema(description = "归档原因", example = "审核通过后归档") String archiveReason,
@Schema(description = "归档操作人ID", example = "5") Long archivedBy,
@Schema(description = "归档时间", example = "2026-05-06T10:30:00") LocalDateTime archivedAt,
@Schema(description = "创建时间", example = "2026-05-06T10:30:00") LocalDateTime createdAt
) {
}

View File

@@ -1,21 +1,22 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import com.labelsys.backend.enums.AnnotationResultStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "标注结果响应")
public record AnnotationResultResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "标注结果状态", example = "MANUAL_REVIEW_PENDING") AnnotationResultStatus runtimeStatus,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "问答存储模式", example = "INLINE") String qaContentStorageMode,
@Schema(description = "审核备注", example = "需统一时间字段口径。") String reviewComment,
@Schema(description = "审核时间", example = "2026-04-27T11:00:00") LocalDateTime reviewedAt,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "标注结果状态", example = "MANUAL_REVIEW_PENDING") AnnotationResultStatus runtimeStatus,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/801.json") String qaContentFilePath,
@Schema(description = "差异摘要文件路径", example = "annotation-results/2/diff/801.json") String diffSummaryFilePath,
@Schema(description = "审核备注", example = "需统一时间字段口径。") String reviewComment,
@Schema(description = "审核时间", example = "2026-04-27T11:00:00") LocalDateTime reviewedAt,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
) {
}
}

View File

@@ -14,7 +14,7 @@ public record LoginResponse(
@Schema(description = "用户名,可为空", example = "alpha-admin") String username,
@Schema(description = "真实姓名", example = "张审核") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role,
@Schema(description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position,
@Schema(description = "岗位枚举枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN公司管理员、SUPER_ADMIN超级管理员", example = "REVIEWER") UserPosition position,
@Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword
) {
public static LoginResponse from(String token, LoginUser loginUser, SysCompany company) {

View File

@@ -13,9 +13,10 @@ public record SourceResourceResponse(
@Schema(description = "文件大小", example = "20480") Long fileSize,
@Schema(description = "资源状态", example = "READY") String sourceStatus,
@Schema(description = "存储提供方", example = "rustfs") String storageProvider,
@Schema(description = "是否有BBOX标注,不显示", example = "false") Boolean hasBbox,
@Schema(description = "备注", example = "第一批导入样本") String remark,
@Schema(description = "创建人名称", example = "张审核") String creatorName,
@Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:05:00") LocalDateTime updatedAt
) {
}
}

View File

@@ -1,37 +1,65 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_result")
public class AnnotationResult {
@TableId(type = IdType.INPUT)
private Long id;
@TableField("company_id")
private Long companyId;
@TableField("creator_id")
private Long creatorId;
private UserRole creatorRole;
@TableField("creator_role")
private String creatorRole;
@TableField("task_id")
private Long taskId;
@TableField("resource_id")
private Long resourceId;
private String qaContentJson;
private String qaContentStorageMode;
@TableField("qa_content_file_path")
private String qaContentFilePath;
private String diffSummary;
@TableField("diff_summary_file_path")
private String diffSummaryFilePath;
@TableField("requires_manual_review")
private Boolean requiresManualReview;
@TableField("is_deleted")
private Boolean isDeleted;
@TableField("reviewer_id")
private Long reviewerId;
@TableField("review_comment")
private String reviewComment;
@TableField("reviewed_at")
private LocalDateTime reviewedAt;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_at")
private LocalDateTime updatedAt;
}
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -20,11 +20,11 @@ public class AnnotationResultHistory {
private Long id;
private Long companyId;
private Long creatorId;
private UserRole creatorRole;
private String creatorRole;
private Long sourceResultId;
private Long taskId;
private Long resourceId;
private String qaContentJson;
//private String qaContentJson;
private String qaContentStorageMode;
private String qaContentFilePath;
private String archiveReason;

View File

@@ -28,7 +28,8 @@ public class SourceResource {
private Long fileSize;
private String sourceStatus;
private String storageProvider;
private Boolean hasBbox;
private String remark;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
}

View File

@@ -1,27 +1,33 @@
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.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.request.MergeReviewResultRequest;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultHistoryPageQuery;
import com.labelsys.backend.dto.response.AnnotationResultHistoryResponse;
import com.labelsys.backend.dto.response.MergeReviewResultResponse;
import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.AnnotationResultHistory;
import com.labelsys.backend.enums.QaContentStorageMode;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.AnnotationResultHistoryMapper;
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.util.IdGenerator;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
@@ -29,59 +35,80 @@ public class AnnotationResultArchiveService {
private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW";
private final AnnotationResultMapper annotationResultMapper;
private final AnnotationResultMapper annotationResultMapper;
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
private final ObjectStorageService objectStorageService;
private final ObjectMapper objectMapper;
private final DataPermissionService dataPermissionService;
@Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
private Duration autoArchiveTimeout;
@Transactional
public MergeReviewResultResponse mergeReview(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
assertReviewer(currentUser);
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "运行态结果不存在");
public PageResult<AnnotationResultHistoryResponse> pageHistory(LoginUser currentUser,
AnnotationResultHistoryPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
var wrapper = new LambdaQueryWrapper<AnnotationResultHistory>()
.eq(AnnotationResultHistory::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResultHistory::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResultHistory::getResourceId, query.resourceId())
.orderByDesc(AnnotationResultHistory::getCreatedAt);
if (shouldFilterByUserId) {
wrapper.eq(AnnotationResultHistory::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) {
wrapper.in(AnnotationResultHistory::getCreatorRole, allowedRoles);
}
LocalDateTime archivedAt = LocalDateTime.now();
AnnotationResultHistory history = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
.qaContentJson(request.qaContentJson())
.qaContentStorageMode(resolveStorageMode(result))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(MANUAL_ARCHIVE_REASON)
.archivedBy(currentUser.userId())
.archivedAt(archivedAt)
.build();
annotationResultHistoryMapper.insert(history);
var page = new Page<AnnotationResultHistory>(query.pageNo(), query.pageSize());
var resultPage = annotationResultHistoryMapper.selectPage(page, wrapper);
int updated = annotationResultMapper.markArchived(
result.getId(),
currentUser.companyId(),
currentUser.userId(),
request.reviewComment(),
archivedAt);
if (updated == 0) {
throw new BusinessException(ResultCode.CONFLICT, "结果已被其他操作处理");
var records = resultPage.getRecords().stream()
.map(this::toResponse)
.toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
}
public AnnotationResultHistoryResponse getHistory(LoginUser currentUser, Long historyId) {
AnnotationResultHistory history = annotationResultHistoryMapper.selectById(historyId);
if (history == null || !history.getCompanyId().equals(currentUser.companyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "历史记录不存在");
}
assertHistoryPermission(currentUser, history);
return toResponse(history);
}
log.info("merged review result, companyId={}, reviewerId={}, resultId={}, historyId={}",
currentUser.companyId(), currentUser.userId(), resultId, history.getId());
return new MergeReviewResultResponse(resultId, history.getId(), MANUAL_ARCHIVE_REASON, archivedAt);
private void assertHistoryPermission(LoginUser currentUser, AnnotationResultHistory history) {
if (!dataPermissionService.canAccessCreator(currentUser, history.getCreatorId(),
UserRole.valueOf(history.getCreatorRole()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问该归档记录");
}
}
private AnnotationResultHistoryResponse toResponse(AnnotationResultHistory history) {
return new AnnotationResultHistoryResponse(
history.getId(),
history.getSourceResultId(),
history.getTaskId(),
history.getResourceId(),
history.getQaContentFilePath(),
history.getArchiveReason(),
history.getArchivedBy(),
history.getArchivedAt(),
history.getCreatedAt()
);
}
@Transactional
public int autoArchiveEligibleResults() {
LocalDateTime cutoff = LocalDateTime.now().minus(autoArchiveTimeout);
List<AnnotationResult> results = annotationResultMapper.selectList(new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getIsDeleted, false)
.eq(AnnotationResult::getRequiresManualReview, false)
.lt(AnnotationResult::getCreatedAt, cutoff));
.eq(AnnotationResult::getIsDeleted, false)
.eq(AnnotationResult::getRequiresManualReview, false)
.lt(AnnotationResult::getCreatedAt, cutoff));
int archivedCount = 0;
for (AnnotationResult result : results) {
if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE", null) != null) {
@@ -97,44 +124,88 @@ public class AnnotationResultArchiveService {
}
}
private String resolveStorageMode(AnnotationResult result) {
if (QaContentStorageMode.isValid(result.getQaContentStorageMode())) {
return result.getQaContentStorageMode();
}
return QaContentStorageMode.INLINE.name();
}
/**
* 归档运行态标注结果到历史表
* 从对象存储读取 qa.json 内容进行归档
*/
private MergeReviewResultResponse archiveRuntimeResult(AnnotationResult result,
Long reviewerId,
String archiveReason,
String reviewComment) {
LocalDateTime archivedAt = LocalDateTime.now();
// 从对象存储读取 qa.json 内容
String qaContentJson = loadQaContentJson(result);
AnnotationResultHistory history = AnnotationResultHistory.builder()
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
.qaContentJson(result.getQaContentJson())
.qaContentStorageMode(resolveStorageMode(result))
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(archiveReason)
.archivedBy(reviewerId)
.archivedAt(archivedAt)
.build();
.id(IdGenerator.nextId())
.companyId(result.getCompanyId())
.creatorId(result.getCreatorId())
.creatorRole(result.getCreatorRole())
.sourceResultId(result.getId())
.taskId(result.getTaskId())
.resourceId(result.getResourceId())
//.qaContentJson(qaContentJson) // 使用从对象存储读取的内容
.qaContentFilePath(result.getQaContentFilePath())
.archiveReason(archiveReason)
.archivedBy(reviewerId)
.archivedAt(archivedAt)
.build();
annotationResultHistoryMapper.insert(history);
int updated = annotationResultMapper.markArchived(
result.getId(),
result.getCompanyId(),
reviewerId,
reviewComment,
archivedAt);
result.getId(),
result.getCompanyId(),
reviewerId,
reviewComment,
archivedAt);
if (updated == 0) {
return null;
}
return new MergeReviewResultResponse(result.getId(), history.getId(), archiveReason, archivedAt);
}
}
/**
* 从对象存储读取 qa.json 内容
*/
private String loadQaContentJson(AnnotationResult result) {
try {
String filePath = result.getQaContentFilePath();
if (filePath == null || filePath.isEmpty()) {
log.warn("qa_content_file_path is null or empty, resultId={}", result.getId());
return "{}";
}
// 解析文件路径,提取 bucket 和 object key
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 load qa content from object storage, resultId={}, filePath={}",
result.getId(), result.getQaContentFilePath(), e);
// 如果读取失败,返回空 JSON
return "{}";
}
}
/**
* 从文件路径中提取 bucket 名称
* 例如annotation-results/2/qa/801.json -> annotation-results
*/
private String extractBucketName(String filePath) {
int firstSlash = filePath.indexOf('/');
return firstSlash > 0 ? filePath.substring(0, firstSlash) : filePath;
}
/**
* 从文件路径中提取 object key
* 例如annotation-results/2/qa/801.json -> 2/qa/801.json
*/
private String extractObjectKey(String filePath) {
int firstSlash = filePath.indexOf('/');
return firstSlash > 0 ? filePath.substring(firstSlash + 1) : "";
}
}

View File

@@ -1,49 +1,59 @@
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.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.QaContentStorageMode;
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 SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
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<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<AnnotationResult> wrapper = new LambdaQueryWrapper<AnnotationResult>()
var wrapper = new LambdaQueryWrapper<AnnotationResult>()
.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());
query.requiresManualReview())
.orderByDesc(AnnotationResult::getCreatedAt);
if (shouldFilterByUserId) {
wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
@@ -51,12 +61,11 @@ public class AnnotationResultService {
wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
}
wrapper.orderByDesc(AnnotationResult::getCreatedAt);
var page = new Page<AnnotationResult>(query.pageNo(), query.pageSize());
var resultPage = annotationResultMapper.selectPage(page, wrapper);
Page<AnnotationResult> page = new Page<>(query.pageNo(), query.pageSize());
Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper);
List<AnnotationResultResponse> records = resultPage.getRecords().stream().map(this::toResponse)
var records = resultPage.getRecords().stream()
.map(this::toResponse)
.filter(response -> query.runtimeStatus() == null
|| query.runtimeStatus().equals(response.runtimeStatus()))
.toList();
@@ -72,14 +81,122 @@ public class AnnotationResultService {
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<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);
}
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());
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) {
@@ -92,69 +209,130 @@ public class AnnotationResultService {
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, "结果不存在");
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, "加载问答内容失败");
}
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 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, "加载差异摘要失败");
}
}
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) {
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, "无效的文件路径格式");
// 从文件路径中提取 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) {
if (filePath.startsWith("/")) {
filePath = filePath.substring(1);
// 从文件路径中提取 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) {
}
int firstSlash = filePath.indexOf("/");
if (firstSlash > 0 && firstSlash < filePath.length() - 1) {
return filePath.substring(firstSlash + 1);
private record Metadata(String createdAt, String updatedAt) {
}
}
// 内部类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) {
}
private record Metadata(String createdAt) {
}
throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式");
}
}

View File

@@ -117,13 +117,19 @@ public class SourceResourceService {
wrapper.orderByDesc(SourceResource::getCreatedAt);
Page<SourceResource> page = new Page<>(query.pageNo(), query.pageSize());
Page<SourceResource> resultPage = sourceResourceMapper.selectPage(page, wrapper);
List<SourceResourceResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
// 判断是否需要分页
if (query.needPagination()) {
Page<SourceResource> page = new Page<>(query.pageNo(), query.pageSize());
Page<SourceResource> resultPage = sourceResourceMapper.selectPage(page, wrapper);
List<SourceResourceResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
} else {
// 不分页,查询全部
List<SourceResource> records = sourceResourceMapper.selectList(wrapper);
List<SourceResourceResponse> responseList = records.stream().map(this::toResponse).toList();
return new PageResult<>(responseList, (long) responseList.size(), 1, responseList.size());
}
}
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
@@ -173,8 +179,8 @@ public class SourceResourceService {
SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(),
resource.getCreatedAt(), resource.getUpdatedAt());
resource.getStorageProvider(), resource.getHasBbox(), resource.getRemark(),
creator == null ? null : creator.getRealName(), resource.getCreatedAt(), resource.getUpdatedAt());
}
private String resolveExtension(String originalFilename, String resourceType) {
@@ -190,22 +196,19 @@ public class SourceResourceService {
}
/**
* 下载图片资源
* 下载资源(支持 TEXT、IMAGE、VIDEO
*
* @param currentUser 当前用户
* @param resourceId 资源ID
* @return 图片字节数组
* @return 资源字节数组
*/
public byte[] downloadImage(LoginUser currentUser, Long resourceId) {
public byte[] downloadResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
log.warn("Resource not found or cross-tenant access attempt: resourceId={}, companyId={}, userId={}",
resourceId, currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!"IMAGE".equals(resource.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持下载");
}
if (!"READY".equals(resource.getSourceStatus())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源未就绪");
}
@@ -213,24 +216,58 @@ public class SourceResourceService {
}
/**
* 获取图片资源的Content-Type
* 获取资源的Content-Type(支持 TEXT、IMAGE、VIDEO
*
* @param resource 资源实体
* @return Content-Type
*/
public String getImageContentType(SourceResource resource) {
public String getContentType(SourceResource resource) {
String filePath = resource.getFilePath();
String resourceType = resource.getResourceType();
// 优先根据文件扩展名判断
if (filePath != null && filePath.contains(".")) {
String extension = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase();
return switch (extension) {
// 图片类型
case "jpg", "jpeg" -> "image/jpeg";
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
default -> "application/octet-stream";
case "bmp" -> "image/bmp";
// 视频类型
case "mp4" -> "video/mp4";
case "webm" -> "video/webm";
case "mov" -> "video/quicktime";
case "avi" -> "video/x-msvideo";
case "mkv" -> "video/x-matroska";
// 文本类型
case "txt" -> "text/plain; charset=UTF-8";
case "json" -> "application/json; charset=UTF-8";
case "xml" -> "application/xml; charset=UTF-8";
case "csv" -> "text/csv; charset=UTF-8";
case "md" -> "text/markdown; charset=UTF-8";
default -> getContentTypeByResourceType(resourceType);
};
}
return "application/octet-stream";
// 如果没有扩展名,根据资源类型判断
return getContentTypeByResourceType(resourceType);
}
/**
* 根据资源类型获取默认Content-Type
*
* @param resourceType 资源类型
* @return Content-Type
*/
private String getContentTypeByResourceType(String resourceType) {
return switch (resourceType) {
case "IMAGE" -> "image/png";
case "VIDEO" -> "video/mp4";
case "TEXT" -> "text/plain; charset=UTF-8";
default -> "application/octet-stream";
};
}
public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) {
@@ -278,6 +315,8 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.BAD_REQUEST, "BBOX数据序列化失败");
}
boolean isNewAnnotation = imageBboxAnnotationMapper.selectByResourceId(resourceId) == null;
ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId);
if (existing != null) {
existing.setBboxJson(bboxJson);
@@ -298,6 +337,12 @@ public class SourceResourceService {
imageBboxAnnotationMapper.insert(annotation);
}
// 更新资源表的has_bbox字段
if (isNewAnnotation || Boolean.FALSE.equals(resource.getHasBbox())) {
resource.setHasBbox(true);
sourceResourceMapper.updateById(resource);
}
return getImageBbox(currentUser, resourceId);
}
@@ -308,6 +353,12 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
imageBboxAnnotationMapper.deleteByResourceId(resourceId);
// 更新资源表的has_bbox字段为false
if (Boolean.TRUE.equals(resource.getHasBbox())) {
resource.setHasBbox(false);
sourceResourceMapper.updateById(resource);
}
}
private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {