Merge branch 'dev54'

This commit is contained in:
wh
2026-05-06 00:11:50 +08:00
39 changed files with 924 additions and 772 deletions

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.labelsys</groupId> <groupId>com.labelsys</groupId>
<artifactId>label-backend</artifactId> <artifactId>label-backend</artifactId>
@@ -15,6 +15,8 @@
<mybatis-plus.version>3.5.3.1</mybatis-plus.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<springdoc-openapi.version>2.3.0</springdoc-openapi.version> <springdoc-openapi.version>2.3.0</springdoc-openapi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -102,7 +104,7 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -1,11 +1,10 @@
package com.labelsys.backend.config; 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.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class MybatisPlusConfig { public class MybatisPlusConfig {

View File

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

View File

@@ -3,21 +3,28 @@ package com.labelsys.backend.controller;
import com.labelsys.backend.common.Result; import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext; import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult; 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.SourceResourcePageQuery;
import com.labelsys.backend.dto.request.SourceUploadRequest; 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.SourceResourceResponse;
import com.labelsys.backend.dto.response.SourceUploadResponse; import com.labelsys.backend.dto.response.SourceUploadResponse;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.service.SourceResourceService; import com.labelsys.backend.service.SourceResourceService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject; 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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -37,15 +44,16 @@ public class SourceResourceController {
@Operation(summary = "分页查询资源") @Operation(summary = "分页查询资源")
@GetMapping @GetMapping
public Result<PageResult<SourceResourceResponse>> page(@ParameterObject @ModelAttribute SourceResourcePageQuery query) { public Result<PageResult<SourceResourceResponse>> page(
@ParameterObject @ModelAttribute SourceResourcePageQuery query) {
return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query)); return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query));
} }
@Operation(summary = "查询资源详情") @Operation(summary = "查询资源详情")
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<SourceResourceResponse> detail( public Result<SourceResourceResponse> detail(
@Parameter(description = "资源ID", example = "191000000000000101") @Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id @PathVariable Long id
) { ) {
return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id)); return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id));
} }
@@ -53,10 +61,59 @@ public class SourceResourceController {
@Operation(summary = "删除资源") @Operation(summary = "删除资源")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Result<Void> delete( public Result<Void> delete(
@Parameter(description = "资源ID", example = "191000000000000101") @Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id @PathVariable Long id
) { ) {
sourceResourceService.deleteResource(UserContext.requireUser(), id); sourceResourceService.deleteResource(UserContext.requireUser(), id);
return Result.success(); return Result.success();
} }
}
@Operation(summary = "下载图片资源")
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> 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<ImageBboxResponse> 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<ImageBboxResponse> 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<Void> deleteImageBbox(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
sourceResourceService.deleteImageBbox(UserContext.requireUser(), id);
return Result.success();
}
}

View File

@@ -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
) {
}

View File

@@ -1,24 +1,18 @@
package com.labelsys.backend.dto.request; package com.labelsys.backend.dto.request;
import java.util.List;
import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "创建标注任务请求") @Schema(description = "创建标注任务请求")
public record CreateAnnotationTaskRequest( public record CreateAnnotationTaskRequest(
@Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName, @Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName,
@Schema(description = "行业类型", defaultValue = "TRANSPORT", example = "TRANSPORT") @NotNull(message = "行业类型不能为空") IndustryType industryType, @Schema(description = "行业类型", defaultValue = "TRANSPORT", example = "TRANSPORT") @NotNull(message = "行业类型不能为空") IndustryType industryType,
@Schema(description = "任务类型", defaultValue = "EXTRACT_QA", example = "EXTRACT_QA") @NotNull(message = "任务类型不能为空") TaskType taskType, @Schema(description = "任务类型", defaultValue = "EXTRACT_QA", example = "EXTRACT_QA") @NotNull(message = "任务类型不能为空") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List<Long> resourceIds, @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List<Long> 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) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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<BboxCoordinate> bboxes,
@Schema(description = "备注") String remark
) {
}

View File

@@ -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) {
}

View File

@@ -6,15 +6,10 @@ import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
@Schema(description = "更新标注任务请求") @Schema(description = "更新标注任务请求")
public record UpdateAnnotationTaskRequest( public record UpdateAnnotationTaskRequest(
@Schema(description = "行业类型", example = "TRANSPORT") IndustryType industryType, @Schema(description = "行业类型", example = "TRANSPORT") IndustryType industryType,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType, @Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds, @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> 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) {
}

View File

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

View File

@@ -1,25 +1,21 @@
package com.labelsys.backend.dto.response; 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.IndustryType;
import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "标注任务响应") @Schema(description = "标注任务响应")
public record AnnotationTaskResponse( public record AnnotationTaskResponse(
@Schema(description = "任务ID", example = "191000000000000301") Long id, @Schema(description = "任务ID", example = "191000000000000301") Long id,
@Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName, @Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName,
@Schema(description = "行业类型:默认值transport暂不显示", example = "transport") IndustryType industryType, @Schema(description = "行业类型", example = "transport") IndustryType industryType,
@Schema(description = "任务类型:暂不显示", example = "EXTRACT_QA") TaskType taskType, @Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus, @Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds, @Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> 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 = "2026-04-27T10:20:00") LocalDateTime createdAt,
@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 = "2026-04-27T10:30:00") LocalDateTime updatedAt
@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
) { ) {
} }

View File

@@ -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<BboxCoordinateResponse> 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
) {
}
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -6,12 +6,13 @@ import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.enums.UserRole; import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@@ -19,30 +20,18 @@ import lombok.NoArgsConstructor;
@TableName("annotation_task") @TableName("annotation_task")
public class AnnotationTask { public class AnnotationTask {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private Long companyId; private Long companyId;
private Long creatorId; private Long creatorId;
private UserRole creatorRole; private UserRole creatorRole;
private String taskName; private String taskName;
private IndustryType industryType; private IndustryType industryType;
private TaskType taskType; private TaskType taskType;
private Long extractModelConfigId; private String taskStatus;
private String extractModelName; private Boolean isDeleted;
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 LocalDateTime startedAt; private LocalDateTime startedAt;
private LocalDateTime finishedAt; private LocalDateTime finishedAt;
private String errorMessage; private String errorMessage;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
} }

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> {
ImageBboxAnnotation selectByResourceId(@Param("resourceId") Long resourceId);
int deleteByResourceId(@Param("resourceId") Long resourceId);
}

