diff --git a/pom.xml b/pom.xml index e95862b..ab3f177 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.labelsys label-backend @@ -15,6 +15,8 @@ 3.5.3.1 2.3.0 UTF-8 + UTF-8 + UTF-8 @@ -102,7 +104,7 @@ - + org.apache.maven.plugins diff --git a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java index d9dbce0..7e1fc34 100644 --- a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java +++ b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java @@ -1,11 +1,10 @@ package com.labelsys.backend.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { diff --git a/src/main/java/com/labelsys/backend/controller/SourceResourceController.java b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java index 2891642..d751676 100644 --- a/src/main/java/com/labelsys/backend/controller/SourceResourceController.java +++ b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java @@ -3,21 +3,28 @@ package com.labelsys.backend.controller; 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.SaveImageBboxRequest; import com.labelsys.backend.dto.request.SourceResourcePageQuery; import com.labelsys.backend.dto.request.SourceUploadRequest; +import com.labelsys.backend.dto.response.ImageBboxResponse; import com.labelsys.backend.dto.response.SourceResourceResponse; import com.labelsys.backend.dto.response.SourceUploadResponse; +import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.service.SourceResourceService; 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.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -37,15 +44,16 @@ public class SourceResourceController { @Operation(summary = "分页查询资源") @GetMapping - public Result> page(@ParameterObject @ModelAttribute SourceResourcePageQuery query) { + public Result> page( + @ParameterObject @ModelAttribute SourceResourcePageQuery query) { return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query)); } @Operation(summary = "查询资源详情") @GetMapping("/{id}") public Result detail( - @Parameter(description = "资源ID", example = "191000000000000101") - @PathVariable Long id + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id ) { return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id)); } @@ -53,10 +61,59 @@ public class SourceResourceController { @Operation(summary = "删除资源") @DeleteMapping("/{id}") public Result delete( - @Parameter(description = "资源ID", example = "191000000000000101") - @PathVariable Long id + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id ) { sourceResourceService.deleteResource(UserContext.requireUser(), id); return Result.success(); } -} + + @Operation(summary = "下载图片资源") + @GetMapping("/{id}/download") + public ResponseEntity downloadImage( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id + ) { + var currentUser = UserContext.requireUser(); + byte[] imageData = sourceResourceService.downloadImage(currentUser, id); + + // 获取资源信息以确定Content-Type + SourceResource resource = sourceResourceService.getResourceEntity(id); + String contentType = sourceResourceService.getImageContentType(resource); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, contentType) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getResourceName() + "\"") + .body(imageData); + } + + // 添加新接口 + @Operation(summary = "查询图片资源BBOX标注") + @GetMapping("/{id}/bbox") + public Result getImageBbox( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id + ) { + return Result.success(sourceResourceService.getImageBbox(UserContext.requireUser(), id)); + } + + @Operation(summary = "保存图片资源BBOX标注") + @PostMapping("/{id}/bbox") + public Result saveImageBbox( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id, + @Valid @RequestBody SaveImageBboxRequest request + ) { + return Result.success(sourceResourceService.saveImageBbox(UserContext.requireUser(), id, request)); + } + + @Operation(summary = "删除图片资源BBOX标注") + @DeleteMapping("/{id}/bbox") + public Result deleteImageBbox( + @Parameter(description = "资源ID", example = "191000000000000101") + @PathVariable Long id + ) { + sourceResourceService.deleteImageBbox(UserContext.requireUser(), id); + return Result.success(); + } +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/request/BboxCoordinate.java b/src/main/java/com/labelsys/backend/dto/request/BboxCoordinate.java new file mode 100644 index 0000000..9e05835 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/BboxCoordinate.java @@ -0,0 +1,16 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "BBOX坐标对象") +public record BboxCoordinate( + @Schema(description = "BBOX标识", example = "bbox_001") String id, + @NotNull @Schema(description = "左上角X坐标", example = "100") Integer x, + @NotNull @Schema(description = "左上角Y坐标", example = "50") Integer y, + @NotNull @Schema(description = "宽度", example = "200") Integer width, + @NotNull @Schema(description = "高度", example = "150") Integer height, + @Schema(description = "标注标签", example = "车辆") String label + //@Schema(description = "置信度", example = "0.95") Double confidence +) { +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java index 7f286da..8acb8af 100644 --- a/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java @@ -1,24 +1,18 @@ package com.labelsys.backend.dto.request; -import java.util.List; - import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.TaskType; - import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.List; + @Schema(description = "创建标注任务请求") public record CreateAnnotationTaskRequest( @Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName, @Schema(description = "行业类型", defaultValue = "TRANSPORT", example = "TRANSPORT") @NotNull(message = "行业类型不能为空") IndustryType industryType, @Schema(description = "任务类型", defaultValue = "EXTRACT_QA", example = "EXTRACT_QA") @NotNull(message = "任务类型不能为空") TaskType taskType, - @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List resourceIds, - @Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel, - @Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel, - @Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt, - @Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt) { -} + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List resourceIds) { +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java deleted file mode 100644 index 85d2eeb..0000000 --- a/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.labelsys.backend.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "手动模型配置请求") -public record ManualModelConfigRequest( - @Schema(description = "模型名称", example = "qwen-plus") @NotBlank(message = "模型名称不能为空") String modelName, - @Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") @NotBlank(message = "模型地址不能为空") String modelUrl, - @Schema(description = "模型密钥", example = "sk-demo1234") @NotBlank(message = "模型密钥不能为空") String apiKey -) { -} diff --git a/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java b/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java deleted file mode 100644 index e8a1195..0000000 --- a/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.labelsys.backend.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "任务提示词配置请求") -public record PromptConfigOptionRequest( - @Schema(description = "已选择的提示词配置名称", example = "qa-extract-v1") String selectedConfigName, - @Schema(description = "手动输入的提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText -) { -} diff --git a/src/main/java/com/labelsys/backend/dto/request/SaveImageBboxRequest.java b/src/main/java/com/labelsys/backend/dto/request/SaveImageBboxRequest.java new file mode 100644 index 0000000..44d7526 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SaveImageBboxRequest.java @@ -0,0 +1,15 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +@Schema(description = "保存图片BBOX请求") +public record SaveImageBboxRequest( + @NotEmpty @Schema(description = "BBOX坐标列表") + @Valid List bboxes, + @Schema(description = "备注") String remark +) { +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java deleted file mode 100644 index 629fb0e..0000000 --- a/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.labelsys.backend.dto.request; - -import com.labelsys.backend.enums.ConfigMode; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "任务模型配置请求") -public record TaskModelConfigRequest( - @Schema(description = "配置模式:SELECT 或 MANUAL", example = "SELECT") @NotNull(message = "配置模式不能为空") ConfigMode mode, - - @Schema(description = "已选择的配置名称", example = "qwen-plus-extract") String selectedConfigName, - - @Schema(description = "手动录入的模型配置", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @Valid ManualModelConfigRequest manualConfig) { -} diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java index d78e9f9..6c5b02a 100644 --- a/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java @@ -6,15 +6,10 @@ import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.TaskType; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; @Schema(description = "更新标注任务请求") public record UpdateAnnotationTaskRequest( @Schema(description = "行业类型", example = "TRANSPORT") IndustryType industryType, @Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType, - @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List resourceIds, - @Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel, - @Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel, - @Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt, - @Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt) { -} + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List resourceIds) { +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java index d321a49..452c34c 100644 --- a/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java @@ -1,25 +1,21 @@ package com.labelsys.backend.dto.response; -import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDateTime; -import java.util.List; - import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.TaskType; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; +import java.util.List; @Schema(description = "标注任务响应") public record AnnotationTaskResponse( - @Schema(description = "任务ID", example = "191000000000000301") Long id, - @Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName, - @Schema(description = "行业类型:默认值transport,暂不显示", example = "transport") IndustryType industryType, - @Schema(description = "任务类型:暂不显示", example = "EXTRACT_QA") TaskType taskType, - @Schema(description = "任务状态", example = "PENDING") String taskStatus, - @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List resourceIds, - @Schema(description = "抽取模型配置", example = "{\"configId\":191000000000000511,\"configName\":\"qwen-plus-extract\",\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****1234\"}") TaskModelConfigResponse extractModel, - @Schema(description = "校验模型配置", example = "{\"configId\":191000000000000512,\"configName\":\"qwen-max-verify\",\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****5678\"}") TaskModelConfigResponse verifyModel, - @Schema(description = "抽取提示词配置", example = "{\"configId\":191000000000000521,\"configName\":\"qa-extract-v1\",\"promptText\":\"请抽取文档中的问答对,并按 JSON 数组输出。\"}") TaskPromptConfigResponse extractPrompt, - @Schema(description = "校验提示词配置", example = "{\"configId\":191000000000000522,\"configName\":\"qa-verify-v1\",\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") TaskPromptConfigResponse verifyPrompt, - @Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt, - @Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt + @Schema(description = "任务ID", example = "191000000000000301") Long id, + @Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName, + @Schema(description = "行业类型", example = "transport") IndustryType industryType, + @Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType, + @Schema(description = "任务状态", example = "PENDING") String taskStatus, + @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List resourceIds, + @Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt, + @Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/response/ImageBboxResponse.java b/src/main/java/com/labelsys/backend/dto/response/ImageBboxResponse.java new file mode 100644 index 0000000..5da61d1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/ImageBboxResponse.java @@ -0,0 +1,30 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "图片BBOX响应") +public record ImageBboxResponse( + @Schema(description = "bbox标识ID", example = "191000000000000101") Long id, + @Schema(description = "资源ID", example = "191000000000000102") Long resourceId, + @Schema(description = "BBOX坐标列表") List bboxes, + @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 +) { + + @Schema(description = "BBOX坐标响应对象") + public record BboxCoordinateResponse( + @Schema(description = "BBOX标识", example = "bbox_001") String id, + @Schema(description = "左上角X坐标", example = "100") Integer x, + @Schema(description = "左上角Y坐标", example = "50") Integer y, + @Schema(description = "宽度", example = "200") Integer width, + @Schema(description = "高度", example = "150") Integer height, + @Schema(description = "标注标签", example = "车辆") String label +// @Schema(description = "置信度", example = "0.95") Double confidence + ) { + } +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java deleted file mode 100644 index fdb608b..0000000 --- a/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.labelsys.backend.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "任务模型配置响应") -public record TaskModelConfigResponse( - @Schema(description = "配置ID", example = "191000000000000511") Long configId, - @Schema(description = "配置名称", example = "qwen-plus-extract") String configName, - @Schema(description = "模型名称", example = "qwen-plus") String modelName, - @Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") String modelUrl, - @Schema(description = "脱敏后的模型密钥", example = "****1234") String maskedApiKey -) { -} diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java deleted file mode 100644 index ad34b8a..0000000 --- a/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.labelsys.backend.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "任务提示词配置响应") -public record TaskPromptConfigResponse( - @Schema(description = "配置ID", example = "191000000000000521") Long configId, - @Schema(description = "配置名称", example = "qa-extract-v1") String configName, - @Schema(description = "提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText -) { -} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationTask.java b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java index 2388196..38026ec 100644 --- a/src/main/java/com/labelsys/backend/entity/AnnotationTask.java +++ b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java @@ -6,12 +6,13 @@ import com.baomidou.mybatisplus.annotation.TableName; import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.UserRole; -import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Data @Builder @NoArgsConstructor @@ -19,30 +20,18 @@ import lombok.NoArgsConstructor; @TableName("annotation_task") public class AnnotationTask { @TableId(type = IdType.INPUT) - private Long id; - private Long companyId; - private Long creatorId; - private UserRole creatorRole; - private String taskName; - private IndustryType industryType; - private TaskType taskType; - private Long extractModelConfigId; - private String extractModelName; - private String extractModelUrl; - private String extractModelApiKey; - private Long verifyModelConfigId; - private String verifyModelName; - private String verifyModelUrl; - private String verifyModelApiKey; - private Long extractPromptConfigId; - private String extractPrompt; - private Long verifyPromptConfigId; - private String verifyPrompt; - private String taskStatus; - private Boolean isDeleted; + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String taskName; + private IndustryType industryType; + private TaskType taskType; + private String taskStatus; + private Boolean isDeleted; private LocalDateTime startedAt; private LocalDateTime finishedAt; - private String errorMessage; + private String errorMessage; private LocalDateTime createdAt; private LocalDateTime updatedAt; -} +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/entity/ImageBboxAnnotation.java b/src/main/java/com/labelsys/backend/entity/ImageBboxAnnotation.java new file mode 100644 index 0000000..9dc0d05 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/ImageBboxAnnotation.java @@ -0,0 +1,68 @@ +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 lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 图片BBOX标注表实体类 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("image_bbox_annotation") +public class ImageBboxAnnotation { + + /** + * 主键ID + */ + @TableId(type = IdType.INPUT) + private Long id; + + /** + * 所属公司ID + */ + private Long companyId; + + /** + * 关联的图片资源ID + */ + private Long resourceId; + + /** + * bbox坐标信息JSON数组 + */ + private String bboxJson; + + /** + * 备注说明 + */ + private String remark; + + /** + * 创建人用户ID + */ + private Long creatorId; + + /** + * 创建人数据权限角色,默认 EMPLOYEE + */ + private UserRole creatorRole; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/mapper/ImageBboxAnnotationMapper.java b/src/main/java/com/labelsys/backend/mapper/ImageBboxAnnotationMapper.java new file mode 100644 index 0000000..5149c82 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/ImageBboxAnnotationMapper.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.ImageBboxAnnotation; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ImageBboxAnnotationMapper extends BaseMapper { + ImageBboxAnnotation selectByResourceId(@Param("resourceId") Long resourceId); + + int deleteByResourceId(@Param("resourceId") Long resourceId); +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java index e2c132f..d63788a 100644 --- a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java +++ b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java @@ -1,15 +1,5 @@ package com.labelsys.backend.service; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.labelsys.backend.common.ResultCode; @@ -20,12 +10,9 @@ import com.labelsys.backend.dto.request.AnnotationTaskPageQuery; import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest; import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest; import com.labelsys.backend.dto.response.AnnotationTaskResponse; -import com.labelsys.backend.dto.response.TaskModelConfigResponse; -import com.labelsys.backend.dto.response.TaskPromptConfigResponse; import com.labelsys.backend.entity.AnnotationTask; import com.labelsys.backend.entity.AnnotationTaskResource; import com.labelsys.backend.entity.SourceResource; -import com.labelsys.backend.entity.SysConfig; import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.SourceStatus; import com.labelsys.backend.enums.TaskStatus; @@ -33,51 +20,52 @@ import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.mapper.AnnotationTaskMapper; import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper; -import com.labelsys.backend.mapper.SysConfigMapper; 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.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; @Slf4j @Service @RequiredArgsConstructor public class AnnotationTaskService { - private final AnnotationTaskMapper annotationTaskMapper; + private final AnnotationTaskMapper annotationTaskMapper; private final AnnotationTaskResourceMapper annotationTaskResourceMapper; - private final SourceResourceMapper sourceResourceMapper; - private final SysConfigMapper sysConfigMapper; - private final SysConfigService sysConfigService; - private final DataPermissionService dataPermissionService; + private final SourceResourceMapper sourceResourceMapper; + private final DataPermissionService dataPermissionService; @Transactional public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) { List resources = loadAndValidateResources(currentUser, request.resourceIds()); - SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser, - request.extractModel()); - SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser, - request.verifyModel()); - SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser, - request.extractPrompt()); - SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, - request.verifyPrompt()); - AnnotationTask task = AnnotationTask.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId()) - .creatorId(currentUser.userId()).creatorRole(currentUser.role()).taskName(request.taskName()) - .industryType(defaultIndustryType(request.industryType())).taskType(defaultTaskType(request.taskType())) - .extractModelConfigId(extractModel.configId()).extractModelName(extractModel.modelName()) - .extractModelUrl(extractModel.modelUrl()).extractModelApiKey(extractModel.apiKey()) - .verifyModelConfigId(verifyModel.configId()).verifyModelName(verifyModel.modelName()) - .verifyModelUrl(verifyModel.modelUrl()).verifyModelApiKey(verifyModel.apiKey()) - .extractPromptConfigId(extractPrompt.configId()).extractPrompt(extractPrompt.promptText()) - .verifyPromptConfigId(verifyPrompt.configId()).verifyPrompt(verifyPrompt.promptText()) - .taskStatus(TaskStatus.PENDING.name()).isDeleted(false).build(); + AnnotationTask task = AnnotationTask.builder() + .id(IdGenerator.nextId()) + .companyId(currentUser.companyId()) + .creatorId(currentUser.userId()) + .creatorRole(currentUser.role()) + .taskName(request.taskName()) + .industryType(defaultIndustryType(request.industryType())) + .taskType(defaultTaskType(request.taskType())) + .taskStatus(TaskStatus.PENDING.name()) + .isDeleted(false) + .build(); + annotationTaskMapper.insert(task); saveTaskBindings(task.getId(), currentUser.companyId(), resources); + log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}", currentUser.companyId(), currentUser.userId(), task.getId(), resources.size()); - return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt); + + return buildTaskResponse(task, resourceIds(resources)); } @Transactional @@ -95,45 +83,13 @@ public class AnnotationTaskService { List currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); List targetResourceIds = normalizeIds(request.resourceIds()); resourcesChanged = !currentResourceIds.equals(targetResourceIds); + if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) { throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源"); } resources = loadAndValidateResources(currentUser, request.resourceIds()); } - SysConfigService.ResolvedModelConfig extractModel = null; - SysConfigService.ResolvedModelConfig verifyModel = null; - SysConfigService.ResolvedPromptConfig extractPrompt = null; - SysConfigService.ResolvedPromptConfig verifyPrompt = null; - - if (request.extractModel() != null) { - extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel()); - task.setExtractModelConfigId(extractModel.configId()); - task.setExtractModelName(extractModel.modelName()); - task.setExtractModelUrl(extractModel.modelUrl()); - task.setExtractModelApiKey(extractModel.apiKey()); - } - - if (request.verifyModel() != null) { - verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel()); - task.setVerifyModelConfigId(verifyModel.configId()); - task.setVerifyModelName(verifyModel.modelName()); - task.setVerifyModelUrl(verifyModel.modelUrl()); - task.setVerifyModelApiKey(verifyModel.apiKey()); - } - - if (request.extractPrompt() != null) { - extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt()); - task.setExtractPromptConfigId(extractPrompt.configId()); - task.setExtractPrompt(extractPrompt.promptText()); - } - - if (request.verifyPrompt() != null) { - verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt()); - task.setVerifyPromptConfigId(verifyPrompt.configId()); - task.setVerifyPrompt(verifyPrompt.promptText()); - } - if (request.industryType() != null) { task.setIndustryType(request.industryType()); } @@ -155,10 +111,7 @@ public class AnnotationTaskService { List finalResourceIds = resources != null ? resourceIds(resources) : normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); - if (extractModel == null || verifyModel == null || extractPrompt == null || verifyPrompt == null) { - return buildTaskResponse(task, finalResourceIds); - } - return buildTaskResponse(task, finalResourceIds, extractModel, verifyModel, extractPrompt, verifyPrompt); + return buildTaskResponse(task, finalResourceIds); } public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) { @@ -195,7 +148,7 @@ public class AnnotationTaskService { List records = resultPage.getRecords().stream() .filter(task -> query.resourceId() == null || annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()) - .contains(query.resourceId())) + .contains(query.resourceId())) .map(task -> buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())))) .toList(); @@ -245,39 +198,26 @@ public class AnnotationTaskService { private void saveTaskBindings(Long taskId, Long companyId, List resources) { for (SourceResource resource : resources) { - annotationTaskResourceMapper.insert(AnnotationTaskResource.builder().id(IdGenerator.nextId()) - .companyId(companyId).taskId(taskId).resourceId(resource.getId()).build()); + annotationTaskResourceMapper.insert(AnnotationTaskResource.builder() + .id(IdGenerator.nextId()) + .companyId(companyId) + .taskId(taskId) + .resourceId(resource.getId()) + .build()); } } - private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List resourceIds, - SysConfigService.ResolvedModelConfig extractModel, SysConfigService.ResolvedModelConfig verifyModel, - SysConfigService.ResolvedPromptConfig extractPrompt, SysConfigService.ResolvedPromptConfig verifyPrompt) { - return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(), - task.getTaskStatus(), resourceIds, sysConfigService.toResponse(extractModel), - sysConfigService.toResponse(verifyModel), sysConfigService.toResponse(extractPrompt), - sysConfigService.toResponse(verifyPrompt), task.getCreatedAt(), task.getUpdatedAt()); - } - private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List resourceIds) { - String extractModelConfigName = resolveConfigName(task.getExtractModelConfigId()); - String verifyModelConfigName = resolveConfigName(task.getVerifyModelConfigId()); - String extractPromptConfigName = resolveConfigName(task.getExtractPromptConfigId()); - String verifyPromptConfigName = resolveConfigName(task.getVerifyPromptConfigId()); - - return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(), - task.getTaskStatus(), resourceIds, - new TaskModelConfigResponse(task.getExtractModelConfigId(), extractModelConfigName, - task.getExtractModelName(), - task.getExtractModelUrl(), maskSecret(task.getExtractModelApiKey())), - new TaskModelConfigResponse(task.getVerifyModelConfigId(), verifyModelConfigName, - task.getVerifyModelName(), - task.getVerifyModelUrl(), maskSecret(task.getVerifyModelApiKey())), - new TaskPromptConfigResponse(task.getExtractPromptConfigId(), extractPromptConfigName, - task.getExtractPrompt()), - new TaskPromptConfigResponse(task.getVerifyPromptConfigId(), verifyPromptConfigName, - task.getVerifyPrompt()), - task.getCreatedAt(), task.getUpdatedAt()); + return new AnnotationTaskResponse( + task.getId(), + task.getTaskName(), + task.getIndustryType(), + task.getTaskType(), + task.getTaskStatus(), + resourceIds, + task.getCreatedAt(), + task.getUpdatedAt() + ); } private List resourceIds(List resources) { @@ -304,22 +244,4 @@ public class AnnotationTaskService { private TaskType defaultTaskType(TaskType taskType) { return taskType != null ? taskType : TaskType.EXTRACT_QA; } - - private String maskSecret(String secret) { - if (!StringUtils.hasText(secret)) { - return null; - } - if (secret.length() <= 4) { - return "****"; - } - return "****" + secret.substring(secret.length() - 4); - } - - private String resolveConfigName(Long configId) { - if (configId == null) { - return null; - } - SysConfig config = sysConfigMapper.selectById(configId); - return config != null ? config.getConfigName() : null; - } -} +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/service/SourceResourceService.java b/src/main/java/com/labelsys/backend/service/SourceResourceService.java index 447dd20..d773446 100644 --- a/src/main/java/com/labelsys/backend/service/SourceResourceService.java +++ b/src/main/java/com/labelsys/backend/service/SourceResourceService.java @@ -1,47 +1,56 @@ package com.labelsys.backend.service; -import java.io.IOException; -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.core.JsonProcessingException; +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.config.ObjectStorageProperties; import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.SaveImageBboxRequest; import com.labelsys.backend.dto.request.SourceResourcePageQuery; import com.labelsys.backend.dto.request.SourceUploadRequest; +import com.labelsys.backend.dto.response.ImageBboxResponse; import com.labelsys.backend.dto.response.SourceResourceResponse; import com.labelsys.backend.dto.response.SourceUploadResponse; +import com.labelsys.backend.entity.ImageBboxAnnotation; import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SysUser; import com.labelsys.backend.enums.ResourceType; import com.labelsys.backend.enums.SourceStatus; import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; +import com.labelsys.backend.mapper.ImageBboxAnnotationMapper; import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SysUserMapper; import com.labelsys.backend.util.IdGenerator; import com.labelsys.backend.util.ObjectStoragePathBuilder; - 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 org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; @Slf4j @Service @RequiredArgsConstructor public class SourceResourceService { - private final SourceResourceMapper sourceResourceMapper; + private final SourceResourceMapper sourceResourceMapper; private final AnnotationTaskResourceMapper annotationTaskResourceMapper; - private final SysUserMapper sysUserMapper; - private final DataPermissionService dataPermissionService; - private final ObjectStorageService objectStorageService; - private final com.labelsys.backend.config.ObjectStorageProperties objectStorageProperties; + private final SysUserMapper sysUserMapper; + private final DataPermissionService dataPermissionService; + private final ObjectStorageService objectStorageService; + private final ObjectStorageProperties objectStorageProperties; + private final ImageBboxAnnotationMapper imageBboxAnnotationMapper; + private final ObjectMapper objectMapper; @Transactional public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) { @@ -53,37 +62,39 @@ public class SourceResourceService { throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法"); } String resourceName = - StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename(); + StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename(); SourceResource existingResource = - sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName); + sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName); if (existingResource != null) { throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName); } - + long resourceId = IdGenerator.nextId(); String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType()); String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(), request.getResourceType(), - resourceId, extension); + resourceId, extension); try { objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(), - file.getContentType()); + file.getContentType()); } catch (IOException ex) { throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败"); } SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId()) - .creatorId(currentUser.userId()).creatorRole(currentUser.role()) - .resourceName( - StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename()) - .resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket()) - .filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name()) - .storageProvider("rustfs").remark(request.getRemark()).build(); + .creatorId(currentUser.userId()).creatorRole(currentUser.role()) + .resourceName( + StringUtils.hasText(request.getResourceName()) ? + request.getResourceName() : + file.getOriginalFilename()) + .resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket()) + .filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name()) + .storageProvider("rustfs").remark(request.getRemark()).build(); sourceResourceMapper.insert(resource); log.info("uploaded source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(), - currentUser.userId(), resourceId); + currentUser.userId(), resourceId); return new SourceUploadResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(), - resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(), - resource.getCreatedAt()); + resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(), + resource.getCreatedAt()); } public PageResult pageResources(LoginUser currentUser, SourceResourcePageQuery query) { @@ -91,10 +102,12 @@ public class SourceResourceService { boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); LambdaQueryWrapper wrapper = - new LambdaQueryWrapper().eq(SourceResource::getCompanyId, currentUser.companyId()) - .eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType()) - .eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus()) - .like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword()); + new LambdaQueryWrapper().eq(SourceResource::getCompanyId, currentUser.companyId()) + .eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, + query.resourceType()) + .eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, + query.sourceStatus()) + .like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword()); if (shouldFilterByUserId) { wrapper.eq(SourceResource::getCreatorId, currentUser.userId()); @@ -109,20 +122,30 @@ public class SourceResourceService { List records = resultPage.getRecords().stream().map(this::toResponse).toList(); - return new PageResult<>(records, resultPage.getTotal(), (int)resultPage.getCurrent(), - (int)resultPage.getSize()); + return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), + (int) resultPage.getSize()); } public SourceResourceResponse getResource(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()); + resourceId, currentUser.companyId(), currentUser.userId()); throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); } return toResponse(resource); } + /** + * 获取资源实体(内部使用) + * + * @param resourceId 资源ID + * @return 资源实体 + */ + public SourceResource getResourceEntity(Long resourceId) { + return sourceResourceMapper.selectById(resourceId); + } + @Transactional public void deleteResource(LoginUser currentUser, Long resourceId) { SourceResource resource = sourceResourceMapper.selectById(resourceId); @@ -137,21 +160,21 @@ public class SourceResourceService { resource.setSourceStatus(SourceStatus.ARCHIVED.name()); sourceResourceMapper.updateById(resource); log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}", - currentUser.companyId(), currentUser.userId(), resourceId); + currentUser.companyId(), currentUser.userId(), resourceId); return; } objectStorageService.delete(resource.getBucketName(), resource.getFilePath()); sourceResourceMapper.deleteById(resourceId); log.info("deleted source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(), - currentUser.userId(), resourceId); + currentUser.userId(), resourceId); } private SourceResourceResponse toResponse(SourceResource resource) { 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.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(), + resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(), + resource.getCreatedAt(), resource.getUpdatedAt()); } private String resolveExtension(String originalFilename, String resourceType) { @@ -165,4 +188,140 @@ public class SourceResourceService { default -> "bin"; }; } -} + + /** + * 下载图片资源 + * + * @param currentUser 当前用户 + * @param resourceId 资源ID + * @return 图片字节数组 + */ + public byte[] downloadImage(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, "资源未就绪"); + } + return objectStorageService.download(resource.getBucketName(), resource.getFilePath()); + } + + /** + * 获取图片资源的Content-Type + * + * @param resource 资源实体 + * @return Content-Type + */ + public String getImageContentType(SourceResource resource) { + String filePath = resource.getFilePath(); + 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"; + }; + } + return "application/octet-stream"; + } + + public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + if (!"IMAGE".equals(resource.getResourceType())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注"); + } + + ImageBboxAnnotation annotation = imageBboxAnnotationMapper.selectByResourceId(resourceId); + if (annotation == null) { + return new ImageBboxResponse(null, resourceId, List.of(), null, null, null, null); + } + + List bboxes = parseBboxJson(annotation.getBboxJson()); + SysUser creator = sysUserMapper.selectById(annotation.getCreatorId()); + + return new ImageBboxResponse( + annotation.getId(), + annotation.getResourceId(), + bboxes, + annotation.getRemark(), + creator == null ? null : creator.getRealName(), + annotation.getCreatedAt(), + annotation.getUpdatedAt() + ); + } + + @Transactional + public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + if (!"IMAGE".equals(resource.getResourceType())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注"); + } + + String bboxJson; + try { + bboxJson = objectMapper.writeValueAsString(request.bboxes()); + } catch (JsonProcessingException e) { + throw new BusinessException(ResultCode.BAD_REQUEST, "BBOX数据序列化失败"); + } + + ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId); + if (existing != null) { + existing.setBboxJson(bboxJson); + existing.setRemark(request.remark()); + existing.setUpdatedAt(LocalDateTime.now()); + imageBboxAnnotationMapper.updateById(existing); + } else { + long annotationId = IdGenerator.nextId(); + ImageBboxAnnotation annotation = ImageBboxAnnotation.builder() + .id(annotationId) + .companyId(currentUser.companyId()) + .resourceId(resourceId) + .bboxJson(bboxJson) + .remark(request.remark()) + .creatorId(currentUser.userId()) + .creatorRole(currentUser.role()) + .build(); + imageBboxAnnotationMapper.insert(annotation); + } + + return getImageBbox(currentUser, resourceId); + } + + @Transactional + public void deleteImageBbox(LoginUser currentUser, Long resourceId) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + imageBboxAnnotationMapper.deleteByResourceId(resourceId); + } + + private List parseBboxJson(String bboxJson) { + if (!StringUtils.hasText(bboxJson)) { + return List.of(); + } + try { + return objectMapper.readValue(bboxJson, + new TypeReference>() { + }); + } catch (JsonProcessingException e) { + log.warn("Failed to parse bbox json: {}", e.getMessage()); + return List.of(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/labelsys/backend/service/SysConfigService.java b/src/main/java/com/labelsys/backend/service/SysConfigService.java index 3f40546..46c9eaa 100644 --- a/src/main/java/com/labelsys/backend/service/SysConfigService.java +++ b/src/main/java/com/labelsys/backend/service/SysConfigService.java @@ -1,46 +1,34 @@ package com.labelsys.backend.service; -import java.util.List; -import java.util.Map; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.fasterxml.jackson.core.JsonProcessingException; -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.ManualModelConfigRequest; -import com.labelsys.backend.dto.request.PromptConfigOptionRequest; import com.labelsys.backend.dto.request.SaveSysConfigRequest; import com.labelsys.backend.dto.request.SysConfigPageQuery; -import com.labelsys.backend.dto.request.TaskModelConfigRequest; import com.labelsys.backend.dto.request.UpdateSysConfigRequest; import com.labelsys.backend.dto.response.SysConfigResponse; -import com.labelsys.backend.dto.response.TaskModelConfigResponse; -import com.labelsys.backend.dto.response.TaskPromptConfigResponse; import com.labelsys.backend.entity.SysConfig; -import com.labelsys.backend.enums.ConfigMode; import com.labelsys.backend.enums.ConfigType; import com.labelsys.backend.enums.UserRole; import com.labelsys.backend.mapper.SysConfigMapper; 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.util.List; @Slf4j @Service @RequiredArgsConstructor public class SysConfigService { - private final SysConfigMapper sysConfigMapper; - private final ObjectMapper objectMapper; + private final SysConfigMapper sysConfigMapper; private final DataPermissionService dataPermissionService; @Transactional @@ -51,14 +39,20 @@ public class SysConfigService { if (existing != null) { throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); } - SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId()) - .configType(request.configType()).configName(request.configName()).configValue(request.configValue()) - .status(request.status()).creatorId(currentUser.userId()).creatorRole(currentUser.role().name()) + SysConfig config = SysConfig.builder() + .id(IdGenerator.nextId()) + .companyId(currentUser.companyId()) + .configType(request.configType()) + .configName(request.configName()) + .configValue(request.configValue()) + .status(request.status()) + .creatorId(currentUser.userId()) + .creatorRole(currentUser.role().name()) .build(); sysConfigMapper.insert(config); log.info("saved sys config, companyId={}, userId={}, userRole={}, configName={}, configType={}", - currentUser.companyId(), currentUser.userId(), currentUser.role().name(), request.configName(), - request.configType()); + currentUser.companyId(), currentUser.userId(), currentUser.role().name(), + request.configName(), request.configType()); return config; } @@ -66,12 +60,7 @@ public class SysConfigService { public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) { validateConfigType(request.configType()); SysConfig existing = getConfigEntity(currentUser, configId); - // SysConfig duplicate = - // sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), - // request.configName()); - // if (duplicate != null && !duplicate.getId().equals(configId)) { - // throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); - // } + if (StringUtils.hasText(request.configName())) { existing.setConfigName(request.configName()); } @@ -85,8 +74,8 @@ public class SysConfigService { existing.setStatus(request.status()); } sysConfigMapper.updateById(existing); - log.info("updated sys config, companyId={}, userId={}, configId={}", currentUser.companyId(), - currentUser.userId(), configId); + log.info("updated sys config, companyId={}, userId={}, configId={}", + currentUser.companyId(), currentUser.userId(), configId); return existing; } @@ -120,96 +109,25 @@ public class SysConfigService { Page page = new Page<>(query.pageNo(), query.pageSize()); Page resultPage = sysConfigMapper.selectPage(page, wrapper); - List records = resultPage.getRecords().stream().map(this::toResponse).toList(); + List records = resultPage.getRecords().stream() + .map(this::toResponse) + .toList(); return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), (int) resultPage.getSize()); } - @Transactional - public ResolvedModelConfig resolveModelConfig(LoginUser currentUser, TaskModelConfigRequest request) { - if (request == null || request.mode() == null) { - throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不能为空"); - } - if (request.mode() == ConfigMode.SELECT) { - return resolveSelectedModel(currentUser, request.selectedConfigName()); - } - if (request.mode() == ConfigMode.MANUAL) { - return resolveManualModel(currentUser, request.manualConfig()); - } - throw new BusinessException(ResultCode.BAD_REQUEST, "不支持的模型配置模式"); - } - - public ResolvedPromptConfig resolvePromptConfig(LoginUser currentUser, PromptConfigOptionRequest request) { - if (request == null) { - throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不能为空"); - } - if (StringUtils.hasText(request.selectedConfigName())) { - SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(), - request.selectedConfigName(), ConfigType.PROMPT.name()); - if (config == null) { - throw new BusinessException(ResultCode.NOT_FOUND, "提示词配置不存在"); - } - return new ResolvedPromptConfig(config.getId(), config.getConfigName(), config.getConfigValue()); - } - if (!StringUtils.hasText(request.promptText())) { - throw new BusinessException(ResultCode.BAD_REQUEST, "提示词内容不能为空"); - } - return new ResolvedPromptConfig(null, null, request.promptText()); - } - - public TaskModelConfigResponse toResponse(ResolvedModelConfig config) { - return new TaskModelConfigResponse(config.configId(), config.configName(), config.modelName(), - config.modelUrl(), maskSecret(config.apiKey())); - } - - public TaskPromptConfigResponse toResponse(ResolvedPromptConfig config) { - return new TaskPromptConfigResponse(config.configId(), config.configName(), config.promptText()); - } - public SysConfigResponse toResponse(SysConfig config) { - return new SysConfigResponse(config.getId(), config.getConfigType(), config.getConfigName(), - config.getConfigValue(), config.getStatus(), config.getCreatorId(), config.getCreatedAt(), - config.getUpdatedAt()); - } - - private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) { - if (!StringUtils.hasText(configName)) { - throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空"); - } - SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(), configName, - ConfigType.MODEL.name()); - if (config == null) { - throw new BusinessException(ResultCode.NOT_FOUND, "模型配置不存在"); - } - ModelConfigValue configValue = parseModelConfig(config.getConfigValue()); - return new ResolvedModelConfig(config.getId(), config.getConfigName(), configValue.modelName(), - configValue.modelUrl(), configValue.apiKey()); - } - - private ResolvedModelConfig resolveManualModel(LoginUser currentUser, ManualModelConfigRequest request) { - if (request == null || !StringUtils.hasText(request.modelName()) || !StringUtils.hasText(request.modelUrl()) - || !StringUtils.hasText(request.apiKey())) { - throw new BusinessException(ResultCode.BAD_REQUEST, "手动模型配置不完整"); - } - SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.modelName()); - if (existing == null) { - String configValue = writeModelConfig(request); - SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId()) - .configType(ConfigType.MODEL.name()).configName(request.modelName()).configValue(configValue) - .status("ENABLED").creatorId(currentUser.userId()).build(); - sysConfigMapper.insert(config); - log.info("auto created model config, companyId={}, userId={}, configName={}", currentUser.companyId(), - currentUser.userId(), request.modelName()); - return new ResolvedModelConfig(config.getId(), config.getConfigName(), request.modelName(), - request.modelUrl(), request.apiKey()); - } - if (!ConfigType.MODEL.name().equals(existing.getConfigType())) { - throw new BusinessException(ResultCode.CONFLICT, "同名配置已被其他类型占用"); - } - ModelConfigValue configValue = parseModelConfig(existing.getConfigValue()); - return new ResolvedModelConfig(existing.getId(), existing.getConfigName(), configValue.modelName(), - configValue.modelUrl(), configValue.apiKey()); + return new SysConfigResponse( + config.getId(), + config.getConfigType(), + config.getConfigName(), + config.getConfigValue(), + config.getStatus(), + config.getCreatorId(), + config.getCreatedAt(), + config.getUpdatedAt() + ); } private SysConfig getConfigEntity(LoginUser currentUser, Long configId) { @@ -225,54 +143,4 @@ public class SysConfigService { throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法"); } } - - private ModelConfigValue parseModelConfig(String value) { - try { - return objectMapper.readValue(value, ModelConfigValue.class); - } catch (JsonProcessingException ex) { - throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值格式非法"); - } - } - - private String writeModelConfig(ManualModelConfigRequest request) { - try { - return objectMapper.writeValueAsString( - Map.of("modelName", request.modelName(), "modelUrl", request.modelUrl(), "apiKey", - request.apiKey())); - } catch (JsonProcessingException ex) { - throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值生成失败"); - } - } - - private String maskSecret(String secret) { - if (!StringUtils.hasText(secret)) { - return null; - } - if (secret.length() <= 4) { - return "****"; - } - return "****" + secret.substring(secret.length() - 4); - } - - // private PageResult paginate(List records, Integer pageNo, Integer - // pageSize) { - // int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo; - // int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; - // int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, - // records.size()); - // int toIndex = Math.min(fromIndex + actualPageSize, records.size()); - // return new PageResult<>(records.subList(fromIndex, toIndex), - // (long)records.size(), actualPageNo, - // actualPageSize); - // } - - private record ModelConfigValue(String modelName, String modelUrl, String apiKey) { - } - - public record ResolvedModelConfig(Long configId, String configName, String modelName, String modelUrl, - String apiKey) { - } - - public record ResolvedPromptConfig(Long configId, String configName, String promptText) { - } -} +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5d2b770..65eee8f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: application: name: lablesys-backend datasource: - url: ${DB_URL:jdbc:postgresql://39.107.112.174:5432/lablesystem} + url: ${DB_URL:jdbc:postgresql://39.107.112.174:5432/lablesystem_test} username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres!Pw} driver-class-name: org.postgresql.Driver diff --git a/src/main/resources/mapper/AnnotationTaskMapper.xml b/src/main/resources/mapper/AnnotationTaskMapper.xml index f5b378c..a49e896 100644 --- a/src/main/resources/mapper/AnnotationTaskMapper.xml +++ b/src/main/resources/mapper/AnnotationTaskMapper.xml @@ -9,18 +9,6 @@ - - - - - - - - - - - - @@ -32,18 +20,16 @@ id, company_id, creator_id, creator_role, task_name, industry_type, task_type, - extract_model_config_id, extract_model_name, extract_model_url, extract_model_api_key, - verify_model_config_id, verify_model_name, verify_model_url, verify_model_api_key, - extract_prompt_config_id, extract_prompt, verify_prompt_config_id, verify_prompt, task_status, is_deleted, started_at, finished_at, error_message, created_at, updated_at \ No newline at end of file diff --git a/src/main/resources/mapper/ImageBboxAnnotationMapper.xml b/src/main/resources/mapper/ImageBboxAnnotationMapper.xml new file mode 100644 index 0000000..0ebf565 --- /dev/null +++ b/src/main/resources/mapper/ImageBboxAnnotationMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + DELETE FROM image_bbox_annotation WHERE resource_id = #{resourceId} + + \ No newline at end of file diff --git a/src/main/resources/sql/data.sql b/src/main/resources/sql/data.sql index 48ff6e2..09563f2 100644 --- a/src/main/resources/sql/data.sql +++ b/src/main/resources/sql/data.sql @@ -67,31 +67,13 @@ ON CONFLICT DO NOTHING; INSERT INTO annotation_task ( id, company_id, creator_id, creator_role, task_name, industry_type, task_type, - extract_model_config_id, - extract_model_name, extract_model_url, extract_model_api_key, - verify_model_config_id, - verify_model_name, verify_model_url, verify_model_api_key, - extract_prompt_config_id, extract_prompt, - verify_prompt_config_id, verify_prompt, task_status, is_deleted, started_at, finished_at, error_message ) VALUES - (701, 2, 4, 'EMPLOYEE', '多资源问答抽取任务', 'electricity', 'EXTRACT_QA', - 401, - 'qwen-max', 'https://api.example.com/extract', 'extract-api-key-demo', - 402, - 'glm-4.5', 'https://api.example.com/verify', 'verify-api-key-demo', - 403, '请根据输入文本提取结构化问答对。', - 404, '请核验生成答案是否与原始内容一致。', - 'PENDING', FALSE, NULL, NULL, NULL), - (702, 2, 4, 'EMPLOYEE', '图片问答抽取任务', 'transport', 'EXTRACT_QA', - 406, - 'qwen-vl-max', 'https://api.example.com/extract-vl', 'extract-vl-api-key-demo', - 407, - 'glm-4.5v', 'https://api.example.com/verify-vl', 'verify-vl-api-key-demo', - 408, '请根据输入图片内容提取结构化问答对。', - 409, '请核验图片问答结果是否准确。', - 'COMPLETED', FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL) -ON CONFLICT DO NOTHING; + (701, 2, 4, 'EMPLOYEE', '多资源问答抽取任务', 'electricity', 'EXTRACT_QA', + 'PENDING', FALSE, NULL, NULL, NULL), + (702, 2, 4, 'EMPLOYEE', '图片问答抽取任务', 'transport', 'EXTRACT_QA', + 'COMPLETED', FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL) + ON CONFLICT DO NOTHING; INSERT INTO annotation_task_resource ( id, company_id, task_id, resource_id @@ -129,6 +111,14 @@ INSERT INTO annotation_result_history ( '审核通过后归档', 5, CURRENT_TIMESTAMP) ON CONFLICT DO NOTHING; +INSERT INTO image_bbox_annotation ( + id, company_id, resource_id, bbox_json, remark, creator_id, creator_role +) VALUES + (1301, 2, 602, + '[{"id":"bbox_001","x":50,"y":30,"width":120,"height":80,"label":"指示灯"},{"id":"bbox_002","x":180,"y":40,"width":100,"height":90,"label":"开关按钮"}]', + '控制柜图片BBOX标注示例', 3, 'EMPLOYEE') +ON CONFLICT DO NOTHING; + INSERT INTO training_dataset ( id, company_id, creator_id, creator_role, result_history_id, sample_type, glm_format_json, dataset_status ) VALUES @@ -147,4 +137,4 @@ INSERT INTO export_batch_item (id, batch_id, dataset_id) VALUES (1201, 1101, 1001) ON CONFLICT DO NOTHING; -COMMIT; +COMMIT; \ No newline at end of file diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index 8d7d6a2..b1796b9 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -1,4 +1,5 @@ --- Active: 1775801470429@@39.107.112.174@5432@lablesystem +-- Active: 1775801470429@@39.107.112.174@5432@lablesystem_test + begin; -- Drop Tables (按依赖关系倒序删除) @@ -15,14 +16,14 @@ DROP TABLE IF EXISTS sys_menu CASCADE; DROP TABLE IF EXISTS sys_user CASCADE; DROP TABLE IF EXISTS sys_company CASCADE; - -CREATE TABLE IF NOT EXISTS sys_company ( - id BIGINT PRIMARY KEY, - company_code VARCHAR(64) NOT NULL UNIQUE, +CREATE TABLE IF NOT EXISTS sys_company +( + id BIGINT PRIMARY KEY, + company_code VARCHAR(64) NOT NULL UNIQUE, company_name VARCHAR(128) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE sys_company IS '公司表。'; @@ -33,22 +34,23 @@ COMMENT ON COLUMN sys_company.status IS '公司状态,默认 ENABLED,可按 COMMENT ON COLUMN sys_company.created_at IS '创建时间。'; COMMENT ON COLUMN sys_company.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS sys_user ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - phone VARCHAR(32) NOT NULL, - username VARCHAR(64), - role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', - position VARCHAR(32) NOT NULL DEFAULT 'ANNOTATOR', - real_name VARCHAR(64) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - must_change_password BOOLEAN NOT NULL DEFAULT TRUE, - status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', - session_version INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +CREATE TABLE IF NOT EXISTS sys_user +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + phone VARCHAR(32) NOT NULL, + username VARCHAR(64), + role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + position VARCHAR(32) NOT NULL DEFAULT 'ANNOTATOR', + real_name VARCHAR(64) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + must_change_password BOOLEAN NOT NULL DEFAULT TRUE, + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + session_version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_sys_user_company_phone UNIQUE (company_id, phone), - CONSTRAINT fk_sys_user_company FOREIGN KEY (company_id) REFERENCES sys_company(id) + CONSTRAINT fk_sys_user_company FOREIGN KEY (company_id) REFERENCES sys_company (id) ); COMMENT ON TABLE sys_user IS '用户表。role 表示数据权限角色,position 表示岗位。'; @@ -66,17 +68,18 @@ COMMENT ON COLUMN sys_user.session_version IS '会话版本号,用于强制旧 COMMENT ON COLUMN sys_user.created_at IS '创建时间。'; COMMENT ON COLUMN sys_user.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS sys_menu ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - menu_code VARCHAR(64) NOT NULL, - menu_name VARCHAR(128) NOT NULL, - path VARCHAR(255) NOT NULL, +CREATE TABLE IF NOT EXISTS sys_menu +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + menu_code VARCHAR(64) NOT NULL, + menu_name VARCHAR(128) NOT NULL, + path VARCHAR(255) NOT NULL, visible_positions VARCHAR(255) NOT NULL DEFAULT 'ADMIN', - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_sys_menu_company FOREIGN KEY (company_id) REFERENCES sys_company(id) + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_sys_menu_company FOREIGN KEY (company_id) REFERENCES sys_company (id) ); COMMENT ON TABLE sys_menu IS '菜单表。'; @@ -90,20 +93,21 @@ COMMENT ON COLUMN sys_menu.sort_order IS '菜单排序号,默认 0。'; COMMENT ON COLUMN sys_menu.created_at IS '创建时间。'; COMMENT ON COLUMN sys_menu.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS sys_config ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - config_type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM', - config_name VARCHAR(128) NOT NULL, - config_value TEXT NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', - creator_id BIGINT NOT NULL, - creator_role VARCHAR(50) NOT NULL DEFAULT 'EMPLOYEE', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +CREATE TABLE IF NOT EXISTS sys_config +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + config_type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM', + config_name VARCHAR(128) NOT NULL, + config_value TEXT NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', + creator_id BIGINT NOT NULL, + creator_role VARCHAR(50) NOT NULL DEFAULT 'EMPLOYEE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_sys_config_company_name UNIQUE (company_id, config_name), - CONSTRAINT fk_sys_config_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_sys_config_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) + CONSTRAINT fk_sys_config_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_sys_config_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id) ); COMMENT ON TABLE sys_config IS '系统配置表,保存模型配置、Prompt 配置和系统参数配置。'; @@ -118,23 +122,24 @@ COMMENT ON COLUMN sys_config.creator_role IS '创建人角色.默认 EMPLOYEE。 COMMENT ON COLUMN sys_config.created_at IS '创建时间。'; COMMENT ON COLUMN sys_config.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS source_resource ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - creator_id BIGINT NOT NULL, - creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', - resource_name VARCHAR(255) NOT NULL, - resource_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', - bucket_name VARCHAR(128) NOT NULL, - file_path VARCHAR(512) NOT NULL, - file_size BIGINT NOT NULL DEFAULT 0, - source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED', - storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs', - remark VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_source_resource_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_source_resource_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) +CREATE TABLE IF NOT EXISTS source_resource +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + resource_name VARCHAR(255) NOT NULL, + resource_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', + bucket_name VARCHAR(128) NOT NULL, + file_path VARCHAR(512) NOT NULL, + file_size BIGINT NOT NULL DEFAULT 0, + source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED', + storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs', + remark VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_source_resource_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_source_resource_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id) ); COMMENT ON TABLE source_resource IS '资源表,保存文本、图片、视频资源元数据。'; @@ -153,61 +158,62 @@ COMMENT ON COLUMN source_resource.remark IS '备注说明。'; COMMENT ON COLUMN source_resource.created_at IS '创建时间。'; COMMENT ON COLUMN source_resource.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS annotation_task ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - creator_id BIGINT NOT NULL, - creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', - task_name VARCHAR(255) NOT NULL, - industry_type VARCHAR(32) NOT NULL DEFAULT 'transport', - task_type VARCHAR(32) NOT NULL DEFAULT 'EXTRACT_QA', - extract_model_config_id BIGINT, - extract_model_name VARCHAR(128), - extract_model_url VARCHAR(255), - extract_model_api_key VARCHAR(255), - verify_model_config_id BIGINT, - verify_model_name VARCHAR(128), - verify_model_url VARCHAR(255), - verify_model_api_key VARCHAR(255), - extract_prompt_config_id BIGINT, - extract_prompt TEXT, - verify_prompt_config_id BIGINT, - verify_prompt TEXT, - task_status VARCHAR(32) NOT NULL DEFAULT 'PENDING', - is_deleted BOOLEAN NOT NULL DEFAULT FALSE, - started_at TIMESTAMP, - finished_at TIMESTAMP, - error_message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_annotation_task_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_annotation_task_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), - CONSTRAINT fk_annotation_task_extract_model_config FOREIGN KEY (extract_model_config_id) REFERENCES sys_config(id), - CONSTRAINT fk_annotation_task_verify_model_config FOREIGN KEY (verify_model_config_id) REFERENCES sys_config(id), - CONSTRAINT fk_annotation_task_extract_prompt_config FOREIGN KEY (extract_prompt_config_id) REFERENCES sys_config(id), - CONSTRAINT fk_annotation_task_verify_prompt_config FOREIGN KEY (verify_prompt_config_id) REFERENCES sys_config(id) +CREATE TABLE IF NOT EXISTS image_bbox_annotation +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + resource_id BIGINT NOT NULL, + bbox_json TEXT, + remark VARCHAR(500), + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_image_bbox_annotation_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_image_bbox_annotation_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id), + CONSTRAINT fk_image_bbox_annotation_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id) ); -COMMENT ON TABLE annotation_task IS '任务表,保存任务、配置引用与执行快照。'; +COMMENT ON TABLE image_bbox_annotation IS '图片BBOX标注表。'; +COMMENT ON COLUMN image_bbox_annotation.id IS '主键ID。'; +COMMENT ON COLUMN image_bbox_annotation.company_id IS '所属公司ID。'; +COMMENT ON COLUMN image_bbox_annotation.resource_id IS '关联的图片资源ID。'; +COMMENT ON COLUMN image_bbox_annotation.bbox_json IS 'bbox坐标信息JSON数组。'; +COMMENT ON COLUMN image_bbox_annotation.remark IS '备注说明。'; +COMMENT ON COLUMN image_bbox_annotation.creator_id IS '创建人用户ID。'; +COMMENT ON COLUMN image_bbox_annotation.creator_role IS '创建人数据权限角色,默认 EMPLOYEE。'; +COMMENT ON COLUMN image_bbox_annotation.created_at IS '创建时间。'; +COMMENT ON COLUMN image_bbox_annotation.updated_at IS '更新时间。'; + +-- 修改 annotation_task 表,删除模型和提示词相关字段 +CREATE TABLE IF NOT EXISTS annotation_task +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + task_name VARCHAR(255) NOT NULL, + industry_type VARCHAR(32) NOT NULL DEFAULT 'transport', + task_type VARCHAR(32) NOT NULL DEFAULT 'EXTRACT_QA', + task_status VARCHAR(32) NOT NULL DEFAULT 'PENDING', + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + started_at TIMESTAMP, + finished_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_annotation_task_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_annotation_task_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id) +); + +COMMENT ON TABLE annotation_task IS '任务表,保存任务配置与执行快照。'; COMMENT ON COLUMN annotation_task.id IS '任务主键ID。'; COMMENT ON COLUMN annotation_task.company_id IS '所属公司ID。'; COMMENT ON COLUMN annotation_task.creator_id IS '任务创建人用户ID。'; COMMENT ON COLUMN annotation_task.creator_role IS '任务创建人数据权限角色,默认 EMPLOYEE。'; COMMENT ON COLUMN annotation_task.task_name IS '任务名称。'; -COMMENT ON COLUMN annotation_task.industry_type IS '行业类型简写,默认 transport,可选值按业务扩展,例如 electricity。'; +COMMENT ON COLUMN annotation_task.industry_type IS '行业类型简写,默认 transport,可选值按业务扩展。'; COMMENT ON COLUMN annotation_task.task_type IS '任务类型,默认 EXTRACT_QA。'; -COMMENT ON COLUMN annotation_task.extract_model_config_id IS '抽取模型配置ID,关联 sys_config.id。'; -COMMENT ON COLUMN annotation_task.extract_model_name IS '抽取模型名称。'; -COMMENT ON COLUMN annotation_task.extract_model_url IS '抽取模型调用地址。'; -COMMENT ON COLUMN annotation_task.extract_model_api_key IS '抽取模型调用密钥。'; -COMMENT ON COLUMN annotation_task.verify_model_config_id IS '校验模型配置ID,关联 sys_config.id。'; -COMMENT ON COLUMN annotation_task.verify_model_name IS '校验模型名称。'; -COMMENT ON COLUMN annotation_task.verify_model_url IS '校验模型调用地址。'; -COMMENT ON COLUMN annotation_task.verify_model_api_key IS '校验模型调用密钥。'; -COMMENT ON COLUMN annotation_task.extract_prompt_config_id IS '抽取Prompt配置ID,关联 sys_config.id。'; -COMMENT ON COLUMN annotation_task.extract_prompt IS '抽取 Prompt 文本。'; -COMMENT ON COLUMN annotation_task.verify_prompt_config_id IS '校验Prompt配置ID,关联 sys_config.id。'; -COMMENT ON COLUMN annotation_task.verify_prompt IS '校验 Prompt 文本。'; COMMENT ON COLUMN annotation_task.task_status IS '任务状态,默认 PENDING,可选 RUNNING、COMPLETED、FAILED。'; COMMENT ON COLUMN annotation_task.is_deleted IS '任务软删除标记,默认 FALSE。'; COMMENT ON COLUMN annotation_task.started_at IS '任务开始时间。'; @@ -216,16 +222,17 @@ COMMENT ON COLUMN annotation_task.error_message IS '任务失败错误信息。' COMMENT ON COLUMN annotation_task.created_at IS '创建时间。'; COMMENT ON COLUMN annotation_task.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS annotation_task_resource ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - task_id BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS annotation_task_resource +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + task_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_annotation_task_resource UNIQUE (task_id, resource_id), - CONSTRAINT fk_annotation_task_resource_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_annotation_task_resource_task FOREIGN KEY (task_id) REFERENCES annotation_task(id), - CONSTRAINT fk_annotation_task_resource_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id) + CONSTRAINT fk_annotation_task_resource_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_annotation_task_resource_task FOREIGN KEY (task_id) REFERENCES annotation_task (id), + CONSTRAINT fk_annotation_task_resource_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id) ); COMMENT ON TABLE annotation_task_resource IS '任务与资源关联表,一个任务可绑定多个资源。'; @@ -235,29 +242,30 @@ 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 '{}', +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, - 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), - CONSTRAINT fk_annotation_result_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id), - CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user(id) + 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, + 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), + CONSTRAINT fk_annotation_result_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id), + CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user (id) ); COMMENT ON TABLE annotation_result IS '当前标注结果表。'; @@ -279,27 +287,28 @@ 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 '{}', +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, - 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), - CONSTRAINT fk_annotation_result_history_task FOREIGN KEY (task_id) REFERENCES annotation_task(id), - CONSTRAINT fk_annotation_result_history_resource FOREIGN KEY (resource_id) REFERENCES source_resource(id), - CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user(id) + qa_content_file_path VARCHAR(512), + archive_reason VARCHAR(255), + 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), + CONSTRAINT fk_annotation_result_history_task FOREIGN KEY (task_id) REFERENCES annotation_task (id), + CONSTRAINT fk_annotation_result_history_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id), + CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user (id) ); COMMENT ON TABLE annotation_result_history IS '历史归档结果表。'; @@ -318,20 +327,21 @@ COMMENT ON COLUMN annotation_result_history.archived_by IS '归档操作人用 COMMENT ON COLUMN annotation_result_history.archived_at IS '归档时间。'; COMMENT ON COLUMN annotation_result_history.created_at IS '创建时间。'; -CREATE TABLE IF NOT EXISTS training_dataset ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - creator_id BIGINT NOT NULL, - creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', - result_history_id BIGINT NOT NULL, - sample_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', - glm_format_json TEXT NOT NULL, - dataset_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_training_dataset_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_training_dataset_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), - CONSTRAINT fk_training_dataset_result_history FOREIGN KEY (result_history_id) REFERENCES annotation_result_history(id) +CREATE TABLE IF NOT EXISTS training_dataset +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + result_history_id BIGINT NOT NULL, + sample_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', + glm_format_json TEXT NOT NULL, + dataset_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_training_dataset_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_training_dataset_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id), + CONSTRAINT fk_training_dataset_result_history FOREIGN KEY (result_history_id) REFERENCES annotation_result_history (id) ); COMMENT ON TABLE training_dataset IS '训练样本表。'; @@ -346,20 +356,21 @@ COMMENT ON COLUMN training_dataset.dataset_status IS '样本状态,默认 DRAF COMMENT ON COLUMN training_dataset.created_at IS '创建时间。'; COMMENT ON COLUMN training_dataset.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS export_batch ( - id BIGINT PRIMARY KEY, - company_id BIGINT NOT NULL, - creator_id BIGINT NOT NULL, - creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', - batch_no VARCHAR(64) NOT NULL UNIQUE, +CREATE TABLE IF NOT EXISTS export_batch +( + id BIGINT PRIMARY KEY, + company_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', + batch_no VARCHAR(64) NOT NULL UNIQUE, dataset_file_path VARCHAR(512), - sample_count INTEGER NOT NULL DEFAULT 0, - finetune_job_id VARCHAR(128), - finetune_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_export_batch_company FOREIGN KEY (company_id) REFERENCES sys_company(id), - CONSTRAINT fk_export_batch_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) + sample_count INTEGER NOT NULL DEFAULT 0, + finetune_job_id VARCHAR(128), + finetune_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_export_batch_company FOREIGN KEY (company_id) REFERENCES sys_company (id), + CONSTRAINT fk_export_batch_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id) ); COMMENT ON TABLE export_batch IS '导出批次表。'; @@ -375,13 +386,14 @@ COMMENT ON COLUMN export_batch.finetune_status IS '微调状态,默认 NOT_STA COMMENT ON COLUMN export_batch.created_at IS '创建时间。'; COMMENT ON COLUMN export_batch.updated_at IS '更新时间。'; -CREATE TABLE IF NOT EXISTS export_batch_item ( - id BIGINT PRIMARY KEY, - batch_id BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS export_batch_item +( + id BIGINT PRIMARY KEY, + batch_id BIGINT NOT NULL, dataset_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_export_batch_item_batch FOREIGN KEY (batch_id) REFERENCES export_batch(id), - CONSTRAINT fk_export_batch_item_dataset FOREIGN KEY (dataset_id) REFERENCES training_dataset(id), + CONSTRAINT fk_export_batch_item_batch FOREIGN KEY (batch_id) REFERENCES export_batch (id), + CONSTRAINT fk_export_batch_item_dataset FOREIGN KEY (dataset_id) REFERENCES training_dataset (id), CONSTRAINT uq_export_batch_item UNIQUE (batch_id, dataset_id) ); @@ -391,26 +403,28 @@ COMMENT ON COLUMN export_batch_item.batch_id IS '关联导出批次ID。'; COMMENT ON COLUMN export_batch_item.dataset_id IS '关联训练样本ID。'; COMMENT ON COLUMN export_batch_item.created_at IS '创建时间。'; -CREATE INDEX IF NOT EXISTS idx_sys_user_company ON sys_user(company_id); -CREATE INDEX IF NOT EXISTS idx_sys_user_role ON sys_user(company_id, role); -CREATE INDEX IF NOT EXISTS idx_sys_user_position ON sys_user(company_id, position); -CREATE INDEX IF NOT EXISTS idx_sys_menu_company_sort ON sys_menu(company_id, sort_order); -CREATE INDEX IF NOT EXISTS idx_sys_config_company_type ON sys_config(company_id, config_type); -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_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); -CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_task ON annotation_task_resource(company_id, task_id); -CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_resource ON annotation_task_resource(company_id, resource_id); -CREATE INDEX IF NOT EXISTS idx_annotation_result_company_deleted ON annotation_result(company_id, is_deleted); -CREATE INDEX IF NOT EXISTS idx_annotation_result_company_manual ON annotation_result(company_id, requires_manual_review); -CREATE INDEX IF NOT EXISTS idx_annotation_result_task ON annotation_result(company_id, task_id); -CREATE INDEX IF NOT EXISTS idx_annotation_result_history_company ON annotation_result_history(company_id); -CREATE INDEX IF NOT EXISTS idx_annotation_result_history_task ON annotation_result_history(company_id, task_id); -CREATE INDEX IF NOT EXISTS idx_annotation_result_history_resource ON annotation_result_history(company_id, resource_id); -CREATE INDEX IF NOT EXISTS idx_training_dataset_company_status ON training_dataset(company_id, dataset_status); -CREATE INDEX IF NOT EXISTS idx_export_batch_company_status ON export_batch(company_id, finetune_status); +CREATE INDEX IF NOT EXISTS idx_sys_user_company ON sys_user (company_id); +CREATE INDEX IF NOT EXISTS idx_sys_user_role ON sys_user (company_id, role); +CREATE INDEX IF NOT EXISTS idx_sys_user_position ON sys_user (company_id, position); +CREATE INDEX IF NOT EXISTS idx_sys_menu_company_sort ON sys_menu (company_id, sort_order); +CREATE INDEX IF NOT EXISTS idx_sys_config_company_type ON sys_config (company_id, config_type); +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_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); +CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_task ON annotation_task_resource (company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_task_resource_company_resource ON annotation_task_resource (company_id, resource_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_company_deleted ON annotation_result (company_id, is_deleted); +CREATE INDEX IF NOT EXISTS idx_annotation_result_company_manual ON annotation_result (company_id, requires_manual_review); +CREATE INDEX IF NOT EXISTS idx_annotation_result_task ON annotation_result (company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_company ON annotation_result_history (company_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_task ON annotation_result_history (company_id, task_id); +CREATE INDEX IF NOT EXISTS idx_annotation_result_history_resource ON annotation_result_history (company_id, resource_id); +CREATE INDEX IF NOT EXISTS idx_training_dataset_company_status ON training_dataset (company_id, dataset_status); +CREATE INDEX IF NOT EXISTS idx_export_batch_company_status ON export_batch (company_id, finetune_status); +CREATE INDEX IF NOT EXISTS idx_image_bbox_annotation_company ON image_bbox_annotation (company_id); +CREATE INDEX IF NOT EXISTS idx_image_bbox_annotation_resource ON image_bbox_annotation (company_id, resource_id); -COMMIT; +COMMIT; \ No newline at end of file