Merge branch 'dev54'
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 添加新接口
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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记录ID,value为合并后的答案")
|
||||
Map<String, String> mergedAnswers,
|
||||
|
||||
@Schema(description = "审核备注")
|
||||
String reviewComment
|
||||
) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,7 @@ public class SourceResource {
|
||||
private Long fileSize;
|
||||
private String sourceStatus;
|
||||
private String storageProvider;
|
||||
private Boolean hasBbox;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -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) : "";
|
||||
}
|
||||
}
|
||||
@@ -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, "无效的文件路径格式");
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -39,7 +39,8 @@ labelsys:
|
||||
ttl: PT2H
|
||||
store-type: redis
|
||||
annotation:
|
||||
auto-archive-timeout: PT2H
|
||||
auto-archive-fixed-delay: 300000 # 定时任务执行间隔(毫秒),默认5分钟
|
||||
auto-archive-timeout: PT2H # 自动归档超时时间,默认2小时
|
||||
object-storage:
|
||||
endpoint: ${OBJECT_STORAGE_ENDPOINT:http://39.107.112.174:9000}
|
||||
region: ${OBJECT_STORAGE_REGION:cn-east-1}
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
<result column="creator_role" property="creatorRole"/>
|
||||
<result column="task_id" property="taskId"/>
|
||||
<result column="resource_id" property="resourceId"/>
|
||||
<result column="qa_content_json" property="qaContentJson"/>
|
||||
<result column="qa_content_storage_mode" property="qaContentStorageMode"/>
|
||||
<result column="qa_content_file_path" property="qaContentFilePath"/>
|
||||
<result column="diff_summary" property="diffSummary"/>
|
||||
<result column="diff_summary_file_path" property="diffSummaryFilePath"/>
|
||||
<result column="requires_manual_review" property="requiresManualReview"/>
|
||||
<result column="is_deleted" property="isDeleted"/>
|
||||
<result column="reviewer_id" property="reviewerId"/>
|
||||
@@ -22,9 +20,9 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="AnnotationResultColumns">
|
||||
id, company_id, creator_id, creator_role, task_id, resource_id, qa_content_json,
|
||||
qa_content_storage_mode, qa_content_file_path, diff_summary, requires_manual_review,
|
||||
is_deleted, reviewer_id, review_comment, reviewed_at, created_at, updated_at
|
||||
id, company_id, creator_id, creator_role, task_id, resource_id, qa_content_file_path,
|
||||
diff_summary_file_path, requires_manual_review, is_deleted, reviewer_id, review_comment,
|
||||
reviewed_at, created_at, updated_at
|
||||
</sql>
|
||||
|
||||
<select id="findActiveByIdAndCompanyId" resultMap="AnnotationResultResultMap">
|
||||
|
||||
@@ -57,12 +57,12 @@ ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO source_resource (
|
||||
id, company_id, creator_id, creator_role, resource_name, resource_type,
|
||||
bucket_name, file_path, file_size, source_status, storage_provider, remark
|
||||
bucket_name, file_path, file_size, source_status, storage_provider, has_bbox, remark
|
||||
) VALUES
|
||||
(601, 2, 3, 'EMPLOYEE', '设备巡检规范.txt', 'TEXT',
|
||||
'source-data', 'text/202604/601.txt', 20480, 'READY', 'rustfs', '文本资源示例'),
|
||||
'source-data', 'text/202604/601.txt', 20480, 'READY', 'rustfs', NULL, '文本资源示例'),
|
||||
(602, 2, 3, 'EMPLOYEE', '控制柜照片.jpg', 'IMAGE',
|
||||
'source-data', 'image/202604/602.jpg', 532480, 'READY', 'rustfs', '图片资源示例')
|
||||
'source-data', 'image/202604/602.jpg', 532480, 'READY', 'rustfs', TRUE, '图片资源示例')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO annotation_task (
|
||||
@@ -85,29 +85,25 @@ ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO annotation_result (
|
||||
id, company_id, creator_id, creator_role, task_id, resource_id,
|
||||
qa_content_json, qa_content_storage_mode, qa_content_file_path, diff_summary,
|
||||
qa_content_file_path, diff_summary_file_path,
|
||||
requires_manual_review, is_deleted, reviewer_id, review_comment, reviewed_at
|
||||
) VALUES
|
||||
(801, 2, 3, 'EMPLOYEE', 701, 601,
|
||||
'{"question":"巡检开始前需要做什么?","answer":"详见外置结果文件,包含完整步骤与注意事项。"}',
|
||||
'EXTERNAL', 'annotation-results/202604/801-qa.json',
|
||||
'{"extract_question":"巡检开始前需要做什么?","extract_answer":"开始前检查设备状态和作业环境。","verify_answer":"开始前应确认设备状态、防护用品和现场环境安全。","mismatch_fields":["answer"],"reason":"抽取答案遗漏了安全检查要点。"}',
|
||||
'annotation-results/qa/801.json',
|
||||
'annotation-results/diff/801.json',
|
||||
TRUE, FALSE, NULL, NULL, NULL),
|
||||
(802, 2, 3, 'EMPLOYEE', 702, 602,
|
||||
'{"question":"图片中的控制柜当前状态如何?","answer":"控制柜处于运行状态,绿色指示灯亮起。"}',
|
||||
'INLINE', NULL,
|
||||
'{"extract_question":"图片中的控制柜当前状态如何?","extract_answer":"控制柜处于运行状态,绿色指示灯亮起。","verify_answer":"控制柜正在运行,指示灯显示正常。","mismatch_fields":[],"reason":"校验结果与抽取结果基本一致。"}',
|
||||
'annotation-results/qa/802.json',
|
||||
NULL,
|
||||
FALSE, FALSE, 5, '结果可通过。', CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO annotation_result_history (
|
||||
id, company_id, creator_id, creator_role, source_result_id, task_id, resource_id,
|
||||
qa_content_json, qa_content_storage_mode, qa_content_file_path, archive_reason, archived_by, archived_at
|
||||
qa_content_file_path, archive_reason, archived_by, archived_at
|
||||
) VALUES
|
||||
(901, 2, 3, 'EMPLOYEE', 802, 702, 602,
|
||||
'{"question":"图片中的控制柜当前状态如何?","answer":"控制柜处于运行状态,绿色指示灯亮起。"}',
|
||||
'INLINE',
|
||||
NULL,
|
||||
'annotation-results/2/qa/802.json',
|
||||
'审核通过后归档', 5, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ CREATE TABLE IF NOT EXISTS source_resource
|
||||
file_size BIGINT NOT NULL DEFAULT 0,
|
||||
source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED',
|
||||
storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs',
|
||||
has_bbox BOOLEAN,
|
||||
remark VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -154,6 +155,7 @@ COMMENT ON COLUMN source_resource.file_path IS '文件存储路径,表示对
|
||||
COMMENT ON COLUMN source_resource.file_size IS '文件大小,单位字节,默认 0。';
|
||||
COMMENT ON COLUMN source_resource.source_status IS '资源状态,默认 UPLOADED,可选 PROCESSING、READY、ARCHIVED。';
|
||||
COMMENT ON COLUMN source_resource.storage_provider IS '存储提供方,默认 rustfs。';
|
||||
COMMENT ON COLUMN source_resource.has_bbox IS '是否有BBOX标注。NULL表示非图片资源或未标注;TRUE表示已标注BBOX;FALSE表示已删除BBOX标注。';
|
||||
COMMENT ON COLUMN source_resource.remark IS '备注说明。';
|
||||
COMMENT ON COLUMN source_resource.created_at IS '创建时间。';
|
||||
COMMENT ON COLUMN source_resource.updated_at IS '更新时间。';
|
||||
@@ -242,25 +244,24 @@ COMMENT ON COLUMN annotation_task_resource.task_id IS '任务ID。';
|
||||
COMMENT ON COLUMN annotation_task_resource.resource_id IS '资源ID。';
|
||||
COMMENT ON COLUMN annotation_task_resource.created_at IS '创建时间。';
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS annotation_result
|
||||
(
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT NOT NULL,
|
||||
creator_id BIGINT NOT NULL,
|
||||
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
|
||||
task_id BIGINT NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
qa_content_json TEXT NOT NULL DEFAULT '{}',
|
||||
qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE',
|
||||
qa_content_file_path VARCHAR(512),
|
||||
diff_summary TEXT NOT NULL DEFAULT '{}',
|
||||
requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
reviewer_id BIGINT,
|
||||
review_comment TEXT,
|
||||
reviewed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT NOT NULL,
|
||||
creator_id BIGINT NOT NULL,
|
||||
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
|
||||
task_id BIGINT NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
qa_content_file_path VARCHAR(512) NOT NULL,
|
||||
diff_summary_file_path VARCHAR(512),
|
||||
requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
reviewer_id BIGINT,
|
||||
review_comment TEXT,
|
||||
reviewed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_annotation_result_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
|
||||
CONSTRAINT fk_annotation_result_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id),
|
||||
CONSTRAINT fk_annotation_result_task FOREIGN KEY (task_id) REFERENCES annotation_task (id),
|
||||
@@ -268,18 +269,16 @@ CREATE TABLE IF NOT EXISTS annotation_result
|
||||
CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE annotation_result IS '当前标注结果表。';
|
||||
COMMENT ON TABLE annotation_result IS '当前标注结果表。问答内容和差异摘要统一存储在对象存储中。';
|
||||
COMMENT ON COLUMN annotation_result.id IS '标注结果主键ID。';
|
||||
COMMENT ON COLUMN annotation_result.company_id IS '所属公司ID。';
|
||||
COMMENT ON COLUMN annotation_result.creator_id IS '结果创建人用户ID。';
|
||||
COMMENT ON COLUMN annotation_result.creator_role IS '结果创建人数据权限角色,默认 EMPLOYEE。';
|
||||
COMMENT ON COLUMN annotation_result.task_id IS '关联任务ID。';
|
||||
COMMENT ON COLUMN annotation_result.resource_id IS '关联资源ID。';
|
||||
COMMENT ON COLUMN annotation_result.qa_content_json IS '问答内容 JSON 字符串。字段类型为 TEXT,建议结构为 {\"question\":\"...\",\"answer\":\"...\"}。中小体积内容默认直接入库。';
|
||||
COMMENT ON COLUMN annotation_result.qa_content_storage_mode IS '问答内容存储模式,默认 INLINE,可选 INLINE、EXTERNAL。当完整问答内容较大时,可设为 EXTERNAL,仅在表内保留摘要或索引信息。';
|
||||
COMMENT ON COLUMN annotation_result.qa_content_file_path IS '当 qa_content_storage_mode = EXTERNAL 时,记录外置问答内容文件路径。';
|
||||
COMMENT ON COLUMN annotation_result.diff_summary IS '差异摘要 JSON 字符串。字段类型为 TEXT,建议结构为 {\"extract_question\":\"...\",\"extract_answer\":\"...\",\"verify_answer\":\"...\",\"mismatch_fields\":[\"question\",\"answer\"],\"reason\":\"...\"}。';
|
||||
COMMENT ON COLUMN annotation_result.requires_manual_review IS '是否需要人工审核,默认 FALSE。';
|
||||
COMMENT ON COLUMN annotation_result.qa_content_file_path IS '问答内容文件路径,存储在对象存储中。文件格式包含taskId、resourceId、records数组(id、question、answer、requiresReview)。';
|
||||
COMMENT ON COLUMN annotation_result.diff_summary_file_path IS '差异摘要文件路径,存储在对象存储中。当存在差异时生成,包含taskId、resourceId、records数组(qaId、question、extractAnswer、verifyAnswer、diffReason、mergedAnswer)。';
|
||||
COMMENT ON COLUMN annotation_result.requires_manual_review IS '是否需要人工审核,默认 FALSE。当diff_summary_file_path不为空时为TRUE。';
|
||||
COMMENT ON COLUMN annotation_result.is_deleted IS '软删除标记,默认 FALSE。';
|
||||
COMMENT ON COLUMN annotation_result.reviewer_id IS '审核人用户ID。';
|
||||
COMMENT ON COLUMN annotation_result.review_comment IS '审核意见。';
|
||||
@@ -287,22 +286,21 @@ COMMENT ON COLUMN annotation_result.reviewed_at IS '审核时间。';
|
||||
COMMENT ON COLUMN annotation_result.created_at IS '创建时间。';
|
||||
COMMENT ON COLUMN annotation_result.updated_at IS '更新时间。';
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS annotation_result_history
|
||||
(
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT NOT NULL,
|
||||
creator_id BIGINT NOT NULL,
|
||||
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
|
||||
source_result_id BIGINT,
|
||||
task_id BIGINT NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
qa_content_json TEXT NOT NULL DEFAULT '{}',
|
||||
qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE',
|
||||
qa_content_file_path VARCHAR(512),
|
||||
archive_reason VARCHAR(255),
|
||||
archived_by BIGINT,
|
||||
archived_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT NOT NULL,
|
||||
creator_id BIGINT NOT NULL,
|
||||
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
|
||||
source_result_id BIGINT,
|
||||
task_id BIGINT NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
qa_content_file_path VARCHAR(512) NOT NULL,
|
||||
archive_reason VARCHAR(256),
|
||||
archived_by BIGINT,
|
||||
archived_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_annotation_result_history_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
|
||||
CONSTRAINT fk_annotation_result_history_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id),
|
||||
CONSTRAINT fk_annotation_result_history_result FOREIGN KEY (source_result_id) REFERENCES annotation_result (id),
|
||||
@@ -311,7 +309,7 @@ CREATE TABLE IF NOT EXISTS annotation_result_history
|
||||
CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE annotation_result_history IS '历史归档结果表。';
|
||||
COMMENT ON TABLE annotation_result_history IS '历史归档结果表。问答内容存储在对象存储中。';
|
||||
COMMENT ON COLUMN annotation_result_history.id IS '历史结果主键ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.company_id IS '所属公司ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.creator_id IS '历史记录创建人用户ID。';
|
||||
@@ -319,9 +317,7 @@ COMMENT ON COLUMN annotation_result_history.creator_role IS '历史记录创建
|
||||
COMMENT ON COLUMN annotation_result_history.source_result_id IS '来源运行态结果ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.task_id IS '关联任务ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.resource_id IS '关联资源ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.qa_content_json IS '归档后的问答内容 JSON 字符串。字段类型为 TEXT,建议结构为 {"question":"...","answer":"..."}。';
|
||||
COMMENT ON COLUMN annotation_result_history.qa_content_storage_mode IS '归档后的问答内容存储模式,默认 INLINE,可选 INLINE、EXTERNAL。';
|
||||
COMMENT ON COLUMN annotation_result_history.qa_content_file_path IS '当 qa_content_storage_mode = EXTERNAL 时,记录归档后的外置问答内容文件路径。';
|
||||
COMMENT ON COLUMN annotation_result_history.qa_content_file_path IS '归档后的问答内容文件路径,存储在对象存储中。';
|
||||
COMMENT ON COLUMN annotation_result_history.archive_reason IS '归档原因说明。';
|
||||
COMMENT ON COLUMN annotation_result_history.archived_by IS '归档操作人用户ID。';
|
||||
COMMENT ON COLUMN annotation_result_history.archived_at IS '归档时间。';
|
||||
@@ -411,6 +407,8 @@ CREATE INDEX IF NOT EXISTS idx_sys_config_company_type ON sys_config (company_id
|
||||
CREATE INDEX IF NOT EXISTS idx_source_resource_company_type ON source_resource (company_id, resource_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_resource_company_status ON source_resource (company_id, source_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_resource_creator ON source_resource (company_id, creator_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_resource_has_bbox ON source_resource (company_id, has_bbox);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_resource_has_bbox ON source_resource (company_id, has_bbox);
|
||||
CREATE INDEX IF NOT EXISTS idx_annotation_task_company_status ON annotation_task (company_id, task_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_annotation_task_company_deleted ON annotation_task (company_id, is_deleted);
|
||||
CREATE INDEX IF NOT EXISTS idx_annotation_task_creator ON annotation_task (company_id, creator_id);
|
||||
|
||||
Reference in New Issue
Block a user