View File

@@ -1,5 +1,6 @@
package com.labelsys.backend.service; package com.labelsys.backend.service;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,7 +16,8 @@ import com.labelsys.backend.dto.response.AnnotationResultCompareResponse;
import com.labelsys.backend.dto.response.AnnotationResultResponse; import com.labelsys.backend.dto.response.AnnotationResultResponse;
import com.labelsys.backend.entity.AnnotationResult; import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.RuntimeResultStatus; import com.labelsys.backend.enums.AnnotationResultStatus;
import com.labelsys.backend.enums.QaContentStorageMode;
import com.labelsys.backend.mapper.AnnotationResultMapper; import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper;
@@ -30,17 +32,18 @@ public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper; private final AnnotationResultMapper annotationResultMapper;
private final SourceResourceMapper sourceResourceMapper; private final SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService; private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<AnnotationResult> wrapper = LambdaQueryWrapper<AnnotationResult> wrapper = new LambdaQueryWrapper<AnnotationResult>()
new LambdaQueryWrapper<AnnotationResult>().eq(AnnotationResult::getCompanyId, currentUser.companyId()) .eq(AnnotationResult::getCompanyId, currentUser.companyId())
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId()) .eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
.eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId()) .eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
.eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview, .eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
query.requiresManualReview()); query.requiresManualReview());
if (shouldFilterByUserId) { if (shouldFilterByUserId) {
wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId()); wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
@@ -54,49 +57,104 @@ public class AnnotationResultService {
Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper); Page<AnnotationResult> resultPage = annotationResultMapper.selectPage(page, wrapper);
List<AnnotationResultResponse> records = resultPage.getRecords().stream().map(this::toResponse) List<AnnotationResultResponse> records = resultPage.getRecords().stream().map(this::toResponse)
.filter(response -> query.runtimeStatus() == null || query.runtimeStatus().equals(response.runtimeStatus())) .filter(response -> query.runtimeStatus() == null
.toList(); || query.runtimeStatus().equals(response.runtimeStatus()))
.toList();
return new PageResult<>(records, resultPage.getTotal(), (int)resultPage.getCurrent(), return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int)resultPage.getSize()); (int) resultPage.getSize());
} }
public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) { public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) { if (result == null) {
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId, log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId,
currentUser.companyId(), currentUser.userId()); currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
} }
return toResponse(result); return toResponse(result);
} }
private AnnotationResultResponse toResponse(AnnotationResult result) {
return new AnnotationResultResponse(result.getId(), result.getTaskId(), result.getResourceId(),
deriveStatus(result), result.getRequiresManualReview(), result.getIsDeleted(),
result.getQaContentStorageMode(), result.getReviewComment(), result.getReviewedAt(),
result.getCreatedAt());
}
private AnnotationResultStatus deriveStatus(AnnotationResult result) {
if (Boolean.TRUE.equals(result.getIsDeleted())) {
return AnnotationResultStatus.ARCHIVED;
}
if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
return AnnotationResultStatus.MANUAL_REVIEW_PENDING;
}
return AnnotationResultStatus.AUTO_ARCHIVE_PENDING;
}
public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) { public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId());
if (result == null) { if (result == null) {
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId, log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", resultId,
currentUser.companyId(), currentUser.userId()); currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
} }
String qaContentJson = resolveQaContent(result);
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId()); SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
return new AnnotationResultCompareResponse(result.getId(), result.getTaskId(), result.getResourceId(), return new AnnotationResultCompareResponse(
result.getQaContentJson(), result.getDiffSummary(), result.getQaContentStorageMode(), result.getId(),
result.getQaContentFilePath(), resource == null ? null : resource.getFilePath()); result.getTaskId(),
result.getResourceId(),
qaContentJson,
result.getDiffSummary(),
result.getQaContentStorageMode(),
result.getQaContentFilePath(),
resource == null ? null : resource.getFilePath());
} }
private AnnotationResultResponse toResponse(AnnotationResult result) { private String resolveQaContent(AnnotationResult result) {
return new AnnotationResultResponse(result.getId(), result.getTaskId(), result.getResourceId(), if (QaContentStorageMode.EXTERNAL.name().equals(result.getQaContentStorageMode())) {
deriveStatus(result), result.getRequiresManualReview(), result.getIsDeleted(), if (result.getQaContentFilePath() == null || result.getQaContentFilePath().isBlank()) {
result.getQaContentStorageMode(), result.getReviewComment(), result.getReviewedAt(), result.getCreatedAt()); log.warn("External storage mode but file path is empty, resultId={}", result.getId());
return "{}";
}
try {
String filePath = result.getQaContentFilePath();
String bucketName = extractBucketName(filePath);
String objectKey = extractObjectKey(filePath);
byte[] content = objectStorageService.download(bucketName, objectKey);
return new String(content, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Failed to download external qa content, resultId={}, filePath={}",
result.getId(), result.getQaContentFilePath(), e);
throw new BusinessException(ResultCode.ERROR, "下载问答内容失败");
}
} else {
return result.getQaContentJson() != null ? result.getQaContentJson() : "{}";
}
} }
private String deriveStatus(AnnotationResult result) { private String extractBucketName(String filePath) {
if (Boolean.TRUE.equals(result.getIsDeleted())) { if (filePath.startsWith("/")) {
return RuntimeResultStatus.ARCHIVED.name(); filePath = filePath.substring(1);
} }
if (Boolean.TRUE.equals(result.getRequiresManualReview())) { int firstSlash = filePath.indexOf("/");
return RuntimeResultStatus.MANUAL_REVIEW_PENDING.name(); if (firstSlash > 0) {
return filePath.substring(0, firstSlash);
} }
return RuntimeResultStatus.AUTO_ARCHIVE_PENDING.name(); throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式");
}
private String extractObjectKey(String filePath) {
if (filePath.startsWith("/")) {
filePath = filePath.substring(1);
}
int firstSlash = filePath.indexOf("/");
if (firstSlash > 0 && firstSlash < filePath.length() - 1) {
return filePath.substring(firstSlash + 1);
}
throw new BusinessException(ResultCode.BAD_REQUEST, "无效的文件路径格式");
} }
} }

