Compare commits
12 Commits
b65b1c6ee0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0b00ed08 | ||
|
|
ccbcfd2c74 | ||
|
|
4708aa0f28 | ||
|
|
5a24ebd49b | ||
|
|
3ce2deb0a6 | ||
|
|
13945b239e | ||
|
|
eb22998b28 | ||
|
|
f6ba09521a | ||
|
|
73a13fd16d | ||
|
|
00032dd491 | ||
| c65fdbab5b | |||
| 9fd8971732 |
@@ -2,7 +2,7 @@ FROM registry.bjzgzp.com:4433/library/eclipse-temurin:21-jdk-ubi10-minimal
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ./target/label-backend-1.0.0-SNAPSHOT.jar /app/label-backend-1.0.0-SNAPSHOT.jar
|
COPY ./label-backend-1.0.0-SNAPSHOT.jar /app/label-backend-1.0.0-SNAPSHOT.jar
|
||||||
|
|
||||||
EXPOSE 18082
|
EXPOSE 18082
|
||||||
|
|
||||||
|
|||||||
@@ -392,4 +392,10 @@ docker build -t label-backend:latest .
|
|||||||
- Service 统一放在 `service/`,不拆 `service/impl`
|
- Service 统一放在 `service/`,不拆 `service/impl`
|
||||||
- 业务规则优先放在 Service,Controller 只负责 HTTP 协议层
|
- 业务规则优先放在 Service,Controller 只负责 HTTP 协议层
|
||||||
- 新增接口需同步补齐 Swagger 注解与测试
|
- 新增接口需同步补齐 Swagger 注解与测试
|
||||||
|
- 所有对外接口参数必须在 Swagger 中明确体现名称、类型和含义
|
||||||
|
- 固定结构请求体禁止继续使用匿名 `Map<String, Object>` 或 `Map<String, String>`,必须定义 DTO 并补齐 `@Schema` 字段说明
|
||||||
|
- 固定结构响应应优先使用明确 DTO,或至少为 Swagger 暴露对象补齐字段级 `@Schema` 注解
|
||||||
|
- 路径参数、查询参数、请求体、分页包装和通用返回体都必须维护可读的 OpenAPI 文档说明
|
||||||
|
- 需要保持历史兼容的原始 JSON 字符串请求体可以继续使用 `String`,但必须在 Swagger `@RequestBody` 中说明完整 JSON body 的提交方式和兼容原因
|
||||||
|
- 修改 Controller 参数、请求 DTO、响应 DTO 或对外实体后,必须运行 `mvn -Dtest=OpenApiAnnotationTest test`,确保 Swagger 参数名称、类型和含义没有回退
|
||||||
- 目录、配置、打包方式变化后,README、设计文档和部署说明必须同步更新
|
- 目录、配置、打包方式变化后,README、设计文档和部署说明必须同步更新
|
||||||
|
|||||||
24
pom.xml
24
pom.xml
@@ -3,12 +3,10 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>com.label</groupId>
|
<groupId>com.label</groupId>
|
||||||
<artifactId>label-backend</artifactId>
|
<artifactId>label-backend</artifactId>
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<spring.boot.version>3.1.5</spring.boot.version>
|
<spring.boot.version>3.1.5</spring.boot.version>
|
||||||
@@ -20,15 +18,13 @@
|
|||||||
</properties>
|
</properties>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
<dependency>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
<version>${spring.boot.version}</version>
|
||||||
<version>${spring.boot.version}</version>
|
<type>pom</type>
|
||||||
<type>pom</type>
|
<scope>import</scope>
|
||||||
<scope>import</scope>
|
</dependency>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- AWS SDK v2 BOM -->
|
<!-- AWS SDK v2 BOM -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>software.amazon.awssdk</groupId>
|
<groupId>software.amazon.awssdk</groupId>
|
||||||
@@ -52,6 +48,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Spring Boot Actuator (health check endpoint) -->
|
<!-- Spring Boot Actuator (health check endpoint) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.label.common.ai;
|
package com.label.common.ai;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -36,87 +37,190 @@ public class AiServiceClient {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public static class ExtractionRequest {
|
public static class TextExtractRequest {
|
||||||
private Long sourceId;
|
@JsonProperty("file_path")
|
||||||
private String filePath;
|
private String filePath;
|
||||||
private String bucket;
|
|
||||||
|
@JsonProperty("file_name")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
private String model;
|
private String model;
|
||||||
private String prompt;
|
|
||||||
|
@JsonProperty("prompt_template")
|
||||||
|
private String promptTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class ImageExtractRequest {
|
||||||
|
@JsonProperty("file_path")
|
||||||
|
private String filePath;
|
||||||
|
|
||||||
|
@JsonProperty("task_id")
|
||||||
|
private Long taskId;
|
||||||
|
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@JsonProperty("prompt_template")
|
||||||
|
private String promptTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class ExtractionResponse {
|
public static class ExtractionResponse {
|
||||||
private List<Map<String, Object>> items; // triple/quadruple items
|
private List<Map<String, Object>> items; // triple/quadruple items
|
||||||
private String rawOutput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public static class VideoProcessRequest {
|
public static class ExtractFramesRequest {
|
||||||
private Long sourceId;
|
@JsonProperty("file_path")
|
||||||
private String filePath;
|
private String filePath;
|
||||||
private String bucket;
|
|
||||||
private Map<String, Object> params; // frameInterval, mode etc.
|
@JsonProperty("source_id")
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
@JsonProperty("job_id")
|
||||||
|
private Long jobId;
|
||||||
|
|
||||||
|
private String mode;
|
||||||
|
|
||||||
|
@JsonProperty("frame_interval")
|
||||||
|
private Integer frameInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class VideoToTextRequest {
|
||||||
|
@JsonProperty("file_path")
|
||||||
|
private String filePath;
|
||||||
|
|
||||||
|
@JsonProperty("source_id")
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
@JsonProperty("job_id")
|
||||||
|
private Long jobId;
|
||||||
|
|
||||||
|
@JsonProperty("start_sec")
|
||||||
|
private Double startSec;
|
||||||
|
|
||||||
|
@JsonProperty("end_sec")
|
||||||
|
private Double endSec;
|
||||||
|
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@JsonProperty("prompt_template")
|
||||||
|
private String promptTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TextQaItem {
|
||||||
|
private String subject;
|
||||||
|
private String predicate;
|
||||||
|
private String object;
|
||||||
|
|
||||||
|
@JsonProperty("source_snippet")
|
||||||
|
private String sourceSnippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class GenTextQaRequest {
|
||||||
|
private List<TextQaItem> items;
|
||||||
|
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@JsonProperty("prompt_template")
|
||||||
|
private String promptTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ImageQaItem {
|
||||||
|
private String subject;
|
||||||
|
private String predicate;
|
||||||
|
private String object;
|
||||||
|
private String qualifier;
|
||||||
|
|
||||||
|
@JsonProperty("cropped_image_path")
|
||||||
|
private String croppedImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class GenImageQaRequest {
|
||||||
|
private List<ImageQaItem> items;
|
||||||
|
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@JsonProperty("prompt_template")
|
||||||
|
private String promptTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class QaGenResponse {
|
public static class QaGenResponse {
|
||||||
private List<Map<String, Object>> qaPairs;
|
private List<Map<String, Object>> pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public static class FinetuneRequest {
|
public static class FinetuneStartRequest {
|
||||||
private String datasetPath; // RustFS path to JSONL file
|
@JsonProperty("jsonl_url")
|
||||||
private String model;
|
private String jsonlUrl;
|
||||||
private Long batchId;
|
|
||||||
|
@JsonProperty("base_model")
|
||||||
|
private String baseModel;
|
||||||
|
|
||||||
|
private Map<String, Object> hyperparams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class FinetuneResponse {
|
public static class FinetuneStartResponse {
|
||||||
|
@JsonProperty("job_id")
|
||||||
private String jobId;
|
private String jobId;
|
||||||
private String status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class FinetuneStatusResponse {
|
public static class FinetuneStatusResponse {
|
||||||
|
@JsonProperty("job_id")
|
||||||
private String jobId;
|
private String jobId;
|
||||||
|
|
||||||
private String status; // PENDING/RUNNING/COMPLETED/FAILED
|
private String status; // PENDING/RUNNING/COMPLETED/FAILED
|
||||||
private Integer progress; // 0-100
|
private Integer progress; // 0-100
|
||||||
|
|
||||||
|
@JsonProperty("error_message")
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 8 endpoints:
|
// The 8 endpoints:
|
||||||
|
|
||||||
public ExtractionResponse extractText(ExtractionRequest request) {
|
public ExtractionResponse extractText(TextExtractRequest request) {
|
||||||
return restTemplate.postForObject("/extract/text", request, ExtractionResponse.class);
|
return restTemplate.postForObject("/api/v1/text/extract", request, ExtractionResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtractionResponse extractImage(ExtractionRequest request) {
|
public ExtractionResponse extractImage(ImageExtractRequest request) {
|
||||||
return restTemplate.postForObject("/extract/image", request, ExtractionResponse.class);
|
return restTemplate.postForObject("/api/v1/image/extract", request, ExtractionResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void extractFrames(VideoProcessRequest request) {
|
public void extractFrames(ExtractFramesRequest request) {
|
||||||
restTemplate.postForLocation("/video/extract-frames", request);
|
restTemplate.postForLocation("/api/v1/video/extract-frames", request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void videoToText(VideoProcessRequest request) {
|
public void videoToText(VideoToTextRequest request) {
|
||||||
restTemplate.postForLocation("/video/to-text", request);
|
restTemplate.postForLocation("/api/v1/video/to-text", request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QaGenResponse genTextQa(ExtractionRequest request) {
|
public QaGenResponse genTextQa(GenTextQaRequest request) {
|
||||||
return restTemplate.postForObject("/qa/gen-text", request, QaGenResponse.class);
|
return restTemplate.postForObject("/api/v1/qa/gen-text", request, QaGenResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QaGenResponse genImageQa(ExtractionRequest request) {
|
public QaGenResponse genImageQa(GenImageQaRequest request) {
|
||||||
return restTemplate.postForObject("/qa/gen-image", request, QaGenResponse.class);
|
return restTemplate.postForObject("/api/v1/qa/gen-image", request, QaGenResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FinetuneResponse startFinetune(FinetuneRequest request) {
|
public FinetuneStartResponse startFinetune(FinetuneStartRequest request) {
|
||||||
return restTemplate.postForObject("/finetune/start", request, FinetuneResponse.class);
|
return restTemplate.postForObject("/api/v1/finetune/start", request, FinetuneStartResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FinetuneStatusResponse getFinetuneStatus(String jobId) {
|
public FinetuneStatusResponse getFinetuneStatus(String jobId) {
|
||||||
return restTemplate.getForObject("/finetune/status/{jobId}", FinetuneStatusResponse.class, jobId);
|
return restTemplate.getForObject("/api/v1/finetune/status/{jobId}", FinetuneStatusResponse.class, jobId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
package com.label.common.result;
|
package com.label.common.result;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "分页响应")
|
||||||
public class PageResult<T> {
|
public class PageResult<T> {
|
||||||
|
@Schema(description = "当前页数据列表")
|
||||||
private List<T> items;
|
private List<T> items;
|
||||||
|
|
||||||
|
@Schema(description = "总条数", example = "123")
|
||||||
private long total;
|
private long total;
|
||||||
|
|
||||||
|
@Schema(description = "页码(从 1 开始)", example = "1")
|
||||||
private int page;
|
private int page;
|
||||||
|
|
||||||
|
@Schema(description = "每页条数", example = "20")
|
||||||
private int pageSize;
|
private int pageSize;
|
||||||
|
|
||||||
public static <T> PageResult<T> of(List<T> items, long total, int page, int pageSize) {
|
public static <T> PageResult<T> of(List<T> items, long total, int page, int pageSize) {
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
package com.label.common.result;
|
package com.label.common.result;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "通用响应包装")
|
||||||
public class Result<T> {
|
public class Result<T> {
|
||||||
|
@Schema(description = "业务状态码", example = "SUCCESS")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "响应数据")
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
|
@Schema(description = "提示信息", example = "操作成功")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
public static <T> Result<T> success(T data) {
|
public static <T> Result<T> success(T data) {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.label.common.statemachine;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public enum DatasetStatus {
|
|
||||||
PENDING_REVIEW, APPROVED, REJECTED;
|
|
||||||
|
|
||||||
public static final Map<DatasetStatus, Set<DatasetStatus>> TRANSITIONS = Map.of(
|
|
||||||
PENDING_REVIEW, Set.of(APPROVED, REJECTED),
|
|
||||||
REJECTED, Set.of(PENDING_REVIEW) // 重新提交审核
|
|
||||||
// APPROVED: terminal state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.label.common.statemachine;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public enum VideoJobStatus {
|
|
||||||
PENDING, RUNNING, SUCCESS, FAILED, RETRYING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatic state machine transitions.
|
|
||||||
* Note: FAILED → PENDING is a manual ADMIN operation, handled separately in VideoProcessService.reset().
|
|
||||||
*/
|
|
||||||
public static final Map<VideoJobStatus, Set<VideoJobStatus>> TRANSITIONS = Map.of(
|
|
||||||
PENDING, Set.of(RUNNING),
|
|
||||||
RUNNING, Set.of(SUCCESS, FAILED, RETRYING),
|
|
||||||
RETRYING, Set.of(RUNNING, FAILED)
|
|
||||||
// SUCCESS: terminal state
|
|
||||||
// FAILED → PENDING: manual ADMIN reset, NOT in this automatic transitions map
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,10 @@ package com.label.common.statemachine;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public enum SourceStatus {
|
public enum VideoSourceStatus {
|
||||||
PENDING, PREPROCESSING, EXTRACTING, QA_REVIEW, APPROVED;
|
PENDING, PREPROCESSING, EXTRACTING, QA_REVIEW, APPROVED;
|
||||||
|
|
||||||
public static final Map<SourceStatus, Set<SourceStatus>> TRANSITIONS = Map.of(
|
public static final Map<VideoSourceStatus, Set<VideoSourceStatus>> TRANSITIONS = Map.of(
|
||||||
PENDING, Set.of(EXTRACTING, PREPROCESSING),
|
PENDING, Set.of(EXTRACTING, PREPROCESSING),
|
||||||
PREPROCESSING, Set.of(PENDING),
|
PREPROCESSING, Set.of(PENDING),
|
||||||
EXTRACTING, Set.of(QA_REVIEW),
|
EXTRACTING, Set.of(QA_REVIEW),
|
||||||
26
src/main/java/com/label/config/AsyncConfig.java
Normal file
26
src/main/java/com/label/config/AsyncConfig.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.label.config;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean("aiTaskExecutor")
|
||||||
|
public Executor aiTaskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(5);
|
||||||
|
executor.setMaxPoolSize(10);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("ai-annotate-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "认证管理", description = "登录、退出和当前用户信息")
|
@Tag(name = "认证管理", description = "登录、退出和当前用户信息")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/label/api/auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
@@ -34,7 +34,11 @@ public class AuthController {
|
|||||||
*/
|
*/
|
||||||
@Operation(summary = "用户登录,返回 Bearer Token")
|
@Operation(summary = "用户登录,返回 Bearer Token")
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public Result<LoginResponse> login(@RequestBody LoginRequest request) {
|
public Result<LoginResponse> login(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "用户登录请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody LoginRequest request) {
|
||||||
return Result.success(authService.login(request));
|
return Result.success(authService.login(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ package com.label.controller;
|
|||||||
import com.label.annotation.RequireRole;
|
import com.label.annotation.RequireRole;
|
||||||
import com.label.common.result.PageResult;
|
import com.label.common.result.PageResult;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.CompanyCreateRequest;
|
||||||
|
import com.label.dto.CompanyStatusUpdateRequest;
|
||||||
|
import com.label.dto.CompanyUpdateRequest;
|
||||||
import com.label.entity.SysCompany;
|
import com.label.entity.SysCompany;
|
||||||
import com.label.service.CompanyService;
|
import com.label.service.CompanyService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -20,11 +24,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Tag(name = "公司管理", description = "租户公司增删改查")
|
@Tag(name = "公司管理", description = "租户公司增删改查")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/companies")
|
@RequestMapping("/label/api/companies")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CompanyController {
|
public class CompanyController {
|
||||||
|
|
||||||
@@ -34,8 +36,11 @@ public class CompanyController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<PageResult<SysCompany>> list(
|
public Result<PageResult<SysCompany>> list(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "公司状态过滤,可选值:ACTIVE、DISABLED", example = "ACTIVE")
|
||||||
@RequestParam(required = false) String status) {
|
@RequestParam(required = false) String status) {
|
||||||
return Result.success(companyService.list(page, pageSize, status));
|
return Result.success(companyService.list(page, pageSize, status));
|
||||||
}
|
}
|
||||||
@@ -44,29 +49,47 @@ public class CompanyController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
public Result<SysCompany> create(@RequestBody Map<String, String> body) {
|
public Result<SysCompany> create(
|
||||||
return Result.success(companyService.create(body.get("companyName"), body.get("companyCode")));
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "创建公司请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody CompanyCreateRequest body) {
|
||||||
|
return Result.success(companyService.create(body.getCompanyName(), body.getCompanyCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "更新公司信息")
|
@Operation(summary = "更新公司信息")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<SysCompany> update(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
public Result<SysCompany> update(
|
||||||
return Result.success(companyService.update(id, body.get("companyName"), body.get("companyCode")));
|
@Parameter(description = "公司 ID", example = "100")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "更新公司信息请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody CompanyUpdateRequest body) {
|
||||||
|
return Result.success(companyService.update(id, body.getCompanyName(), body.getCompanyCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "更新公司状态")
|
@Operation(summary = "更新公司状态")
|
||||||
@PutMapping("/{id}/status")
|
@PutMapping("/{id}/status")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> updateStatus(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
public Result<Void> updateStatus(
|
||||||
companyService.updateStatus(id, body.get("status"));
|
@Parameter(description = "公司 ID", example = "100")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "更新公司状态请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody CompanyStatusUpdateRequest body) {
|
||||||
|
companyService.updateStatus(id, body.getStatus());
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "删除公司")
|
@Operation(summary = "删除公司")
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> delete(@PathVariable Long id) {
|
public Result<Void> delete(
|
||||||
|
@Parameter(description = "公司 ID", example = "100")
|
||||||
|
@PathVariable Long id) {
|
||||||
companyService.delete(id);
|
companyService.delete(id);
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import com.label.annotation.RequireRole;
|
|||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.PageResult;
|
import com.label.common.result.PageResult;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.ExportBatchCreateRequest;
|
||||||
|
import com.label.dto.FinetuneJobResponse;
|
||||||
import com.label.entity.TrainingDataset;
|
import com.label.entity.TrainingDataset;
|
||||||
import com.label.entity.ExportBatch;
|
import com.label.entity.ExportBatch;
|
||||||
import com.label.service.ExportService;
|
import com.label.service.ExportService;
|
||||||
import com.label.service.FinetuneService;
|
import com.label.service.FinetuneService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -23,6 +26,7 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "导出管理", description = "训练样本查询、导出批次和微调任务")
|
@Tag(name = "导出管理", description = "训练样本查询、导出批次和微调任务")
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/label")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ExportController {
|
public class ExportController {
|
||||||
|
|
||||||
@@ -34,9 +38,13 @@ public class ExportController {
|
|||||||
@GetMapping("/api/training/samples")
|
@GetMapping("/api/training/samples")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<PageResult<TrainingDataset>> listSamples(
|
public Result<PageResult<TrainingDataset>> listSamples(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "样本类型过滤,可选值:EXTRACTION、QA_GENERATION", example = "EXTRACTION")
|
||||||
@RequestParam(required = false) String sampleType,
|
@RequestParam(required = false) String sampleType,
|
||||||
|
@Parameter(description = "是否已导出过滤", example = "false")
|
||||||
@RequestParam(required = false) Boolean exported,
|
@RequestParam(required = false) Boolean exported,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(exportService.listSamples(page, pageSize, sampleType, exported, principal(request)));
|
return Result.success(exportService.listSamples(page, pageSize, sampleType, exported, principal(request)));
|
||||||
@@ -47,32 +55,35 @@ public class ExportController {
|
|||||||
@PostMapping("/api/export/batch")
|
@PostMapping("/api/export/batch")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
public Result<ExportBatch> createBatch(@RequestBody Map<String, Object> body,
|
public Result<ExportBatch> createBatch(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "创建训练数据导出批次请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody ExportBatchCreateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
@SuppressWarnings("unchecked")
|
return Result.success(exportService.createBatch(body.getSampleIds(), principal(request)));
|
||||||
List<Object> rawIds = (List<Object>) body.get("sampleIds");
|
|
||||||
List<Long> sampleIds = rawIds.stream()
|
|
||||||
.map(id -> Long.parseLong(id.toString()))
|
|
||||||
.toList();
|
|
||||||
return Result.success(exportService.createBatch(sampleIds, principal(request)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /api/export/{batchId}/finetune — 提交微调任务 */
|
/** POST /api/export/{batchId}/finetune — 提交微调任务 */
|
||||||
@Operation(summary = "提交微调任务")
|
@Operation(summary = "提交微调任务")
|
||||||
@PostMapping("/api/export/{batchId}/finetune")
|
@PostMapping("/api/export/{batchId}/finetune")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Map<String, Object>> triggerFinetune(@PathVariable Long batchId,
|
public Result<FinetuneJobResponse> triggerFinetune(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "导出批次 ID", example = "501")
|
||||||
return Result.success(finetuneService.trigger(batchId, principal(request)));
|
@PathVariable Long batchId,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
return Result.success(toFinetuneJobResponse(finetuneService.trigger(batchId, principal(request))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/export/{batchId}/status — 查询微调状态 */
|
/** GET /api/export/{batchId}/status — 查询微调状态 */
|
||||||
@Operation(summary = "查询微调状态")
|
@Operation(summary = "查询微调状态")
|
||||||
@GetMapping("/api/export/{batchId}/status")
|
@GetMapping("/api/export/{batchId}/status")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Map<String, Object>> getFinetuneStatus(@PathVariable Long batchId,
|
public Result<FinetuneJobResponse> getFinetuneStatus(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "导出批次 ID", example = "501")
|
||||||
return Result.success(finetuneService.getStatus(batchId, principal(request)));
|
@PathVariable Long batchId,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
return Result.success(toFinetuneJobResponse(finetuneService.getStatus(batchId, principal(request))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/export/list — 分页查询导出批次列表 */
|
/** GET /api/export/list — 分页查询导出批次列表 */
|
||||||
@@ -80,12 +91,36 @@ public class ExportController {
|
|||||||
@GetMapping("/api/export/list")
|
@GetMapping("/api/export/list")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<PageResult<ExportBatch>> listBatches(
|
public Result<PageResult<ExportBatch>> listBatches(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(exportService.listBatches(page, pageSize, principal(request)));
|
return Result.success(exportService.listBatches(page, pageSize, principal(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FinetuneJobResponse toFinetuneJobResponse(Map<String, Object> values) {
|
||||||
|
FinetuneJobResponse response = new FinetuneJobResponse();
|
||||||
|
response.setBatchId(asLong(values.get("batchId")));
|
||||||
|
response.setGlmJobId(asString(values.get("glmJobId")));
|
||||||
|
response.setFinetuneStatus(asString(values.get("finetuneStatus")));
|
||||||
|
response.setProgress(asInteger(values.get("progress")));
|
||||||
|
response.setErrorMessage(asString(values.get("errorMessage")));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long asLong(Object value) {
|
||||||
|
return value == null ? null : Long.parseLong(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer asInteger(Object value) {
|
||||||
|
return value == null ? null : Integer.parseInt(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String asString(Object value) {
|
||||||
|
return value == null ? null : value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private TokenPrincipal principal(HttpServletRequest request) {
|
private TokenPrincipal principal(HttpServletRequest request) {
|
||||||
return (TokenPrincipal) request.getAttribute("__token_principal__");
|
return (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,56 @@
|
|||||||
package com.label.controller;
|
package com.label.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
import com.label.annotation.RequireRole;
|
import com.label.annotation.RequireRole;
|
||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.RejectRequest;
|
||||||
import com.label.service.ExtractionService;
|
import com.label.service.ExtractionService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取阶段标注工作台接口(5 个端点)。
|
* 提取阶段标注工作台接口(5 个端点)。
|
||||||
*/
|
*/
|
||||||
@Tag(name = "提取标注", description = "提取阶段的查看、编辑、提交和审批")
|
@Tag(name = "提取标注", description = "提取阶段的查看、编辑、提交和审批")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/extraction")
|
@RequestMapping("/label/api/extraction")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ExtractionController {
|
public class ExtractionController {
|
||||||
|
|
||||||
private final ExtractionService extractionService;
|
private final ExtractionService extractionService;
|
||||||
|
|
||||||
|
/** POST /api/extraction/{taskId}/ai-annotate — AI 辅助预标注 */
|
||||||
|
@Operation(summary = "AI 辅助预标注", description = "调用 AI 服务自动生成预标注结果,可重复调用")
|
||||||
|
@PostMapping("/{taskId}/ai-annotate")
|
||||||
|
@RequireRole("ANNOTATOR")
|
||||||
|
public Result<Void> aiPreAnnotate(
|
||||||
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
extractionService.aiPreAnnotate(taskId, principal(request));
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
/** GET /api/extraction/{taskId} — 获取当前标注结果 */
|
/** GET /api/extraction/{taskId} — 获取当前标注结果 */
|
||||||
@Operation(summary = "获取提取标注结果")
|
@Operation(summary = "获取提取标注结果")
|
||||||
@GetMapping("/{taskId}")
|
@GetMapping("/{taskId}")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Map<String, Object>> getResult(@PathVariable Long taskId,
|
public Result<Map<String, Object>> getResult(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
return Result.success(extractionService.getResult(taskId, principal(request)));
|
return Result.success(extractionService.getResult(taskId, principal(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +58,10 @@ public class ExtractionController {
|
|||||||
@Operation(summary = "更新提取标注结果")
|
@Operation(summary = "更新提取标注结果")
|
||||||
@PutMapping("/{taskId}")
|
@PutMapping("/{taskId}")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> updateResult(@PathVariable Long taskId,
|
public Result<Void> updateResult(
|
||||||
@RequestBody String resultJson,
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
HttpServletRequest request) {
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "完整提取标注结果 JSON 字符串,保持原始 JSON body 直接提交", required = true) @RequestBody String resultJson,
|
||||||
|
HttpServletRequest request) {
|
||||||
extractionService.updateResult(taskId, resultJson, principal(request));
|
extractionService.updateResult(taskId, resultJson, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -47,8 +70,9 @@ public class ExtractionController {
|
|||||||
@Operation(summary = "提交提取标注结果")
|
@Operation(summary = "提交提取标注结果")
|
||||||
@PostMapping("/{taskId}/submit")
|
@PostMapping("/{taskId}/submit")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> submit(@PathVariable Long taskId,
|
public Result<Void> submit(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
extractionService.submit(taskId, principal(request));
|
extractionService.submit(taskId, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -57,8 +81,9 @@ public class ExtractionController {
|
|||||||
@Operation(summary = "审批通过提取结果")
|
@Operation(summary = "审批通过提取结果")
|
||||||
@PostMapping("/{taskId}/approve")
|
@PostMapping("/{taskId}/approve")
|
||||||
@RequireRole("REVIEWER")
|
@RequireRole("REVIEWER")
|
||||||
public Result<Void> approve(@PathVariable Long taskId,
|
public Result<Void> approve(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
extractionService.approve(taskId, principal(request));
|
extractionService.approve(taskId, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -67,10 +92,11 @@ public class ExtractionController {
|
|||||||
@Operation(summary = "驳回提取结果")
|
@Operation(summary = "驳回提取结果")
|
||||||
@PostMapping("/{taskId}/reject")
|
@PostMapping("/{taskId}/reject")
|
||||||
@RequireRole("REVIEWER")
|
@RequireRole("REVIEWER")
|
||||||
public Result<Void> reject(@PathVariable Long taskId,
|
public Result<Void> reject(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "任务 ID", example = "1001") @PathVariable Long taskId,
|
||||||
HttpServletRequest request) {
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "驳回提取结果请求体", required = true) @RequestBody RejectRequest body,
|
||||||
String reason = body != null ? body.get("reason") : null;
|
HttpServletRequest request) {
|
||||||
|
String reason = body != null ? body.getReason() : null;
|
||||||
extractionService.reject(taskId, reason, principal(request));
|
extractionService.reject(taskId, reason, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.label.controller;
|
|||||||
import com.label.annotation.RequireRole;
|
import com.label.annotation.RequireRole;
|
||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.RejectRequest;
|
||||||
import com.label.service.QaService;
|
import com.label.service.QaService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,7 +19,7 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "问答生成", description = "问答生成阶段的查看、编辑、提交和审批")
|
@Tag(name = "问答生成", description = "问答生成阶段的查看、编辑、提交和审批")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/qa")
|
@RequestMapping("/label/api/qa")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class QaController {
|
public class QaController {
|
||||||
|
|
||||||
@@ -27,8 +29,10 @@ public class QaController {
|
|||||||
@Operation(summary = "获取候选问答对")
|
@Operation(summary = "获取候选问答对")
|
||||||
@GetMapping("/{taskId}")
|
@GetMapping("/{taskId}")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Map<String, Object>> getResult(@PathVariable Long taskId,
|
public Result<Map<String, Object>> getResult(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
return Result.success(qaService.getResult(taskId, principal(request)));
|
return Result.success(qaService.getResult(taskId, principal(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +40,13 @@ public class QaController {
|
|||||||
@Operation(summary = "更新候选问答对")
|
@Operation(summary = "更新候选问答对")
|
||||||
@PutMapping("/{taskId}")
|
@PutMapping("/{taskId}")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> updateResult(@PathVariable Long taskId,
|
public Result<Void> updateResult(
|
||||||
@RequestBody String body,
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "完整问答标注结果 JSON 字符串,保持原始 JSON body 直接提交",
|
||||||
|
required = true)
|
||||||
|
@RequestBody String body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
qaService.updateResult(taskId, body, principal(request));
|
qaService.updateResult(taskId, body, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
@@ -47,8 +56,10 @@ public class QaController {
|
|||||||
@Operation(summary = "提交问答对")
|
@Operation(summary = "提交问答对")
|
||||||
@PostMapping("/{taskId}/submit")
|
@PostMapping("/{taskId}/submit")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> submit(@PathVariable Long taskId,
|
public Result<Void> submit(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
qaService.submit(taskId, principal(request));
|
qaService.submit(taskId, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -57,8 +68,10 @@ public class QaController {
|
|||||||
@Operation(summary = "审批通过问答对")
|
@Operation(summary = "审批通过问答对")
|
||||||
@PostMapping("/{taskId}/approve")
|
@PostMapping("/{taskId}/approve")
|
||||||
@RequireRole("REVIEWER")
|
@RequireRole("REVIEWER")
|
||||||
public Result<Void> approve(@PathVariable Long taskId,
|
public Result<Void> approve(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
HttpServletRequest request) {
|
||||||
qaService.approve(taskId, principal(request));
|
qaService.approve(taskId, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -67,10 +80,15 @@ public class QaController {
|
|||||||
@Operation(summary = "驳回答案对")
|
@Operation(summary = "驳回答案对")
|
||||||
@PostMapping("/{taskId}/reject")
|
@PostMapping("/{taskId}/reject")
|
||||||
@RequireRole("REVIEWER")
|
@RequireRole("REVIEWER")
|
||||||
public Result<Void> reject(@PathVariable Long taskId,
|
public Result<Void> reject(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "驳回问答结果请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody RejectRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
String reason = body != null ? body.get("reason") : null;
|
String reason = body != null ? body.getReason() : null;
|
||||||
qaService.reject(taskId, reason, principal(request));
|
qaService.reject(taskId, reason, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.label.common.result.Result;
|
|||||||
import com.label.dto.SourceResponse;
|
import com.label.dto.SourceResponse;
|
||||||
import com.label.service.SourceService;
|
import com.label.service.SourceService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -23,7 +24,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "资料管理", description = "原始资料上传、查询和删除")
|
@Tag(name = "资料管理", description = "原始资料上传、查询和删除")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/source")
|
@RequestMapping("/label/api/source")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SourceController {
|
public class SourceController {
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ public class SourceController {
|
|||||||
@RequireRole("UPLOADER")
|
@RequireRole("UPLOADER")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
public Result<SourceResponse> upload(
|
public Result<SourceResponse> upload(
|
||||||
|
@Parameter(description = "上传文件,支持文本、图片、视频", required = true)
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@Parameter(description = "资料类型,可选值:text、image、video", example = "text", required = true)
|
||||||
@RequestParam("dataType") String dataType,
|
@RequestParam("dataType") String dataType,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||||
@@ -53,9 +56,13 @@ public class SourceController {
|
|||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@RequireRole("UPLOADER")
|
@RequireRole("UPLOADER")
|
||||||
public Result<PageResult<SourceResponse>> list(
|
public Result<PageResult<SourceResponse>> list(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "资料类型过滤,可选值:text、image、video", example = "text")
|
||||||
@RequestParam(required = false) String dataType,
|
@RequestParam(required = false) String dataType,
|
||||||
|
@Parameter(description = "资料状态过滤", example = "PENDING")
|
||||||
@RequestParam(required = false) String status,
|
@RequestParam(required = false) String status,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||||
@@ -68,7 +75,9 @@ public class SourceController {
|
|||||||
@Operation(summary = "查询资料详情")
|
@Operation(summary = "查询资料详情")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@RequireRole("UPLOADER")
|
@RequireRole("UPLOADER")
|
||||||
public Result<SourceResponse> findById(@PathVariable Long id) {
|
public Result<SourceResponse> findById(
|
||||||
|
@Parameter(description = "资料 ID", example = "1001")
|
||||||
|
@PathVariable Long id) {
|
||||||
return Result.success(sourceService.findById(id));
|
return Result.success(sourceService.findById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +88,10 @@ public class SourceController {
|
|||||||
@Operation(summary = "删除资料")
|
@Operation(summary = "删除资料")
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> delete(@PathVariable Long id, HttpServletRequest request) {
|
public Result<Void> delete(
|
||||||
|
@Parameter(description = "资料 ID", example = "1001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
HttpServletRequest request) {
|
||||||
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||||
sourceService.delete(id, principal.getCompanyId());
|
sourceService.delete(id, principal.getCompanyId());
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package com.label.controller;
|
|||||||
import com.label.annotation.RequireRole;
|
import com.label.annotation.RequireRole;
|
||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.SysConfigItemResponse;
|
||||||
|
import com.label.dto.SysConfigUpdateRequest;
|
||||||
import com.label.entity.SysConfig;
|
import com.label.entity.SysConfig;
|
||||||
import com.label.service.SysConfigService;
|
import com.label.service.SysConfigService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -22,6 +25,7 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "系统配置", description = "全局和公司级系统配置管理")
|
@Tag(name = "系统配置", description = "全局和公司级系统配置管理")
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/label")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SysConfigController {
|
public class SysConfigController {
|
||||||
|
|
||||||
@@ -37,9 +41,11 @@ public class SysConfigController {
|
|||||||
@Operation(summary = "查询合并后的系统配置")
|
@Operation(summary = "查询合并后的系统配置")
|
||||||
@GetMapping("/api/config")
|
@GetMapping("/api/config")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<List<Map<String, Object>>> listConfig(HttpServletRequest request) {
|
public Result<List<SysConfigItemResponse>> listConfig(HttpServletRequest request) {
|
||||||
TokenPrincipal principal = principal(request);
|
TokenPrincipal principal = principal(request);
|
||||||
return Result.success(sysConfigService.list(principal.getCompanyId()));
|
return Result.success(sysConfigService.list(principal.getCompanyId()).stream()
|
||||||
|
.map(this::toConfigItemResponse)
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,14 +56,36 @@ public class SysConfigController {
|
|||||||
@Operation(summary = "更新或创建公司专属配置")
|
@Operation(summary = "更新或创建公司专属配置")
|
||||||
@PutMapping("/api/config/{key}")
|
@PutMapping("/api/config/{key}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<SysConfig> updateConfig(@PathVariable String key,
|
public Result<SysConfig> updateConfig(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "系统配置键,可选值:token_ttl_seconds、model_default、video_frame_interval", example = "model_default")
|
||||||
|
@PathVariable String key,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "系统配置更新请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody SysConfigUpdateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
String value = body.get("value");
|
|
||||||
String description = body.get("description");
|
|
||||||
TokenPrincipal principal = principal(request);
|
TokenPrincipal principal = principal(request);
|
||||||
return Result.success(
|
return Result.success(
|
||||||
sysConfigService.update(key, value, description, principal.getCompanyId()));
|
sysConfigService.update(key, body.getValue(), body.getDescription(), principal.getCompanyId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysConfigItemResponse toConfigItemResponse(Map<String, Object> item) {
|
||||||
|
SysConfigItemResponse response = new SysConfigItemResponse();
|
||||||
|
response.setId(asLong(item.get("id")));
|
||||||
|
response.setConfigKey(asString(item.get("configKey")));
|
||||||
|
response.setConfigValue(asString(item.get("configValue")));
|
||||||
|
response.setDescription(asString(item.get("description")));
|
||||||
|
response.setScope(asString(item.get("scope")));
|
||||||
|
response.setCompanyId(asLong(item.get("companyId")));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long asLong(Object value) {
|
||||||
|
return value == null ? null : Long.parseLong(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String asString(Object value) {
|
||||||
|
return value == null ? null : value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TokenPrincipal principal(HttpServletRequest request) {
|
private TokenPrincipal principal(HttpServletRequest request) {
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ import com.label.annotation.RequireRole;
|
|||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.PageResult;
|
import com.label.common.result.PageResult;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.CreateTaskRequest;
|
||||||
|
import com.label.dto.TaskReassignRequest;
|
||||||
import com.label.dto.TaskResponse;
|
import com.label.dto.TaskResponse;
|
||||||
import com.label.service.TaskClaimService;
|
import com.label.service.TaskClaimService;
|
||||||
import com.label.service.TaskService;
|
import com.label.service.TaskService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务管理接口(10 个端点)。
|
* 任务管理接口(10 个端点)。
|
||||||
*/
|
*/
|
||||||
@Tag(name = "任务管理", description = "任务池、我的任务、审批队列和管理操作")
|
@Tag(name = "任务管理", description = "任务池、我的任务、审批队列和管理操作")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/tasks")
|
@RequestMapping("/label/api/tasks")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TaskController {
|
public class TaskController {
|
||||||
|
|
||||||
@@ -32,7 +33,9 @@ public class TaskController {
|
|||||||
@GetMapping("/pool")
|
@GetMapping("/pool")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<PageResult<TaskResponse>> getPool(
|
public Result<PageResult<TaskResponse>> getPool(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(taskService.getPool(page, pageSize, principal(request)));
|
return Result.success(taskService.getPool(page, pageSize, principal(request)));
|
||||||
@@ -43,8 +46,11 @@ public class TaskController {
|
|||||||
@GetMapping("/mine")
|
@GetMapping("/mine")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<PageResult<TaskResponse>> getMine(
|
public Result<PageResult<TaskResponse>> getMine(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "任务状态过滤,可选值:UNCLAIMED、IN_PROGRESS、SUBMITTED、APPROVED、REJECTED", example = "IN_PROGRESS")
|
||||||
@RequestParam(required = false) String status,
|
@RequestParam(required = false) String status,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(taskService.getMine(page, pageSize, status, principal(request)));
|
return Result.success(taskService.getMine(page, pageSize, status, principal(request)));
|
||||||
@@ -55,8 +61,11 @@ public class TaskController {
|
|||||||
@GetMapping("/pending-review")
|
@GetMapping("/pending-review")
|
||||||
@RequireRole("REVIEWER")
|
@RequireRole("REVIEWER")
|
||||||
public Result<PageResult<TaskResponse>> getPendingReview(
|
public Result<PageResult<TaskResponse>> getPendingReview(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "任务类型过滤,可选值:EXTRACTION、QA_GENERATION", example = "EXTRACTION")
|
||||||
@RequestParam(required = false) String taskType) {
|
@RequestParam(required = false) String taskType) {
|
||||||
return Result.success(taskService.getPendingReview(page, pageSize, taskType));
|
return Result.success(taskService.getPendingReview(page, pageSize, taskType));
|
||||||
}
|
}
|
||||||
@@ -64,11 +73,15 @@ public class TaskController {
|
|||||||
/** GET /api/tasks — 查询全部任务(ADMIN) */
|
/** GET /api/tasks — 查询全部任务(ADMIN) */
|
||||||
@Operation(summary = "管理员查询全部任务")
|
@Operation(summary = "管理员查询全部任务")
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<PageResult<TaskResponse>> getAll(
|
public Result<PageResult<TaskResponse>> getAll(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@Parameter(description = "任务状态过滤,可选值:UNCLAIMED、IN_PROGRESS、SUBMITTED、APPROVED、REJECTED", example = "SUBMITTED")
|
||||||
@RequestParam(required = false) String status,
|
@RequestParam(required = false) String status,
|
||||||
|
@Parameter(description = "任务类型过滤,可选值:EXTRACTION、QA_GENERATION", example = "QA_GENERATION")
|
||||||
@RequestParam(required = false) String taskType) {
|
@RequestParam(required = false) String taskType) {
|
||||||
return Result.success(taskService.getAll(page, pageSize, status, taskType));
|
return Result.success(taskService.getAll(page, pageSize, status, taskType));
|
||||||
}
|
}
|
||||||
@@ -77,20 +90,24 @@ public class TaskController {
|
|||||||
@Operation(summary = "管理员创建任务")
|
@Operation(summary = "管理员创建任务")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<TaskResponse> createTask(@RequestBody Map<String, Object> body,
|
public Result<TaskResponse> createTask(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "创建标注任务请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody CreateTaskRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
Long sourceId = Long.parseLong(body.get("sourceId").toString());
|
|
||||||
String taskType = body.get("taskType").toString();
|
|
||||||
TokenPrincipal principal = principal(request);
|
TokenPrincipal principal = principal(request);
|
||||||
return Result.success(taskService.toPublicResponse(
|
return Result.success(taskService.toPublicResponse(
|
||||||
taskService.createTask(sourceId, taskType, principal.getCompanyId())));
|
taskService.createTask(body.getSourceId(), body.getTaskType(), principal.getCompanyId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/tasks/{id} — 查询任务详情 */
|
/** GET /api/tasks/{id} — 查询任务详情 */
|
||||||
@Operation(summary = "查询任务详情")
|
@Operation(summary = "查询任务详情")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<TaskResponse> getById(@PathVariable Long id) {
|
public Result<TaskResponse> getById(
|
||||||
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long id) {
|
||||||
return Result.success(taskService.toPublicResponse(taskService.getById(id)));
|
return Result.success(taskService.toPublicResponse(taskService.getById(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +115,10 @@ public class TaskController {
|
|||||||
@Operation(summary = "领取任务")
|
@Operation(summary = "领取任务")
|
||||||
@PostMapping("/{id}/claim")
|
@PostMapping("/{id}/claim")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> claim(@PathVariable Long id, HttpServletRequest request) {
|
public Result<Void> claim(
|
||||||
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
HttpServletRequest request) {
|
||||||
taskClaimService.claim(id, principal(request));
|
taskClaimService.claim(id, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -107,7 +127,10 @@ public class TaskController {
|
|||||||
@Operation(summary = "放弃任务")
|
@Operation(summary = "放弃任务")
|
||||||
@PostMapping("/{id}/unclaim")
|
@PostMapping("/{id}/unclaim")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> unclaim(@PathVariable Long id, HttpServletRequest request) {
|
public Result<Void> unclaim(
|
||||||
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
HttpServletRequest request) {
|
||||||
taskClaimService.unclaim(id, principal(request));
|
taskClaimService.unclaim(id, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -116,7 +139,10 @@ public class TaskController {
|
|||||||
@Operation(summary = "重领被驳回的任务")
|
@Operation(summary = "重领被驳回的任务")
|
||||||
@PostMapping("/{id}/reclaim")
|
@PostMapping("/{id}/reclaim")
|
||||||
@RequireRole("ANNOTATOR")
|
@RequireRole("ANNOTATOR")
|
||||||
public Result<Void> reclaim(@PathVariable Long id, HttpServletRequest request) {
|
public Result<Void> reclaim(
|
||||||
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
HttpServletRequest request) {
|
||||||
taskClaimService.reclaim(id, principal(request));
|
taskClaimService.reclaim(id, principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
@@ -125,11 +151,15 @@ public class TaskController {
|
|||||||
@Operation(summary = "管理员强制指派任务")
|
@Operation(summary = "管理员强制指派任务")
|
||||||
@PutMapping("/{id}/reassign")
|
@PutMapping("/{id}/reassign")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> reassign(@PathVariable Long id,
|
public Result<Void> reassign(
|
||||||
@RequestBody Map<String, Object> body,
|
@Parameter(description = "任务 ID", example = "1001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "管理员强制改派任务请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody TaskReassignRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
Long targetUserId = Long.parseLong(body.get("userId").toString());
|
taskService.reassign(id, body.getUserId(), principal(request));
|
||||||
taskService.reassign(id, targetUserId, principal(request));
|
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.label.controller;
|
package com.label.controller;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -15,10 +13,15 @@ import com.label.annotation.RequireRole;
|
|||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.PageResult;
|
import com.label.common.result.PageResult;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.UserCreateRequest;
|
||||||
|
import com.label.dto.UserRoleUpdateRequest;
|
||||||
|
import com.label.dto.UserStatusUpdateRequest;
|
||||||
|
import com.label.dto.UserUpdateRequest;
|
||||||
import com.label.entity.SysUser;
|
import com.label.entity.SysUser;
|
||||||
import com.label.service.UserService;
|
import com.label.service.UserService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -28,7 +31,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "用户管理", description = "管理员维护公司用户")
|
@Tag(name = "用户管理", description = "管理员维护公司用户")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users")
|
@RequestMapping("/label/api/users")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@@ -39,7 +42,9 @@ public class UserController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<PageResult<SysUser>> listUsers(
|
public Result<PageResult<SysUser>> listUsers(
|
||||||
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@Parameter(description = "每页条数", example = "20")
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(userService.listUsers(page, pageSize, principal(request)));
|
return Result.success(userService.listUsers(page, pageSize, principal(request)));
|
||||||
@@ -49,13 +54,17 @@ public class UserController {
|
|||||||
@Operation(summary = "创建用户")
|
@Operation(summary = "创建用户")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<SysUser> createUser(@RequestBody Map<String, String> body,
|
public Result<SysUser> createUser(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "创建用户请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody UserCreateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(userService.createUser(
|
return Result.success(userService.createUser(
|
||||||
body.get("username"),
|
body.getUsername(),
|
||||||
body.get("password"),
|
body.getPassword(),
|
||||||
body.get("realName"),
|
body.getRealName(),
|
||||||
body.get("role"),
|
body.getRole(),
|
||||||
principal(request)));
|
principal(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,13 +72,18 @@ public class UserController {
|
|||||||
@Operation(summary = "更新用户基本信息")
|
@Operation(summary = "更新用户基本信息")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<SysUser> updateUser(@PathVariable Long id,
|
public Result<SysUser> updateUser(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "用户 ID", example = "2001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "更新用户基本信息请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody UserUpdateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
return Result.success(userService.updateUser(
|
return Result.success(userService.updateUser(
|
||||||
id,
|
id,
|
||||||
body.get("realName"),
|
body.getRealName(),
|
||||||
body.get("password"),
|
body.getPassword(),
|
||||||
principal(request)));
|
principal(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +91,15 @@ public class UserController {
|
|||||||
@Operation(summary = "变更用户状态", description = "status:ACTIVE、DISABLED")
|
@Operation(summary = "变更用户状态", description = "status:ACTIVE、DISABLED")
|
||||||
@PutMapping("/{id}/status")
|
@PutMapping("/{id}/status")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> updateStatus(@PathVariable Long id,
|
public Result<Void> updateStatus(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "用户 ID", example = "2001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "更新用户状态请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody UserStatusUpdateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
userService.updateStatus(id, body.get("status"), principal(request));
|
userService.updateStatus(id, body.getStatus(), principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +107,15 @@ public class UserController {
|
|||||||
@Operation(summary = "变更用户角色", description = "role:ADMIN、UPLOADER、VIEWER")
|
@Operation(summary = "变更用户角色", description = "role:ADMIN、UPLOADER、VIEWER")
|
||||||
@PutMapping("/{id}/role")
|
@PutMapping("/{id}/role")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<Void> updateRole(@PathVariable Long id,
|
public Result<Void> updateRole(
|
||||||
@RequestBody Map<String, String> body,
|
@Parameter(description = "用户 ID", example = "2001")
|
||||||
|
@PathVariable Long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "更新用户角色请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody UserRoleUpdateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
userService.updateRole(id, body.get("role"), principal(request));
|
userService.updateRole(id, body.getRole(), principal(request));
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package com.label.controller;
|
|||||||
import com.label.annotation.RequireRole;
|
import com.label.annotation.RequireRole;
|
||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
import com.label.common.result.Result;
|
import com.label.common.result.Result;
|
||||||
|
import com.label.dto.VideoProcessCallbackRequest;
|
||||||
|
import com.label.dto.VideoProcessCreateRequest;
|
||||||
import com.label.entity.VideoProcessJob;
|
import com.label.entity.VideoProcessJob;
|
||||||
import com.label.service.VideoProcessService;
|
import com.label.service.VideoProcessService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -13,8 +16,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视频处理接口(4 个端点)。
|
* 视频处理接口(4 个端点)。
|
||||||
*
|
*
|
||||||
@@ -26,6 +27,7 @@ import java.util.Map;
|
|||||||
@Tag(name = "视频处理", description = "视频处理任务创建、查询、重置和回调")
|
@Tag(name = "视频处理", description = "视频处理任务创建、查询、重置和回调")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/label")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VideoController {
|
public class VideoController {
|
||||||
|
|
||||||
@@ -38,16 +40,18 @@ public class VideoController {
|
|||||||
@Operation(summary = "触发视频处理任务")
|
@Operation(summary = "触发视频处理任务")
|
||||||
@PostMapping("/api/video/process")
|
@PostMapping("/api/video/process")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<VideoProcessJob> createJob(@RequestBody Map<String, Object> body,
|
public Result<VideoProcessJob> createJob(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "创建视频处理任务请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody VideoProcessCreateRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
Object sourceIdVal = body.get("sourceId");
|
Long sourceId = body.getSourceId();
|
||||||
Object jobTypeVal = body.get("jobType");
|
String jobType = body.getJobType();
|
||||||
if (sourceIdVal == null || jobTypeVal == null) {
|
if (sourceId == null || jobType == null) {
|
||||||
return Result.failure("INVALID_PARAMS", "sourceId 和 jobType 不能为空");
|
return Result.failure("INVALID_PARAMS", "sourceId 和 jobType 不能为空");
|
||||||
}
|
}
|
||||||
Long sourceId = Long.parseLong(sourceIdVal.toString());
|
String params = body.getParams();
|
||||||
String jobType = jobTypeVal.toString();
|
|
||||||
String params = body.containsKey("params") ? body.get("params").toString() : null;
|
|
||||||
|
|
||||||
TokenPrincipal principal = principal(request);
|
TokenPrincipal principal = principal(request);
|
||||||
return Result.success(
|
return Result.success(
|
||||||
@@ -58,8 +62,10 @@ public class VideoController {
|
|||||||
@Operation(summary = "查询视频处理任务状态")
|
@Operation(summary = "查询视频处理任务状态")
|
||||||
@GetMapping("/api/video/jobs/{jobId}")
|
@GetMapping("/api/video/jobs/{jobId}")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<VideoProcessJob> getJob(@PathVariable Long jobId,
|
public Result<VideoProcessJob> getJob(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "视频处理任务 ID", example = "9001")
|
||||||
|
@PathVariable Long jobId,
|
||||||
|
HttpServletRequest request) {
|
||||||
return Result.success(videoProcessService.getJob(jobId, principal(request).getCompanyId()));
|
return Result.success(videoProcessService.getJob(jobId, principal(request).getCompanyId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +73,10 @@ public class VideoController {
|
|||||||
@Operation(summary = "重置失败的视频处理任务")
|
@Operation(summary = "重置失败的视频处理任务")
|
||||||
@PostMapping("/api/video/jobs/{jobId}/reset")
|
@PostMapping("/api/video/jobs/{jobId}/reset")
|
||||||
@RequireRole("ADMIN")
|
@RequireRole("ADMIN")
|
||||||
public Result<VideoProcessJob> resetJob(@PathVariable Long jobId,
|
public Result<VideoProcessJob> resetJob(
|
||||||
HttpServletRequest request) {
|
@Parameter(description = "视频处理任务 ID", example = "9001")
|
||||||
|
@PathVariable Long jobId,
|
||||||
|
HttpServletRequest request) {
|
||||||
return Result.success(videoProcessService.reset(jobId, principal(request).getCompanyId()));
|
return Result.success(videoProcessService.reset(jobId, principal(request).getCompanyId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +92,11 @@ public class VideoController {
|
|||||||
*/
|
*/
|
||||||
@Operation(summary = "接收 AI 服务视频处理回调")
|
@Operation(summary = "接收 AI 服务视频处理回调")
|
||||||
@PostMapping("/api/video/callback")
|
@PostMapping("/api/video/callback")
|
||||||
public Result<Void> handleCallback(@RequestBody Map<String, Object> body,
|
public Result<Void> handleCallback(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "AI 服务视频处理回调请求体",
|
||||||
|
required = true)
|
||||||
|
@RequestBody VideoProcessCallbackRequest body,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
// 共享密钥校验(配置了 VIDEO_CALLBACK_SECRET 时强制校验)
|
// 共享密钥校验(配置了 VIDEO_CALLBACK_SECRET 时强制校验)
|
||||||
if (callbackSecret != null && !callbackSecret.isBlank()) {
|
if (callbackSecret != null && !callbackSecret.isBlank()) {
|
||||||
@@ -94,10 +106,10 @@ public class VideoController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Long jobId = Long.parseLong(body.get("jobId").toString());
|
Long jobId = body.getJobId();
|
||||||
String status = (String) body.get("status");
|
String status = body.getStatus();
|
||||||
String outputPath = body.containsKey("outputPath") ? (String) body.get("outputPath") : null;
|
String outputPath = body.getOutputPath();
|
||||||
String errorMessage = body.containsKey("errorMessage") ? (String) body.get("errorMessage") : null;
|
String errorMessage = body.getErrorMessage();
|
||||||
|
|
||||||
log.info("视频处理回调:jobId={}, status={}", jobId, status);
|
log.info("视频处理回调:jobId={}, status={}", jobId, status);
|
||||||
videoProcessService.handleCallback(jobId, status, outputPath, errorMessage);
|
videoProcessService.handleCallback(jobId, status, outputPath, errorMessage);
|
||||||
|
|||||||
14
src/main/java/com/label/dto/CompanyCreateRequest.java
Normal file
14
src/main/java/com/label/dto/CompanyCreateRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建公司请求")
|
||||||
|
public class CompanyCreateRequest {
|
||||||
|
@Schema(description = "公司名称", example = "示例科技")
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
@Schema(description = "公司代码(英文简写)", example = "DEMO")
|
||||||
|
private String companyCode;
|
||||||
|
}
|
||||||
11
src/main/java/com/label/dto/CompanyStatusUpdateRequest.java
Normal file
11
src/main/java/com/label/dto/CompanyStatusUpdateRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "公司状态变更请求")
|
||||||
|
public class CompanyStatusUpdateRequest {
|
||||||
|
@Schema(description = "公司状态,可选值:ACTIVE / DISABLED", example = "ACTIVE")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
14
src/main/java/com/label/dto/CompanyUpdateRequest.java
Normal file
14
src/main/java/com/label/dto/CompanyUpdateRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "更新公司请求")
|
||||||
|
public class CompanyUpdateRequest {
|
||||||
|
@Schema(description = "公司名称", example = "示例科技(升级版)")
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
@Schema(description = "公司代码(英文简写)", example = "DEMO")
|
||||||
|
private String companyCode;
|
||||||
|
}
|
||||||
14
src/main/java/com/label/dto/CreateTaskRequest.java
Normal file
14
src/main/java/com/label/dto/CreateTaskRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建任务请求")
|
||||||
|
public class CreateTaskRequest {
|
||||||
|
@Schema(description = "资料 ID", example = "1001")
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
@Schema(description = "任务类型,可选值:EXTRACTION / QA_GENERATION", example = "EXTRACTION")
|
||||||
|
private String taskType;
|
||||||
|
}
|
||||||
13
src/main/java/com/label/dto/DynamicJsonResponse.java
Normal file
13
src/main/java/com/label/dto/DynamicJsonResponse.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "动态 JSON 响应")
|
||||||
|
public class DynamicJsonResponse {
|
||||||
|
@Schema(description = "动态 JSON 内容", example = "{\"label\":\"cat\",\"score\":0.98}")
|
||||||
|
private Map<String, Object> content;
|
||||||
|
}
|
||||||
13
src/main/java/com/label/dto/ExportBatchCreateRequest.java
Normal file
13
src/main/java/com/label/dto/ExportBatchCreateRequest.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建导出批次请求")
|
||||||
|
public class ExportBatchCreateRequest {
|
||||||
|
@Schema(description = "样本 ID 列表", example = "[101, 102, 103]")
|
||||||
|
private List<Long> sampleIds;
|
||||||
|
}
|
||||||
23
src/main/java/com/label/dto/FinetuneJobResponse.java
Normal file
23
src/main/java/com/label/dto/FinetuneJobResponse.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "微调任务响应")
|
||||||
|
public class FinetuneJobResponse {
|
||||||
|
@Schema(description = "导出批次 ID", example = "501")
|
||||||
|
private Long batchId;
|
||||||
|
|
||||||
|
@Schema(description = "GLM 微调任务 ID", example = "glm-ft-001")
|
||||||
|
private String glmJobId;
|
||||||
|
|
||||||
|
@Schema(description = "微调状态", example = "RUNNING")
|
||||||
|
private String finetuneStatus;
|
||||||
|
|
||||||
|
@Schema(description = "进度百分比", example = "35")
|
||||||
|
private Integer progress;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息", example = "")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -15,10 +15,10 @@ public class LoginResponse {
|
|||||||
@Schema(description = "Bearer Token", example = "550e8400-e29b-41d4-a716-446655440000")
|
@Schema(description = "Bearer Token", example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
private String token;
|
private String token;
|
||||||
/** 用户主键 */
|
/** 用户主键 */
|
||||||
@Schema(description = "用户主键")
|
@Schema(description = "用户主键", example = "1")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
/** 登录用户名 */
|
/** 登录用户名 */
|
||||||
@Schema(description = "登录用户名")
|
@Schema(description = "登录用户名", example = "admin")
|
||||||
private String username;
|
private String username;
|
||||||
/** 角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN */
|
/** 角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN */
|
||||||
@Schema(description = "角色", example = "ADMIN")
|
@Schema(description = "角色", example = "ADMIN")
|
||||||
|
|||||||
11
src/main/java/com/label/dto/RejectRequest.java
Normal file
11
src/main/java/com/label/dto/RejectRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "驳回请求")
|
||||||
|
public class RejectRequest {
|
||||||
|
@Schema(description = "驳回原因", example = "标注结果缺少关键字段")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -14,25 +14,25 @@ import java.time.LocalDateTime;
|
|||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "原始资料响应")
|
@Schema(description = "原始资料响应")
|
||||||
public class SourceResponse {
|
public class SourceResponse {
|
||||||
@Schema(description = "资料主键")
|
@Schema(description = "资料主键", example = "2001")
|
||||||
private Long id;
|
private Long id;
|
||||||
@Schema(description = "文件名")
|
@Schema(description = "文件名", example = "demo.txt")
|
||||||
private String fileName;
|
private String fileName;
|
||||||
@Schema(description = "资料类型", example = "TEXT")
|
@Schema(description = "资料类型", example = "TEXT")
|
||||||
private String dataType;
|
private String dataType;
|
||||||
@Schema(description = "文件大小(字节)")
|
@Schema(description = "文件大小(字节)", example = "1024")
|
||||||
private Long fileSize;
|
private Long fileSize;
|
||||||
@Schema(description = "资料状态", example = "PENDING")
|
@Schema(description = "资料状态", example = "PENDING")
|
||||||
private String status;
|
private String status;
|
||||||
/** 上传用户 ID(列表端点返回) */
|
/** 上传用户 ID(列表端点返回) */
|
||||||
@Schema(description = "上传用户 ID")
|
@Schema(description = "上传用户 ID", example = "1")
|
||||||
private Long uploaderId;
|
private Long uploaderId;
|
||||||
/** 15 分钟预签名下载链接(详情端点返回) */
|
/** 15 分钟预签名下载链接(详情端点返回) */
|
||||||
@Schema(description = "预签名下载链接")
|
@Schema(description = "预签名下载链接", example = "https://example.com/presigned-url")
|
||||||
private String presignedUrl;
|
private String presignedUrl;
|
||||||
/** 父资料 ID(视频帧 / 文本片段;详情端点返回) */
|
/** 父资料 ID(视频帧 / 文本片段;详情端点返回) */
|
||||||
@Schema(description = "父资料 ID")
|
@Schema(description = "父资料 ID", example = "1001")
|
||||||
private Long parentSourceId;
|
private Long parentSourceId;
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/main/java/com/label/dto/SysConfigItemResponse.java
Normal file
26
src/main/java/com/label/dto/SysConfigItemResponse.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "系统配置项响应")
|
||||||
|
public class SysConfigItemResponse {
|
||||||
|
@Schema(description = "配置主键", example = "1")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "配置键", example = "model_default")
|
||||||
|
private String configKey;
|
||||||
|
|
||||||
|
@Schema(description = "配置值", example = "glm-4-flash")
|
||||||
|
private String configValue;
|
||||||
|
|
||||||
|
@Schema(description = "配置说明", example = "默认文本模型")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "配置来源作用域,可选值:COMPANY、GLOBAL", example = "COMPANY")
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
@Schema(description = "所属公司 ID;GLOBAL 配置为空,COMPANY 配置为当前公司 ID", example = "100")
|
||||||
|
private Long companyId;
|
||||||
|
}
|
||||||
14
src/main/java/com/label/dto/SysConfigUpdateRequest.java
Normal file
14
src/main/java/com/label/dto/SysConfigUpdateRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "系统配置更新请求")
|
||||||
|
public class SysConfigUpdateRequest {
|
||||||
|
@Schema(description = "配置值", example = "https://api.example.com")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@Schema(description = "配置说明", example = "AI 服务基础地址")
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
11
src/main/java/com/label/dto/TaskReassignRequest.java
Normal file
11
src/main/java/com/label/dto/TaskReassignRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "任务改派请求")
|
||||||
|
public class TaskReassignRequest {
|
||||||
|
@Schema(description = "目标用户 ID", example = "2001")
|
||||||
|
private Long userId;
|
||||||
|
}
|
||||||
@@ -13,26 +13,28 @@ import java.time.LocalDateTime;
|
|||||||
@Builder
|
@Builder
|
||||||
@Schema(description = "标注任务响应")
|
@Schema(description = "标注任务响应")
|
||||||
public class TaskResponse {
|
public class TaskResponse {
|
||||||
@Schema(description = "任务主键")
|
@Schema(description = "任务主键", example = "1001")
|
||||||
private Long id;
|
private Long id;
|
||||||
@Schema(description = "关联资料 ID")
|
@Schema(description = "关联资料 ID", example = "2001")
|
||||||
private Long sourceId;
|
private Long sourceId;
|
||||||
/** 任务类型(对应 taskType 字段):EXTRACTION / QA_GENERATION */
|
/** 任务类型(对应 taskType 字段):EXTRACTION / QA_GENERATION */
|
||||||
@Schema(description = "任务类型", example = "EXTRACTION")
|
@Schema(description = "任务类型", example = "EXTRACTION")
|
||||||
private String taskType;
|
private String taskType;
|
||||||
@Schema(description = "任务状态", example = "UNCLAIMED")
|
@Schema(description = "任务状态", example = "UNCLAIMED")
|
||||||
private String status;
|
private String status;
|
||||||
@Schema(description = "领取人用户 ID")
|
@Schema(description = "领取人用户 ID", example = "1")
|
||||||
private Long claimedBy;
|
private Long claimedBy;
|
||||||
@Schema(description = "领取时间")
|
@Schema(description = "AI 预标注状态:PENDING/PROCESSING/COMPLETED/FAILED", example = "COMPLETED")
|
||||||
|
private String aiStatus;
|
||||||
|
@Schema(description = "领取时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime claimedAt;
|
private LocalDateTime claimedAt;
|
||||||
@Schema(description = "提交时间")
|
@Schema(description = "提交时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime submittedAt;
|
private LocalDateTime submittedAt;
|
||||||
@Schema(description = "完成时间")
|
@Schema(description = "完成时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
/** 驳回原因(REJECTED 状态时非空) */
|
/** 驳回原因(REJECTED 状态时非空) */
|
||||||
@Schema(description = "驳回原因")
|
@Schema(description = "驳回原因")
|
||||||
private String rejectReason;
|
private String rejectReason;
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/main/java/com/label/dto/UserCreateRequest.java
Normal file
20
src/main/java/com/label/dto/UserCreateRequest.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建用户请求")
|
||||||
|
public class UserCreateRequest {
|
||||||
|
@Schema(description = "登录用户名", example = "reviewer01")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Schema(description = "明文密码", example = "Pass@123")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "真实姓名", example = "张三")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Schema(description = "角色,可选值:ADMIN / REVIEWER / ANNOTATOR / UPLOADER", example = "REVIEWER")
|
||||||
|
private String role;
|
||||||
|
}
|
||||||
@@ -11,16 +11,16 @@ import lombok.Data;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Schema(description = "当前登录用户信息")
|
@Schema(description = "当前登录用户信息")
|
||||||
public class UserInfoResponse {
|
public class UserInfoResponse {
|
||||||
@Schema(description = "用户主键")
|
@Schema(description = "用户主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
@Schema(description = "用户名")
|
@Schema(description = "用户名", example = "admin")
|
||||||
private String username;
|
private String username;
|
||||||
@Schema(description = "真实姓名")
|
@Schema(description = "真实姓名", example = "张三")
|
||||||
private String realName;
|
private String realName;
|
||||||
@Schema(description = "角色", example = "ADMIN")
|
@Schema(description = "角色", example = "ADMIN")
|
||||||
private String role;
|
private String role;
|
||||||
@Schema(description = "所属公司 ID")
|
@Schema(description = "所属公司 ID", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
@Schema(description = "所属公司名称")
|
@Schema(description = "所属公司名称", example = "示例科技有限公司")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main/java/com/label/dto/UserRoleUpdateRequest.java
Normal file
11
src/main/java/com/label/dto/UserRoleUpdateRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "用户角色变更请求")
|
||||||
|
public class UserRoleUpdateRequest {
|
||||||
|
@Schema(description = "用户角色,可选值:ADMIN / REVIEWER / ANNOTATOR / UPLOADER", example = "ANNOTATOR")
|
||||||
|
private String role;
|
||||||
|
}
|
||||||
11
src/main/java/com/label/dto/UserStatusUpdateRequest.java
Normal file
11
src/main/java/com/label/dto/UserStatusUpdateRequest.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "用户状态变更请求")
|
||||||
|
public class UserStatusUpdateRequest {
|
||||||
|
@Schema(description = "用户状态,可选值:ACTIVE / DISABLED", example = "DISABLED")
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
14
src/main/java/com/label/dto/UserUpdateRequest.java
Normal file
14
src/main/java/com/label/dto/UserUpdateRequest.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "更新用户请求")
|
||||||
|
public class UserUpdateRequest {
|
||||||
|
@Schema(description = "真实姓名", example = "李四")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Schema(description = "新密码,可为空或 null 表示保持不变", example = "")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
20
src/main/java/com/label/dto/VideoProcessCallbackRequest.java
Normal file
20
src/main/java/com/label/dto/VideoProcessCallbackRequest.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "视频处理回调请求")
|
||||||
|
public class VideoProcessCallbackRequest {
|
||||||
|
@Schema(description = "视频处理任务 ID", example = "9001")
|
||||||
|
private Long jobId;
|
||||||
|
|
||||||
|
@Schema(description = "处理状态", example = "SUCCESS")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "输出文件路径", example = "/data/output/video-9001.json")
|
||||||
|
private String outputPath;
|
||||||
|
|
||||||
|
@Schema(description = "失败时的错误信息", example = "ffmpeg error")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
17
src/main/java/com/label/dto/VideoProcessCreateRequest.java
Normal file
17
src/main/java/com/label/dto/VideoProcessCreateRequest.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.label.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建视频处理任务请求")
|
||||||
|
public class VideoProcessCreateRequest {
|
||||||
|
@Schema(description = "资料 ID", example = "3001")
|
||||||
|
private Long sourceId;
|
||||||
|
|
||||||
|
@Schema(description = "处理任务类型,可选值:FRAME_EXTRACT、VIDEO_TO_TEXT", example = "FRAME_EXTRACT")
|
||||||
|
private String jobType;
|
||||||
|
|
||||||
|
@Schema(description = "任务参数 JSON 字符串", example = "{\"frameInterval\":5}")
|
||||||
|
private String params;
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ public class AnnotationTask {
|
|||||||
/** 完成时间(APPROVED 时设置) */
|
/** 完成时间(APPROVED 时设置) */
|
||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
|
|
||||||
/** 是否最终结果(APPROVED 且无需再审)*/
|
/** 是否最终结果(APPROVED 且无需再审) */
|
||||||
private Boolean isFinal;
|
private Boolean isFinal;
|
||||||
|
|
||||||
/** 使用的 AI 模型名称 */
|
/** 使用的 AI 模型名称 */
|
||||||
@@ -53,6 +53,9 @@ public class AnnotationTask {
|
|||||||
/** 驳回原因 */
|
/** 驳回原因 */
|
||||||
private String rejectReason;
|
private String rejectReason;
|
||||||
|
|
||||||
|
/** AI 预标注状态:PENDING / PROCESSING / COMPLETED / FAILED */
|
||||||
|
private String aiStatus;
|
||||||
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.label.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -15,30 +16,40 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("export_batch")
|
@TableName("export_batch")
|
||||||
|
@Schema(description = "导出批次")
|
||||||
public class ExportBatch {
|
public class ExportBatch {
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "导出批次主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 所属公司(多租户键) */
|
/** 所属公司(多租户键) */
|
||||||
|
@Schema(description = "所属公司 ID", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
|
|
||||||
/** 批次唯一标识(UUID,DB 默认 gen_random_uuid()) */
|
/** 批次唯一标识(UUID,DB 默认 gen_random_uuid()) */
|
||||||
|
@Schema(description = "批次 UUID", example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
private UUID batchUuid;
|
private UUID batchUuid;
|
||||||
|
|
||||||
/** 本批次样本数量 */
|
/** 本批次样本数量 */
|
||||||
|
@Schema(description = "样本数量", example = "1000")
|
||||||
private Integer sampleCount;
|
private Integer sampleCount;
|
||||||
|
|
||||||
/** 导出 JSONL 的 RustFS 路径 */
|
/** 导出 JSONL 的 RustFS 路径 */
|
||||||
|
@Schema(description = "数据集文件路径(JSONL)", example = "datasets/export/2026-04-15/batch.jsonl")
|
||||||
private String datasetFilePath;
|
private String datasetFilePath;
|
||||||
|
|
||||||
/** GLM fine-tune 任务 ID(提交微调后填写) */
|
/** GLM fine-tune 任务 ID(提交微调后填写) */
|
||||||
|
@Schema(description = "GLM 微调任务 ID", example = "glm-job-123456")
|
||||||
private String glmJobId;
|
private String glmJobId;
|
||||||
|
|
||||||
/** 微调任务状态:NOT_STARTED / RUNNING / COMPLETED / FAILED */
|
/** 微调任务状态:NOT_STARTED / RUNNING / COMPLETED / FAILED */
|
||||||
|
@Schema(description = "微调任务状态", example = "NOT_STARTED")
|
||||||
private String finetuneStatus;
|
private String finetuneStatus;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.label.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -13,22 +14,29 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("sys_company")
|
@TableName("sys_company")
|
||||||
|
@Schema(description = "租户公司")
|
||||||
public class SysCompany {
|
public class SysCompany {
|
||||||
|
|
||||||
/** 公司主键,自增 */
|
/** 公司主键,自增 */
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "公司主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 公司全称,全局唯一 */
|
/** 公司全称,全局唯一 */
|
||||||
|
@Schema(description = "公司全称", example = "示例科技有限公司")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 公司代码(英文简写),全局唯一 */
|
/** 公司代码(英文简写),全局唯一 */
|
||||||
|
@Schema(description = "公司代码(英文简写)", example = "DEMO")
|
||||||
private String companyCode;
|
private String companyCode;
|
||||||
|
|
||||||
/** 状态:ACTIVE / DISABLED */
|
/** 状态:ACTIVE / DISABLED */
|
||||||
|
@Schema(description = "状态", example = "ACTIVE")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.label.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -15,27 +16,35 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("sys_config")
|
@TableName("sys_config")
|
||||||
|
@Schema(description = "系统配置")
|
||||||
public class SysConfig {
|
public class SysConfig {
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "配置主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 所属公司 ID(NULL = 全局默认配置;非 NULL = 租户专属配置)。
|
* 所属公司 ID(NULL = 全局默认配置;非 NULL = 租户专属配置)。
|
||||||
* 注意:不能用 @TableField(exist = false) 排除,必须保留以支持 company_id IS NULL 查询。
|
* 注意:不能用 @TableField(exist = false) 排除,必须保留以支持 company_id IS NULL 查询。
|
||||||
*/
|
*/
|
||||||
|
@Schema(description = "所属公司 ID(NULL 表示全局默认配置)", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
|
|
||||||
/** 配置键 */
|
/** 配置键 */
|
||||||
|
@Schema(description = "配置键", example = "STORAGE_BUCKET")
|
||||||
private String configKey;
|
private String configKey;
|
||||||
|
|
||||||
/** 配置值 */
|
/** 配置值 */
|
||||||
|
@Schema(description = "配置值", example = "label-bucket")
|
||||||
private String configValue;
|
private String configValue;
|
||||||
|
|
||||||
/** 配置说明 */
|
/** 配置说明 */
|
||||||
|
@Schema(description = "配置说明", example = "对象存储桶名称")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -15,16 +16,20 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("sys_user")
|
@TableName("sys_user")
|
||||||
|
@Schema(description = "系统用户")
|
||||||
public class SysUser {
|
public class SysUser {
|
||||||
|
|
||||||
/** 用户主键,自增 */
|
/** 用户主键,自增 */
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "用户主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 所属公司 ID(多租户键) */
|
/** 所属公司 ID(多租户键) */
|
||||||
|
@Schema(description = "所属公司 ID", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
|
|
||||||
/** 登录用户名(同公司内唯一) */
|
/** 登录用户名(同公司内唯一) */
|
||||||
|
@Schema(description = "登录用户名", example = "admin")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,18 +37,24 @@ public class SysUser {
|
|||||||
* 序列化时排除,防止密码哈希泄漏到 API 响应。
|
* 序列化时排除,防止密码哈希泄漏到 API 响应。
|
||||||
*/
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@Schema(description = "密码哈希(不会在响应中返回)")
|
||||||
private String passwordHash;
|
private String passwordHash;
|
||||||
|
|
||||||
/** 真实姓名 */
|
/** 真实姓名 */
|
||||||
|
@Schema(description = "真实姓名", example = "张三")
|
||||||
private String realName;
|
private String realName;
|
||||||
|
|
||||||
/** 角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN */
|
/** 角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN */
|
||||||
|
@Schema(description = "角色", example = "ADMIN")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/** 状态:ACTIVE / DISABLED */
|
/** 状态:ACTIVE / DISABLED */
|
||||||
|
@Schema(description = "状态", example = "ACTIVE")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.label.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -15,32 +16,44 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("training_dataset")
|
@TableName("training_dataset")
|
||||||
|
@Schema(description = "训练数据集样本")
|
||||||
public class TrainingDataset {
|
public class TrainingDataset {
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "样本主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 所属公司(多租户键) */
|
/** 所属公司(多租户键) */
|
||||||
|
@Schema(description = "所属公司 ID", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
|
|
||||||
|
@Schema(description = "关联任务 ID", example = "1001")
|
||||||
private Long taskId;
|
private Long taskId;
|
||||||
|
|
||||||
|
@Schema(description = "关联资料 ID", example = "2001")
|
||||||
private Long sourceId;
|
private Long sourceId;
|
||||||
|
|
||||||
/** 样本类型:TEXT / IMAGE / VIDEO_FRAME */
|
/** 样本类型:TEXT / IMAGE / VIDEO_FRAME */
|
||||||
|
@Schema(description = "样本类型", example = "TEXT")
|
||||||
private String sampleType;
|
private String sampleType;
|
||||||
|
|
||||||
/** GLM fine-tune 格式的 JSON 字符串(JSONB) */
|
/** GLM fine-tune 格式的 JSON 字符串(JSONB) */
|
||||||
|
@Schema(description = "GLM 微调格式 JSON", example = "{\"messages\":[{\"role\":\"user\",\"content\":\"...\"},{\"role\":\"assistant\",\"content\":\"...\"}]}")
|
||||||
private String glmFormatJson;
|
private String glmFormatJson;
|
||||||
|
|
||||||
/** 状态:PENDING_REVIEW / APPROVED / REJECTED */
|
/** 状态:PENDING_REVIEW / APPROVED / REJECTED */
|
||||||
|
@Schema(description = "状态", example = "APPROVED")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "导出批次 ID", example = "3001")
|
||||||
private Long exportBatchId;
|
private Long exportBatchId;
|
||||||
|
|
||||||
|
@Schema(description = "导出时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime exportedAt;
|
private LocalDateTime exportedAt;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.label.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -15,43 +16,58 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("video_process_job")
|
@TableName("video_process_job")
|
||||||
|
@Schema(description = "视频处理任务")
|
||||||
public class VideoProcessJob {
|
public class VideoProcessJob {
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
|
@Schema(description = "任务主键", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 所属公司(多租户键) */
|
/** 所属公司(多租户键) */
|
||||||
|
@Schema(description = "所属公司 ID", example = "1")
|
||||||
private Long companyId;
|
private Long companyId;
|
||||||
|
|
||||||
/** 关联资料 ID */
|
/** 关联资料 ID */
|
||||||
|
@Schema(description = "关联资料 ID", example = "2001")
|
||||||
private Long sourceId;
|
private Long sourceId;
|
||||||
|
|
||||||
/** 任务类型:FRAME_EXTRACT / VIDEO_TO_TEXT */
|
/** 任务类型:FRAME_EXTRACT / VIDEO_TO_TEXT */
|
||||||
|
@Schema(description = "任务类型", example = "FRAME_EXTRACT")
|
||||||
private String jobType;
|
private String jobType;
|
||||||
|
|
||||||
/** 任务状态:PENDING / RUNNING / SUCCESS / FAILED / RETRYING */
|
/** 任务状态:PENDING / RUNNING / SUCCESS / FAILED / RETRYING */
|
||||||
|
@Schema(description = "任务状态", example = "PENDING")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/** 任务参数(JSONB,例如 {"frameInterval": 30}) */
|
/** 任务参数(JSONB,例如 {"frameInterval": 30}) */
|
||||||
|
@Schema(description = "任务参数(JSON)", example = "{\"frameInterval\":30}")
|
||||||
private String params;
|
private String params;
|
||||||
|
|
||||||
/** AI 处理输出路径(成功后填写) */
|
/** AI 处理输出路径(成功后填写) */
|
||||||
|
@Schema(description = "输出路径", example = "outputs/video/2026-04-15/result.json")
|
||||||
private String outputPath;
|
private String outputPath;
|
||||||
|
|
||||||
/** 已重试次数 */
|
/** 已重试次数 */
|
||||||
|
@Schema(description = "已重试次数", example = "0")
|
||||||
private Integer retryCount;
|
private Integer retryCount;
|
||||||
|
|
||||||
/** 最大重试次数(默认 3) */
|
/** 最大重试次数(默认 3) */
|
||||||
|
@Schema(description = "最大重试次数", example = "3")
|
||||||
private Integer maxRetries;
|
private Integer maxRetries;
|
||||||
|
|
||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
|
@Schema(description = "错误信息")
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|
||||||
|
@Schema(description = "开始时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime startedAt;
|
private LocalDateTime startedAt;
|
||||||
|
|
||||||
|
@Schema(description = "完成时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", example = "2026-04-15T12:34:56")
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthInterceptor implements HandlerInterceptor {
|
public class AuthInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private static final String API_PREFIX = "/label";
|
||||||
|
private static final String API_ROOT = API_PREFIX + "/api/";
|
||||||
|
|
||||||
private final RedisService redisService;
|
private final RedisService redisService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@@ -155,9 +158,9 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPublicPath(String path) {
|
private boolean isPublicPath(String path) {
|
||||||
return !path.startsWith("/api/")
|
return !path.startsWith(API_ROOT)
|
||||||
|| path.equals("/api/auth/login")
|
|| path.equals(API_PREFIX + "/api/auth/login")
|
||||||
|| path.equals("/api/video/callback")
|
|| path.equals(API_PREFIX + "/api/video/callback")
|
||||||
|| path.startsWith("/swagger-ui")
|
|| path.startsWith("/swagger-ui")
|
||||||
|| path.startsWith("/v3/api-docs");
|
|| path.startsWith("/v3/api-docs");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,28 @@
|
|||||||
package com.label.listener;
|
package com.label.listener;
|
||||||
|
|
||||||
import java.util.Collections;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.util.List;
|
import com.label.common.ai.AiServiceClient;
|
||||||
import java.util.Map;
|
import com.label.common.context.CompanyContext;
|
||||||
|
import com.label.entity.AnnotationResult;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import com.label.entity.SourceData;
|
||||||
|
import com.label.entity.TrainingDataset;
|
||||||
|
import com.label.event.ExtractionApprovedEvent;
|
||||||
|
import com.label.mapper.AnnotationResultMapper;
|
||||||
|
import com.label.mapper.SourceDataMapper;
|
||||||
|
import com.label.mapper.TrainingDatasetMapper;
|
||||||
|
import com.label.service.TaskService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.event.TransactionPhase;
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
import org.springframework.transaction.event.TransactionalEventListener;
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import java.util.Collections;
|
||||||
import com.label.common.ai.AiServiceClient;
|
import java.util.List;
|
||||||
import com.label.common.context.CompanyContext;
|
import java.util.Map;
|
||||||
import com.label.entity.SourceData;
|
|
||||||
import com.label.entity.TrainingDataset;
|
|
||||||
import com.label.event.ExtractionApprovedEvent;
|
|
||||||
import com.label.mapper.SourceDataMapper;
|
|
||||||
import com.label.mapper.TrainingDatasetMapper;
|
|
||||||
import com.label.service.TaskService;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取审批通过后的异步处理器。
|
|
||||||
*
|
|
||||||
* 设计约束(关键):
|
|
||||||
* - @TransactionalEventListener(AFTER_COMMIT):确保在审批事务提交后才触发 AI 调用
|
|
||||||
* - @Transactional(REQUIRES_NEW):在独立新事务中写 DB,与审批事务完全隔离
|
|
||||||
* - 异常不会回滚审批事务(已提交),但会在日志中记录
|
|
||||||
*
|
|
||||||
* 处理流程:
|
|
||||||
* 1. 调用 AI 生成候选问答对(Text/Image 走不同端点)
|
|
||||||
* 2. 写入 training_dataset(status=PENDING_REVIEW)
|
|
||||||
* 3. 创建 QA_GENERATION 任务(status=UNCLAIMED)
|
|
||||||
* 4. 更新 source_data 状态为 QA_REVIEW
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -47,23 +32,19 @@ public class ExtractionApprovedEventListener {
|
|||||||
private final SourceDataMapper sourceDataMapper;
|
private final SourceDataMapper sourceDataMapper;
|
||||||
private final TaskService taskService;
|
private final TaskService taskService;
|
||||||
private final AiServiceClient aiServiceClient;
|
private final AiServiceClient aiServiceClient;
|
||||||
|
private final AnnotationResultMapper annotationResultMapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Value("${rustfs.bucket:label-source-data}")
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void onExtractionApproved(ExtractionApprovedEvent event) {
|
public void onExtractionApproved(ExtractionApprovedEvent event) {
|
||||||
log.info("处理提取审批通过事件: taskId={}, sourceId={}", event.getTaskId(), event.getSourceId());
|
log.info("处理提取审批通过事件: taskId={}, sourceId={}", event.getTaskId(), event.getSourceId());
|
||||||
|
|
||||||
// 设置多租户上下文(新事务中 ThreadLocal 已清除)
|
|
||||||
CompanyContext.set(event.getCompanyId());
|
CompanyContext.set(event.getCompanyId());
|
||||||
try {
|
try {
|
||||||
processEvent(event);
|
processEvent(event);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理审批通过事件失败(taskId={}):{}", event.getTaskId(), e.getMessage(), e);
|
log.error("处理审批通过事件失败(taskId={}): {}", event.getTaskId(), e.getMessage(), e);
|
||||||
// 不向上抛出,审批操作已提交,此处失败不回滚审批
|
|
||||||
} finally {
|
} finally {
|
||||||
CompanyContext.clear();
|
CompanyContext.clear();
|
||||||
}
|
}
|
||||||
@@ -76,57 +57,79 @@ public class ExtractionApprovedEventListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 调用 AI 生成候选问答对
|
|
||||||
AiServiceClient.ExtractionRequest req = AiServiceClient.ExtractionRequest.builder()
|
|
||||||
.sourceId(source.getId())
|
|
||||||
.filePath(source.getFilePath())
|
|
||||||
.bucket(bucket)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<Map<String, Object>> qaPairs;
|
List<Map<String, Object>> qaPairs;
|
||||||
try {
|
try {
|
||||||
AiServiceClient.QaGenResponse response = "IMAGE".equals(source.getDataType())
|
AiServiceClient.QaGenResponse response = "IMAGE".equals(source.getDataType())
|
||||||
? aiServiceClient.genImageQa(req)
|
? aiServiceClient.genImageQa(buildImageQaRequest(event.getTaskId()))
|
||||||
: aiServiceClient.genTextQa(req);
|
: aiServiceClient.genTextQa(buildTextQaRequest(event.getTaskId()));
|
||||||
qaPairs = response != null && response.getQaPairs() != null
|
qaPairs = response != null && response.getPairs() != null
|
||||||
? response.getQaPairs()
|
? response.getPairs()
|
||||||
: Collections.emptyList();
|
: Collections.emptyList();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("AI 问答生成失败(taskId={}):{},将使用空问答对", event.getTaskId(), e.getMessage());
|
log.warn("AI 问答生成失败(taskId={}): {},将使用空问答对", event.getTaskId(), e.getMessage());
|
||||||
qaPairs = Collections.emptyList();
|
qaPairs = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 写入 training_dataset(PENDING_REVIEW)
|
|
||||||
String sampleType = "IMAGE".equals(source.getDataType()) ? "IMAGE" : "TEXT";
|
String sampleType = "IMAGE".equals(source.getDataType()) ? "IMAGE" : "TEXT";
|
||||||
String glmJson = buildGlmJson(qaPairs);
|
|
||||||
|
|
||||||
TrainingDataset dataset = new TrainingDataset();
|
TrainingDataset dataset = new TrainingDataset();
|
||||||
dataset.setCompanyId(event.getCompanyId());
|
dataset.setCompanyId(event.getCompanyId());
|
||||||
dataset.setTaskId(event.getTaskId());
|
dataset.setTaskId(event.getTaskId());
|
||||||
dataset.setSourceId(event.getSourceId());
|
dataset.setSourceId(event.getSourceId());
|
||||||
dataset.setSampleType(sampleType);
|
dataset.setSampleType(sampleType);
|
||||||
dataset.setGlmFormatJson(glmJson);
|
dataset.setGlmFormatJson(buildGlmJson(qaPairs));
|
||||||
dataset.setStatus("PENDING_REVIEW");
|
dataset.setStatus("PENDING_REVIEW");
|
||||||
datasetMapper.insert(dataset);
|
datasetMapper.insert(dataset);
|
||||||
|
|
||||||
// 3. 创建 QA_GENERATION 任务(UNCLAIMED)
|
|
||||||
taskService.createTask(event.getSourceId(), "QA_GENERATION", event.getCompanyId());
|
taskService.createTask(event.getSourceId(), "QA_GENERATION", event.getCompanyId());
|
||||||
|
|
||||||
// 4. 更新 source_data 状态为 QA_REVIEW
|
|
||||||
sourceDataMapper.updateStatus(event.getSourceId(), "QA_REVIEW", event.getCompanyId());
|
sourceDataMapper.updateStatus(event.getSourceId(), "QA_REVIEW", event.getCompanyId());
|
||||||
|
|
||||||
log.info("审批通过后续处理完成: taskId={}, 新 QA 任务已创建", event.getTaskId());
|
log.info("审批通过后续处理完成: taskId={}", event.getTaskId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 AI 生成的问答对列表转换为 GLM fine-tune 格式 JSON。
|
|
||||||
*/
|
|
||||||
private String buildGlmJson(List<Map<String, Object>> qaPairs) {
|
private String buildGlmJson(List<Map<String, Object>> qaPairs) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(Map.of("conversations", qaPairs));
|
return objectMapper.writeValueAsString(Map.of("conversations", qaPairs));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("构建 GLM JSON 失败", e);
|
log.error("构建微调 JSON 失败", e);
|
||||||
return "{\"conversations\":[]}";
|
return "{\"conversations\":[]}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AiServiceClient.GenTextQaRequest buildTextQaRequest(Long taskId) {
|
||||||
|
List<AiServiceClient.TextQaItem> items = readAnnotationItems(taskId).stream()
|
||||||
|
.map(item -> objectMapper.convertValue(item, AiServiceClient.TextQaItem.class))
|
||||||
|
.toList();
|
||||||
|
return AiServiceClient.GenTextQaRequest.builder()
|
||||||
|
.items(items)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiServiceClient.GenImageQaRequest buildImageQaRequest(Long taskId) {
|
||||||
|
List<AiServiceClient.ImageQaItem> items = readAnnotationItems(taskId).stream()
|
||||||
|
.map(item -> objectMapper.convertValue(item, AiServiceClient.ImageQaItem.class))
|
||||||
|
.toList();
|
||||||
|
return AiServiceClient.GenImageQaRequest.builder()
|
||||||
|
.items(items)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> readAnnotationItems(Long taskId) {
|
||||||
|
AnnotationResult result = annotationResultMapper.selectByTaskId(taskId);
|
||||||
|
if (result == null || result.getResultJson() == null || result.getResultJson().isBlank()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> parsed = objectMapper.readValue(result.getResultJson(), Map.class);
|
||||||
|
Object items = parsed.get("items");
|
||||||
|
if (items instanceof List<?>) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> typedItems = (List<Map<String, Object>>) items;
|
||||||
|
return typedItems;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析提取结果失败,taskId={},将使用空 items: {}", taskId, e.getMessage());
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ public interface AnnotationResultMapper extends BaseMapper<AnnotationResult> {
|
|||||||
"SET result_json = #{resultJson}::jsonb, updated_at = NOW() " +
|
"SET result_json = #{resultJson}::jsonb, updated_at = NOW() " +
|
||||||
"WHERE task_id = #{taskId} AND company_id = #{companyId}")
|
"WHERE task_id = #{taskId} AND company_id = #{companyId}")
|
||||||
int updateResultJson(@Param("taskId") Long taskId,
|
int updateResultJson(@Param("taskId") Long taskId,
|
||||||
@Param("resultJson") String resultJson,
|
@Param("resultJson") String resultJson,
|
||||||
@Param("companyId") Long companyId);
|
@Param("companyId") Long companyId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按任务 ID 查询标注结果。
|
* 按任务 ID 查询标注结果。
|
||||||
@@ -33,4 +33,9 @@ public interface AnnotationResultMapper extends BaseMapper<AnnotationResult> {
|
|||||||
*/
|
*/
|
||||||
@Select("SELECT * FROM annotation_result WHERE task_id = #{taskId}")
|
@Select("SELECT * FROM annotation_result WHERE task_id = #{taskId}")
|
||||||
AnnotationResult selectByTaskId(@Param("taskId") Long taskId);
|
AnnotationResult selectByTaskId(@Param("taskId") Long taskId);
|
||||||
|
|
||||||
|
@Insert("INSERT INTO annotation_result (task_id, company_id, result_json, created_at, updated_at) " +
|
||||||
|
"VALUES (#{taskId}, #{companyId}, #{resultJson}::jsonb, NOW(), NOW())")
|
||||||
|
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||||
|
int insertWithJsonb(AnnotationResult result);
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/main/java/com/label/service/AiAnnotationAsyncService.java
Normal file
143
src/main/java/com/label/service/AiAnnotationAsyncService.java
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package com.label.service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.label.common.ai.AiServiceClient;
|
||||||
|
import com.label.common.context.CompanyContext;
|
||||||
|
import com.label.entity.AnnotationResult;
|
||||||
|
import com.label.entity.AnnotationTask;
|
||||||
|
import com.label.entity.SourceData;
|
||||||
|
import com.label.mapper.AnnotationResultMapper;
|
||||||
|
import com.label.mapper.AnnotationTaskMapper;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiAnnotationAsyncService {
|
||||||
|
|
||||||
|
private final AnnotationTaskMapper taskMapper;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final AnnotationResultMapper resultMapper;
|
||||||
|
private final AiServiceClient aiServiceClient;
|
||||||
|
|
||||||
|
@Async("aiTaskExecutor")
|
||||||
|
public void processAnnotation(Long taskId, Long companyId, SourceData source) {
|
||||||
|
CompanyContext.set(companyId);
|
||||||
|
|
||||||
|
log.info("开始异步执行 AI 预标注,任务ID: {}", taskId);
|
||||||
|
String dataType = source.getDataType().toUpperCase();
|
||||||
|
AiServiceClient.ExtractionResponse aiResponse = null;
|
||||||
|
int maxRetries = 2;
|
||||||
|
Exception lastException = null;
|
||||||
|
String finalStatus = "FAILED";
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
if ("IMAGE".equals(dataType)) {
|
||||||
|
AiServiceClient.ImageExtractRequest req = AiServiceClient.ImageExtractRequest.builder()
|
||||||
|
.filePath(source.getFilePath())
|
||||||
|
.taskId(taskId)
|
||||||
|
.build();
|
||||||
|
aiResponse = aiServiceClient.extractImage(req);
|
||||||
|
} else {
|
||||||
|
AiServiceClient.TextExtractRequest req = AiServiceClient.TextExtractRequest.builder()
|
||||||
|
.filePath(source.getFilePath())
|
||||||
|
.fileName(source.getFileName())
|
||||||
|
.build();
|
||||||
|
aiResponse = aiServiceClient.extractText(req);
|
||||||
|
}
|
||||||
|
if (aiResponse != null) {
|
||||||
|
log.info("AI 预标注成功,任务ID: {}, 尝试次数: {}", taskId, attempt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastException = e;
|
||||||
|
log.warn("AI 预标注调用失败(任务 {}),第 {} 次尝试:{}", taskId, attempt, e.getMessage());
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000L * attempt);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<?> items = Collections.emptyList();
|
||||||
|
if (aiResponse != null && aiResponse.getItems() != null) {
|
||||||
|
items = aiResponse.getItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOrUpdateResult(taskId, companyId, items);
|
||||||
|
finalStatus = "COMPLETED";
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastException = e;
|
||||||
|
log.error("AI 预标注处理过程中发生未知异常,任务ID: {}", taskId, e);
|
||||||
|
finalStatus = "FAILED";
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
AnnotationTask updateEntity = new AnnotationTask();
|
||||||
|
updateEntity.setId(taskId);
|
||||||
|
updateEntity.setAiStatus(finalStatus);
|
||||||
|
|
||||||
|
if ("FAILED".equals(finalStatus)) {
|
||||||
|
String reason = lastException != null ? lastException.getMessage() : "AI处理失败";
|
||||||
|
if (reason != null && reason.length() > 500) {
|
||||||
|
reason = reason.substring(0, 500);
|
||||||
|
}
|
||||||
|
updateEntity.setRejectReason(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rows = taskMapper.updateById(updateEntity);
|
||||||
|
log.info("异步 AI 预标注结束,任务ID: {}, 最终状态: {}, row {}", taskId, finalStatus, rows);
|
||||||
|
} catch (Exception updateEx) {
|
||||||
|
log.error("更新任务 AI 状态失败,任务ID: {}", taskId, updateEx);
|
||||||
|
} finally {
|
||||||
|
CompanyContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeOrUpdateResult(Long taskId, Long companyId, List<?> items) {
|
||||||
|
try {
|
||||||
|
String json = objectMapper
|
||||||
|
.writeValueAsString(Map.of("items", items != null ? items : Collections.emptyList()));
|
||||||
|
|
||||||
|
int updated = resultMapper.updateResultJson(taskId, json, companyId);
|
||||||
|
|
||||||
|
if (updated == 0) {
|
||||||
|
try {
|
||||||
|
AnnotationResult result = new AnnotationResult();
|
||||||
|
result.setTaskId(taskId);
|
||||||
|
result.setCompanyId(companyId);
|
||||||
|
result.setResultJson(json);
|
||||||
|
resultMapper.insertWithJsonb(result);
|
||||||
|
log.info("新建AI预标注结果,任务ID: {}", taskId);
|
||||||
|
} catch (Exception insertEx) {
|
||||||
|
if (insertEx.getMessage() != null && insertEx.getMessage().contains("duplicate key")) {
|
||||||
|
log.warn("检测到并发插入冲突,转为更新模式,任务ID: {}", taskId);
|
||||||
|
resultMapper.updateResultJson(taskId, json, companyId);
|
||||||
|
} else {
|
||||||
|
throw insertEx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("更新AI预标注结果,任务ID: {}", taskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入 AI 预标注结果失败, taskId={}", taskId, e);
|
||||||
|
throw new RuntimeException("RESULT_WRITE_FAILED: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,30 @@
|
|||||||
package com.label.service;
|
package com.label.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import java.time.LocalDateTime;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import java.util.Map;
|
||||||
import com.label.common.ai.AiServiceClient;
|
|
||||||
import com.label.common.exception.BusinessException;
|
|
||||||
import com.label.common.auth.TokenPrincipal;
|
|
||||||
import com.label.common.statemachine.StateValidator;
|
|
||||||
import com.label.common.statemachine.TaskStatus;
|
|
||||||
import com.label.entity.AnnotationResult;
|
|
||||||
import com.label.entity.TrainingDataset;
|
|
||||||
import com.label.event.ExtractionApprovedEvent;
|
|
||||||
import com.label.mapper.AnnotationResultMapper;
|
|
||||||
import com.label.mapper.TrainingDatasetMapper;
|
|
||||||
import com.label.entity.SourceData;
|
|
||||||
import com.label.mapper.SourceDataMapper;
|
|
||||||
import com.label.entity.AnnotationTask;
|
|
||||||
import com.label.mapper.AnnotationTaskMapper;
|
|
||||||
import com.label.service.TaskClaimService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import java.util.Collections;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.util.Map;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
|
import com.label.common.exception.BusinessException;
|
||||||
|
import com.label.common.statemachine.StateValidator;
|
||||||
|
import com.label.common.statemachine.TaskStatus;
|
||||||
|
import com.label.entity.AnnotationResult;
|
||||||
|
import com.label.entity.AnnotationTask;
|
||||||
|
import com.label.entity.SourceData;
|
||||||
|
import com.label.event.ExtractionApprovedEvent;
|
||||||
|
import com.label.mapper.AnnotationResultMapper;
|
||||||
|
import com.label.mapper.AnnotationTaskMapper;
|
||||||
|
import com.label.mapper.SourceDataMapper;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取阶段标注服务:AI 预标注、更新结果、提交、审批、驳回。
|
* 提取阶段标注服务:AI 预标注、更新结果、提交、审批、驳回。
|
||||||
@@ -43,12 +40,13 @@ public class ExtractionService {
|
|||||||
|
|
||||||
private final AnnotationTaskMapper taskMapper;
|
private final AnnotationTaskMapper taskMapper;
|
||||||
private final AnnotationResultMapper resultMapper;
|
private final AnnotationResultMapper resultMapper;
|
||||||
private final TrainingDatasetMapper datasetMapper;
|
// private final TrainingDatasetMapper datasetMapper;
|
||||||
private final SourceDataMapper sourceDataMapper;
|
private final SourceDataMapper sourceDataMapper;
|
||||||
private final TaskClaimService taskClaimService;
|
private final TaskClaimService taskClaimService;
|
||||||
private final AiServiceClient aiServiceClient;
|
// private final AiServiceClient aiServiceClient;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final AiAnnotationAsyncService aiAnnotationAsyncService; // 注入异步服务
|
||||||
|
|
||||||
@Value("${rustfs.bucket:label-source-data}")
|
@Value("${rustfs.bucket:label-source-data}")
|
||||||
private String bucket;
|
private String bucket;
|
||||||
@@ -67,32 +65,30 @@ public class ExtractionService {
|
|||||||
throw new BusinessException("NOT_FOUND", "关联资料不存在", HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "关联资料不存在", HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用 AI 服务(在事务外,避免长时间持有 DB 连接)
|
if (source.getFilePath() == null || source.getFilePath().isEmpty()) {
|
||||||
AiServiceClient.ExtractionRequest req = AiServiceClient.ExtractionRequest.builder()
|
throw new BusinessException("INVALID_SOURCE", "源文件路径不能为空", HttpStatus.BAD_REQUEST);
|
||||||
.sourceId(source.getId())
|
|
||||||
.filePath(source.getFilePath())
|
|
||||||
.bucket(bucket)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AiServiceClient.ExtractionResponse aiResponse;
|
|
||||||
try {
|
|
||||||
if ("IMAGE".equals(source.getDataType())) {
|
|
||||||
aiResponse = aiServiceClient.extractImage(req);
|
|
||||||
} else {
|
|
||||||
aiResponse = aiServiceClient.extractText(req);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("AI 预标注调用失败(任务 {}):{}", taskId, e.getMessage());
|
|
||||||
// AI 失败不阻塞流程,写入空结果
|
|
||||||
aiResponse = new AiServiceClient.ExtractionResponse();
|
|
||||||
aiResponse.setItems(Collections.emptyList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 AI 结果写入 annotation_result(UPSERT 语义)
|
if (source.getDataType() == null || source.getDataType().isEmpty()) {
|
||||||
writeOrUpdateResult(taskId, principal.getCompanyId(), aiResponse.getItems());
|
throw new BusinessException("INVALID_SOURCE", "数据类型不能为空", HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 更新结果 --
|
String dataType = source.getDataType().toUpperCase();
|
||||||
|
if (!"IMAGE".equals(dataType) && !"TEXT".equals(dataType)) {
|
||||||
|
log.warn("不支持的数据类型: {}, 任务ID: {}", dataType, taskId);
|
||||||
|
throw new BusinessException("UNSUPPORTED_TYPE",
|
||||||
|
"不支持的数据类型: " + dataType, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务状态为 PROCESSING
|
||||||
|
taskMapper.update(null, new LambdaUpdateWrapper<AnnotationTask>()
|
||||||
|
.eq(AnnotationTask::getId, taskId)
|
||||||
|
.set(AnnotationTask::getAiStatus, "PROCESSING"));
|
||||||
|
|
||||||
|
// 触发异步任务
|
||||||
|
aiAnnotationAsyncService.processAnnotation(taskId, principal.getCompanyId(), source);
|
||||||
|
// executeAiAnnotationAsync(taskId, principal.getCompanyId(), source);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人工更新标注结果(整体覆盖,PUT 语义)。
|
* 人工更新标注结果(整体覆盖,PUT 语义)。
|
||||||
@@ -237,8 +233,7 @@ public class ExtractionService {
|
|||||||
"sourceType", source != null ? source.getDataType() : "",
|
"sourceType", source != null ? source.getDataType() : "",
|
||||||
"sourceFilePath", source != null && source.getFilePath() != null ? source.getFilePath() : "",
|
"sourceFilePath", source != null && source.getFilePath() != null ? source.getFilePath() : "",
|
||||||
"isFinal", task.getIsFinal() != null && task.getIsFinal(),
|
"isFinal", task.getIsFinal() != null && task.getIsFinal(),
|
||||||
"resultJson", result != null ? result.getResultJson() : "[]"
|
"resultJson", result != null ? result.getResultJson() : "[]");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 私有工具 --
|
// ------------------------------------------------------------------ 私有工具 --
|
||||||
@@ -253,20 +248,4 @@ public class ExtractionService {
|
|||||||
}
|
}
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeOrUpdateResult(Long taskId, Long companyId, java.util.List<?> items) {
|
|
||||||
try {
|
|
||||||
String json = objectMapper.writeValueAsString(Map.of("items", items != null ? items : Collections.emptyList()));
|
|
||||||
int updated = resultMapper.updateResultJson(taskId, json, companyId);
|
|
||||||
if (updated == 0) {
|
|
||||||
AnnotationResult result = new AnnotationResult();
|
|
||||||
result.setTaskId(taskId);
|
|
||||||
result.setCompanyId(companyId);
|
|
||||||
result.setResultJson(json);
|
|
||||||
resultMapper.insert(result);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("写入 AI 预标注结果失败: taskId={}", taskId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,73 @@
|
|||||||
package com.label.service;
|
package com.label.service;
|
||||||
|
|
||||||
import com.label.common.ai.AiServiceClient;
|
import com.label.common.ai.AiServiceClient;
|
||||||
import com.label.common.exception.BusinessException;
|
|
||||||
import com.label.common.auth.TokenPrincipal;
|
import com.label.common.auth.TokenPrincipal;
|
||||||
|
import com.label.common.exception.BusinessException;
|
||||||
|
import com.label.common.storage.RustFsClient;
|
||||||
import com.label.entity.ExportBatch;
|
import com.label.entity.ExportBatch;
|
||||||
import com.label.mapper.ExportBatchMapper;
|
import com.label.mapper.ExportBatchMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
|
||||||
* GLM 微调服务:提交任务、查询状态。
|
|
||||||
*
|
|
||||||
* 注意:trigger() 包含 AI HTTP 调用,不在 @Transactional 注解下。
|
|
||||||
* 仅在 DB 写入时开启事务(updateFinetuneInfo)。
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class FinetuneService {
|
public class FinetuneService {
|
||||||
|
|
||||||
|
private static final String FINETUNE_BUCKET = "finetune-export";
|
||||||
|
private static final int PRESIGNED_URL_MINUTES = 60;
|
||||||
|
|
||||||
private final ExportBatchMapper exportBatchMapper;
|
private final ExportBatchMapper exportBatchMapper;
|
||||||
private final ExportService exportService;
|
private final ExportService exportService;
|
||||||
private final AiServiceClient aiServiceClient;
|
private final AiServiceClient aiServiceClient;
|
||||||
|
private final RustFsClient rustFsClient;
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 提交微调 --
|
private String finetuneBaseModel = "qwen3-14b";
|
||||||
|
|
||||||
/**
|
|
||||||
* 向 GLM AI 服务提交微调任务。
|
|
||||||
*
|
|
||||||
* T074 设计:AI 调用不在 @Transactional 内执行,避免持有 DB 连接期间发起 HTTP 请求。
|
|
||||||
* DB 写入(updateFinetuneInfo)是单条 UPDATE,不需要显式事务(自动提交)。
|
|
||||||
* 如果 AI 调用成功但 DB 写入失败,下次查询状态仍可通过 AI 服务的 jobId 重建状态。
|
|
||||||
*
|
|
||||||
* @param batchId 批次 ID
|
|
||||||
* @param principal 当前用户
|
|
||||||
* @return 包含 glmJobId 和 finetuneStatus 的 Map
|
|
||||||
*/
|
|
||||||
public Map<String, Object> trigger(Long batchId, TokenPrincipal principal) {
|
public Map<String, Object> trigger(Long batchId, TokenPrincipal principal) {
|
||||||
ExportBatch batch = exportService.getById(batchId, principal);
|
ExportBatch batch = exportService.getById(batchId, principal);
|
||||||
|
|
||||||
if (!"NOT_STARTED".equals(batch.getFinetuneStatus())) {
|
if (!"NOT_STARTED".equals(batch.getFinetuneStatus())) {
|
||||||
throw new BusinessException("FINETUNE_ALREADY_STARTED",
|
throw new BusinessException(
|
||||||
"微调任务已提交,当前状态: " + batch.getFinetuneStatus(), HttpStatus.CONFLICT);
|
"FINETUNE_ALREADY_STARTED",
|
||||||
|
"微调任务已提交,当前状态 " + batch.getFinetuneStatus(),
|
||||||
|
HttpStatus.CONFLICT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用 AI 服务(无事务,不持有 DB 连接)
|
String jsonlUrl = rustFsClient.getPresignedUrl(
|
||||||
AiServiceClient.FinetuneRequest req = AiServiceClient.FinetuneRequest.builder()
|
FINETUNE_BUCKET,
|
||||||
.datasetPath(batch.getDatasetFilePath())
|
batch.getDatasetFilePath(),
|
||||||
.model("glm-4")
|
PRESIGNED_URL_MINUTES
|
||||||
.batchId(batchId)
|
);
|
||||||
|
|
||||||
|
AiServiceClient.FinetuneStartRequest req = AiServiceClient.FinetuneStartRequest.builder()
|
||||||
|
.jsonlUrl(jsonlUrl)
|
||||||
|
.baseModel(finetuneBaseModel)
|
||||||
|
.hyperparams(Map.of())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AiServiceClient.FinetuneResponse response;
|
AiServiceClient.FinetuneStartResponse response;
|
||||||
try {
|
try {
|
||||||
response = aiServiceClient.startFinetune(req);
|
response = aiServiceClient.startFinetune(req);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BusinessException("FINETUNE_TRIGGER_FAILED",
|
throw new BusinessException(
|
||||||
"提交微调任务失败: " + e.getMessage(), HttpStatus.SERVICE_UNAVAILABLE);
|
"FINETUNE_TRIGGER_FAILED",
|
||||||
|
"提交微调任务失败: " + e.getMessage(),
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 调用成功后更新批次记录(单条 UPDATE,自动提交)
|
exportBatchMapper.updateFinetuneInfo(
|
||||||
exportBatchMapper.updateFinetuneInfo(batchId,
|
batchId,
|
||||||
response.getJobId(), "RUNNING", principal.getCompanyId());
|
response.getJobId(),
|
||||||
|
"RUNNING",
|
||||||
log.info("微调任务已提交: batchId={}, glmJobId={}", batchId, response.getJobId());
|
principal.getCompanyId()
|
||||||
|
);
|
||||||
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"glmJobId", response.getJobId(),
|
"glmJobId", response.getJobId(),
|
||||||
@@ -76,15 +75,6 @@ public class FinetuneService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 查询状态 --
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询微调任务实时状态(向 AI 服务查询)。
|
|
||||||
*
|
|
||||||
* @param batchId 批次 ID
|
|
||||||
* @param principal 当前用户
|
|
||||||
* @return 状态 Map
|
|
||||||
*/
|
|
||||||
public Map<String, Object> getStatus(Long batchId, TokenPrincipal principal) {
|
public Map<String, Object> getStatus(Long batchId, TokenPrincipal principal) {
|
||||||
ExportBatch batch = exportService.getById(batchId, principal);
|
ExportBatch batch = exportService.getById(batchId, principal);
|
||||||
|
|
||||||
@@ -98,13 +88,11 @@ public class FinetuneService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向 AI 服务实时查询
|
|
||||||
AiServiceClient.FinetuneStatusResponse statusResp;
|
AiServiceClient.FinetuneStatusResponse statusResp;
|
||||||
try {
|
try {
|
||||||
statusResp = aiServiceClient.getFinetuneStatus(batch.getGlmJobId());
|
statusResp = aiServiceClient.getFinetuneStatus(batch.getGlmJobId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("查询微调状态失败(batchId={}):{}", batchId, e.getMessage());
|
log.warn("查询微调状态失败(batchId={}): {}", batchId, e.getMessage());
|
||||||
// 查询失败时返回 DB 中的缓存状态
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"batchId", batchId,
|
"batchId", batchId,
|
||||||
"glmJobId", batch.getGlmJobId(),
|
"glmJobId", batch.getGlmJobId(),
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ public class TaskService {
|
|||||||
.sourceId(task.getSourceId())
|
.sourceId(task.getSourceId())
|
||||||
.taskType(task.getTaskType())
|
.taskType(task.getTaskType())
|
||||||
.status(task.getStatus())
|
.status(task.getStatus())
|
||||||
|
.aiStatus(task.getAiStatus())
|
||||||
.claimedBy(task.getClaimedBy())
|
.claimedBy(task.getClaimedBy())
|
||||||
.claimedAt(task.getClaimedAt())
|
.claimedAt(task.getClaimedAt())
|
||||||
.submittedAt(task.getSubmittedAt())
|
.submittedAt(task.getSubmittedAt())
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package com.label.service;
|
package com.label.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.label.common.ai.AiServiceClient;
|
import com.label.common.ai.AiServiceClient;
|
||||||
import com.label.common.exception.BusinessException;
|
import com.label.common.exception.BusinessException;
|
||||||
import com.label.common.statemachine.SourceStatus;
|
|
||||||
import com.label.common.statemachine.StateValidator;
|
import com.label.common.statemachine.StateValidator;
|
||||||
|
import com.label.common.statemachine.VideoSourceStatus;
|
||||||
import com.label.entity.SourceData;
|
import com.label.entity.SourceData;
|
||||||
import com.label.mapper.SourceDataMapper;
|
|
||||||
import com.label.entity.VideoProcessJob;
|
import com.label.entity.VideoProcessJob;
|
||||||
|
import com.label.mapper.SourceDataMapper;
|
||||||
import com.label.mapper.VideoProcessJobMapper;
|
import com.label.mapper.VideoProcessJobMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -21,20 +22,6 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
|
||||||
* 视频处理服务:创建任务、处理回调、管理员重置。
|
|
||||||
*
|
|
||||||
* 状态流转:
|
|
||||||
* - 创建时:source_data → PREPROCESSING,job → PENDING
|
|
||||||
* - 回调成功:job → SUCCESS,source_data → PENDING(进入提取队列)
|
|
||||||
* - 回调失败(可重试):job → RETRYING,retryCount++,重新触发 AI
|
|
||||||
* - 回调失败(超出上限):job → FAILED,source_data → PENDING
|
|
||||||
* - 管理员重置:job → PENDING(可手动重新触发)
|
|
||||||
*
|
|
||||||
* T074 设计说明:
|
|
||||||
* AI 调用通过 TransactionSynchronizationManager.registerSynchronization().afterCommit()
|
|
||||||
* 延迟到事务提交后执行,避免在持有 DB 连接期间进行 HTTP 调用。
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -43,44 +30,27 @@ public class VideoProcessService {
|
|||||||
private final VideoProcessJobMapper jobMapper;
|
private final VideoProcessJobMapper jobMapper;
|
||||||
private final SourceDataMapper sourceDataMapper;
|
private final SourceDataMapper sourceDataMapper;
|
||||||
private final AiServiceClient aiServiceClient;
|
private final AiServiceClient aiServiceClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Value("${rustfs.bucket:label-source-data}")
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 创建任务 --
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建视频处理任务并在事务提交后触发 AI 服务。
|
|
||||||
*
|
|
||||||
* DB 写入(source_data→PREPROCESSING + 插入 job)在 @Transactional 内完成;
|
|
||||||
* AI 触发通过 afterCommit() 在事务提交后执行,不占用 DB 连接。
|
|
||||||
*
|
|
||||||
* @param sourceId 资料 ID
|
|
||||||
* @param jobType 任务类型(FRAME_EXTRACT / VIDEO_TO_TEXT)
|
|
||||||
* @param params JSON 参数(如 {"frameInterval": 30})
|
|
||||||
* @param companyId 租户 ID
|
|
||||||
* @return 新建的 VideoProcessJob
|
|
||||||
*/
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public VideoProcessJob createJob(Long sourceId, String jobType,
|
public VideoProcessJob createJob(Long sourceId, String jobType, String params, Long companyId) {
|
||||||
String params, Long companyId) {
|
|
||||||
SourceData source = sourceDataMapper.selectById(sourceId);
|
SourceData source = sourceDataMapper.selectById(sourceId);
|
||||||
if (source == null || !companyId.equals(source.getCompanyId())) {
|
if (source == null || !companyId.equals(source.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "资料不存在: " + sourceId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "资料不存在 " + sourceId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateJobType(jobType);
|
validateJobType(jobType);
|
||||||
|
|
||||||
// source_data → PREPROCESSING
|
|
||||||
StateValidator.assertTransition(
|
StateValidator.assertTransition(
|
||||||
SourceStatus.TRANSITIONS,
|
VideoSourceStatus.TRANSITIONS,
|
||||||
SourceStatus.valueOf(source.getStatus()), SourceStatus.PREPROCESSING);
|
VideoSourceStatus.valueOf(source.getStatus()),
|
||||||
|
VideoSourceStatus.PREPROCESSING
|
||||||
|
);
|
||||||
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
||||||
.eq(SourceData::getId, sourceId)
|
.eq(SourceData::getId, sourceId)
|
||||||
.set(SourceData::getStatus, "PREPROCESSING")
|
.set(SourceData::getStatus, "PREPROCESSING")
|
||||||
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
// 插入 PENDING 任务
|
|
||||||
VideoProcessJob job = new VideoProcessJob();
|
VideoProcessJob job = new VideoProcessJob();
|
||||||
job.setCompanyId(companyId);
|
job.setCompanyId(companyId);
|
||||||
job.setSourceId(sourceId);
|
job.setSourceId(sourceId);
|
||||||
@@ -91,48 +61,32 @@ public class VideoProcessService {
|
|||||||
job.setMaxRetries(3);
|
job.setMaxRetries(3);
|
||||||
jobMapper.insert(job);
|
jobMapper.insert(job);
|
||||||
|
|
||||||
// 事务提交后触发 AI(不在事务内,不占用 DB 连接)
|
final Long jobId = job.getId();
|
||||||
final Long jobId = job.getId();
|
|
||||||
final String filePath = source.getFilePath();
|
final String filePath = source.getFilePath();
|
||||||
final String finalJobType = jobType;
|
final String finalJobType = jobType;
|
||||||
|
final String finalParams = job.getParams();
|
||||||
|
|
||||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
@Override
|
@Override
|
||||||
public void afterCommit() {
|
public void afterCommit() {
|
||||||
triggerAi(jobId, sourceId, filePath, finalJobType);
|
triggerAi(jobId, sourceId, filePath, finalJobType, finalParams);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("视频处理任务已创建(AI 将在事务提交后触发): jobId={}, sourceId={}", jobId, sourceId);
|
log.info("视频处理任务已创建: jobId={}, sourceId={}", jobId, sourceId);
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 处理回调 --
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 AI 服务异步回调(POST /api/video/callback,无需用户 Token)。
|
|
||||||
*
|
|
||||||
* 幂等:若 job 已为 SUCCESS,直接返回,防止重复处理。
|
|
||||||
* 重试触发同样延迟到事务提交后(afterCommit),不在事务内执行。
|
|
||||||
*
|
|
||||||
* @param jobId 任务 ID
|
|
||||||
* @param callbackStatus AI 回调状态(SUCCESS / FAILED)
|
|
||||||
* @param outputPath 成功时的输出路径(可选)
|
|
||||||
* @param errorMessage 失败时的错误信息(可选)
|
|
||||||
*/
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void handleCallback(Long jobId, String callbackStatus,
|
public void handleCallback(Long jobId, String callbackStatus, String outputPath, String errorMessage) {
|
||||||
String outputPath, String errorMessage) {
|
|
||||||
// video_process_job 在 IGNORED_TABLES 中(回调无 CompanyContext),此处显式校验
|
|
||||||
VideoProcessJob job = jobMapper.selectById(jobId);
|
VideoProcessJob job = jobMapper.selectById(jobId);
|
||||||
if (job == null || job.getCompanyId() == null) {
|
if (job == null || job.getCompanyId() == null) {
|
||||||
log.warn("视频处理回调:job 不存在,jobId={}", jobId);
|
log.warn("视频处理回调时 job 不存在: jobId={}", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 幂等:已成功则忽略重复回调
|
|
||||||
if ("SUCCESS".equals(job.getStatus())) {
|
if ("SUCCESS".equals(job.getStatus())) {
|
||||||
log.info("视频处理回调幂等:jobId={} 已为 SUCCESS,跳过", jobId);
|
log.info("视频处理回调幂等跳过: jobId={}", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,28 +97,19 @@ public class VideoProcessService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 管理员重置 --
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理员手动重置失败任务(FAILED → PENDING)。
|
|
||||||
*
|
|
||||||
* 仅允许 FAILED 状态的任务重置,重置后 retryCount 清零,
|
|
||||||
* 管理员可随后重新调用 createJob 触发处理。
|
|
||||||
*
|
|
||||||
* @param jobId 任务 ID
|
|
||||||
* @param companyId 租户 ID
|
|
||||||
*/
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public VideoProcessJob reset(Long jobId, Long companyId) {
|
public VideoProcessJob reset(Long jobId, Long companyId) {
|
||||||
VideoProcessJob job = jobMapper.selectById(jobId);
|
VideoProcessJob job = jobMapper.selectById(jobId);
|
||||||
if (job == null || !companyId.equals(job.getCompanyId())) {
|
if (job == null || !companyId.equals(job.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "视频处理任务不存在: " + jobId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "视频处理任务不存在 " + jobId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"FAILED".equals(job.getStatus())) {
|
if (!"FAILED".equals(job.getStatus())) {
|
||||||
throw new BusinessException("INVALID_TRANSITION",
|
throw new BusinessException(
|
||||||
"只有 FAILED 状态的任务可以重置,当前状态: " + job.getStatus(),
|
"INVALID_TRANSITION",
|
||||||
HttpStatus.BAD_REQUEST);
|
"只有 FAILED 状态的任务可以重置,当前状态 " + job.getStatus(),
|
||||||
|
HttpStatus.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
||||||
@@ -176,24 +121,18 @@ public class VideoProcessService {
|
|||||||
|
|
||||||
job.setStatus("PENDING");
|
job.setStatus("PENDING");
|
||||||
job.setRetryCount(0);
|
job.setRetryCount(0);
|
||||||
log.info("视频处理任务已重置: jobId={}", jobId);
|
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 查询 --
|
|
||||||
|
|
||||||
public VideoProcessJob getJob(Long jobId, Long companyId) {
|
public VideoProcessJob getJob(Long jobId, Long companyId) {
|
||||||
VideoProcessJob job = jobMapper.selectById(jobId);
|
VideoProcessJob job = jobMapper.selectById(jobId);
|
||||||
if (job == null || !companyId.equals(job.getCompanyId())) {
|
if (job == null || !companyId.equals(job.getCompanyId())) {
|
||||||
throw new BusinessException("NOT_FOUND", "视频处理任务不存在: " + jobId, HttpStatus.NOT_FOUND);
|
throw new BusinessException("NOT_FOUND", "视频处理任务不存在 " + jobId, HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ 私有方法 --
|
|
||||||
|
|
||||||
private void handleSuccess(VideoProcessJob job, String outputPath) {
|
private void handleSuccess(VideoProcessJob job, String outputPath) {
|
||||||
// job → SUCCESS
|
|
||||||
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
||||||
.eq(VideoProcessJob::getId, job.getId())
|
.eq(VideoProcessJob::getId, job.getId())
|
||||||
.set(VideoProcessJob::getStatus, "SUCCESS")
|
.set(VideoProcessJob::getStatus, "SUCCESS")
|
||||||
@@ -201,13 +140,10 @@ public class VideoProcessService {
|
|||||||
.set(VideoProcessJob::getCompletedAt, LocalDateTime.now())
|
.set(VideoProcessJob::getCompletedAt, LocalDateTime.now())
|
||||||
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
// source_data PREPROCESSING → PENDING(进入提取队列)
|
|
||||||
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
||||||
.eq(SourceData::getId, job.getSourceId())
|
.eq(SourceData::getId, job.getSourceId())
|
||||||
.set(SourceData::getStatus, "PENDING")
|
.set(SourceData::getStatus, "PENDING")
|
||||||
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
log.info("视频处理成功:jobId={}, sourceId={}", job.getId(), job.getSourceId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleFailure(VideoProcessJob job, String errorMessage) {
|
private void handleFailure(VideoProcessJob job, String errorMessage) {
|
||||||
@@ -215,7 +151,6 @@ public class VideoProcessService {
|
|||||||
int maxRetries = job.getMaxRetries() != null ? job.getMaxRetries() : 3;
|
int maxRetries = job.getMaxRetries() != null ? job.getMaxRetries() : 3;
|
||||||
|
|
||||||
if (newRetryCount < maxRetries) {
|
if (newRetryCount < maxRetries) {
|
||||||
// 仍有重试次数:job → RETRYING,事务提交后重新触发 AI
|
|
||||||
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
||||||
.eq(VideoProcessJob::getId, job.getId())
|
.eq(VideoProcessJob::getId, job.getId())
|
||||||
.set(VideoProcessJob::getStatus, "RETRYING")
|
.set(VideoProcessJob::getStatus, "RETRYING")
|
||||||
@@ -223,26 +158,22 @@ public class VideoProcessService {
|
|||||||
.set(VideoProcessJob::getErrorMessage, errorMessage)
|
.set(VideoProcessJob::getErrorMessage, errorMessage)
|
||||||
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
log.warn("视频处理失败,开始第 {} 次重试:jobId={}, error={}",
|
|
||||||
newRetryCount, job.getId(), errorMessage);
|
|
||||||
|
|
||||||
// 重试 AI 触发延迟到事务提交后
|
|
||||||
SourceData source = sourceDataMapper.selectById(job.getSourceId());
|
SourceData source = sourceDataMapper.selectById(job.getSourceId());
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
final Long jobId = job.getId();
|
final Long jobId = job.getId();
|
||||||
final Long sourceId = job.getSourceId();
|
final Long sourceId = job.getSourceId();
|
||||||
final String filePath = source.getFilePath();
|
final String filePath = source.getFilePath();
|
||||||
final String jobType = job.getJobType();
|
final String jobType = job.getJobType();
|
||||||
|
final String params = job.getParams();
|
||||||
|
|
||||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
@Override
|
@Override
|
||||||
public void afterCommit() {
|
public void afterCommit() {
|
||||||
triggerAi(jobId, sourceId, filePath, jobType);
|
triggerAi(jobId, sourceId, filePath, jobType, params);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 超出最大重试次数:job → FAILED,source_data → PENDING
|
|
||||||
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
jobMapper.update(null, new LambdaUpdateWrapper<VideoProcessJob>()
|
||||||
.eq(VideoProcessJob::getId, job.getId())
|
.eq(VideoProcessJob::getId, job.getId())
|
||||||
.set(VideoProcessJob::getStatus, "FAILED")
|
.set(VideoProcessJob::getStatus, "FAILED")
|
||||||
@@ -251,40 +182,87 @@ public class VideoProcessService {
|
|||||||
.set(VideoProcessJob::getCompletedAt, LocalDateTime.now())
|
.set(VideoProcessJob::getCompletedAt, LocalDateTime.now())
|
||||||
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
.set(VideoProcessJob::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
// source_data PREPROCESSING → PENDING(管理员可重新处理)
|
|
||||||
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
sourceDataMapper.update(null, new LambdaUpdateWrapper<SourceData>()
|
||||||
.eq(SourceData::getId, job.getSourceId())
|
.eq(SourceData::getId, job.getSourceId())
|
||||||
.set(SourceData::getStatus, "PENDING")
|
.set(SourceData::getStatus, "PENDING")
|
||||||
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
.set(SourceData::getUpdatedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
log.error("视频处理永久失败:jobId={}, sourceId={}, error={}",
|
|
||||||
job.getId(), job.getSourceId(), errorMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerAi(Long jobId, Long sourceId, String filePath, String jobType) {
|
private void triggerAi(Long jobId, Long sourceId, String filePath, String jobType, String paramsJson) {
|
||||||
AiServiceClient.VideoProcessRequest req = AiServiceClient.VideoProcessRequest.builder()
|
Map<String, Object> params = parseParams(paramsJson);
|
||||||
.sourceId(sourceId)
|
|
||||||
.filePath(filePath)
|
|
||||||
.bucket(bucket)
|
|
||||||
.params(Map.of("jobId", jobId, "jobType", jobType))
|
|
||||||
.build();
|
|
||||||
try {
|
try {
|
||||||
if ("FRAME_EXTRACT".equals(jobType)) {
|
if ("FRAME_EXTRACT".equals(jobType)) {
|
||||||
aiServiceClient.extractFrames(req);
|
aiServiceClient.extractFrames(AiServiceClient.ExtractFramesRequest.builder()
|
||||||
|
.filePath(filePath)
|
||||||
|
.sourceId(sourceId)
|
||||||
|
.jobId(jobId)
|
||||||
|
.mode(stringParam(params, "mode", "interval"))
|
||||||
|
.frameInterval(intParam(params, "frameInterval", 30))
|
||||||
|
.build());
|
||||||
} else {
|
} else {
|
||||||
aiServiceClient.videoToText(req);
|
aiServiceClient.videoToText(AiServiceClient.VideoToTextRequest.builder()
|
||||||
|
.filePath(filePath)
|
||||||
|
.sourceId(sourceId)
|
||||||
|
.jobId(jobId)
|
||||||
|
.startSec(doubleParam(params, "startSec", 0.0))
|
||||||
|
.endSec(doubleParam(params, "endSec", 120.0))
|
||||||
|
.model(stringParam(params, "model", null))
|
||||||
|
.promptTemplate(stringParam(params, "promptTemplate", null))
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
log.info("AI 触发成功: jobId={}", jobId);
|
log.info("AI 视频任务已触发: jobId={}", jobId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("触发视频处理 AI 失败(jobId={}):{},job 保持当前状态,需管理员手动重置", jobId, e.getMessage());
|
log.error("触发视频处理 AI 失败(jobId={}): {}", jobId, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> parseParams(String paramsJson) {
|
||||||
|
if (paramsJson == null || paramsJson.isBlank()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(paramsJson, new TypeReference<>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析视频处理参数失败,将使用默认值: {}", e.getMessage());
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stringParam(Map<String, Object> params, String key, String defaultValue) {
|
||||||
|
Object value = params.get(key);
|
||||||
|
return value == null ? defaultValue : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer intParam(Map<String, Object> params, String key, Integer defaultValue) {
|
||||||
|
Object value = params.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (value instanceof String text && !text.isBlank()) {
|
||||||
|
return Integer.parseInt(text);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double doubleParam(Map<String, Object> params, String key, Double defaultValue) {
|
||||||
|
Object value = params.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.doubleValue();
|
||||||
|
}
|
||||||
|
if (value instanceof String text && !text.isBlank()) {
|
||||||
|
return Double.parseDouble(text);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
private void validateJobType(String jobType) {
|
private void validateJobType(String jobType) {
|
||||||
if (!"FRAME_EXTRACT".equals(jobType) && !"VIDEO_TO_TEXT".equals(jobType)) {
|
if (!"FRAME_EXTRACT".equals(jobType) && !"VIDEO_TO_TEXT".equals(jobType)) {
|
||||||
throw new BusinessException("INVALID_JOB_TYPE",
|
throw new BusinessException(
|
||||||
"任务类型不合法,应为 FRAME_EXTRACT 或 VIDEO_TO_TEXT", HttpStatus.BAD_REQUEST);
|
"INVALID_JOB_TYPE",
|
||||||
|
"任务类型不合法,应为 FRAME_EXTRACT 或 VIDEO_TO_TEXT",
|
||||||
|
HttpStatus.BAD_REQUEST
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
server:
|
server:
|
||||||
port: 8080
|
|
||||||
port: 18082
|
port: 18082
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: label-backend
|
name: label-backend
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 500MB
|
||||||
|
max-request-size: 500MB
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://39.107.112.174:5432/labeldb}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://39.107.112.174:5432/labeldb}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
||||||
@@ -61,11 +64,12 @@ rustfs:
|
|||||||
region: us-east-1
|
region: us-east-1
|
||||||
|
|
||||||
ai-service:
|
ai-service:
|
||||||
base-url: ${AI_SERVICE_BASE_URL:http://39.107.112.174:18000}
|
base-url: ${AI_SERVICE_BASE_URL:http://172.28.77.215:18000}
|
||||||
timeout: 30000
|
#base-url: ${AI_SERVICE_BASE_URL:http://127.0.0.1:18000}
|
||||||
|
timeout: 300000
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
enabled: false
|
enabled: true
|
||||||
mock-company-id: 1
|
mock-company-id: 1
|
||||||
mock-user-id: 1
|
mock-user-id: 1
|
||||||
mock-role: ADMIN
|
mock-role: ADMIN
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<!-- <appender-ref ref="CONSOLE"/> -->
|
<appender-ref ref="CONSOLE"/>
|
||||||
<appender-ref ref="FILE"/>
|
<appender-ref ref="FILE"/>
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ CREATE TABLE IF NOT EXISTS annotation_task (
|
|||||||
completed_at TIMESTAMP,
|
completed_at TIMESTAMP,
|
||||||
is_final BOOLEAN NOT NULL DEFAULT FALSE, -- true 即 APPROVED 且无需再审
|
is_final BOOLEAN NOT NULL DEFAULT FALSE, -- true 即 APPROVED 且无需再审
|
||||||
ai_model VARCHAR(50),
|
ai_model VARCHAR(50),
|
||||||
|
ai_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
reject_reason TEXT,
|
reject_reason TEXT,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
@@ -313,7 +314,7 @@ INSERT INTO sys_config (company_id, config_key, config_value, description)
|
|||||||
VALUES
|
VALUES
|
||||||
(NULL, 'token_ttl_seconds', '7200',
|
(NULL, 'token_ttl_seconds', '7200',
|
||||||
'会话凭证有效期(秒)'),
|
'会话凭证有效期(秒)'),
|
||||||
(NULL, 'model_default', 'glm-4',
|
(NULL, 'model_default', 'qwen-plus',
|
||||||
'AI 辅助默认模型'),
|
'AI 辅助默认模型'),
|
||||||
(NULL, 'video_frame_interval', '30',
|
(NULL, 'video_frame_interval', '30',
|
||||||
'视频帧提取间隔(帧数)'),
|
'视频帧提取间隔(帧数)'),
|
||||||
|
|||||||
Reference in New Issue
Block a user