From 5662c1fda9a5950e1664cb1a6d96a4624b25d1c7 Mon Sep 17 00:00:00 2001 From: wh Date: Mon, 27 Apr 2026 10:27:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 10 +- .../backend/LabelsysBackendApplication.java | 2 + .../backend/config/MybatisPlusConfig.java | 17 ++ .../backend/config/ObjectStorageConfig.java | 31 ++ .../config/ObjectStorageProperties.java | 17 ++ .../AnnotationResultController.java | 61 ++++ .../controller/AnnotationTaskController.java | 62 ++++ .../controller/SourceResourceController.java | 54 ++++ .../controller/SysConfigController.java | 57 ++++ .../backend/dto/common/PageResult.java | 17 ++ .../request/AnnotationResultPageQuery.java | 14 + .../dto/request/AnnotationTaskPageQuery.java | 15 + .../request/CreateAnnotationTaskRequest.java | 20 ++ .../dto/request/ManualModelConfigRequest.java | 12 + .../dto/request/MergeReviewResultRequest.java | 12 + .../request/PromptConfigOptionRequest.java | 10 + .../dto/request/SaveSysConfigRequest.java | 13 + .../dto/request/SourceResourcePageQuery.java | 13 + .../dto/request/SourceUploadRequest.java | 22 ++ .../dto/request/SysConfigPageQuery.java | 13 + .../dto/request/TaskModelConfigRequest.java | 13 + .../request/UpdateAnnotationTaskRequest.java | 18 ++ .../AnnotationResultCompareResponse.java | 16 + .../response/AnnotationResultResponse.java | 19 ++ .../dto/response/AnnotationTaskResponse.java | 22 ++ .../response/MergeReviewResultResponse.java | 13 + .../dto/response/SourceResourceResponse.java | 21 ++ .../dto/response/SourceUploadResponse.java | 17 ++ .../dto/response/SysConfigResponse.java | 17 ++ .../dto/response/TaskModelConfigResponse.java | 13 + .../response/TaskPromptConfigResponse.java | 11 + .../backend/entity/AnnotationResult.java | 37 +++ .../entity/AnnotationResultHistory.java | 34 +++ .../backend/entity/AnnotationTask.java | 46 +++ .../entity/AnnotationTaskResource.java | 24 ++ .../backend/entity/BizDataRecord.java | 22 ++ .../backend/entity/SourceResource.java | 34 +++ .../labelsys/backend/entity/SysConfig.java | 28 ++ .../labelsys/backend/enums/ConfigType.java | 13 + .../backend/enums/QaContentStorageMode.java | 12 + .../labelsys/backend/enums/ResourceType.java | 13 + .../backend/enums/RuntimeResultStatus.java | 7 + .../labelsys/backend/enums/SourceStatus.java | 14 + .../labelsys/backend/enums/TaskStatus.java | 14 + .../backend/interceptor/AuthInterceptor.java | 14 +- .../mapper/AnnotationResultHistoryMapper.java | 7 + .../mapper/AnnotationResultMapper.java | 17 ++ .../backend/mapper/AnnotationTaskMapper.java | 10 + .../mapper/AnnotationTaskResourceMapper.java | 15 + .../backend/mapper/SourceResourceMapper.java | 11 + .../backend/mapper/SysConfigMapper.java | 14 + .../AutoArchiveAnnotationResultJob.java | 23 ++ .../AnnotationResultArchiveService.java | 140 +++++++++ .../service/AnnotationResultService.java | 99 ++++++ .../service/AnnotationTaskService.java | 284 ++++++++++++++++++ .../service/DataPermissionService.java | 27 ++ .../backend/service/ObjectStorageService.java | 8 + .../service/RustfsObjectStorageService.java | 45 +++ .../service/SourceResourceService.java | 179 +++++++++++ .../backend/service/SysConfigService.java | 258 ++++++++++++++++ .../util/ObjectStoragePathBuilder.java | 38 +++ src/main/resources/application.yml | 11 + .../mapper/AnnotationResultMapper.xml | 50 +++ .../resources/mapper/AnnotationTaskMapper.xml | 49 +++ .../mapper/AnnotationTaskResourceMapper.xml | 28 ++ .../resources/mapper/SourceResourceMapper.xml | 35 +++ src/main/resources/mapper/SysConfigMapper.xml | 36 +++ 67 files changed, 2343 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java create mode 100644 src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java create mode 100644 src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java create mode 100644 src/main/java/com/labelsys/backend/controller/AnnotationResultController.java create mode 100644 src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java create mode 100644 src/main/java/com/labelsys/backend/controller/SourceResourceController.java create mode 100644 src/main/java/com/labelsys/backend/controller/SysConfigController.java create mode 100644 src/main/java/com/labelsys/backend/dto/common/PageResult.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java create mode 100644 src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java create mode 100644 src/main/java/com/labelsys/backend/entity/AnnotationResult.java create mode 100644 src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java create mode 100644 src/main/java/com/labelsys/backend/entity/AnnotationTask.java create mode 100644 src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java create mode 100644 src/main/java/com/labelsys/backend/entity/BizDataRecord.java create mode 100644 src/main/java/com/labelsys/backend/entity/SourceResource.java create mode 100644 src/main/java/com/labelsys/backend/entity/SysConfig.java create mode 100644 src/main/java/com/labelsys/backend/enums/ConfigType.java create mode 100644 src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java create mode 100644 src/main/java/com/labelsys/backend/enums/ResourceType.java create mode 100644 src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java create mode 100644 src/main/java/com/labelsys/backend/enums/SourceStatus.java create mode 100644 src/main/java/com/labelsys/backend/enums/TaskStatus.java create mode 100644 src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java create mode 100644 src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java create mode 100644 src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java create mode 100644 src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java create mode 100644 src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java create mode 100644 src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java create mode 100644 src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java create mode 100644 src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java create mode 100644 src/main/java/com/labelsys/backend/service/AnnotationResultService.java create mode 100644 src/main/java/com/labelsys/backend/service/AnnotationTaskService.java create mode 100644 src/main/java/com/labelsys/backend/service/ObjectStorageService.java create mode 100644 src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java create mode 100644 src/main/java/com/labelsys/backend/service/SourceResourceService.java create mode 100644 src/main/java/com/labelsys/backend/service/SysConfigService.java create mode 100644 src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java create mode 100644 src/main/resources/mapper/AnnotationResultMapper.xml create mode 100644 src/main/resources/mapper/AnnotationTaskMapper.xml create mode 100644 src/main/resources/mapper/AnnotationTaskResourceMapper.xml create mode 100644 src/main/resources/mapper/SourceResourceMapper.xml create mode 100644 src/main/resources/mapper/SysConfigMapper.xml diff --git a/pom.xml b/pom.xml index 3cf8ba9..e95862b 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,14 @@ lombok true + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + sts + com.h2database h2 @@ -126,4 +134,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java b/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java index bbf8c6c..e502d34 100644 --- a/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java +++ b/src/main/java/com/labelsys/backend/LabelsysBackendApplication.java @@ -3,9 +3,11 @@ package com.labelsys.backend; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @MapperScan("com.labelsys.backend.mapper") +@EnableScheduling public class LabelsysBackendApplication { public static void main(String[] args) { diff --git a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java new file mode 100644 index 0000000..1f7f155 --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java new file mode 100644 index 0000000..161698b --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java @@ -0,0 +1,31 @@ +package com.labelsys.backend.config; + +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(ObjectStorageProperties.class) +public class ObjectStorageConfig { + + private final ObjectStorageProperties properties; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .endpointOverride(URI.create(properties.getEndpoint())) + .region(Region.of(properties.getRegion())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()))) + .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(properties.isPathStyleAccess()).build()) + .build(); + } +} diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java new file mode 100644 index 0000000..b3ca457 --- /dev/null +++ b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "labelsys.object-storage") +public class ObjectStorageProperties { + private String endpoint; + private String region; + private String accessKey; + private String secretKey; + private boolean pathStyleAccess = true; + private String sourceBucket; + private String artifactBucket; + private String exportBucket; +} diff --git a/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java b/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java new file mode 100644 index 0000000..0a9cdac --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/AnnotationResultController.java @@ -0,0 +1,61 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.annotation.RequirePosition; +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.AnnotationResultPageQuery; +import com.labelsys.backend.dto.request.MergeReviewResultRequest; +import com.labelsys.backend.dto.response.AnnotationResultCompareResponse; +import com.labelsys.backend.dto.response.AnnotationResultResponse; +import com.labelsys.backend.dto.response.MergeReviewResultResponse; +import com.labelsys.backend.enums.UserPosition; +import com.labelsys.backend.service.AnnotationResultArchiveService; +import com.labelsys.backend.service.AnnotationResultService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "标注结果管理") +@RestController +@RequestMapping("/api/annotation-results") +@RequiredArgsConstructor +public class AnnotationResultController { + + private final AnnotationResultService annotationResultService; + private final AnnotationResultArchiveService annotationResultArchiveService; + + @Operation(summary = "分页查询标注结果") + @GetMapping + public Result> page(AnnotationResultPageQuery query) { + return Result.success(annotationResultService.pageResults(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询标注结果详情") + @GetMapping("/{id}") + public Result detail(@PathVariable Long id) { + return Result.success(annotationResultService.getResult(UserContext.requireUser(), id)); + } + + @Operation(summary = "查询标注结果比对信息") + @RequirePosition(UserPosition.REVIEWER) + @GetMapping("/{id}/compare") + public Result compare(@PathVariable Long id) { + return Result.success(annotationResultService.compareResult(UserContext.requireUser(), id)); + } + + @Operation(summary = "提交合并审核结果") + @RequirePosition(UserPosition.REVIEWER) + @PostMapping("/{id}/merge-review") + public Result mergeReview(@PathVariable Long id, + @Valid @RequestBody MergeReviewResultRequest request) { + return Result.success(annotationResultArchiveService.mergeReview(UserContext.requireUser(), id, request)); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java b/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java new file mode 100644 index 0000000..f36274c --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/AnnotationTaskController.java @@ -0,0 +1,62 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.AnnotationTaskPageQuery; +import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest; +import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest; +import com.labelsys.backend.dto.response.AnnotationTaskResponse; +import com.labelsys.backend.service.AnnotationTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "标注任务管理") +@RestController +@RequestMapping("/api/annotation-tasks") +@RequiredArgsConstructor +public class AnnotationTaskController { + + private final AnnotationTaskService annotationTaskService; + + @Operation(summary = "创建标注任务") + @PostMapping + public Result create(@Valid @RequestBody CreateAnnotationTaskRequest request) { + return Result.success(annotationTaskService.createTask(UserContext.requireUser(), request)); + } + + @Operation(summary = "更新标注任务") + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @Valid @RequestBody UpdateAnnotationTaskRequest request) { + return Result.success(annotationTaskService.updateTask(UserContext.requireUser(), id, request)); + } + + @Operation(summary = "分页查询标注任务") + @GetMapping + public Result> page(AnnotationTaskPageQuery query) { + return Result.success(annotationTaskService.pageTasks(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询标注任务详情") + @GetMapping("/{id}") + public Result detail(@PathVariable Long id) { + return Result.success(annotationTaskService.getTask(UserContext.requireUser(), id)); + } + + @Operation(summary = "删除标注任务") + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + annotationTaskService.deleteTask(UserContext.requireUser(), id); + return Result.success(); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/SourceResourceController.java b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java new file mode 100644 index 0000000..36fb83b --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/SourceResourceController.java @@ -0,0 +1,54 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.SourceResourcePageQuery; +import com.labelsys.backend.dto.request.SourceUploadRequest; +import com.labelsys.backend.dto.response.SourceResourceResponse; +import com.labelsys.backend.dto.response.SourceUploadResponse; +import com.labelsys.backend.service.SourceResourceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "资源管理") +@RestController +@RequestMapping("/api/source-resources") +@RequiredArgsConstructor +public class SourceResourceController { + + private final SourceResourceService sourceResourceService; + + @Operation(summary = "上传资源") + @PostMapping("/upload") + public Result upload(@ModelAttribute SourceUploadRequest request) { + return Result.success(sourceResourceService.upload(UserContext.requireUser(), request)); + } + + @Operation(summary = "分页查询资源") + @GetMapping + public Result> page(SourceResourcePageQuery query) { + return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询资源详情") + @GetMapping("/{id}") + public Result detail(@PathVariable Long id) { + return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id)); + } + + @Operation(summary = "删除资源") + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + sourceResourceService.deleteResource(UserContext.requireUser(), id); + return Result.success(); + } +} diff --git a/src/main/java/com/labelsys/backend/controller/SysConfigController.java b/src/main/java/com/labelsys/backend/controller/SysConfigController.java new file mode 100644 index 0000000..959e057 --- /dev/null +++ b/src/main/java/com/labelsys/backend/controller/SysConfigController.java @@ -0,0 +1,57 @@ +package com.labelsys.backend.controller; + +import com.labelsys.backend.annotation.RequirePosition; +import com.labelsys.backend.common.Result; +import com.labelsys.backend.context.UserContext; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.SaveSysConfigRequest; +import com.labelsys.backend.dto.request.SysConfigPageQuery; +import com.labelsys.backend.dto.response.SysConfigResponse; +import com.labelsys.backend.enums.UserPosition; +import com.labelsys.backend.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "系统配置管理") +@RestController +@RequestMapping("/api/sys-configs") +@RequiredArgsConstructor +public class SysConfigController { + + private final SysConfigService sysConfigService; + + @Operation(summary = "创建系统配置") + @RequirePosition(UserPosition.ADMIN) + @PostMapping + public Result create(@Valid @RequestBody SaveSysConfigRequest request) { + return Result.success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request))); + } + + @Operation(summary = "更新系统配置") + @RequirePosition(UserPosition.ADMIN) + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @Valid @RequestBody SaveSysConfigRequest request) { + return Result.success(sysConfigService.toResponse(sysConfigService.updateConfig(UserContext.requireUser(), id, request))); + } + + @Operation(summary = "分页查询系统配置") + @GetMapping + public Result> page(SysConfigPageQuery query) { + return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query)); + } + + @Operation(summary = "查询系统配置详情") + @GetMapping("/{id}") + public Result detail(@PathVariable Long id) { + return Result.success(sysConfigService.getConfig(UserContext.requireUser(), id)); + } +} diff --git a/src/main/java/com/labelsys/backend/dto/common/PageResult.java b/src/main/java/com/labelsys/backend/dto/common/PageResult.java new file mode 100644 index 0000000..62f40e1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/common/PageResult.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.common; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "分页结果") +public record PageResult( + @Schema(description = "当前页记录") List records, + @Schema(description = "总记录数") Long total, + @Schema(description = "页码") Integer pageNo, + @Schema(description = "每页数量") Integer pageSize +) { + public static PageResult from(IPage page) { + return new PageResult<>(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize()); + } +} diff --git a/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java new file mode 100644 index 0000000..d07cbcf --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/AnnotationResultPageQuery.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注结果分页查询请求") +public record AnnotationResultPageQuery( + @Schema(description = "任务ID") Long taskId, + @Schema(description = "资源ID") Long resourceId, + @Schema(description = "是否需要人工审核") Boolean requiresManualReview, + @Schema(description = "运行态状态") String runtimeStatus, + @Schema(description = "页码") Integer pageNo, + @Schema(description = "每页数量") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java new file mode 100644 index 0000000..bdd5d43 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/AnnotationTaskPageQuery.java @@ -0,0 +1,15 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注任务分页查询请求") +public record AnnotationTaskPageQuery( + @Schema(description = "关键字") String keyword, + @Schema(description = "任务类型") String taskType, + @Schema(description = "任务状态") String taskStatus, + @Schema(description = "资源ID") Long resourceId, + @Schema(description = "是否已删除") Boolean isDeleted, + @Schema(description = "页码") Integer pageNo, + @Schema(description = "每页数量") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java new file mode 100644 index 0000000..5dbc324 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/CreateAnnotationTaskRequest.java @@ -0,0 +1,20 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "创建标注任务请求") +public record CreateAnnotationTaskRequest( + @Schema(description = "任务名称") @NotBlank(message = "任务名称不能为空") String taskName, + @Schema(description = "行业类型") String industryType, + @Schema(description = "任务类型") String taskType, + @Schema(description = "资源ID列表") @NotEmpty(message = "资源列表不能为空") List resourceIds, + @Schema(description = "抽取模型配置") @Valid TaskModelConfigRequest extractModel, + @Schema(description = "校验模型配置") @Valid TaskModelConfigRequest verifyModel, + @Schema(description = "抽取提示词配置") @Valid PromptConfigOptionRequest extractPrompt, + @Schema(description = "校验提示词配置") @Valid PromptConfigOptionRequest verifyPrompt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java new file mode 100644 index 0000000..6fce101 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/ManualModelConfigRequest.java @@ -0,0 +1,12 @@ +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 = "模型名称") @NotBlank(message = "模型名称不能为空") String modelName, + @Schema(description = "模型地址") @NotBlank(message = "模型地址不能为空") String modelUrl, + @Schema(description = "模型密钥") @NotBlank(message = "模型密钥不能为空") String apiKey +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java b/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java new file mode 100644 index 0000000..dc51d22 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/MergeReviewResultRequest.java @@ -0,0 +1,12 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "合并审核结果请求") +public record MergeReviewResultRequest( + @Schema(description = "差异摘要 JSON") @NotBlank(message = "差异摘要不能为空") String diffSummary, + @Schema(description = "最终问答内容 JSON") @NotBlank(message = "问答内容不能为空") String qaContentJson, + @Schema(description = "审核备注") String reviewComment +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java b/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java new file mode 100644 index 0000000..19f0b9c --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/PromptConfigOptionRequest.java @@ -0,0 +1,10 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务提示词配置请求") +public record PromptConfigOptionRequest( + @Schema(description = "已选择的提示词配置名称") String selectedConfigName, + @Schema(description = "手动输入的提示词内容") String promptText +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java new file mode 100644 index 0000000..566c56f --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SaveSysConfigRequest.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "保存系统配置请求") +public record SaveSysConfigRequest( + @Schema(description = "配置类型") @NotBlank(message = "配置类型不能为空") String configType, + @Schema(description = "配置名称") @NotBlank(message = "配置名称不能为空") String configName, + @Schema(description = "配置值") @NotBlank(message = "配置值不能为空") String configValue, + @Schema(description = "配置状态") @NotBlank(message = "配置状态不能为空") String status +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java b/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java new file mode 100644 index 0000000..84a50ab --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SourceResourcePageQuery.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "资源分页查询请求") +public record SourceResourcePageQuery( + @Schema(description = "关键字") String keyword, + @Schema(description = "资源类型") String resourceType, + @Schema(description = "资源状态") String sourceStatus, + @Schema(description = "页码") Integer pageNo, + @Schema(description = "每页数量") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java b/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java new file mode 100644 index 0000000..e84a548 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SourceUploadRequest.java @@ -0,0 +1,22 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Data +@Schema(description = "资源上传请求") +public class SourceUploadRequest { + + @Schema(description = "资源名称") + private String resourceName; + + @Schema(description = "资源类型:TEXT、IMAGE、VIDEO") + private String resourceType; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "上传文件") + private MultipartFile file; +} diff --git a/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java b/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java new file mode 100644 index 0000000..38aff48 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/SysConfigPageQuery.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "系统配置分页查询请求") +public record SysConfigPageQuery( + @Schema(description = "配置类型") String configType, + @Schema(description = "配置名称") String configName, + @Schema(description = "配置状态") String status, + @Schema(description = "页码") Integer pageNo, + @Schema(description = "每页数量") Integer pageSize +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java b/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java new file mode 100644 index 0000000..b242b89 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/TaskModelConfigRequest.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "任务模型配置请求") +public record TaskModelConfigRequest( + @Schema(description = "配置模式:SELECT 或 MANUAL") @NotBlank(message = "配置模式不能为空") String mode, + @Schema(description = "已选择的配置名称") String selectedConfigName, + @Schema(description = "手动录入的模型配置") @Valid ManualModelConfigRequest manualConfig +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java new file mode 100644 index 0000000..4b8b55b --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/request/UpdateAnnotationTaskRequest.java @@ -0,0 +1,18 @@ +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 = "更新标注任务请求") +public record UpdateAnnotationTaskRequest( + @Schema(description = "行业类型") String industryType, + @Schema(description = "任务类型") String taskType, + @Schema(description = "资源ID列表") @NotEmpty(message = "资源列表不能为空") List resourceIds, + @Schema(description = "抽取模型配置") @Valid TaskModelConfigRequest extractModel, + @Schema(description = "校验模型配置") @Valid TaskModelConfigRequest verifyModel, + @Schema(description = "抽取提示词配置") @Valid PromptConfigOptionRequest extractPrompt, + @Schema(description = "校验提示词配置") @Valid PromptConfigOptionRequest verifyPrompt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java new file mode 100644 index 0000000..45d5e05 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultCompareResponse.java @@ -0,0 +1,16 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标注结果比对响应") +public record AnnotationResultCompareResponse( + @Schema(description = "结果ID") Long id, + @Schema(description = "任务ID") Long taskId, + @Schema(description = "资源ID") Long resourceId, + @Schema(description = "问答内容 JSON") String qaContentJson, + @Schema(description = "差异摘要 JSON") String diffSummary, + @Schema(description = "问答存储模式") String qaContentStorageMode, + @Schema(description = "外置问答文件路径") String qaContentFilePath, + @Schema(description = "资源预览路径") String sourcePreviewPath +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java new file mode 100644 index 0000000..88bdcb7 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationResultResponse.java @@ -0,0 +1,19 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "标注结果响应") +public record AnnotationResultResponse( + @Schema(description = "结果ID") Long id, + @Schema(description = "任务ID") Long taskId, + @Schema(description = "资源ID") Long resourceId, + @Schema(description = "运行态状态") String runtimeStatus, + @Schema(description = "是否需要人工审核") Boolean requiresManualReview, + @Schema(description = "是否已删除") Boolean isDeleted, + @Schema(description = "问答存储模式") String qaContentStorageMode, + @Schema(description = "审核备注") String reviewComment, + @Schema(description = "审核时间") LocalDateTime reviewedAt, + @Schema(description = "创建时间") LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java new file mode 100644 index 0000000..d9255d3 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/AnnotationTaskResponse.java @@ -0,0 +1,22 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "标注任务响应") +public record AnnotationTaskResponse( + @Schema(description = "任务ID") Long id, + @Schema(description = "任务名称") String taskName, + @Schema(description = "行业类型") String industryType, + @Schema(description = "任务类型") String taskType, + @Schema(description = "任务状态") String taskStatus, + @Schema(description = "资源ID列表") List resourceIds, + @Schema(description = "抽取模型配置") TaskModelConfigResponse extractModel, + @Schema(description = "校验模型配置") TaskModelConfigResponse verifyModel, + @Schema(description = "抽取提示词配置") TaskPromptConfigResponse extractPrompt, + @Schema(description = "校验提示词配置") TaskPromptConfigResponse verifyPrompt, + @Schema(description = "创建时间") LocalDateTime createdAt, + @Schema(description = "更新时间") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java b/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java new file mode 100644 index 0000000..4af8e53 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/MergeReviewResultResponse.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "合并审核结果响应") +public record MergeReviewResultResponse( + @Schema(description = "运行态结果ID") Long resultId, + @Schema(description = "历史结果ID") Long historyId, + @Schema(description = "归档原因") String archiveReason, + @Schema(description = "归档时间") LocalDateTime archivedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java b/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java new file mode 100644 index 0000000..1922ccc --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SourceResourceResponse.java @@ -0,0 +1,21 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "资源响应") +public record SourceResourceResponse( + @Schema(description = "资源ID") Long id, + @Schema(description = "资源名称") String resourceName, + @Schema(description = "资源类型") String resourceType, + @Schema(description = "桶名称") String bucketName, + @Schema(description = "文件路径") String filePath, + @Schema(description = "文件大小") Long fileSize, + @Schema(description = "资源状态") String sourceStatus, + @Schema(description = "存储提供方") String storageProvider, + @Schema(description = "备注") String remark, + @Schema(description = "创建人名称") String creatorName, + @Schema(description = "创建时间") LocalDateTime createdAt, + @Schema(description = "更新时间") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java b/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java new file mode 100644 index 0000000..7029138 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SourceUploadResponse.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "资源上传响应") +public record SourceUploadResponse( + @Schema(description = "资源ID") Long id, + @Schema(description = "资源名称") String resourceName, + @Schema(description = "资源类型") String resourceType, + @Schema(description = "桶名称") String bucketName, + @Schema(description = "文件路径") String filePath, + @Schema(description = "文件大小") Long fileSize, + @Schema(description = "资源状态") String sourceStatus, + @Schema(description = "创建时间") LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java new file mode 100644 index 0000000..67b53fa --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/SysConfigResponse.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +@Schema(description = "系统配置响应") +public record SysConfigResponse( + @Schema(description = "配置ID") Long id, + @Schema(description = "配置类型") String configType, + @Schema(description = "配置名称") String configName, + @Schema(description = "配置值") String configValue, + @Schema(description = "配置状态") String status, + @Schema(description = "创建人ID") Long creatorId, + @Schema(description = "创建时间") LocalDateTime createdAt, + @Schema(description = "更新时间") LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java new file mode 100644 index 0000000..6ead3e0 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/TaskModelConfigResponse.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务模型配置响应") +public record TaskModelConfigResponse( + @Schema(description = "配置ID") Long configId, + @Schema(description = "配置名称") String configName, + @Schema(description = "模型名称") String modelName, + @Schema(description = "模型地址") String modelUrl, + @Schema(description = "脱敏后的模型密钥") String maskedApiKey +) { +} diff --git a/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java b/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java new file mode 100644 index 0000000..91a4326 --- /dev/null +++ b/src/main/java/com/labelsys/backend/dto/response/TaskPromptConfigResponse.java @@ -0,0 +1,11 @@ +package com.labelsys.backend.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "任务提示词配置响应") +public record TaskPromptConfigResponse( + @Schema(description = "配置ID") Long configId, + @Schema(description = "配置名称") String configName, + @Schema(description = "提示词内容") String promptText +) { +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationResult.java b/src/main/java/com/labelsys/backend/entity/AnnotationResult.java new file mode 100644 index 0000000..9a5921c --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationResult.java @@ -0,0 +1,37 @@ +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; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_result") +public class AnnotationResult { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private Long taskId; + private Long resourceId; + private String qaContentJson; + private String qaContentStorageMode; + private String qaContentFilePath; + private String diffSummary; + private Boolean requiresManualReview; + private Boolean isDeleted; + private Long reviewerId; + private String reviewComment; + private LocalDateTime reviewedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java b/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java new file mode 100644 index 0000000..d180683 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationResultHistory.java @@ -0,0 +1,34 @@ +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; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_result_history") +public class AnnotationResultHistory { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private Long sourceResultId; + private Long taskId; + private Long resourceId; + private String qaContentJson; + private String qaContentStorageMode; + private String qaContentFilePath; + private String archiveReason; + private Long archivedBy; + private LocalDateTime archivedAt; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationTask.java b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java new file mode 100644 index 0000000..31b3898 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationTask.java @@ -0,0 +1,46 @@ +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; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_task") +public class AnnotationTask { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String taskName; + private String industryType; + private String taskType; + private Long extractModelConfigId; + private String extractModelName; + private String extractModelUrl; + private String extractModelApiKey; + private Long verifyModelConfigId; + private String verifyModelName; + private String verifyModelUrl; + private String verifyModelApiKey; + private Long extractPromptConfigId; + private String extractPrompt; + private Long verifyPromptConfigId; + private String verifyPrompt; + private String taskStatus; + private Boolean isDeleted; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private String errorMessage; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java b/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java new file mode 100644 index 0000000..c5c18ce --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/AnnotationTaskResource.java @@ -0,0 +1,24 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("annotation_task_resource") +public class AnnotationTaskResource { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long taskId; + private Long resourceId; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/BizDataRecord.java b/src/main/java/com/labelsys/backend/entity/BizDataRecord.java new file mode 100644 index 0000000..9acc68c --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/BizDataRecord.java @@ -0,0 +1,22 @@ +package com.labelsys.backend.entity; + +import com.labelsys.backend.enums.UserRole; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BizDataRecord { + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String recordName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/SourceResource.java b/src/main/java/com/labelsys/backend/entity/SourceResource.java new file mode 100644 index 0000000..ef469ab --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/SourceResource.java @@ -0,0 +1,34 @@ +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; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("source_resource") +public class SourceResource { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private Long creatorId; + private UserRole creatorRole; + private String resourceName; + private String resourceType; + private String bucketName; + private String filePath; + private Long fileSize; + private String sourceStatus; + private String storageProvider; + private String remark; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/entity/SysConfig.java b/src/main/java/com/labelsys/backend/entity/SysConfig.java new file mode 100644 index 0000000..26b5559 --- /dev/null +++ b/src/main/java/com/labelsys/backend/entity/SysConfig.java @@ -0,0 +1,28 @@ +package com.labelsys.backend.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("sys_config") +public class SysConfig { + @TableId(type = IdType.INPUT) + private Long id; + private Long companyId; + private String configType; + private String configName; + private String configValue; + private String status; + private Long creatorId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/labelsys/backend/enums/ConfigType.java b/src/main/java/com/labelsys/backend/enums/ConfigType.java new file mode 100644 index 0000000..6a52f31 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/ConfigType.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum ConfigType { + MODEL, + PROMPT, + SYSTEM; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(type -> type.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java b/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java new file mode 100644 index 0000000..20e6c11 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/QaContentStorageMode.java @@ -0,0 +1,12 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum QaContentStorageMode { + INLINE, + EXTERNAL; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(mode -> mode.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/ResourceType.java b/src/main/java/com/labelsys/backend/enums/ResourceType.java new file mode 100644 index 0000000..f3e4883 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/ResourceType.java @@ -0,0 +1,13 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum ResourceType { + TEXT, + IMAGE, + VIDEO; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(type -> type.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java b/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java new file mode 100644 index 0000000..8f3878d --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/RuntimeResultStatus.java @@ -0,0 +1,7 @@ +package com.labelsys.backend.enums; + +public enum RuntimeResultStatus { + MANUAL_REVIEW_PENDING, + AUTO_ARCHIVE_PENDING, + ARCHIVED +} diff --git a/src/main/java/com/labelsys/backend/enums/SourceStatus.java b/src/main/java/com/labelsys/backend/enums/SourceStatus.java new file mode 100644 index 0000000..9dfede0 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/SourceStatus.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum SourceStatus { + UPLOADED, + PROCESSING, + READY, + ARCHIVED; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(status -> status.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/enums/TaskStatus.java b/src/main/java/com/labelsys/backend/enums/TaskStatus.java new file mode 100644 index 0000000..1ab8ab9 --- /dev/null +++ b/src/main/java/com/labelsys/backend/enums/TaskStatus.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.enums; + +import java.util.Arrays; + +public enum TaskStatus { + PENDING, + RUNNING, + COMPLETED, + FAILED; + + public static boolean isValid(String value) { + return Arrays.stream(values()).anyMatch(status -> status.name().equals(value)); + } +} diff --git a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java index 7467e42..8dadd0d 100644 --- a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java +++ b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java @@ -27,11 +27,17 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class AuthInterceptor implements HandlerInterceptor { - private static final Set OPEN_PATHS = Set.of("/label/api/auth/companies", "/label/api/auth/login", - "/label/swagger-ui.html", "/label/v3/api-docs", "/label/v3/api-docs/swagger-config"); + private static final Set OPEN_PATHS = Set.of( + "/api/auth/companies", "/label/api/auth/companies", + "/api/auth/login", "/label/api/auth/login", + "/swagger-ui.html", "/label/swagger-ui.html", + "/v3/api-docs", "/label/v3/api-docs", + "/v3/api-docs/swagger-config", "/label/v3/api-docs/swagger-config"); - private static final Set ALLOWED_WHEN_MUST_CHANGE_PASSWORD = - Set.of("/label/api/auth/change-password", "/label/api/auth/logout", "/label/api/auth/me"); + private static final Set ALLOWED_WHEN_MUST_CHANGE_PASSWORD = Set.of( + "/api/auth/change-password", "/label/api/auth/change-password", + "/api/auth/logout", "/label/api/auth/logout", + "/api/auth/me", "/label/api/auth/me"); private final TokenSessionRepository tokenSessionRepository; private final SysUserMapper sysUserMapper; diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java new file mode 100644 index 0000000..203a3d1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationResultHistoryMapper.java @@ -0,0 +1,7 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationResultHistory; + +public interface AnnotationResultHistoryMapper extends BaseMapper { +} diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java new file mode 100644 index 0000000..2fc55bf --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationResultMapper.java @@ -0,0 +1,17 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationResult; +import java.time.LocalDateTime; +import org.apache.ibatis.annotations.Param; + +public interface AnnotationResultMapper extends BaseMapper { + + AnnotationResult findActiveByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId); + + int markArchived(@Param("id") Long id, + @Param("companyId") Long companyId, + @Param("reviewerId") Long reviewerId, + @Param("reviewComment") String reviewComment, + @Param("reviewedAt") LocalDateTime reviewedAt); +} diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java new file mode 100644 index 0000000..2be455b --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskMapper.java @@ -0,0 +1,10 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationTask; +import org.apache.ibatis.annotations.Param; + +public interface AnnotationTaskMapper extends BaseMapper { + + AnnotationTask findByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId); +} diff --git a/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java new file mode 100644 index 0000000..07c05e8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/AnnotationTaskResourceMapper.java @@ -0,0 +1,15 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.AnnotationTaskResource; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface AnnotationTaskResourceMapper extends BaseMapper { + + List listResourceIdsByTaskId(@Param("taskId") Long taskId); + + int deleteByTaskId(@Param("taskId") Long taskId); + + int countByResourceId(@Param("resourceId") Long resourceId); +} diff --git a/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java b/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java new file mode 100644 index 0000000..0270e76 --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/SourceResourceMapper.java @@ -0,0 +1,11 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.SourceResource; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface SourceResourceMapper extends BaseMapper { + + List selectByCompanyIdAndIds(@Param("companyId") Long companyId, @Param("resourceIds") List resourceIds); +} diff --git a/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java b/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java new file mode 100644 index 0000000..2a81c2b --- /dev/null +++ b/src/main/java/com/labelsys/backend/mapper/SysConfigMapper.java @@ -0,0 +1,14 @@ +package com.labelsys.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.labelsys.backend.entity.SysConfig; +import org.apache.ibatis.annotations.Param; + +public interface SysConfigMapper extends BaseMapper { + + SysConfig findByCompanyIdAndConfigName(@Param("companyId") Long companyId, @Param("configName") String configName); + + SysConfig findByCompanyIdAndConfigNameAndType(@Param("companyId") Long companyId, + @Param("configName") String configName, + @Param("configType") String configType); +} diff --git a/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java new file mode 100644 index 0000000..f18bbd1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java @@ -0,0 +1,23 @@ +package com.labelsys.backend.scheduled; + +import com.labelsys.backend.service.AnnotationResultArchiveService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AutoArchiveAnnotationResultJob { + + private final AnnotationResultArchiveService annotationResultArchiveService; + + @Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}") + public void autoArchiveEligibleResults() { + int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults(); + if (archivedCount > 0) { + log.info("auto archived annotation results, count={}", archivedCount); + } + } +} diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java new file mode 100644 index 0000000..d1a568d --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java @@ -0,0 +1,140 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.dto.request.MergeReviewResultRequest; +import com.labelsys.backend.dto.response.MergeReviewResultResponse; +import com.labelsys.backend.entity.AnnotationResult; +import com.labelsys.backend.entity.AnnotationResultHistory; +import com.labelsys.backend.enums.QaContentStorageMode; +import com.labelsys.backend.enums.UserPosition; +import com.labelsys.backend.mapper.AnnotationResultHistoryMapper; +import com.labelsys.backend.mapper.AnnotationResultMapper; +import com.labelsys.backend.util.IdGenerator; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AnnotationResultArchiveService { + + private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW"; + + private final AnnotationResultMapper annotationResultMapper; + private final AnnotationResultHistoryMapper annotationResultHistoryMapper; + @Value("${labelsys.annotation.auto-archive-timeout:PT2H}") + private Duration autoArchiveTimeout; + + @Transactional + public MergeReviewResultResponse mergeReview(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) { + assertReviewer(currentUser); + AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); + if (result == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "运行态结果不存在"); + } + + LocalDateTime archivedAt = LocalDateTime.now(); + AnnotationResultHistory history = AnnotationResultHistory.builder() + .id(IdGenerator.nextId()) + .companyId(result.getCompanyId()) + .creatorId(result.getCreatorId()) + .creatorRole(result.getCreatorRole()) + .sourceResultId(result.getId()) + .taskId(result.getTaskId()) + .resourceId(result.getResourceId()) + .qaContentJson(request.qaContentJson()) + .qaContentStorageMode(resolveStorageMode(result)) + .qaContentFilePath(result.getQaContentFilePath()) + .archiveReason(MANUAL_ARCHIVE_REASON) + .archivedBy(currentUser.userId()) + .archivedAt(archivedAt) + .build(); + annotationResultHistoryMapper.insert(history); + + int updated = annotationResultMapper.markArchived( + result.getId(), + currentUser.companyId(), + currentUser.userId(), + request.reviewComment(), + archivedAt); + if (updated == 0) { + throw new BusinessException(ResultCode.CONFLICT, "结果已被其他操作处理"); + } + + log.info("merged review result, companyId={}, reviewerId={}, resultId={}, historyId={}", + currentUser.companyId(), currentUser.userId(), resultId, history.getId()); + return new MergeReviewResultResponse(resultId, history.getId(), MANUAL_ARCHIVE_REASON, archivedAt); + } + + @Transactional + public int autoArchiveEligibleResults() { + LocalDateTime cutoff = LocalDateTime.now().minus(autoArchiveTimeout); + List results = annotationResultMapper.selectList(new LambdaQueryWrapper() + .eq(AnnotationResult::getIsDeleted, false) + .eq(AnnotationResult::getRequiresManualReview, false) + .lt(AnnotationResult::getCreatedAt, cutoff)); + int archivedCount = 0; + for (AnnotationResult result : results) { + if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE", null) != null) { + archivedCount++; + } + } + return archivedCount; + } + + private void assertReviewer(LoginUser currentUser) { + if (currentUser.position() != UserPosition.REVIEWER && currentUser.position() != UserPosition.ADMIN) { + throw new BusinessException(ResultCode.FORBIDDEN, "当前用户没有审核权限"); + } + } + + private String resolveStorageMode(AnnotationResult result) { + if (QaContentStorageMode.isValid(result.getQaContentStorageMode())) { + return result.getQaContentStorageMode(); + } + return QaContentStorageMode.INLINE.name(); + } + + private MergeReviewResultResponse archiveRuntimeResult(AnnotationResult result, + Long reviewerId, + String archiveReason, + String reviewComment) { + LocalDateTime archivedAt = LocalDateTime.now(); + AnnotationResultHistory history = AnnotationResultHistory.builder() + .id(IdGenerator.nextId()) + .companyId(result.getCompanyId()) + .creatorId(result.getCreatorId()) + .creatorRole(result.getCreatorRole()) + .sourceResultId(result.getId()) + .taskId(result.getTaskId()) + .resourceId(result.getResourceId()) + .qaContentJson(result.getQaContentJson()) + .qaContentStorageMode(resolveStorageMode(result)) + .qaContentFilePath(result.getQaContentFilePath()) + .archiveReason(archiveReason) + .archivedBy(reviewerId) + .archivedAt(archivedAt) + .build(); + annotationResultHistoryMapper.insert(history); + + int updated = annotationResultMapper.markArchived( + result.getId(), + result.getCompanyId(), + reviewerId, + reviewComment, + archivedAt); + if (updated == 0) { + return null; + } + return new MergeReviewResultResponse(result.getId(), history.getId(), archiveReason, archivedAt); + } +} diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java new file mode 100644 index 0000000..195ef85 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java @@ -0,0 +1,99 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.AnnotationResultPageQuery; +import com.labelsys.backend.dto.response.AnnotationResultCompareResponse; +import com.labelsys.backend.dto.response.AnnotationResultResponse; +import com.labelsys.backend.entity.AnnotationResult; +import com.labelsys.backend.entity.SourceResource; +import com.labelsys.backend.enums.RuntimeResultStatus; +import com.labelsys.backend.mapper.AnnotationResultMapper; +import com.labelsys.backend.mapper.SourceResourceMapper; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AnnotationResultService { + + private final AnnotationResultMapper annotationResultMapper; + private final SourceResourceMapper sourceResourceMapper; + + public PageResult pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(AnnotationResult::getCompanyId, currentUser.companyId()) + .eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId()) + .eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId()) + .eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview, query.requiresManualReview()) + .orderByDesc(AnnotationResult::getCreatedAt); + List records = annotationResultMapper.selectList(wrapper).stream() + .map(this::toResponse) + .filter(response -> query.runtimeStatus() == null || query.runtimeStatus().equals(response.runtimeStatus())) + .sorted(Comparator.comparing(AnnotationResultResponse::createdAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) + .toList(); + return paginate(records, query.pageNo(), query.pageSize()); + } + + public AnnotationResultResponse getResult(LoginUser currentUser, Long resultId) { + AnnotationResult result = annotationResultMapper.selectById(resultId); + if (result == null || !currentUser.companyId().equals(result.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); + } + return toResponse(result); + } + + public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) { + AnnotationResult result = annotationResultMapper.selectById(resultId); + if (result == null || !currentUser.companyId().equals(result.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); + } + SourceResource resource = sourceResourceMapper.selectById(result.getResourceId()); + return new AnnotationResultCompareResponse( + result.getId(), + result.getTaskId(), + result.getResourceId(), + result.getQaContentJson(), + result.getDiffSummary(), + result.getQaContentStorageMode(), + result.getQaContentFilePath(), + resource == null ? null : resource.getFilePath()); + } + + 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 String deriveStatus(AnnotationResult result) { + if (Boolean.TRUE.equals(result.getIsDeleted())) { + return RuntimeResultStatus.ARCHIVED.name(); + } + if (Boolean.TRUE.equals(result.getRequiresManualReview())) { + return RuntimeResultStatus.MANUAL_REVIEW_PENDING.name(); + } + return RuntimeResultStatus.AUTO_ARCHIVE_PENDING.name(); + } + + private PageResult paginate(List records, Integer pageNo, Integer pageSize) { + int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo; + int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; + int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size()); + int toIndex = Math.min(fromIndex + actualPageSize, records.size()); + return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize); + } +} diff --git a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java new file mode 100644 index 0000000..9773109 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java @@ -0,0 +1,284 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.AnnotationTaskPageQuery; +import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest; +import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest; +import com.labelsys.backend.dto.response.AnnotationTaskResponse; +import com.labelsys.backend.dto.response.TaskModelConfigResponse; +import com.labelsys.backend.dto.response.TaskPromptConfigResponse; +import com.labelsys.backend.entity.AnnotationTask; +import com.labelsys.backend.entity.AnnotationTaskResource; +import com.labelsys.backend.entity.SourceResource; +import com.labelsys.backend.enums.SourceStatus; +import com.labelsys.backend.enums.TaskStatus; +import com.labelsys.backend.mapper.AnnotationTaskMapper; +import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; +import com.labelsys.backend.mapper.SourceResourceMapper; +import com.labelsys.backend.util.IdGenerator; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AnnotationTaskService { + + private final AnnotationTaskMapper annotationTaskMapper; + private final AnnotationTaskResourceMapper annotationTaskResourceMapper; + private final SourceResourceMapper sourceResourceMapper; + private final SysConfigService sysConfigService; + private final DataPermissionService dataPermissionService; + + @Transactional + public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) { + List resources = loadAndValidateResources(currentUser, request.resourceIds()); + SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel()); + SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel()); + SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt()); + SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt()); + + AnnotationTask task = AnnotationTask.builder() + .id(IdGenerator.nextId()) + .companyId(currentUser.companyId()) + .creatorId(currentUser.userId()) + .creatorRole(currentUser.role()) + .taskName(request.taskName()) + .industryType(defaultIndustryType(request.industryType())) + .taskType(defaultTaskType(request.taskType())) + .extractModelConfigId(extractModel.configId()) + .extractModelName(extractModel.modelName()) + .extractModelUrl(extractModel.modelUrl()) + .extractModelApiKey(extractModel.apiKey()) + .verifyModelConfigId(verifyModel.configId()) + .verifyModelName(verifyModel.modelName()) + .verifyModelUrl(verifyModel.modelUrl()) + .verifyModelApiKey(verifyModel.apiKey()) + .extractPromptConfigId(extractPrompt.configId()) + .extractPrompt(extractPrompt.promptText()) + .verifyPromptConfigId(verifyPrompt.configId()) + .verifyPrompt(verifyPrompt.promptText()) + .taskStatus(TaskStatus.PENDING.name()) + .isDeleted(false) + .build(); + annotationTaskMapper.insert(task); + saveTaskBindings(task.getId(), currentUser.companyId(), resources); + log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}", + currentUser.companyId(), currentUser.userId(), task.getId(), resources.size()); + return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt); + } + + @Transactional + public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) { + AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId()); + if (task == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); + } + assertTaskPermission(currentUser, task); + + List currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); + List targetResourceIds = normalizeIds(request.resourceIds()); + boolean resourcesChanged = !currentResourceIds.equals(targetResourceIds); + if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) { + throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源"); + } + + List resources = loadAndValidateResources(currentUser, request.resourceIds()); + SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel()); + SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel()); + SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt()); + SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt()); + + task.setIndustryType(defaultIndustryType(request.industryType())); + task.setTaskType(defaultTaskType(request.taskType())); + task.setExtractModelConfigId(extractModel.configId()); + task.setExtractModelName(extractModel.modelName()); + task.setExtractModelUrl(extractModel.modelUrl()); + task.setExtractModelApiKey(extractModel.apiKey()); + task.setVerifyModelConfigId(verifyModel.configId()); + task.setVerifyModelName(verifyModel.modelName()); + task.setVerifyModelUrl(verifyModel.modelUrl()); + task.setVerifyModelApiKey(verifyModel.apiKey()); + task.setExtractPromptConfigId(extractPrompt.configId()); + task.setExtractPrompt(extractPrompt.promptText()); + task.setVerifyPromptConfigId(verifyPrompt.configId()); + task.setVerifyPrompt(verifyPrompt.promptText()); + annotationTaskMapper.updateById(task); + + if (resourcesChanged) { + annotationTaskResourceMapper.deleteByTaskId(taskId); + saveTaskBindings(taskId, currentUser.companyId(), resources); + } + log.info("updated annotation task, companyId={}, userId={}, taskId={}, resourcesChanged={}", + currentUser.companyId(), currentUser.userId(), taskId, resourcesChanged); + return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt); + } + + public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) { + AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId()); + if (task == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); + } + assertTaskPermission(currentUser, task); + return buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId))); + } + + public PageResult pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(AnnotationTask::getCompanyId, currentUser.companyId()) + .eq(StringUtils.hasText(query.taskType()), AnnotationTask::getTaskType, query.taskType()) + .eq(StringUtils.hasText(query.taskStatus()), AnnotationTask::getTaskStatus, query.taskStatus()) + .eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted()) + .like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword()) + .orderByDesc(AnnotationTask::getCreatedAt); + List records = annotationTaskMapper.selectList(wrapper).stream() + .filter(task -> dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) + .filter(task -> query.resourceId() == null || annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()).contains(query.resourceId())) + .sorted(Comparator.comparing(AnnotationTask::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) + .map(task -> buildTaskResponse(task, normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())))) + .toList(); + return paginate(records, query.pageNo(), query.pageSize()); + } + + @Transactional + public void deleteTask(LoginUser currentUser, Long taskId) { + AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId()); + if (task == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); + } + assertTaskPermission(currentUser, task); + if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) { + throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除"); + } + task.setIsDeleted(true); + annotationTaskMapper.updateById(task); + log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}", + currentUser.companyId(), currentUser.userId(), taskId); + } + + private List loadAndValidateResources(LoginUser currentUser, List resourceIds) { + if (resourceIds == null || resourceIds.isEmpty()) { + throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空"); + } + List normalizedIds = normalizeIds(resourceIds); + List resources = sourceResourceMapper.selectByCompanyIdAndIds(currentUser.companyId(), normalizedIds); + if (resources.size() != normalizedIds.size()) { + throw new BusinessException(ResultCode.BAD_REQUEST, "存在无效资源"); + } + for (SourceResource resource : resources) { + if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) { + throw new BusinessException(ResultCode.FORBIDDEN, "无权访问资源"); + } + if (!SourceStatus.READY.name().equals(resource.getSourceStatus())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "仅允许选择已就绪资源"); + } + } + resources.sort(Comparator.comparing(SourceResource::getId)); + return resources; + } + + private void saveTaskBindings(Long taskId, Long companyId, List resources) { + for (SourceResource resource : resources) { + annotationTaskResourceMapper.insert(AnnotationTaskResource.builder() + .id(IdGenerator.nextId()) + .companyId(companyId) + .taskId(taskId) + .resourceId(resource.getId()) + .build()); + } + } + + private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, + List resourceIds, + SysConfigService.ResolvedModelConfig extractModel, + SysConfigService.ResolvedModelConfig verifyModel, + SysConfigService.ResolvedPromptConfig extractPrompt, + SysConfigService.ResolvedPromptConfig verifyPrompt) { + return new AnnotationTaskResponse( + task.getId(), + task.getTaskName(), + task.getIndustryType(), + task.getTaskType(), + task.getTaskStatus(), + resourceIds, + sysConfigService.toResponse(extractModel), + sysConfigService.toResponse(verifyModel), + sysConfigService.toResponse(extractPrompt), + sysConfigService.toResponse(verifyPrompt), + task.getCreatedAt(), + task.getUpdatedAt()); + } + + private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List resourceIds) { + return new AnnotationTaskResponse( + task.getId(), + task.getTaskName(), + task.getIndustryType(), + task.getTaskType(), + task.getTaskStatus(), + resourceIds, + new TaskModelConfigResponse(task.getExtractModelConfigId(), null, task.getExtractModelName(), + task.getExtractModelUrl(), maskSecret(task.getExtractModelApiKey())), + new TaskModelConfigResponse(task.getVerifyModelConfigId(), null, task.getVerifyModelName(), + task.getVerifyModelUrl(), maskSecret(task.getVerifyModelApiKey())), + new TaskPromptConfigResponse(task.getExtractPromptConfigId(), null, task.getExtractPrompt()), + new TaskPromptConfigResponse(task.getVerifyPromptConfigId(), null, task.getVerifyPrompt()), + task.getCreatedAt(), + task.getUpdatedAt()); + } + + private List resourceIds(List resources) { + return resources.stream().map(SourceResource::getId).sorted().toList(); + } + + private List normalizeIds(List resourceIds) { + Set uniqueIds = new HashSet<>(resourceIds); + List sortedIds = new ArrayList<>(uniqueIds); + sortedIds.sort(Long::compareTo); + return sortedIds; + } + + private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) { + if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) { + throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务"); + } + } + + private String defaultIndustryType(String industryType) { + return StringUtils.hasText(industryType) ? industryType : "transport"; + } + + private String defaultTaskType(String taskType) { + return StringUtils.hasText(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 PageResult paginate(List records, Integer pageNo, Integer pageSize) { + int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo; + int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; + int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size()); + int toIndex = Math.min(fromIndex + actualPageSize, records.size()); + return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize); + } +} diff --git a/src/main/java/com/labelsys/backend/service/DataPermissionService.java b/src/main/java/com/labelsys/backend/service/DataPermissionService.java index d674f8f..43c3c3a 100644 --- a/src/main/java/com/labelsys/backend/service/DataPermissionService.java +++ b/src/main/java/com/labelsys/backend/service/DataPermissionService.java @@ -1,15 +1,21 @@ package com.labelsys.backend.service; import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.entity.BizDataRecord; import com.labelsys.backend.enums.UserRole; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.jdbc.core.JdbcTemplate; @Service +@RequiredArgsConstructor public class DataPermissionService { + private final JdbcTemplate jdbcTemplate; + public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) { return switch (currentUser.role()) { case EMPLOYEE -> currentUser.userId().equals(creatorId); @@ -65,4 +71,25 @@ public class DataPermissionService { public boolean shouldFilterByUserId(LoginUser currentUser) { return currentUser.role() == UserRole.EMPLOYEE; } + + public List listVisibleRecords(LoginUser currentUser) { + List allRecords = jdbcTemplate.query(""" + select id, company_id, creator_id, creator_role, record_name, created_at, updated_at + from biz_data_record + where company_id = ? + order by id + """, + (rs, rowNum) -> BizDataRecord.builder() + .id(rs.getLong("id")) + .companyId(rs.getLong("company_id")) + .creatorId(rs.getLong("creator_id")) + .creatorRole(UserRole.valueOf(rs.getString("creator_role"))) + .recordName(rs.getString("record_name")) + .createdAt(rs.getTimestamp("created_at") == null ? null : rs.getTimestamp("created_at").toLocalDateTime()) + .updatedAt(rs.getTimestamp("updated_at") == null ? null : rs.getTimestamp("updated_at").toLocalDateTime()) + .build(), + currentUser.companyId()); + + return filterByRole(currentUser, allRecords, BizDataRecord::getCreatorRole, BizDataRecord::getCreatorId); + } } diff --git a/src/main/java/com/labelsys/backend/service/ObjectStorageService.java b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java new file mode 100644 index 0000000..b043112 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java @@ -0,0 +1,8 @@ +package com.labelsys.backend.service; + +public interface ObjectStorageService { + + String upload(String bucketName, String objectKey, byte[] content, String contentType); + + void delete(String bucketName, String objectKey); +} diff --git a/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java new file mode 100644 index 0000000..0dd9365 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java @@ -0,0 +1,45 @@ +package com.labelsys.backend.service; + +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +@Service +@RequiredArgsConstructor +public class RustfsObjectStorageService implements ObjectStorageService { + + private final S3Client s3Client; + + @Override + public String upload(String bucketName, String objectKey, byte[] content, String contentType) { + try { + PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder() + .bucket(bucketName) + .key(objectKey); + if (contentType != null && !contentType.isBlank()) { + requestBuilder.contentType(contentType); + } + s3Client.putObject(requestBuilder.build(), RequestBody.fromBytes(content)); + return objectKey; + } catch (Exception ex) { + throw new BusinessException(ResultCode.ERROR, "对象存储上传失败"); + } + } + + @Override + public void delete(String bucketName, String objectKey) { + try { + s3Client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build()); + } catch (Exception ex) { + throw new BusinessException(ResultCode.ERROR, "对象存储删除失败"); + } + } +} diff --git a/src/main/java/com/labelsys/backend/service/SourceResourceService.java b/src/main/java/com/labelsys/backend/service/SourceResourceService.java new file mode 100644 index 0000000..cfae1d8 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/SourceResourceService.java @@ -0,0 +1,179 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.SourceResourcePageQuery; +import com.labelsys.backend.dto.request.SourceUploadRequest; +import com.labelsys.backend.dto.response.SourceResourceResponse; +import com.labelsys.backend.dto.response.SourceUploadResponse; +import com.labelsys.backend.entity.SourceResource; +import com.labelsys.backend.entity.SysUser; +import com.labelsys.backend.enums.ResourceType; +import com.labelsys.backend.enums.SourceStatus; +import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; +import com.labelsys.backend.mapper.SourceResourceMapper; +import com.labelsys.backend.mapper.SysUserMapper; +import com.labelsys.backend.util.IdGenerator; +import com.labelsys.backend.util.ObjectStoragePathBuilder; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SourceResourceService { + + private final SourceResourceMapper sourceResourceMapper; + private final AnnotationTaskResourceMapper annotationTaskResourceMapper; + private final SysUserMapper sysUserMapper; + private final DataPermissionService dataPermissionService; + private final ObjectStorageService objectStorageService; + private final com.labelsys.backend.config.ObjectStorageProperties objectStorageProperties; + + @Transactional + public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) { + MultipartFile file = request.getFile(); + if (file == null || file.isEmpty()) { + throw new BusinessException(ResultCode.BAD_REQUEST, "上传文件不能为空"); + } + if (!ResourceType.isValid(request.getResourceType())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法"); + } + long resourceId = IdGenerator.nextId(); + String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType()); + String objectKey = ObjectStoragePathBuilder.sourceObjectKey( + currentUser.companyId(), request.getResourceType(), resourceId, extension); + try { + objectStorageService.upload( + objectStorageProperties.getSourceBucket(), + objectKey, + file.getBytes(), + file.getContentType()); + } catch (IOException ex) { + throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败"); + } + + SourceResource resource = SourceResource.builder() + .id(resourceId) + .companyId(currentUser.companyId()) + .creatorId(currentUser.userId()) + .creatorRole(currentUser.role()) + .resourceName(StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename()) + .resourceType(request.getResourceType()) + .bucketName(objectStorageProperties.getSourceBucket()) + .filePath(objectKey) + .fileSize(file.getSize()) + .sourceStatus(SourceStatus.READY.name()) + .storageProvider("rustfs") + .remark(request.getRemark()) + .build(); + sourceResourceMapper.insert(resource); + log.info("uploaded source resource, companyId={}, userId={}, resourceId={}", + currentUser.companyId(), currentUser.userId(), resourceId); + return new SourceUploadResponse( + resource.getId(), + resource.getResourceName(), + resource.getResourceType(), + resource.getBucketName(), + resource.getFilePath(), + resource.getFileSize(), + resource.getSourceStatus(), + resource.getCreatedAt()); + } + + public PageResult pageResources(LoginUser currentUser, SourceResourcePageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SourceResource::getCompanyId, currentUser.companyId()) + .eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType()) + .eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus()) + .like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword()) + .orderByDesc(SourceResource::getCreatedAt); + List records = sourceResourceMapper.selectList(wrapper).stream() + .filter(resource -> dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) + .sorted(Comparator.comparing(SourceResource::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) + .map(this::toResponse) + .toList(); + return paginate(records, query.pageNo(), query.pageSize()); + } + + public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) { + throw new BusinessException(ResultCode.FORBIDDEN, "无权访问资源"); + } + return toResponse(resource); + } + + @Transactional + public void deleteResource(LoginUser currentUser, Long resourceId) { + SourceResource resource = sourceResourceMapper.selectById(resourceId); + if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); + } + if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), resource.getCreatorRole())) { + throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源"); + } + int bindCount = annotationTaskResourceMapper.countByResourceId(resourceId); + if (bindCount > 0) { + resource.setSourceStatus(SourceStatus.ARCHIVED.name()); + sourceResourceMapper.updateById(resource); + log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}", + currentUser.companyId(), currentUser.userId(), resourceId); + return; + } + objectStorageService.delete(resource.getBucketName(), resource.getFilePath()); + sourceResourceMapper.deleteById(resourceId); + log.info("deleted source resource, companyId={}, userId={}, resourceId={}", + currentUser.companyId(), currentUser.userId(), resourceId); + } + + private SourceResourceResponse toResponse(SourceResource resource) { + SysUser creator = sysUserMapper.selectById(resource.getCreatorId()); + return new SourceResourceResponse( + resource.getId(), + resource.getResourceName(), + resource.getResourceType(), + resource.getBucketName(), + resource.getFilePath(), + resource.getFileSize(), + resource.getSourceStatus(), + resource.getStorageProvider(), + resource.getRemark(), + creator == null ? null : creator.getRealName(), + resource.getCreatedAt(), + resource.getUpdatedAt()); + } + + private String resolveExtension(String originalFilename, String resourceType) { + if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) { + return originalFilename.substring(originalFilename.lastIndexOf('.') + 1); + } + return switch (resourceType) { + case "TEXT" -> "txt"; + case "IMAGE" -> "png"; + case "VIDEO" -> "mp4"; + default -> "bin"; + }; + } + + private PageResult paginate(List records, Integer pageNo, Integer pageSize) { + int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo; + int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; + int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size()); + int toIndex = Math.min(fromIndex + actualPageSize, records.size()); + return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize); + } +} diff --git a/src/main/java/com/labelsys/backend/service/SysConfigService.java b/src/main/java/com/labelsys/backend/service/SysConfigService.java new file mode 100644 index 0000000..0979969 --- /dev/null +++ b/src/main/java/com/labelsys/backend/service/SysConfigService.java @@ -0,0 +1,258 @@ +package com.labelsys.backend.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.labelsys.backend.common.ResultCode; +import com.labelsys.backend.common.exception.BusinessException; +import com.labelsys.backend.context.LoginUser; +import com.labelsys.backend.dto.common.PageResult; +import com.labelsys.backend.dto.request.ManualModelConfigRequest; +import com.labelsys.backend.dto.request.PromptConfigOptionRequest; +import com.labelsys.backend.dto.request.SaveSysConfigRequest; +import com.labelsys.backend.dto.request.SysConfigPageQuery; +import com.labelsys.backend.dto.request.TaskModelConfigRequest; +import com.labelsys.backend.dto.response.SysConfigResponse; +import com.labelsys.backend.dto.response.TaskModelConfigResponse; +import com.labelsys.backend.dto.response.TaskPromptConfigResponse; +import com.labelsys.backend.entity.SysConfig; +import com.labelsys.backend.enums.ConfigType; +import com.labelsys.backend.mapper.SysConfigMapper; +import com.labelsys.backend.util.IdGenerator; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SysConfigService { + + private final SysConfigMapper sysConfigMapper; + private final ObjectMapper objectMapper; + + @Transactional + public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) { + validateConfigType(request.configType()); + SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.configName()); + if (existing != null) { + throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); + } + SysConfig config = SysConfig.builder() + .id(IdGenerator.nextId()) + .companyId(currentUser.companyId()) + .configType(request.configType()) + .configName(request.configName()) + .configValue(request.configValue()) + .status(request.status()) + .creatorId(currentUser.userId()) + .build(); + sysConfigMapper.insert(config); + log.info("saved sys config, companyId={}, userId={}, configName={}, configType={}", + currentUser.companyId(), currentUser.userId(), request.configName(), request.configType()); + return config; + } + + @Transactional + public SysConfig updateConfig(LoginUser currentUser, Long configId, SaveSysConfigRequest request) { + validateConfigType(request.configType()); + SysConfig existing = getConfigEntity(currentUser, configId); + SysConfig duplicate = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.configName()); + if (duplicate != null && !duplicate.getId().equals(configId)) { + throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); + } + existing.setConfigType(request.configType()); + existing.setConfigName(request.configName()); + existing.setConfigValue(request.configValue()); + existing.setStatus(request.status()); + sysConfigMapper.updateById(existing); + log.info("updated sys config, companyId={}, userId={}, configId={}", + currentUser.companyId(), currentUser.userId(), configId); + return existing; + } + + public SysConfigResponse getConfig(LoginUser currentUser, Long configId) { + return toResponse(getConfigEntity(currentUser, configId)); + } + + public PageResult pageConfigs(LoginUser currentUser, SysConfigPageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SysConfig::getCompanyId, currentUser.companyId()) + .eq(StringUtils.hasText(query.configType()), SysConfig::getConfigType, query.configType()) + .eq(StringUtils.hasText(query.status()), SysConfig::getStatus, query.status()) + .like(StringUtils.hasText(query.configName()), SysConfig::getConfigName, query.configName()) + .orderByDesc(SysConfig::getCreatedAt); + List records = sysConfigMapper.selectList(wrapper).stream() + .sorted(Comparator.comparing(SysConfig::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())).reversed()) + .map(this::toResponse) + .toList(); + return paginate(records, query.pageNo(), query.pageSize()); + } + + @Transactional + public ResolvedModelConfig resolveModelConfig(LoginUser currentUser, TaskModelConfigRequest request) { + if (request == null || !StringUtils.hasText(request.mode())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不能为空"); + } + if ("SELECT".equalsIgnoreCase(request.mode())) { + return resolveSelectedModel(currentUser, request.selectedConfigName()); + } + if ("MANUAL".equalsIgnoreCase(request.mode())) { + return resolveManualModel(currentUser, request.manualConfig()); + } + throw new BusinessException(ResultCode.BAD_REQUEST, "不支持的模型配置模式"); + } + + public ResolvedPromptConfig resolvePromptConfig(LoginUser currentUser, PromptConfigOptionRequest request) { + if (request == null) { + throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不能为空"); + } + if (StringUtils.hasText(request.selectedConfigName())) { + SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType( + currentUser.companyId(), request.selectedConfigName(), ConfigType.PROMPT.name()); + if (config == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "提示词配置不存在"); + } + return new ResolvedPromptConfig(config.getId(), config.getConfigName(), config.getConfigValue()); + } + if (!StringUtils.hasText(request.promptText())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "提示词内容不能为空"); + } + return new ResolvedPromptConfig(null, null, request.promptText()); + } + + public TaskModelConfigResponse toResponse(ResolvedModelConfig config) { + return new TaskModelConfigResponse( + config.configId(), + config.configName(), + config.modelName(), + config.modelUrl(), + maskSecret(config.apiKey())); + } + + public TaskPromptConfigResponse toResponse(ResolvedPromptConfig config) { + return new TaskPromptConfigResponse(config.configId(), config.configName(), config.promptText()); + } + + public SysConfigResponse toResponse(SysConfig config) { + return new SysConfigResponse( + config.getId(), + config.getConfigType(), + config.getConfigName(), + config.getConfigValue(), + config.getStatus(), + config.getCreatorId(), + config.getCreatedAt(), + config.getUpdatedAt()); + } + + private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) { + if (!StringUtils.hasText(configName)) { + throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空"); + } + SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType( + currentUser.companyId(), configName, ConfigType.MODEL.name()); + if (config == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "模型配置不存在"); + } + ModelConfigValue configValue = parseModelConfig(config.getConfigValue()); + return new ResolvedModelConfig(config.getId(), config.getConfigName(), + configValue.modelName(), configValue.modelUrl(), configValue.apiKey()); + } + + private ResolvedModelConfig resolveManualModel(LoginUser currentUser, ManualModelConfigRequest request) { + if (request == null || !StringUtils.hasText(request.modelName()) + || !StringUtils.hasText(request.modelUrl()) || !StringUtils.hasText(request.apiKey())) { + throw new BusinessException(ResultCode.BAD_REQUEST, "手动模型配置不完整"); + } + SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.modelName()); + if (existing == null) { + String configValue = writeModelConfig(request); + SysConfig config = SysConfig.builder() + .id(IdGenerator.nextId()) + .companyId(currentUser.companyId()) + .configType(ConfigType.MODEL.name()) + .configName(request.modelName()) + .configValue(configValue) + .status("ENABLED") + .creatorId(currentUser.userId()) + .build(); + sysConfigMapper.insert(config); + log.info("auto created model config, companyId={}, userId={}, configName={}", + currentUser.companyId(), currentUser.userId(), request.modelName()); + return new ResolvedModelConfig(config.getId(), config.getConfigName(), + request.modelName(), request.modelUrl(), request.apiKey()); + } + if (!ConfigType.MODEL.name().equals(existing.getConfigType())) { + throw new BusinessException(ResultCode.CONFLICT, "同名配置已被其他类型占用"); + } + ModelConfigValue configValue = parseModelConfig(existing.getConfigValue()); + return new ResolvedModelConfig(existing.getId(), existing.getConfigName(), + configValue.modelName(), configValue.modelUrl(), configValue.apiKey()); + } + + private SysConfig getConfigEntity(LoginUser currentUser, Long configId) { + SysConfig config = sysConfigMapper.selectById(configId); + if (config == null || !currentUser.companyId().equals(config.getCompanyId())) { + throw new BusinessException(ResultCode.NOT_FOUND, "配置不存在"); + } + return config; + } + + private void validateConfigType(String configType) { + if (!ConfigType.isValid(configType)) { + throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法"); + } + } + + private ModelConfigValue parseModelConfig(String value) { + try { + return objectMapper.readValue(value, ModelConfigValue.class); + } catch (JsonProcessingException ex) { + throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值格式非法"); + } + } + + private String writeModelConfig(ManualModelConfigRequest request) { + try { + return objectMapper.writeValueAsString(Map.of( + "modelName", request.modelName(), + "modelUrl", request.modelUrl(), + "apiKey", request.apiKey())); + } catch (JsonProcessingException ex) { + throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值生成失败"); + } + } + + private String maskSecret(String secret) { + if (!StringUtils.hasText(secret)) { + return null; + } + if (secret.length() <= 4) { + return "****"; + } + return "****" + secret.substring(secret.length() - 4); + } + + private PageResult paginate(List records, Integer pageNo, Integer pageSize) { + int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo; + int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; + int fromIndex = Math.min((actualPageNo - 1) * actualPageSize, records.size()); + int toIndex = Math.min(fromIndex + actualPageSize, records.size()); + return new PageResult<>(records.subList(fromIndex, toIndex), (long) records.size(), actualPageNo, actualPageSize); + } + + private record ModelConfigValue(String modelName, String modelUrl, String apiKey) { + } + + public record ResolvedModelConfig(Long configId, String configName, String modelName, String modelUrl, String apiKey) { + } + + public record ResolvedPromptConfig(Long configId, String configName, String promptText) { + } +} diff --git a/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java new file mode 100644 index 0000000..30574a1 --- /dev/null +++ b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java @@ -0,0 +1,38 @@ +package com.labelsys.backend.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class ObjectStoragePathBuilder { + + private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM"); + + private ObjectStoragePathBuilder() { + } + + public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) { + String category = resourceType.toLowerCase(); + return "source/%d/%s/%s/%d/original.%s".formatted( + companyId, + category, + YEAR_MONTH.format(LocalDateTime.now()), + resourceId, + extension.toLowerCase()); + } + + public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) { + return "result/%d/%s/%d/%d/qa.json".formatted( + companyId, + YEAR_MONTH.format(LocalDateTime.now()), + taskId, + resultId); + } + + public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) { + return "result/%d/%s/%d/%d/diff.json".formatted( + companyId, + YEAR_MONTH.format(LocalDateTime.now()), + taskId, + resultId); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a175856..7e911ec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,6 +34,17 @@ labelsys: session: ttl: PT2H store-type: redis + annotation: + auto-archive-timeout: PT2H + object-storage: + endpoint: ${OBJECT_STORAGE_ENDPOINT:http://127.0.0.1:9000} + region: ${OBJECT_STORAGE_REGION:us-east-1} + access-key: ${OBJECT_STORAGE_ACCESS_KEY:demo-access-key} + secret-key: ${OBJECT_STORAGE_SECRET_KEY:demo-secret-key} + path-style-access: ${OBJECT_STORAGE_PATH_STYLE:true} + source-bucket: ${OBJECT_STORAGE_SOURCE_BUCKET:source-data} + artifact-bucket: ${OBJECT_STORAGE_ARTIFACT_BUCKET:annotation-artifacts} + export-bucket: ${OBJECT_STORAGE_EXPORT_BUCKET:finetune-export} logging: level: diff --git a/src/main/resources/mapper/AnnotationResultMapper.xml b/src/main/resources/mapper/AnnotationResultMapper.xml new file mode 100644 index 0000000..1b1ebd8 --- /dev/null +++ b/src/main/resources/mapper/AnnotationResultMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, company_id, creator_id, creator_role, task_id, resource_id, qa_content_json, + qa_content_storage_mode, qa_content_file_path, diff_summary, requires_manual_review, + is_deleted, reviewer_id, review_comment, reviewed_at, created_at, updated_at + + + + + + update annotation_result + set is_deleted = true, + reviewer_id = #{reviewerId}, + review_comment = #{reviewComment}, + reviewed_at = #{reviewedAt}, + updated_at = #{reviewedAt} + where id = #{id} + and company_id = #{companyId} + and is_deleted = false + + diff --git a/src/main/resources/mapper/AnnotationTaskMapper.xml b/src/main/resources/mapper/AnnotationTaskMapper.xml new file mode 100644 index 0000000..68f6e69 --- /dev/null +++ b/src/main/resources/mapper/AnnotationTaskMapper.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + diff --git a/src/main/resources/mapper/AnnotationTaskResourceMapper.xml b/src/main/resources/mapper/AnnotationTaskResourceMapper.xml new file mode 100644 index 0000000..407be46 --- /dev/null +++ b/src/main/resources/mapper/AnnotationTaskResourceMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + delete from annotation_task_resource where task_id = #{taskId} + + + + diff --git a/src/main/resources/mapper/SourceResourceMapper.xml b/src/main/resources/mapper/SourceResourceMapper.xml new file mode 100644 index 0000000..f725ee9 --- /dev/null +++ b/src/main/resources/mapper/SourceResourceMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + id, company_id, creator_id, creator_role, resource_name, resource_type, bucket_name, file_path, + file_size, source_status, storage_provider, remark, created_at, updated_at + + + + diff --git a/src/main/resources/mapper/SysConfigMapper.xml b/src/main/resources/mapper/SysConfigMapper.xml new file mode 100644 index 0000000..4040d56 --- /dev/null +++ b/src/main/resources/mapper/SysConfigMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + id, company_id, config_type, config_name, config_value, status, creator_id, created_at, updated_at + + + + + +