View File

@@ -1,15 +1,5 @@
package com.labelsys.backend.service; 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.labelsys.backend.common.ResultCode; 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.CreateAnnotationTaskRequest;
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest; import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
import com.labelsys.backend.dto.response.AnnotationTaskResponse; 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.AnnotationTask;
import com.labelsys.backend.entity.AnnotationTaskResource; import com.labelsys.backend.entity.AnnotationTaskResource;
import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.IndustryType; import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.SourceStatus; import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.enums.TaskStatus; 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.AnnotationTaskMapper;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator; import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AnnotationTaskService { public class AnnotationTaskService {
private final AnnotationTaskMapper annotationTaskMapper; private final AnnotationTaskMapper annotationTaskMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper; private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SourceResourceMapper sourceResourceMapper; private final SourceResourceMapper sourceResourceMapper;
private final SysConfigMapper sysConfigMapper; private final DataPermissionService dataPermissionService;
private final SysConfigService sysConfigService;
private final DataPermissionService dataPermissionService;
@Transactional @Transactional
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) { public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds()); List<SourceResource> 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()) AnnotationTask task = AnnotationTask.builder()
.creatorId(currentUser.userId()).creatorRole(currentUser.role()).taskName(request.taskName()) .id(IdGenerator.nextId())
.industryType(defaultIndustryType(request.industryType())).taskType(defaultTaskType(request.taskType())) .companyId(currentUser.companyId())
.extractModelConfigId(extractModel.configId()).extractModelName(extractModel.modelName()) .creatorId(currentUser.userId())
.extractModelUrl(extractModel.modelUrl()).extractModelApiKey(extractModel.apiKey()) .creatorRole(currentUser.role())
.verifyModelConfigId(verifyModel.configId()).verifyModelName(verifyModel.modelName()) .taskName(request.taskName())
.verifyModelUrl(verifyModel.modelUrl()).verifyModelApiKey(verifyModel.apiKey()) .industryType(defaultIndustryType(request.industryType()))
.extractPromptConfigId(extractPrompt.configId()).extractPrompt(extractPrompt.promptText()) .taskType(defaultTaskType(request.taskType()))
.verifyPromptConfigId(verifyPrompt.configId()).verifyPrompt(verifyPrompt.promptText()) .taskStatus(TaskStatus.PENDING.name())
.taskStatus(TaskStatus.PENDING.name()).isDeleted(false).build(); .isDeleted(false)
.build();
annotationTaskMapper.insert(task); annotationTaskMapper.insert(task);
saveTaskBindings(task.getId(), currentUser.companyId(), resources); saveTaskBindings(task.getId(), currentUser.companyId(), resources);
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}", log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
currentUser.companyId(), currentUser.userId(), task.getId(), resources.size()); currentUser.companyId(), currentUser.userId(), task.getId(), resources.size());
return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt);
return buildTaskResponse(task, resourceIds(resources));
} }
@Transactional @Transactional
@@ -95,45 +83,13 @@ public class AnnotationTaskService {
List<Long> currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); List<Long> currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List<Long> targetResourceIds = normalizeIds(request.resourceIds()); List<Long> targetResourceIds = normalizeIds(request.resourceIds());
resourcesChanged = !currentResourceIds.equals(targetResourceIds); resourcesChanged = !currentResourceIds.equals(targetResourceIds);
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) { if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源"); throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
} }
resources = loadAndValidateResources(currentUser, request.resourceIds()); 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) { if (request.industryType() != null) {
task.setIndustryType(request.industryType()); task.setIndustryType(request.industryType());
} }
@@ -155,10 +111,7 @@ public class AnnotationTaskService {
List<Long> finalResourceIds = resources != null ? resourceIds(resources) List<Long> finalResourceIds = resources != null ? resourceIds(resources)
: normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); : normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
if (extractModel == null || verifyModel == null || extractPrompt == null || verifyPrompt == null) { return buildTaskResponse(task, finalResourceIds);
return buildTaskResponse(task, finalResourceIds);
}
return buildTaskResponse(task, finalResourceIds, extractModel, verifyModel, extractPrompt, verifyPrompt);
} }
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) { public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
@@ -195,7 +148,7 @@ public class AnnotationTaskService {
List<AnnotationTaskResponse> records = resultPage.getRecords().stream() List<AnnotationTaskResponse> records = resultPage.getRecords().stream()
.filter(task -> query.resourceId() == null .filter(task -> query.resourceId() == null
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()) || annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
.contains(query.resourceId())) .contains(query.resourceId()))
.map(task -> buildTaskResponse(task, .map(task -> buildTaskResponse(task,
normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())))) normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()))))
.toList(); .toList();
@@ -245,39 +198,26 @@ public class AnnotationTaskService {
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) { private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
for (SourceResource resource : resources) { for (SourceResource resource : resources) {
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder().id(IdGenerator.nextId()) annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
.companyId(companyId).taskId(taskId).resourceId(resource.getId()).build()); .id(IdGenerator.nextId())
.companyId(companyId)
.taskId(taskId)
.resourceId(resource.getId())
.build());
} }
} }
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> 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<Long> resourceIds) { private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
String extractModelConfigName = resolveConfigName(task.getExtractModelConfigId()); return new AnnotationTaskResponse(
String verifyModelConfigName = resolveConfigName(task.getVerifyModelConfigId()); task.getId(),
String extractPromptConfigName = resolveConfigName(task.getExtractPromptConfigId()); task.getTaskName(),
String verifyPromptConfigName = resolveConfigName(task.getVerifyPromptConfigId()); task.getIndustryType(),
task.getTaskType(),
return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(), task.getTaskStatus(),
task.getTaskStatus(), resourceIds, resourceIds,
new TaskModelConfigResponse(task.getExtractModelConfigId(), extractModelConfigName, task.getCreatedAt(),
task.getExtractModelName(), task.getUpdatedAt()
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());
} }
private List<Long> resourceIds(List<SourceResource> resources) { private List<Long> resourceIds(List<SourceResource> resources) {
@@ -304,22 +244,4 @@ public class AnnotationTaskService {
private TaskType defaultTaskType(TaskType taskType) { private TaskType defaultTaskType(TaskType taskType) {
return taskType != null ? taskType : TaskType.EXTRACT_QA; 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;
}
}

View File

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

View File

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

View File

@@ -1,47 +1,56 @@
package com.labelsys.backend.service; 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.ResultCode;
import com.labelsys.backend.common.exception.BusinessException; import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.config.ObjectStorageProperties;
import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult; 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.SourceResourcePageQuery;
import com.labelsys.backend.dto.request.SourceUploadRequest; 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.SourceResourceResponse;
import com.labelsys.backend.dto.response.SourceUploadResponse; import com.labelsys.backend.dto.response.SourceUploadResponse;
import com.labelsys.backend.entity.ImageBboxAnnotation;
import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.entity.SysUser; import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.ResourceType; import com.labelsys.backend.enums.ResourceType;
import com.labelsys.backend.enums.SourceStatus; import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.ImageBboxAnnotationMapper;
import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysUserMapper; import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.util.IdGenerator; import com.labelsys.backend.util.IdGenerator;
import com.labelsys.backend.util.ObjectStoragePathBuilder; import com.labelsys.backend.util.ObjectStoragePathBuilder;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SourceResourceService { public class SourceResourceService {
private final SourceResourceMapper sourceResourceMapper; private final SourceResourceMapper sourceResourceMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper; private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
private final DataPermissionService dataPermissionService; private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService; private final ObjectStorageService objectStorageService;
private final com.labelsys.backend.config.ObjectStorageProperties objectStorageProperties; private final ObjectStorageProperties objectStorageProperties;
private final ImageBboxAnnotationMapper imageBboxAnnotationMapper;
private final ObjectMapper objectMapper;
@Transactional @Transactional
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) { public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
@@ -53,37 +62,39 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法"); throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
} }
String resourceName = String resourceName =
StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename(); StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename();
SourceResource existingResource = SourceResource existingResource =
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName); sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
if (existingResource != null) { if (existingResource != null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName); throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName);
} }
long resourceId = IdGenerator.nextId(); long resourceId = IdGenerator.nextId();
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType()); String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(), request.getResourceType(), String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(), request.getResourceType(),
resourceId, extension); resourceId, extension);
try { try {
objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(), objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(),
file.getContentType()); file.getContentType());
} catch (IOException ex) { } catch (IOException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败"); throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
} }
SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId()) SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId())
.creatorId(currentUser.userId()).creatorRole(currentUser.role()) .creatorId(currentUser.userId()).creatorRole(currentUser.role())
.resourceName( .resourceName(
StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename()) StringUtils.hasText(request.getResourceName()) ?
.resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket()) request.getResourceName() :
.filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name()) file.getOriginalFilename())
.storageProvider("rustfs").remark(request.getRemark()).build(); .resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket())
.filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name())
.storageProvider("rustfs").remark(request.getRemark()).build();
sourceResourceMapper.insert(resource); sourceResourceMapper.insert(resource);
log.info("uploaded source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(), 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(), return new SourceUploadResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(), resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getCreatedAt()); resource.getCreatedAt());
} }
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) { public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
@@ -91,10 +102,12 @@ public class SourceResourceService {
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<SourceResource> wrapper = LambdaQueryWrapper<SourceResource> wrapper =
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId()) new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType()) .eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType,
.eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus()) query.resourceType())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword()); .eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus,
query.sourceStatus())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword());
if (shouldFilterByUserId) { if (shouldFilterByUserId) {
wrapper.eq(SourceResource::getCreatorId, currentUser.userId()); wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
@@ -109,20 +122,30 @@ public class SourceResourceService {
List<SourceResourceResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList(); List<SourceResourceResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
return new PageResult<>(records, resultPage.getTotal(), (int)resultPage.getCurrent(), return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int)resultPage.getSize()); (int) resultPage.getSize());
} }
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) { public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId); SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
log.warn("Resource not found or cross-tenant access attempt: resourceId={}, companyId={}, userId={}", 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, "资源不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
} }
return toResponse(resource); return toResponse(resource);
} }
/**
* 获取资源实体(内部使用)
*
* @param resourceId 资源ID
* @return 资源实体
*/
public SourceResource getResourceEntity(Long resourceId) {
return sourceResourceMapper.selectById(resourceId);
}
@Transactional @Transactional
public void deleteResource(LoginUser currentUser, Long resourceId) { public void deleteResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId); SourceResource resource = sourceResourceMapper.selectById(resourceId);
@@ -137,21 +160,21 @@ public class SourceResourceService {
resource.setSourceStatus(SourceStatus.ARCHIVED.name()); resource.setSourceStatus(SourceStatus.ARCHIVED.name());
sourceResourceMapper.updateById(resource); sourceResourceMapper.updateById(resource);
log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}", log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}",
currentUser.companyId(), currentUser.userId(), resourceId); currentUser.companyId(), currentUser.userId(), resourceId);
return; return;
} }
objectStorageService.delete(resource.getBucketName(), resource.getFilePath()); objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
sourceResourceMapper.deleteById(resourceId); sourceResourceMapper.deleteById(resourceId);
log.info("deleted source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(), log.info("deleted source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(),
currentUser.userId(), resourceId); currentUser.userId(), resourceId);
} }
private SourceResourceResponse toResponse(SourceResource resource) { private SourceResourceResponse toResponse(SourceResource resource) {
SysUser creator = sysUserMapper.selectById(resource.getCreatorId()); SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(), return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(), resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(), resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(),
resource.getCreatedAt(), resource.getUpdatedAt()); resource.getCreatedAt(), resource.getUpdatedAt());
} }
private String resolveExtension(String originalFilename, String resourceType) { private String resolveExtension(String originalFilename, String resourceType) {
@@ -165,4 +188,140 @@ public class SourceResourceService {
default -> "bin"; 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<ImageBboxResponse.BboxCoordinateResponse> 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<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {
if (!StringUtils.hasText(bboxJson)) {
return List.of();
}
try {
return objectMapper.readValue(bboxJson,
new TypeReference<List<ImageBboxResponse.BboxCoordinateResponse>>() {
});
} catch (JsonProcessingException e) {
log.warn("Failed to parse bbox json: {}", e.getMessage());
return List.of();
}
}
}

View File

@@ -1,46 +1,34 @@
package com.labelsys.backend.service; 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.ResultCode;
import com.labelsys.backend.common.exception.BusinessException; import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult; 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.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery; 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.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse; 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.entity.SysConfig;
import com.labelsys.backend.enums.ConfigMode;
import com.labelsys.backend.enums.ConfigType; import com.labelsys.backend.enums.ConfigType;
import com.labelsys.backend.enums.UserRole; import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.SysConfigMapper; import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator; import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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 @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysConfigService { public class SysConfigService {
private final SysConfigMapper sysConfigMapper; private final SysConfigMapper sysConfigMapper;
private final ObjectMapper objectMapper;
private final DataPermissionService dataPermissionService; private final DataPermissionService dataPermissionService;
@Transactional @Transactional
@@ -51,14 +39,20 @@ public class SysConfigService {
if (existing != null) { if (existing != null) {
throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
} }
SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId()) SysConfig config = SysConfig.builder()
.configType(request.configType()).configName(request.configName()).configValue(request.configValue()) .id(IdGenerator.nextId())
.status(request.status()).creatorId(currentUser.userId()).creatorRole(currentUser.role().name()) .companyId(currentUser.companyId())
.configType(request.configType())
.configName(request.configName())
.configValue(request.configValue())
.status(request.status())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role().name())
.build(); .build();
sysConfigMapper.insert(config); sysConfigMapper.insert(config);
log.info("saved sys config, companyId={}, userId={}, userRole={}, configName={}, configType={}", log.info("saved sys config, companyId={}, userId={}, userRole={}, configName={}, configType={}",
currentUser.companyId(), currentUser.userId(), currentUser.role().name(), request.configName(), currentUser.companyId(), currentUser.userId(), currentUser.role().name(),
request.configType()); request.configName(), request.configType());
return config; return config;
} }
@@ -66,12 +60,7 @@ public class SysConfigService {
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) { public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) {
validateConfigType(request.configType()); validateConfigType(request.configType());
SysConfig existing = getConfigEntity(currentUser, configId); 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())) { if (StringUtils.hasText(request.configName())) {
existing.setConfigName(request.configName()); existing.setConfigName(request.configName());
} }
@@ -85,8 +74,8 @@ public class SysConfigService {
existing.setStatus(request.status()); existing.setStatus(request.status());
} }
sysConfigMapper.updateById(existing); sysConfigMapper.updateById(existing);
log.info("updated sys config, companyId={}, userId={}, configId={}", currentUser.companyId(), log.info("updated sys config, companyId={}, userId={}, configId={}",
currentUser.userId(), configId); currentUser.companyId(), currentUser.userId(), configId);
return existing; return existing;
} }
@@ -120,96 +109,25 @@ public class SysConfigService {
Page<SysConfig> page = new Page<>(query.pageNo(), query.pageSize()); Page<SysConfig> page = new Page<>(query.pageNo(), query.pageSize());
Page<SysConfig> resultPage = sysConfigMapper.selectPage(page, wrapper); Page<SysConfig> resultPage = sysConfigMapper.selectPage(page, wrapper);
List<SysConfigResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList(); List<SysConfigResponse> records = resultPage.getRecords().stream()
.map(this::toResponse)
.toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(), return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize()); (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) { public SysConfigResponse toResponse(SysConfig config) {
return new SysConfigResponse(config.getId(), config.getConfigType(), config.getConfigName(), return new SysConfigResponse(
config.getConfigValue(), config.getStatus(), config.getCreatorId(), config.getCreatedAt(), config.getId(),
config.getUpdatedAt()); config.getConfigType(),
} config.getConfigName(),
config.getConfigValue(),
private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) { config.getStatus(),
if (!StringUtils.hasText(configName)) { config.getCreatorId(),
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空"); config.getCreatedAt(),
} config.getUpdatedAt()
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());
} }
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) { private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
@@ -225,54 +143,4 @@ public class SysConfigService {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法"); 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 <T> PageResult<T> paginate(List<T> 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) {
}
}

View File

@@ -9,18 +9,6 @@
<result column="task_name" property="taskName"/> <result column="task_name" property="taskName"/>
<result column="industry_type" property="industryType"/> <result column="industry_type" property="industryType"/>
<result column="task_type" property="taskType"/> <result column="task_type" property="taskType"/>
<result column="extract_model_config_id" property="extractModelConfigId"/>
<result column="extract_model_name" property="extractModelName"/>
<result column="extract_model_url" property="extractModelUrl"/>
<result column="extract_model_api_key" property="extractModelApiKey"/>
<result column="verify_model_config_id" property="verifyModelConfigId"/>
<result column="verify_model_name" property="verifyModelName"/>
<result column="verify_model_url" property="verifyModelUrl"/>
<result column="verify_model_api_key" property="verifyModelApiKey"/>
<result column="extract_prompt_config_id" property="extractPromptConfigId"/>
<result column="extract_prompt" property="extractPrompt"/>
<result column="verify_prompt_config_id" property="verifyPromptConfigId"/>
<result column="verify_prompt" property="verifyPrompt"/>
<result column="task_status" property="taskStatus"/> <result column="task_status" property="taskStatus"/>
<result column="is_deleted" property="isDeleted"/> <result column="is_deleted" property="isDeleted"/>
<result column="started_at" property="startedAt"/> <result column="started_at" property="startedAt"/>
@@ -32,18 +20,16 @@
<sql id="AnnotationTaskColumns"> <sql id="AnnotationTaskColumns">
id, company_id, creator_id, creator_role, task_name, industry_type, task_type, 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 task_status, is_deleted, started_at, finished_at, error_message, created_at, updated_at
</sql> </sql>
<select id="findByIdAndCompanyId" resultMap="AnnotationTaskResultMap"> <select id="findByIdAndCompanyId" resultMap="AnnotationTaskResultMap">
select <include refid="AnnotationTaskColumns"/> select
<include refid="AnnotationTaskColumns"/>
from annotation_task from annotation_task
where id = #{id} where id = #{id}
and company_id = #{companyId} and company_id = #{companyId}
and is_deleted = false and is_deleted = false
limit 1 limit 1
</select> </select>
</mapper> </mapper>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.labelsys.backend.mapper.ImageBboxAnnotationMapper">
<resultMap id="ImageBboxAnnotationResultMap" type="com.labelsys.backend.entity.ImageBboxAnnotation">
<id column="id" property="id"/>
<result column="company_id" property="companyId"/>
<result column="resource_id" property="resourceId"/>
<result column="bbox_json" property="bboxJson"/>
<result column="remark" property="remark"/>
<result column="creator_id" property="creatorId"/>
<result column="creator_role" property="creatorRole"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="selectByResourceId" resultMap="ImageBboxAnnotationResultMap">
SELECT id, company_id, resource_id, bbox_json, remark, creator_id, creator_role, created_at, updated_at
FROM image_bbox_annotation WHERE resource_id = #{resourceId}
</select>
<delete id="deleteByResourceId">
DELETE FROM image_bbox_annotation WHERE resource_id = #{resourceId}
</delete>
</mapper>

View File

@@ -67,31 +67,13 @@ ON CONFLICT DO NOTHING;
INSERT INTO annotation_task ( INSERT INTO annotation_task (
id, company_id, creator_id, creator_role, task_name, industry_type, task_type, 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 task_status, is_deleted, started_at, finished_at, error_message
) VALUES ) VALUES
(701, 2, 4, 'EMPLOYEE', '多资源问答抽取任务', 'electricity', 'EXTRACT_QA', (701, 2, 4, 'EMPLOYEE', '多资源问答抽取任务', 'electricity', 'EXTRACT_QA',
401, 'PENDING', FALSE, NULL, NULL, NULL),
'qwen-max', 'https://api.example.com/extract', 'extract-api-key-demo', (702, 2, 4, 'EMPLOYEE', '图片问答抽取任务', 'transport', 'EXTRACT_QA',
402, 'COMPLETED', FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)
'glm-4.5', 'https://api.example.com/verify', 'verify-api-key-demo', ON CONFLICT DO NOTHING;
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;
INSERT INTO annotation_task_resource ( INSERT INTO annotation_task_resource (
id, company_id, task_id, resource_id id, company_id, task_id, resource_id
@@ -129,6 +111,14 @@ INSERT INTO annotation_result_history (
'审核通过后归档', 5, CURRENT_TIMESTAMP) '审核通过后归档', 5, CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING; 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 ( INSERT INTO training_dataset (
id, company_id, creator_id, creator_role, result_history_id, sample_type, glm_format_json, dataset_status id, company_id, creator_id, creator_role, result_history_id, sample_type, glm_format_json, dataset_status
) VALUES ) VALUES
@@ -147,4 +137,4 @@ INSERT INTO export_batch_item (id, batch_id, dataset_id) VALUES
(1201, 1101, 1001) (1201, 1101, 1001)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
COMMIT; COMMIT;

View File

@@ -1,4 +1,5 @@
-- Active: 1775801470429@@39.107.112.174@5432@lablesystem -- Active: 1775801470429@@39.107.112.174@5432@lablesystem_test
begin; begin;
-- Drop Tables (按依赖关系倒序删除) -- 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_user CASCADE;
DROP TABLE IF EXISTS sys_company CASCADE; DROP TABLE IF EXISTS sys_company CASCADE;
CREATE TABLE IF NOT EXISTS sys_company
CREATE TABLE IF NOT EXISTS sys_company ( (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
company_code VARCHAR(64) NOT NULL UNIQUE, company_code VARCHAR(64) NOT NULL UNIQUE,
company_name VARCHAR(128) NOT NULL, company_name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', status VARCHAR(32) NOT NULL DEFAULT 'ENABLED',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
COMMENT ON TABLE sys_company IS '公司表。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN sys_company.updated_at IS '更新时间。'; COMMENT ON COLUMN sys_company.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS sys_user ( CREATE TABLE IF NOT EXISTS sys_user
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
phone VARCHAR(32) NOT NULL, company_id BIGINT NOT NULL,
username VARCHAR(64), phone VARCHAR(32) NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', username VARCHAR(64),
position VARCHAR(32) NOT NULL DEFAULT 'ANNOTATOR', role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
real_name VARCHAR(64) NOT NULL, position VARCHAR(32) NOT NULL DEFAULT 'ANNOTATOR',
password_hash VARCHAR(255) NOT NULL, real_name VARCHAR(64) NOT NULL,
must_change_password BOOLEAN NOT NULL DEFAULT TRUE, password_hash VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', must_change_password BOOLEAN NOT NULL DEFAULT TRUE,
session_version INTEGER NOT NULL DEFAULT 1, status VARCHAR(32) NOT NULL DEFAULT 'ENABLED',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, session_version INTEGER NOT NULL DEFAULT 1,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_sys_user_company_phone UNIQUE (company_id, phone), 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 表示岗位。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN sys_user.updated_at IS '更新时间。'; COMMENT ON COLUMN sys_user.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS sys_menu ( CREATE TABLE IF NOT EXISTS sys_menu
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
menu_code VARCHAR(64) NOT NULL, company_id BIGINT NOT NULL,
menu_name VARCHAR(128) NOT NULL, menu_code VARCHAR(64) NOT NULL,
path VARCHAR(255) NOT NULL, menu_name VARCHAR(128) NOT NULL,
path VARCHAR(255) NOT NULL,
visible_positions VARCHAR(255) NOT NULL DEFAULT 'ADMIN', visible_positions VARCHAR(255) NOT NULL DEFAULT 'ADMIN',
sort_order INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_sys_menu_company FOREIGN KEY (company_id) REFERENCES sys_company(id) CONSTRAINT fk_sys_menu_company FOREIGN KEY (company_id) REFERENCES sys_company (id)
); );
COMMENT ON TABLE sys_menu IS '菜单表。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN sys_menu.updated_at IS '更新时间。'; COMMENT ON COLUMN sys_menu.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS sys_config ( CREATE TABLE IF NOT EXISTS sys_config
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
config_type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM', company_id BIGINT NOT NULL,
config_name VARCHAR(128) NOT NULL, config_type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM',
config_value TEXT NOT NULL, config_name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', config_value TEXT NOT NULL,
creator_id BIGINT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'ENABLED',
creator_role VARCHAR(50) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, creator_role VARCHAR(50) NOT NULL DEFAULT 'EMPLOYEE',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_sys_config_company_name UNIQUE (company_id, config_name), 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_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_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id)
); );
COMMENT ON TABLE sys_config IS '系统配置表保存模型配置、Prompt 配置和系统参数配置。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN sys_config.updated_at IS '更新时间。'; COMMENT ON COLUMN sys_config.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS source_resource ( CREATE TABLE IF NOT EXISTS source_resource
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
resource_name VARCHAR(255) NOT NULL, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
resource_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', resource_name VARCHAR(255) NOT NULL,
bucket_name VARCHAR(128) NOT NULL, resource_type VARCHAR(32) NOT NULL DEFAULT 'TEXT',
file_path VARCHAR(512) NOT NULL, bucket_name VARCHAR(128) NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0, file_path VARCHAR(512) NOT NULL,
source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED', file_size BIGINT NOT NULL DEFAULT 0,
storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs', source_status VARCHAR(32) NOT NULL DEFAULT 'UPLOADED',
remark VARCHAR(255), storage_provider VARCHAR(64) NOT NULL DEFAULT 'rustfs',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_source_resource_company FOREIGN KEY (company_id) REFERENCES sys_company(id), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_source_resource_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) 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 '资源表,保存文本、图片、视频资源元数据。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN source_resource.updated_at IS '更新时间。'; COMMENT ON COLUMN source_resource.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS annotation_task ( CREATE TABLE IF NOT EXISTS image_bbox_annotation
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', resource_id BIGINT NOT NULL,
task_name VARCHAR(255) NOT NULL, bbox_json TEXT,
industry_type VARCHAR(32) NOT NULL DEFAULT 'transport', remark VARCHAR(500),
task_type VARCHAR(32) NOT NULL DEFAULT 'EXTRACT_QA', creator_id BIGINT NOT NULL,
extract_model_config_id BIGINT, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
extract_model_name VARCHAR(128), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
extract_model_url VARCHAR(255), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
extract_model_api_key VARCHAR(255), CONSTRAINT fk_image_bbox_annotation_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
verify_model_config_id BIGINT, CONSTRAINT fk_image_bbox_annotation_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id),
verify_model_name VARCHAR(128), CONSTRAINT fk_image_bbox_annotation_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id)
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)
); );
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.id IS '任务主键ID。';
COMMENT ON COLUMN annotation_task.company_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_id IS '任务创建人用户ID。';
COMMENT ON COLUMN annotation_task.creator_role IS '任务创建人数据权限角色,默认 EMPLOYEE。'; COMMENT ON COLUMN annotation_task.creator_role IS '任务创建人数据权限角色,默认 EMPLOYEE。';
COMMENT ON COLUMN annotation_task.task_name IS '任务名称。'; 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.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.task_status IS '任务状态,默认 PENDING可选 RUNNING、COMPLETED、FAILED。';
COMMENT ON COLUMN annotation_task.is_deleted IS '任务软删除标记,默认 FALSE。'; COMMENT ON COLUMN annotation_task.is_deleted IS '任务软删除标记,默认 FALSE。';
COMMENT ON COLUMN annotation_task.started_at IS '任务开始时间。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN annotation_task.updated_at IS '更新时间。'; COMMENT ON COLUMN annotation_task.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS annotation_task_resource ( CREATE TABLE IF NOT EXISTS annotation_task_resource
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
task_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
task_id BIGINT NOT NULL,
resource_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 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_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_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_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id)
); );
COMMENT ON TABLE annotation_task_resource IS '任务与资源关联表,一个任务可绑定多个资源。'; 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.resource_id IS '资源ID。';
COMMENT ON COLUMN annotation_task_resource.created_at IS '创建时间。'; COMMENT ON COLUMN annotation_task_resource.created_at IS '创建时间。';
CREATE TABLE IF NOT EXISTS annotation_result ( CREATE TABLE IF NOT EXISTS annotation_result
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
task_id BIGINT NOT NULL, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
resource_id BIGINT NOT NULL, task_id BIGINT NOT NULL,
qa_content_json TEXT NOT NULL DEFAULT '{}', resource_id BIGINT NOT NULL,
qa_content_json TEXT NOT NULL DEFAULT '{}',
qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE', qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE',
qa_content_file_path VARCHAR(512), qa_content_file_path VARCHAR(512),
diff_summary TEXT NOT NULL DEFAULT '{}', diff_summary TEXT NOT NULL DEFAULT '{}',
requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE, requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
reviewer_id BIGINT, reviewer_id BIGINT,
review_comment TEXT, review_comment TEXT,
reviewed_at TIMESTAMP, reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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_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_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_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_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id),
CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user(id) CONSTRAINT fk_annotation_result_reviewer FOREIGN KEY (reviewer_id) REFERENCES sys_user (id)
); );
COMMENT ON TABLE annotation_result IS '当前标注结果表。'; COMMENT ON TABLE annotation_result IS '当前标注结果表。';
@@ -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.created_at IS '创建时间。';
COMMENT ON COLUMN annotation_result.updated_at IS '更新时间。'; COMMENT ON COLUMN annotation_result.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS annotation_result_history ( CREATE TABLE IF NOT EXISTS annotation_result_history
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
source_result_id BIGINT, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
task_id BIGINT NOT NULL, source_result_id BIGINT,
resource_id BIGINT NOT NULL, task_id BIGINT NOT NULL,
qa_content_json TEXT NOT NULL DEFAULT '{}', resource_id BIGINT NOT NULL,
qa_content_json TEXT NOT NULL DEFAULT '{}',
qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE', qa_content_storage_mode VARCHAR(32) NOT NULL DEFAULT 'INLINE',
qa_content_file_path VARCHAR(512), qa_content_file_path VARCHAR(512),
archive_reason VARCHAR(255), archive_reason VARCHAR(255),
archived_by BIGINT, archived_by BIGINT,
archived_at TIMESTAMP, archived_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_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_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_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_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_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_resource FOREIGN KEY (resource_id) REFERENCES source_resource (id),
CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user(id) CONSTRAINT fk_annotation_result_history_archived_by FOREIGN KEY (archived_by) REFERENCES sys_user (id)
); );
COMMENT ON TABLE annotation_result_history IS '历史归档结果表。'; COMMENT ON TABLE annotation_result_history IS '历史归档结果表。';
@@ -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.archived_at IS '归档时间。';
COMMENT ON COLUMN annotation_result_history.created_at IS '创建时间。'; COMMENT ON COLUMN annotation_result_history.created_at IS '创建时间。';
CREATE TABLE IF NOT EXISTS training_dataset ( CREATE TABLE IF NOT EXISTS training_dataset
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
result_history_id BIGINT NOT NULL, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
sample_type VARCHAR(32) NOT NULL DEFAULT 'TEXT', result_history_id BIGINT NOT NULL,
glm_format_json TEXT NOT NULL, sample_type VARCHAR(32) NOT NULL DEFAULT 'TEXT',
dataset_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', glm_format_json TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, dataset_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_training_dataset_company FOREIGN KEY (company_id) REFERENCES sys_company(id), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_training_dataset_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id), CONSTRAINT fk_training_dataset_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
CONSTRAINT fk_training_dataset_result_history FOREIGN KEY (result_history_id) REFERENCES annotation_result_history(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 '训练样本表。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN training_dataset.updated_at IS '更新时间。'; COMMENT ON COLUMN training_dataset.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS export_batch ( CREATE TABLE IF NOT EXISTS export_batch
id BIGINT PRIMARY KEY, (
company_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
creator_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE', creator_id BIGINT NOT NULL,
batch_no VARCHAR(64) NOT NULL UNIQUE, creator_role VARCHAR(32) NOT NULL DEFAULT 'EMPLOYEE',
batch_no VARCHAR(64) NOT NULL UNIQUE,
dataset_file_path VARCHAR(512), dataset_file_path VARCHAR(512),
sample_count INTEGER NOT NULL DEFAULT 0, sample_count INTEGER NOT NULL DEFAULT 0,
finetune_job_id VARCHAR(128), finetune_job_id VARCHAR(128),
finetune_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', finetune_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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_company FOREIGN KEY (company_id) REFERENCES sys_company (id),
CONSTRAINT fk_export_batch_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) CONSTRAINT fk_export_batch_creator FOREIGN KEY (creator_id) REFERENCES sys_user (id)
); );
COMMENT ON TABLE export_batch IS '导出批次表。'; 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.created_at IS '创建时间。';
COMMENT ON COLUMN export_batch.updated_at IS '更新时间。'; COMMENT ON COLUMN export_batch.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS export_batch_item ( CREATE TABLE IF NOT EXISTS export_batch_item
id BIGINT PRIMARY KEY, (
batch_id BIGINT NOT NULL, id BIGINT PRIMARY KEY,
batch_id BIGINT NOT NULL,
dataset_id BIGINT NOT NULL, dataset_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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_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_dataset FOREIGN KEY (dataset_id) REFERENCES training_dataset (id),
CONSTRAINT uq_export_batch_item UNIQUE (batch_id, 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.dataset_id IS '关联训练样本ID。';
COMMENT ON COLUMN export_batch_item.created_at IS '创建时间。'; 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_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_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_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_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_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_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_company_status ON source_resource (company_id, source_status);
CREATE INDEX IF NOT EXISTS idx_source_resource_creator ON source_resource(company_id, creator_id); CREATE INDEX IF NOT EXISTS idx_source_resource_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_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_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_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_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_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_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_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_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_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_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_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_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_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;