Compare commits

...

53 Commits

Author SHA1 Message Date
wh
954f9b14cf 添加mapper注释 2026-05-13 23:46:40 +08:00
wh
3619d34739 Merge branch 'dev54' 2026-05-13 23:30:39 +08:00
wh
c51f645227 优化任务删除方法,核心代码注释 2026-05-13 23:29:43 +08:00
wh
92335758bd 修改联调问题
(cherry picked from commit b9000e18d8)
2026-05-12 17:05:39 +08:00
wh
b9000e18d8 修改联调问题 2026-05-12 17:04:37 +08:00
wh
a226d1f1ad Merge branch 'dev54' 2026-05-12 16:11:22 +08:00
wh
965f3cf179 修改联调问题 2026-05-12 16:10:39 +08:00
wh
bb0a043a88 修复同步更新agent configs问题
(cherry picked from commit 4a84af3023)
2026-05-12 09:29:12 +08:00
wh
4a84af3023 修复同步更新agent configs问题 2026-05-12 00:38:21 +08:00
wh
eb68b8b13b Merge branch 'dev54'
# Conflicts:
#	src/main/java/com/labelsys/backend/service/SysConfigService.java
2026-05-12 00:26:15 +08:00
wh
989003243a 系统配置更新后同步更新agent configs 2026-05-12 00:25:16 +08:00
wh
1f9c056378 优化系统配置功能
(cherry picked from commit fd8e9a4a2f)
2026-05-12 00:00:38 +08:00
wh
fd8e9a4a2f 优化系统配置功能 2026-05-11 21:56:47 +08:00
wh
642a2fa99a Merge branch 'dev54' 2026-05-11 21:34:44 +08:00
wh
77dae2ea49 添加apikey国密sm4加解密,agent configs配置功能 2026-05-11 21:34:03 +08:00
wh
e88f6b43ad 添加国密sm4加解密 2026-05-10 22:04:52 +08:00
wh
080417f42e 去掉资源预览路径previewpath
(cherry picked from commit 642200776d)
2026-05-10 17:36:23 +08:00
wh
642200776d 去掉资源预览路径previewpath 2026-05-10 17:10:15 +08:00
wh
2410e1ab6f Merge branch 'dev54'
# Conflicts:
#	src/main/resources/sql/schema.sql
2026-05-10 16:44:11 +08:00
wh
84ac2fe5c7 数据库去掉industrytype,适配QA新结构,解决自动和手动归档产生两次记录问题 2026-05-10 16:40:24 +08:00
wh
9425ff3a1e 适配QA问答对数据结构 2026-05-09 23:46:56 +08:00
wh
e637e8a6a4 资源软删除判断,bbxo查询返回临时签名链接 2026-05-09 17:58:18 +08:00
wh
e1c1628e29 资源软删除判断,bbxo查询返回临时签名链接 2026-05-09 17:57:38 +08:00
wh
f01c991390 Merge branch 'dev54' 2026-05-08 22:39:15 +08:00
wh
4065d993e2 根据界面需求优化 2026-05-08 22:38:32 +08:00
wh
c3fdf96ceb Merge branch 'dev54' 2026-05-08 16:50:01 +08:00
wh
d679340f3c 修改结果状态比较bug 2026-05-08 16:25:44 +08:00
wh
7fbe76559c 后台异常处理优化 2026-05-08 16:07:12 +08:00
wh
2bf06dce54 Merge branch 'dev54' 2026-05-07 16:01:02 +08:00
wh
83a412d3fd QA问答对审核功能以及历史记录归档管理优化 2026-05-07 16:00:17 +08:00
wh
cc2508d014 Merge branch 'dev54' 2026-05-07 00:24:21 +08:00
wh
2ccd8f39fe 标注结果和归档优化 2026-05-07 00:23:27 +08:00
wh
9d8d06427c 标注结果和标注结果归档优化 2026-05-06 19:07:45 +08:00
wh
3d5b693d22 Merge branch 'dev54' 2026-05-06 00:11:50 +08:00
wh
99ffa9d490 修改数据库名称 2026-05-06 00:11:00 +08:00
wh
fbbd73c916 去掉任务中相关模型和提示词绑定,新增资源图片bbox处理 2026-05-05 18:40:44 +08:00
wh
74674990d8 标注结果比较优化 2026-04-29 14:02:01 +08:00
wh
942c68989c Merge commit 'ca81514d4373039356d1684f93ae24c9b2182c79' 2026-04-28 20:14:39 +08:00
wh
ca81514d43 任务管理优化 2026-04-28 20:14:14 +08:00
wh
7e35cf146e Merge commit '343df65c698d579335d9c7c47160b79ae0c14f8a' 2026-04-28 12:15:39 +08:00
wh
343df65c69 系统资源管理优化 2026-04-28 12:15:10 +08:00
wh
a01a06d4c1 提交readme 2026-04-27 23:26:25 +08:00
wh
1314e290d5 Merge commit 'd404c7d1879e1dc8854731e4c8d8578d92fccb1b' 2026-04-27 23:23:06 +08:00
wh
d404c7d187 资源管理模块优化 2026-04-27 23:22:14 +08:00
wh
79812b2c77 新增系统管理员岗位 2026-04-27 16:25:39 +08:00
wh
5662c1fda9 提交代码实现v1.0 2026-04-27 10:27:57 +08:00
wh
849e78658d 去掉演示数据bizdata 2026-04-27 00:05:59 +08:00
wh
ebe8b6c7ed 提交数据库设计 2026-04-26 23:41:58 +08:00
wh
b3c9fdfedd datapermissve优化 2026-04-23 17:38:39 +08:00
wh
298e8eb35c Merge commit '2beebbe4697356016295740ea01df1c99b26adc7' 2026-04-23 17:00:34 +08:00
wh
2beebbe469 优化mybatis方法去掉冗余方法 2026-04-23 17:00:02 +08:00
wh
f087687bfa Merge commit '9483bea005d27391f2d2cc3b394c7a6a460283c7' 2026-04-23 12:31:45 +08:00
wh
9483bea005 取消uplaoder 岗位 2026-04-23 12:27:44 +08:00
135 changed files with 9051 additions and 921 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ CLAUDE.md
*.iws *.iws
.agents/ .agents/
.history/ .history/
.trae/
logs/ logs/
# ========================================== # ==========================================

1723
README.md

File diff suppressed because it is too large Load Diff

20
pom.xml
View File

@@ -15,6 +15,8 @@
<mybatis-plus.version>3.5.3.1</mybatis-plus.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<springdoc-openapi.version>2.3.0</springdoc-openapi.version> <springdoc-openapi.version>2.3.0</springdoc-openapi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -82,6 +84,19 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
@@ -101,8 +116,8 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version> <version>3.11.0</version>
<configuration> <configuration>
<source>${java.version}</source> <source>21</source>
<target>${java.version}</target> <target>21</target>
<annotationProcessorPaths> <annotationProcessorPaths>
<path> <path>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@@ -110,6 +125,7 @@
<version>1.18.30</version> <version>1.18.30</version>
</path> </path>
</annotationProcessorPaths> </annotationProcessorPaths>
<compilerArgs>--enable-preview</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>

View File

@@ -3,9 +3,11 @@ package com.labelsys.backend;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.labelsys.backend.mapper") @MapperScan("com.labelsys.backend.mapper")
@EnableScheduling
public class LabelsysBackendApplication { public class LabelsysBackendApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -11,13 +11,13 @@ import lombok.NoArgsConstructor;
@Schema(description = "统一返回结果") @Schema(description = "统一返回结果")
public class Result<T> { public class Result<T> {
@Schema(description = "业务状态码") @Schema(description = "业务状态码", example = "0")
private Integer code; private Integer code;
@Schema(description = "返回消息") @Schema(description = "返回消息", example = "success")
private String message; private String message;
@Schema(description = "返回数据") @Schema(description = "返回数据", example = "{}")
private T data; private T data;
public static <T> Result<T> success() { public static <T> Result<T> success() {

View File

@@ -3,44 +3,78 @@ package com.labelsys.backend.common.exception;
import com.labelsys.backend.common.Result; import com.labelsys.backend.common.Result;
import com.labelsys.backend.common.ResultCode; import com.labelsys.backend.common.ResultCode;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* <p>
* 处理策略:
* 1. 业务异常BusinessException记录 INFO 级别日志,返回详细错误信息给前端
* 2. 参数校验异常:记录 WARN 级别日志,返回字段错误信息
* 3. 系统异常Exception记录 ERROR 级别日志,隐藏详细信息,返回通用错误提示
*/
@Slf4j
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/**
* 处理业务异常
* 业务异常是预期内的错误,需要详细返回给前端
*/
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException exception, HttpServletResponse response) { public Result<Void> handleBusiness(BusinessException exception, HttpServletResponse response) {
// 记录 INFO 级别日志(业务异常属于预期内错误)
log.info("Business exception occurred: code={}, message={}",
exception.getResultCode().getCode(), exception.getMessage());
response.setStatus(toHttpStatus(exception.getResultCode()).value()); response.setStatus(toHttpStatus(exception.getResultCode()).value());
return new Result<>(exception.getResultCode().getCode(), exception.getMessage(), null); return new Result<>(exception.getResultCode().getCode(), exception.getMessage(), null);
} }
/**
* 处理参数校验异常
*/
@ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class }) @ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class })
public Result<Void> handleValidation(Exception exception, HttpServletResponse response) { public Result<Void> handleValidation(Exception exception, HttpServletResponse response) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
String message; String message;
if (exception instanceof MethodArgumentNotValidException methodArgumentNotValidException) { if (exception instanceof MethodArgumentNotValidException methodException) {
message = methodArgumentNotValidException.getBindingResult().getFieldErrors().stream() message = methodException.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + error.getDefaultMessage()) .map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; ")); .collect(Collectors.joining("; "));
} else if (exception instanceof BindException bindException) { } else if (exception instanceof BindException bindException) {
message = bindException.getBindingResult().getFieldErrors().stream() message = bindException.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + error.getDefaultMessage()) .map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; ")); .collect(Collectors.joining("; "));
} else { } else {
message = ResultCode.BAD_REQUEST.getMessage(); message = ResultCode.BAD_REQUEST.getMessage();
} }
// 记录 WARN 级别日志
log.warn("Validation failed: {}", message);
response.setStatus(HttpStatus.BAD_REQUEST.value());
return new Result<>(ResultCode.BAD_REQUEST.getCode(), message, null); return new Result<>(ResultCode.BAD_REQUEST.getCode(), message, null);
} }
/**
* 处理系统异常
* 系统异常是预期外的错误,需要隐藏详细信息
*/
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public Result<Void> handleUnexpected(Exception exception, HttpServletResponse response) { public Result<Void> handleUnexpected(Exception exception, HttpServletResponse response) {
// 记录 ERROR 级别日志(包含完整堆栈)
log.error("Unexpected exception occurred", exception);
// 返回通用错误信息,不暴露内部细节
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return new Result<>(ResultCode.ERROR.getCode(), exception.getMessage(), null); return new Result<>(ResultCode.ERROR.getCode(), "系统内部错误,请稍后重试", null);
} }
private HttpStatus toHttpStatus(ResultCode resultCode) { private HttpStatus toHttpStatus(ResultCode resultCode) {

View File

@@ -0,0 +1,43 @@
package com.labelsys.backend.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus 配置类
*
* <p>配置 MyBatis Plus 的增强功能,包括分页插件等。
*/
@Configuration
public class MybatisPlusConfig {
/**
* 配置 MyBatis Plus 拦截器
*
* <p>注册分页拦截器,支持 PostgreSQL 数据库的分页查询。
* 设置分页溢出处理为 false超出页数返回空页最大分页限制为 200 条。
*
* @return MyBatis Plus 拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 创建分页拦截器,指定数据库类型为 PostgreSQL
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL);
// 设置分页溢出处理false 表示超出页数时返回空页,不进行页码修正
paginationInnerInterceptor.setOverflow(false);
// 设置最大分页限制200 条,防止一次性查询过多数据
paginationInnerInterceptor.setMaxLimit(200L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}

View File

@@ -0,0 +1,59 @@
package com.labelsys.backend.config;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import java.net.URI;
/**
* 对象存储配置类
*
* <p>配置 AWS S3 客户端,用于与对象存储服务(如 MinIO、AWS S3进行交互。
* 通过配置属性读取 endpoint、region、accessKey、secretKey 等参数。
*/
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(ObjectStorageProperties.class)
public class ObjectStorageConfig {
/**
* 对象存储配置属性
*/
private final ObjectStorageProperties properties;
/**
* 创建并配置 S3 客户端
*
* 配置内容包括:
* - endpoint: 对象存储服务地址
* - region: 区域标识
* - credentials: 访问密钥accessKey 和 secretKey
* - pathStyleAccess: 是否启用路径风格访问
*
* @return 配置完成的 S3 客户端
*/
@Bean
public S3Client s3Client() {
return S3Client.builder()
// 设置自定义 endpoint支持非 AWS S3 服务如 MinIO
.endpointOverride(URI.create(properties.getEndpoint()))
// 设置区域
.region(Region.of(properties.getRegion()))
// 设置凭证提供者
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
// 配置路径风格访问(对于 MinIO 等自建存储服务通常需要启用)
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(properties.isPathStyleAccess())
.build())
.build();
}
}

View File

@@ -0,0 +1,56 @@
package com.labelsys.backend.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 对象存储配置属性类
*
* 用于读取 `labelsys.object-storage` 前缀的配置项,
* 包括连接信息和存储桶配置。
*/
@Data
@ConfigurationProperties(prefix = "labelsys.object-storage")
public class ObjectStorageProperties {
/**
* 对象存储服务地址
*/
private String endpoint;
/**
* 区域标识
*/
private String region;
/**
* 访问密钥 ID
*/
private String accessKey;
/**
* 访问密钥
*/
private String secretKey;
/**
* 是否启用路径风格访问(默认 true适用于 MinIO 等自建存储)
*/
private boolean pathStyleAccess = true;
/**
* 源文件存储桶名称
*/
private String sourceBucket;
/**
* 产物存储桶名称
*/
private String artifactBucket;
/**
* 导出文件存储桶名称
*/
private String exportBucket;
}

View File

@@ -5,9 +5,19 @@ import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/**
* OpenAPI (Swagger) 配置类
*
* 配置 API 文档的基本信息,包括标题、版本和描述。
*/
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
/**
* 创建 OpenAPI 文档配置
*
* @return OpenAPI 配置实例
*/
@Bean @Bean
public OpenAPI labelsysOpenApi() { public OpenAPI labelsysOpenApi() {
return new OpenAPI().info(new Info() return new OpenAPI().info(new Info()
@@ -15,4 +25,5 @@ public class OpenApiConfig {
.version("0.0.1") .version("0.0.1")
.description("公司、员工、认证、岗位权限和数据权限接口")); .description("公司、员工、认证、岗位权限和数据权限接口"));
} }
} }

View File

@@ -5,9 +5,22 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 安全配置类
*
* 配置安全相关的 Bean如密码编码器等。
*/
@Configuration @Configuration
public class SecurityBeanConfig { public class SecurityBeanConfig {
/**
* 创建密码编码器
*
* <p>使用 BCrypt 算法对密码进行加密BCrypt 是一种安全的密码哈希算法,
* 具有自动加盐和可配置的计算复杂度特性。
*
* @return BCryptPasswordEncoder 实例
*/
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();

View File

@@ -6,14 +6,30 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置类
* <p>
* 实现 WebMvcConfigurer 接口,配置 Spring MVC 拦截器等功能。
*/
@Configuration @Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
/**
* 认证拦截器
*/
private final AuthInterceptor authInterceptor; private final AuthInterceptor authInterceptor;
/**
* 注册拦截器
* <p>
* 将认证拦截器注册到拦截器链中,拦截所有 `/api/**` 路径的请求。
*
* @param registry 拦截器注册器
*/
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**"); registry.addInterceptor(authInterceptor).addPathPatterns("/api/**");
} }
} }

View File

@@ -7,37 +7,28 @@ import com.labelsys.backend.enums.UserRole;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "当前登录用户上下文") @Schema(description = "当前登录用户上下文")
public record LoginUser( public record LoginUser(@Schema(description = "用户ID") Long userId, @Schema(description = "公司ID") Long companyId,
@Schema(description = "用户ID") Long userId, @Schema(description = "公司编码") String companyCode, @Schema(description = "公司名称") String companyName,
@Schema(description = "公司ID") Long companyId,
@Schema(description = "公司编码") String companyCode,
@Schema(description = "公司名称") String companyName,
@Schema(description = "手机号") String phone, @Schema(description = "手机号") String phone,
@Schema(description = "用户名,可为空", example = "alpha-reviewer") String username, @Schema(description = "用户名,可为空", example = "alpha-reviewer") String username,
@Schema(description = "真实姓名") String realName, @Schema(description = "真实姓名") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role,
@Schema(description = "岗位枚举值UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, @Schema(
description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员、SUPER_ADMIN系统管理员") UserPosition position,
@Schema(description = "是否必须修改密码") boolean mustChangePassword, @Schema(description = "是否必须修改密码") boolean mustChangePassword,
@Schema(description = "会话版本") Integer sessionVersion @Schema(description = "会话版本") Integer sessionVersion) {
) {
public static LoginUser from(SysUser user, SysCompany company) { public static LoginUser from(SysUser user, SysCompany company) {
return new LoginUser( return new LoginUser(user.getId(), company.getId(), company.getCompanyCode(), company.getCompanyName(),
user.getId(), user.getPhone(), user.getUsername(), user.getRealName(), user.getRole(), user.getPosition(),
company.getId(), Boolean.TRUE.equals(user.getMustChangePassword()), user.getSessionVersion());
company.getCompanyCode(),
company.getCompanyName(),
user.getPhone(),
user.getUsername(),
user.getRealName(),
user.getRole(),
user.getPosition(),
Boolean.TRUE.equals(user.getMustChangePassword()),
user.getSessionVersion()
);
} }
public boolean isPlatformAdmin() { // public boolean isPlatformAdmin() {
return "PLATFORM".equals(companyCode) && position == UserPosition.ADMIN; // return "PLATFORM".equals(companyCode) && position == UserPosition.ADMIN;
// }
public boolean isSuperAdmin() {
return position == UserPosition.SUPER_ADMIN;
} }
} }

View File

@@ -1,27 +1,61 @@
package com.labelsys.backend.context; package com.labelsys.backend.context;
import com.labelsys.backend.common.exception.UnauthorizedException; import com.labelsys.backend.common.exception.UnauthorizedException;
import java.util.Optional; import java.util.Optional;
/**
* 用户上下文管理类
*
* 使用 ThreadLocal 存储当前线程的登录用户信息,
* 实现请求级别的用户上下文传递。
*/
public final class UserContext { public final class UserContext {
/**
* 用户上下文持有者,存储当前线程的登录用户
*/
private static final ThreadLocal<LoginUser> HOLDER = new ThreadLocal<>(); private static final ThreadLocal<LoginUser> HOLDER = new ThreadLocal<>();
/**
* 私有构造函数,防止实例化
*/
private UserContext() { private UserContext() {
} }
/**
* 设置当前登录用户
*
* @param loginUser 登录用户信息
*/
public static void set(LoginUser loginUser) { public static void set(LoginUser loginUser) {
HOLDER.set(loginUser); HOLDER.set(loginUser);
} }
/**
* 获取当前登录用户
*
* @return 当前登录用户的 Optional 对象,未登录时返回 empty
*/
public static Optional<LoginUser> get() { public static Optional<LoginUser> get() {
return Optional.ofNullable(HOLDER.get()); return Optional.ofNullable(HOLDER.get());
} }
/**
* 获取当前登录用户,未登录时抛出异常
*
* @return 当前登录用户
* @throws UnauthorizedException 未登录或登录已过期时抛出
*/
public static LoginUser requireUser() { public static LoginUser requireUser() {
return get().orElseThrow(() -> new UnauthorizedException("未登录或登录已过期")); return get().orElseThrow(() -> new UnauthorizedException("未登录或登录已过期"));
} }
/**
* 清理用户上下文
*
* 在请求结束时调用,防止 ThreadLocal 内存泄漏
*/
public static void clear() { public static void clear() {
HOLDER.remove(); HOLDER.remove();
} }

View File

@@ -0,0 +1,46 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.request.SaveAgentConfigRequest;
import com.labelsys.backend.dto.response.AgentConfigListResponse;
import com.labelsys.backend.entity.AnnotationAgentConfig;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.AnnotationAgentConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "Agent配置管理")
@RestController
@RequestMapping("/api/agent-configs")
@RequiredArgsConstructor
public class AnnotationAgentConfigController {
private final AnnotationAgentConfigService annotationAgentConfigService;
@Operation(summary = "保存Agent配置")
@PostMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<AgentConfigListResponse> save(@Valid @RequestBody SaveAgentConfigRequest request) {
return Result.success(
annotationAgentConfigService.saveAgentConfigs(UserContext.requireUser(), request));
}
@Operation(summary = "获取公司Agent对应配置列表模型配置和提示词配置")
@GetMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<AgentConfigListResponse> list() {
var user = UserContext.requireUser();
return Result.success(
annotationAgentConfigService.getAgentConfigsForCompany(user.companyId()));
}
}

View File

@@ -0,0 +1,51 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultHistoryPageQuery;
import com.labelsys.backend.dto.response.AnnotationResultHistoryDetailResponse;
import com.labelsys.backend.dto.response.AnnotationResultHistoryResponse;
import com.labelsys.backend.service.AnnotationResultArchiveService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/annotation-result-history")
@RequiredArgsConstructor
@Tag(name = "标注结果归档管理", description = "标注结果归档历史查询接口")
public class AnnotationResultArchiveController {
private final AnnotationResultArchiveService annotationResultArchiveService;
@Operation(summary = "分页查询归档历史")
@GetMapping
public ResponseEntity<PageResult<AnnotationResultHistoryResponse>> pageHistory(
@Valid AnnotationResultHistoryPageQuery query) {
return ResponseEntity.ok(annotationResultArchiveService.pageHistory(UserContext.requireUser(), query));
}
@Operation(summary = "查询归档历史详情")
@GetMapping("/{id}")
public ResponseEntity<AnnotationResultHistoryDetailResponse> getHistory(
@Parameter(description = "历史记录ID", example = "901")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultArchiveService.getHistory(UserContext.requireUser(), id));
}
}
// @Operation(summary = "加载归档文件内容")
// @GetMapping("/{id}/content")
// public ResponseEntity<FileContentResponse> loadFileContent(
// @Parameter(description = "历史记录ID", example = "901")
// @PathVariable Long id) {
// return ResponseEntity.ok(annotationResultArchiveService.loadFileContent(UserContext.requireUser(), id));
// }
//}

View File

@@ -0,0 +1,68 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationResultPageQuery;
import com.labelsys.backend.dto.request.MergeReviewResultRequest;
import com.labelsys.backend.dto.response.AnnotationResultCompareResponse;
import com.labelsys.backend.dto.response.AnnotationResultDetailResponse;
import com.labelsys.backend.dto.response.AnnotationResultResponse;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.AnnotationResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/annotation-results")
@RequiredArgsConstructor
@Tag(name = "标注结果管理", description = "标注结果相关接口")
public class AnnotationResultController {
private final AnnotationResultService annotationResultService;
@Operation(summary = "分页查询标注结果")
@GetMapping
public ResponseEntity<PageResult<AnnotationResultResponse>> pageResults(
@Valid AnnotationResultPageQuery query) {
return ResponseEntity.ok(annotationResultService.pageResults(UserContext.requireUser(), query));
}
@Operation(summary = "查询标注结果详情")
@GetMapping("/{id}")
public ResponseEntity<AnnotationResultDetailResponse> getResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultService.getResult(UserContext.requireUser(), id));
}
@Operation(summary = "查询结果比对信息REVIEWER岗位以上可操作")
@GetMapping("/{id}/compare")
@RequirePosition(UserPosition.REVIEWER)
public ResponseEntity<AnnotationResultCompareResponse> compareResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id) {
return ResponseEntity.ok(annotationResultService.compareResult(UserContext.requireUser(), id));
}
@Operation(summary = "提交合并审核结果,REVIEWER岗位以上可操作")
@PostMapping("/{id}/merge")
@RequirePosition(UserPosition.REVIEWER)
public ResponseEntity<Void> mergeReviewResult(
@Parameter(description = "结果ID", example = "191000000000000401")
@PathVariable Long id,
@Valid @RequestBody MergeReviewResultRequest request) {
annotationResultService.mergeReviewResult(UserContext.requireUser(), id, request);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,68 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.AnnotationTaskPageQuery;
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
import com.labelsys.backend.dto.response.AnnotationTaskResponse;
import com.labelsys.backend.service.AnnotationTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "标注任务管理")
@RestController
@RequestMapping("/api/annotation-tasks")
@RequiredArgsConstructor
public class AnnotationTaskController {
private final AnnotationTaskService annotationTaskService;
@Operation(summary = "创建标注任务")
@PostMapping
public Result<AnnotationTaskResponse> create(@Valid @RequestBody CreateAnnotationTaskRequest request) {
return Result.success(annotationTaskService.createTask(UserContext.requireUser(), request));
}
@Operation(summary = "更新标注任务")
@PutMapping("/{id}")
public Result<AnnotationTaskResponse> update(
@Parameter(description = "任务ID", example = "191000000000000301") @PathVariable Long id,
@Valid @RequestBody UpdateAnnotationTaskRequest request) {
return Result.success(annotationTaskService.updateTask(UserContext.requireUser(), id, request));
}
@Operation(summary = "分页查询标注任务")
@GetMapping
public Result<PageResult<AnnotationTaskResponse>> page(@ParameterObject AnnotationTaskPageQuery query) {
return Result.success(annotationTaskService.pageTasks(UserContext.requireUser(), query));
}
@Operation(summary = "查询标注任务详情")
@GetMapping("/{id}")
public Result<AnnotationTaskResponse> detail(
@Parameter(description = "任务ID", example = "191000000000000301") @PathVariable Long id) {
return Result.success(annotationTaskService.getTask(UserContext.requireUser(), id));
}
@Operation(summary = "删除标注任务")
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(description = "任务ID", example = "191000000000000301") @PathVariable Long id) {
annotationTaskService.deleteTask(UserContext.requireUser(), id);
return Result.success();
}
}

View File

@@ -10,6 +10,7 @@ import com.labelsys.backend.dto.response.CurrentUserResponse;
import com.labelsys.backend.dto.response.LoginResponse; import com.labelsys.backend.dto.response.LoginResponse;
import com.labelsys.backend.service.AuthService; import com.labelsys.backend.service.AuthService;
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.validation.Valid; import jakarta.validation.Valid;
import java.util.List; import java.util.List;
@@ -33,7 +34,10 @@ public class AuthController {
@Operation(summary = "根据手机号查询可登录公司") @Operation(summary = "根据手机号查询可登录公司")
@GetMapping("/companies") @GetMapping("/companies")
public Result<List<CompanyOptionResponse>> listCompanies(@RequestParam String phone) { public Result<List<CompanyOptionResponse>> listCompanies(
@Parameter(description = "手机号", example = "13800138000")
@RequestParam String phone
) {
return Result.success(authService.listAvailableCompanies(phone)); return Result.success(authService.listAvailableCompanies(phone));
} }
@@ -52,7 +56,10 @@ public class AuthController {
@Operation(summary = "退出登录") @Operation(summary = "退出登录")
@PostMapping("/logout") @PostMapping("/logout")
public Result<Void> logout(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) { public Result<Void> logout(
@Parameter(description = "Bearer 访问令牌", example = "Bearer eyJhbGciOiJIUzI1NiJ9.demo.token")
@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization
) {
authService.logout(extractToken(authorization)); authService.logout(extractToken(authorization));
return Result.success(); return Result.success();
} }

View File

@@ -10,6 +10,7 @@ import com.labelsys.backend.dto.response.UserResponse;
import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.UserService; import com.labelsys.backend.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.validation.Valid; import jakarta.validation.Valid;
import java.util.List; import java.util.List;
@@ -45,14 +46,22 @@ public class CompanyUserController {
@Operation(summary = "修改员工角色和岗位") @Operation(summary = "修改员工角色和岗位")
@PutMapping("/{userId}/assignment") @PutMapping("/{userId}/assignment")
public Result<Void> updateAssignment(@PathVariable Long userId, @Valid @RequestBody UpdateUserAssignmentRequest request) { public Result<Void> updateAssignment(
@Parameter(description = "用户ID", example = "191000000000000021")
@PathVariable Long userId,
@Valid @RequestBody UpdateUserAssignmentRequest request
) {
userService.updateAssignment(UserContext.requireUser(), userId, request); userService.updateAssignment(UserContext.requireUser(), userId, request);
return Result.success(); return Result.success();
} }
@Operation(summary = "修改员工状态") @Operation(summary = "修改员工状态")
@PutMapping("/{userId}/status") @PutMapping("/{userId}/status")
public Result<Void> updateStatus(@PathVariable Long userId, @Valid @RequestBody UpdateUserStatusRequest request) { public Result<Void> updateStatus(
@Parameter(description = "用户ID", example = "191000000000000021")
@PathVariable Long userId,
@Valid @RequestBody UpdateUserStatusRequest request
) {
userService.updateStatus(UserContext.requireUser(), userId, request); userService.updateStatus(UserContext.requireUser(), userId, request);
return Result.success(); return Result.success();
} }

View File

@@ -4,11 +4,13 @@ import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.common.Result; import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext; import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.request.CreateCompanyAdminRequest; import com.labelsys.backend.dto.request.CreateCompanyAdminRequest;
import com.labelsys.backend.dto.request.CreateSystemEngineerAdminRequest;
import com.labelsys.backend.dto.request.UpdateUserStatusRequest; import com.labelsys.backend.dto.request.UpdateUserStatusRequest;
import com.labelsys.backend.dto.response.UserResponse; import com.labelsys.backend.dto.response.UserResponse;
import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.UserService; import com.labelsys.backend.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.validation.Valid; import jakarta.validation.Valid;
import java.util.List; import java.util.List;
@@ -25,7 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
@Tag(name = "平台公司管理员管理") @Tag(name = "平台公司管理员管理")
@RestController @RestController
@RequestMapping("/api/platform/company-admins") @RequestMapping("/api/platform/company-admins")
@RequirePosition(UserPosition.ADMIN) @RequirePosition(UserPosition.SUPER_ADMIN)
@RequiredArgsConstructor @RequiredArgsConstructor
public class PlatformCompanyAdminController { public class PlatformCompanyAdminController {
@@ -33,20 +35,37 @@ public class PlatformCompanyAdminController {
@Operation(summary = "查询指定公司管理员列表") @Operation(summary = "查询指定公司管理员列表")
@GetMapping @GetMapping
public Result<List<UserResponse>> listCompanyAdmins(@RequestParam Long companyId) { public Result<List<UserResponse>> listCompanyAdmins(
@Parameter(description = "公司ID", example = "191000000000000001")
@RequestParam Long companyId
) {
return Result.success(userService.listCompanyAdmins(UserContext.requireUser(), companyId).stream().map(UserResponse::from).toList()); return Result.success(userService.listCompanyAdmins(UserContext.requireUser(), companyId).stream().map(UserResponse::from).toList());
} }
@Operation(summary = "查询所有公司用户列表")
@GetMapping("/all")
public Result<List<UserResponse>> listAllUsers() {
return Result.success(userService.listAllUsers(UserContext.requireUser()).stream().map(UserResponse::from).toList());
}
@Operation(summary = "创建公司管理员") @Operation(summary = "创建公司管理员")
@PostMapping @PostMapping
public Result<UserResponse> createCompanyAdmin(@Valid @RequestBody CreateCompanyAdminRequest request) { public Result<UserResponse> createCompanyAdmin(@Valid @RequestBody CreateCompanyAdminRequest request) {
return Result.success(UserResponse.from(userService.createCompanyAdmin(UserContext.requireUser(), request))); return Result.success(UserResponse.from(userService.createCompanyAdmin(UserContext.requireUser(), request)));
} }
@Operation(summary = "创建系统管理员")
@PostMapping("/system-engineer")
public Result<UserResponse> createSystemEngineerAdmin(@Valid @RequestBody CreateSystemEngineerAdminRequest request) {
return Result.success(UserResponse.from(userService.createSystemEngineerAdmin(UserContext.requireUser(), request)));
}
@Operation(summary = "修改公司管理员状态") @Operation(summary = "修改公司管理员状态")
@PutMapping("/{companyId}/{userId}/status") @PutMapping("/{companyId}/{userId}/status")
public Result<Void> updateCompanyAdminStatus( public Result<Void> updateCompanyAdminStatus(
@Parameter(description = "公司ID", example = "191000000000000001")
@PathVariable Long companyId, @PathVariable Long companyId,
@Parameter(description = "用户ID", example = "191000000000000021")
@PathVariable Long userId, @PathVariable Long userId,
@Valid @RequestBody UpdateUserStatusRequest request @Valid @RequestBody UpdateUserStatusRequest request
) { ) {

View File

@@ -9,6 +9,7 @@ import com.labelsys.backend.dto.response.CompanyResponse;
import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.CompanyService; import com.labelsys.backend.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 jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.List; import java.util.List;
@@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
@Tag(name = "平台公司管理") @Tag(name = "平台公司管理")
@RestController @RestController
@RequestMapping("/api/platform/companies") @RequestMapping("/api/platform/companies")
@RequirePosition(UserPosition.ADMIN) @RequirePosition(UserPosition.SUPER_ADMIN)
@RequiredArgsConstructor @RequiredArgsConstructor
public class PlatformCompanyController { public class PlatformCompanyController {
@@ -44,7 +45,11 @@ public class PlatformCompanyController {
@Operation(summary = "修改公司状态") @Operation(summary = "修改公司状态")
@PutMapping("/{companyId}/status") @PutMapping("/{companyId}/status")
public Result<Void> updateCompanyStatus(@PathVariable Long companyId, @Valid @RequestBody UpdateCompanyStatusRequest request) { public Result<Void> updateCompanyStatus(
@Parameter(description = "公司ID", example = "191000000000000001")
@PathVariable Long companyId,
@Valid @RequestBody UpdateCompanyStatusRequest request
) {
companyService.updateStatus(UserContext.requireUser(), companyId, request); companyService.updateStatus(UserContext.requireUser(), companyId, request);
return Result.success(); return Result.success();
} }

View File

@@ -0,0 +1,120 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.SaveImageBboxRequest;
import com.labelsys.backend.dto.request.SourceResourcePageQuery;
import com.labelsys.backend.dto.request.SourceUploadRequest;
import com.labelsys.backend.dto.response.ImageBboxResponse;
import com.labelsys.backend.dto.response.SourceResourceResponse;
import com.labelsys.backend.dto.response.SourceUploadResponse;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.service.SourceResourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "资源管理")
@RestController
@RequestMapping("/api/source-resources")
@RequiredArgsConstructor
public class SourceResourceController {
private final SourceResourceService sourceResourceService;
@Operation(summary = "上传资源")
@PostMapping("/upload")
public Result<SourceUploadResponse> upload(@ParameterObject @ModelAttribute SourceUploadRequest request) {
return Result.success(sourceResourceService.upload(UserContext.requireUser(), request));
}
@Operation(summary = "分页查询资源")
@GetMapping
public Result<PageResult<SourceResourceResponse>> page(
@ParameterObject @ModelAttribute SourceResourcePageQuery query) {
return Result.success(sourceResourceService.pageResources(UserContext.requireUser(), query));
}
@Operation(summary = "查询资源详情")
@GetMapping("/{id}")
public Result<SourceResourceResponse> detail(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
return Result.success(sourceResourceService.getResource(UserContext.requireUser(), id));
}
@Operation(summary = "删除资源")
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
sourceResourceService.deleteResource(UserContext.requireUser(), id);
return Result.success();
}
@Operation(summary = "下载资源")
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadResource(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
var currentUser = UserContext.requireUser();
byte[] resourceData = sourceResourceService.downloadResource(currentUser, id);
// 获取资源信息以确定Content-Type
SourceResource resource = sourceResourceService.getResourceEntity(id);
String contentType = sourceResourceService.getContentType(resource);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getResourceName() + "\"")
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(resource.getFileSize()))
.body(resourceData);
}
// 添加新接口
@Operation(summary = "查询图片资源BBOX标注")
@GetMapping("/{id}/bbox")
public Result<ImageBboxResponse> getImageBbox(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
return Result.success(sourceResourceService.getImageBbox(UserContext.requireUser(), id));
}
@Operation(summary = "保存图片资源BBOX标注")
@PostMapping("/{id}/bbox")
public Result<ImageBboxResponse> saveImageBbox(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id,
@Valid @RequestBody SaveImageBboxRequest request
) {
return Result.success(sourceResourceService.saveImageBbox(UserContext.requireUser(), id, request));
}
@Operation(summary = "删除图片资源BBOX标注")
@DeleteMapping("/{id}/bbox")
public Result<Void> deleteImageBbox(
@Parameter(description = "资源ID", example = "191000000000000101")
@PathVariable Long id
) {
sourceResourceService.deleteImageBbox(UserContext.requireUser(), id);
return Result.success();
}
}

View File

@@ -0,0 +1,67 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery;
import com.labelsys.backend.dto.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.SysConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "系统配置管理")
@RestController
@RequestMapping("/api/sys-configs")
@RequiredArgsConstructor
public class SysConfigController {
private final SysConfigService sysConfigService;
@Operation(summary = "创建系统配置")
@PostMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<SysConfigResponse> create(@Valid @RequestBody SaveSysConfigRequest request) {
return Result
.success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request)));
}
@Operation(summary = "更新系统配置")
@PutMapping("/{id}")
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<SysConfigResponse> update(
@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id,
@Valid @RequestBody UpdateSysConfigRequest request) {
return Result.success(
sysConfigService.toResponse(sysConfigService.updateConfig(UserContext.requireUser(), id, request)));
}
@Operation(summary = "分页查询系统配置")
@GetMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<PageResult<SysConfigResponse>> page(@ParameterObject SysConfigPageQuery query) {
return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query));
}
@Operation(summary = "查询配置详情")
@GetMapping("/{id}")
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<SysConfigResponse> detail(
@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id) {
return Result.success(sysConfigService.getConfigDetail(UserContext.requireUser(), id));
}
}

View File

@@ -0,0 +1,20 @@
package com.labelsys.backend.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "大模型配置模型")
@Data
public class LlmConfigModel {
@Schema(description = "多模态类型枚举")
private String llmType;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "模型调用地址")
private String modelUrl;
@Schema(description = "API密钥加密存储")
private String apiKey;
}

View File

@@ -0,0 +1,17 @@
package com.labelsys.backend.dto.common;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "分页结果")
public record PageResult<T>(
@Schema(description = "当前页记录", example = "[]") List<T> records,
@Schema(description = "总记录数", example = "2") Long total,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
public static <T> PageResult<T> from(IPage<T> page) {
return new PageResult<>(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
}
}

View File

@@ -0,0 +1,13 @@
// AnnotationResultHistoryPageQuery.java
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "归档历史分页查询")
public record AnnotationResultHistoryPageQuery(
@Schema(description = "任务ID", example = "701") Long taskId,
@Schema(description = "资源ID", example = "601") Long resourceId,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
}

View File

@@ -0,0 +1,14 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注结果分页查询请求")
public record AnnotationResultPageQuery(
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "运行态状态", example = "MANUAL_REVIEW_PENDING") String runtimeStatus,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
}

View File

@@ -0,0 +1,17 @@
package com.labelsys.backend.dto.request;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标注任务分页查询请求")
public record AnnotationTaskPageQuery(
@Schema(description = "关键字", example = "运输") String keyword,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize
) {
}

View File

@@ -0,0 +1,16 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
@Schema(description = "BBOX坐标对象")
public record BboxCoordinate(
@Schema(description = "BBOX标识", example = "bbox_001") String id,
@NotNull @Schema(description = "左上角X坐标", example = "100") Integer x,
@NotNull @Schema(description = "左上角Y坐标", example = "50") Integer y,
@NotNull @Schema(description = "宽度", example = "200") Integer width,
@NotNull @Schema(description = "高度", example = "150") Integer height,
@Schema(description = "标注标签", example = "车辆") String label
//@Schema(description = "置信度", example = "0.95") Double confidence
) {
}

View File

@@ -5,8 +5,8 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "修改密码请求") @Schema(description = "修改密码请求")
public record ChangePasswordRequest( public record ChangePasswordRequest(
@Schema(description = "旧密码") @NotBlank(message = "不能为空") String oldPassword, @Schema(description = "旧密码", example = "P@ssw0rd!") @NotBlank(message = "不能为空") String oldPassword,
@Schema(description = "新密码") @NotBlank(message = "不能为空") String newPassword, @Schema(description = "新密码", example = "N3wP@ssw0rd!") @NotBlank(message = "不能为空") String newPassword,
@Schema(description = "确认新密码") @NotBlank(message = "不能为空") String confirmPassword @Schema(description = "确认新密码", example = "N3wP@ssw0rd!") @NotBlank(message = "不能为空") String confirmPassword
) { ) {
} }

View File

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

View File

@@ -6,9 +6,9 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "创建公司管理员请求") @Schema(description = "创建公司管理员请求")
public record CreateCompanyAdminRequest( public record CreateCompanyAdminRequest(
@Schema(description = "公司ID") @NotNull(message = "不能为空") Long companyId, @Schema(description = "公司ID", example = "191000000000000001") @NotNull(message = "不能为空") Long companyId,
@Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, @Schema(description = "手机号", example = "13800138001") @NotBlank(message = "不能为空") String phone,
@Schema(description = "用户名,前端展示用,可为空", example = "platform-ops") String username, @Schema(description = "用户名,前端展示用,可为空", example = "platform-ops") String username,
@Schema(description = "真实姓名") @NotBlank(message = "不能为空") String realName @Schema(description = "真实姓名", example = "平台运维") @NotBlank(message = "不能为空") String realName
) { ) {
} }

View File

@@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "创建公司请求") @Schema(description = "创建公司请求")
public record CreateCompanyRequest( public record CreateCompanyRequest(
@Schema(description = "公司编码") @NotBlank(message = "不能为空") String companyCode, @Schema(description = "公司编码", example = "ALPHA") @NotBlank(message = "不能为空") String companyCode,
@Schema(description = "公司名称") @NotBlank(message = "不能为空") String companyName @Schema(description = "公司名称", example = "阿尔法标注有限公司") @NotBlank(message = "不能为空") String companyName
) { ) {
} }

View File

@@ -0,0 +1,11 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "创建系统工程师管理员请求")
public record CreateSystemEngineerAdminRequest(
@Schema(description = "手机号", example = "13800138002") @NotBlank(message = "不能为空") String phone,
@Schema(description = "用户名,前端展示用,可为空", example = "system-engineer") String username,
@Schema(description = "真实姓名", example = "系统工程师") @NotBlank(message = "不能为空") String realName) {
}

View File

@@ -8,10 +8,10 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "创建员工请求") @Schema(description = "创建员工请求")
public record CreateUserRequest( public record CreateUserRequest(
@Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, @Schema(description = "手机号", example = "13800138002") @NotBlank(message = "不能为空") String phone,
@Schema(description = "用户名,前端展示用,可为空", example = "alpha-admin") String username, @Schema(description = "用户名,前端展示用,可为空", example = "alpha-admin") String username,
@Schema(description = "真实姓名") @NotBlank(message = "不能为空") String realName, @Schema(description = "真实姓名", example = "张审核") @NotBlank(message = "不能为空") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") @NotNull(message = "不能为空") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") @NotNull(message = "不能为空") UserRole role,
@Schema(description = "岗位,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") @NotNull(message = "不能为空") UserPosition position @Schema(description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "ANNOTATOR") @NotNull(message = "不能为空") UserPosition position
) { ) {
} }

View File

@@ -5,8 +5,8 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "登录请求") @Schema(description = "登录请求")
public record LoginRequest( public record LoginRequest(
@Schema(description = "手机号") @NotBlank(message = "不能为空") String phone, @Schema(description = "手机号", example = "平台管理员:13900000000") @NotBlank(message = "不能为空") String phone,
@Schema(description = "公司编码") @NotBlank(message = "不能为空") String companyCode, @Schema(description = "公司编码", example = "平台编号:PLATFORM") @NotBlank(message = "不能为空") String companyCode,
@Schema(description = "登录密码") @NotBlank(message = "不能为空") String password @Schema(description = "登录密码", example = "平台密码:admin@123") @NotBlank(message = "不能为空") String password
) { ) {
} }

View File

@@ -0,0 +1,15 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map;
@Schema(description = "合并审核结果请求")
public record MergeReviewResultRequest(
@Schema(description = "合并后的答案映射key为qa记录IDvalue为合并后的答案")
Map<String, String> mergedAnswers,
@Schema(description = "每条QA记录的审核评论映射key为qa记录IDvalue为审核评论")
Map<String, String> reviewComments
) {
}

View File

@@ -0,0 +1,28 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
@Schema(description = "保存Agent配置请求: 请求体中agentConfigs为map结构key为AgentType枚举值值为用户选择的模型配置ID和提示词配置ID")
@Data
public class SaveAgentConfigRequest {
@Schema(description = "Agent配置映射key为AgentType枚举类型codevalue为配置信息。支持的Agent类型RedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
@NotNull(message = "Agent配置不能为空")
private Map<String, AgentConfigItem> agentConfigs;
@Schema(description = "Agent配置项")
@Data
public static class AgentConfigItem {
@Schema(description = "模型配置ID", example = "191000000000000501")
@NotNull(message = "模型配置ID不能为空")
private Long modelConfigId;
@Schema(description = "提示词配置ID", example = "191000000000000502")
private Long promptConfigId;
}
}

View File

@@ -0,0 +1,15 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Schema(description = "保存图片BBOX请求")
public record SaveImageBboxRequest(
@NotEmpty @Schema(description = "BBOX坐标列表")
@Valid List<BboxCoordinate> bboxes,
@Schema(description = "备注") String remark
) {
}

View File

@@ -0,0 +1,14 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "保存系统配置请求")
public record SaveSysConfigRequest(
@Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项", example = "MODEL") @NotBlank(message = "配置类型不能为空") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") @NotBlank(message = "配置名称不能为空") String configName,
@Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank(
message = "配置值不能为空") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status) {
}

View File

@@ -0,0 +1,19 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源分页查询请求")
public record SourceResourcePageQuery(
@Schema(description = "关键字", example = "运输") String keyword,
@Schema(description = "资源类型", example = "TEXT") String resourceType,
@Schema(description = "页码可选与pageSize同时提供时启用分页", example = "1") Integer pageNo,
@Schema(description = "每页数量可选与pageNo同时提供时启用分页", example = "10") Integer pageSize
) {
/**
* 判断是否需要分页
* @return true表示需要分页false表示查询全部
*/
public boolean needPagination() {
return pageNo != null && pageSize != null;
}
}

View File

@@ -0,0 +1,22 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Schema(description = "资源上传请求")
public class SourceUploadRequest {
@Schema(description = "资源名称", example = "2026年4月运输巡检记录")
private String resourceName;
@Schema(description = "资源类型TEXT、IMAGE、VIDEO", example = "TEXT")
private String resourceType;
@Schema(description = "备注", example = "第一批导入样本")
private String remark;
@Schema(description = "上传文件", example = "inspection-record.txt")
private MultipartFile file;
}

View File

@@ -0,0 +1,10 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "系统配置分页查询请求")
public record SysConfigPageQuery(@Schema(description = "配置类型", example = "MODEL") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "配置状态", example = "ENABLED") String status,
@Schema(description = "页码", example = "1") Integer pageNo,
@Schema(description = "每页数量", example = "10") Integer pageSize) {}

View File

@@ -0,0 +1,13 @@
package com.labelsys.backend.dto.request;
import java.util.List;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "更新标注任务请求")
public record UpdateAnnotationTaskRequest(
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds) {
}

View File

@@ -6,6 +6,6 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "修改公司状态请求") @Schema(description = "修改公司状态请求")
public record UpdateCompanyStatusRequest( public record UpdateCompanyStatusRequest(
@Schema(description = "公司状态枚举值ENABLED启用、DISABLED禁用") @NotNull(message = "不能为空") CompanyStatus status @Schema(description = "公司状态枚举值ENABLED启用、DISABLED禁用", example = "ENABLED") @NotNull(message = "不能为空") CompanyStatus status
) { ) {
} }

View File

@@ -0,0 +1,14 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "更新系统配置请求")
public record UpdateSysConfigRequest(
@Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项\", example = \"MODEL\"", example = "MODEL") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank(
message = "配置值不能为空") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status) {}

View File

@@ -7,7 +7,7 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "修改员工角色岗位请求") @Schema(description = "修改员工角色岗位请求")
public record UpdateUserAssignmentRequest( public record UpdateUserAssignmentRequest(
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") @NotNull(message = "不能为空") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") @NotNull(message = "不能为空") UserRole role,
@Schema(description = "岗位,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") @NotNull(message = "不能为空") UserPosition position @Schema(description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") @NotNull(message = "不能为空") UserPosition position
) { ) {
} }

View File

@@ -6,6 +6,6 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "修改员工状态请求") @Schema(description = "修改员工状态请求")
public record UpdateUserStatusRequest( public record UpdateUserStatusRequest(
@Schema(description = "用户状态枚举值ENABLED启用、DISABLED禁用") @NotNull(message = "不能为空") UserStatus status @Schema(description = "用户状态枚举值ENABLED启用、DISABLED禁用", example = "DISABLED") @NotNull(message = "不能为空") UserStatus status
) { ) {
} }

View File

@@ -0,0 +1,59 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "Agent配置信息响应")
@Data
@Builder
public class AgentConfigInfoResponse {
@Schema(description = "配置ID")
private Long id;
@Schema(description = "Agent类型。支持的Agent类型RedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
private String agentType;
@Schema(description = "模型配置信息")
private ModelConfigInfo modelConfig;
@Schema(description = "Prompt配置信息")
private PromptConfigInfo promptConfig;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Data
@Builder
@Schema(description = "模型配置信息")
public static class ModelConfigInfo {
@Schema(description = "模型配置ID")
private Long id;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "模型URL")
private String modelUrl;
@Schema(description = "API密钥")
private String apiKey;
@Schema(description = "LLM类型")
private String llmType;
}
@Data
@Builder
@Schema(description = "Prompt配置信息")
public static class PromptConfigInfo {
@Schema(description = "Prompt配置ID")
private Long id;
@Schema(description = "Prompt文本")
private String promptText;
}
}

View File

@@ -0,0 +1,23 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "Agent配置列表响应:返回结果agentConfigs为公司Agent配置Mapkey为AgentType枚举值固定六个Agent类型key, 值为Agent配置对象")
@Data
@Builder
public class AgentConfigListResponse {
@Schema(description = "Agent配置映射key为AgentType枚举类型codevalue为Agent配置信息。AgentType类型codeRedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
private Map<String, AgentConfigInfoResponse> agentConfigs;
@Schema(description = "公司所有Prompt配置列表")
private List<AgentConfigInfoResponse.PromptConfigInfo> promptConfigs;
@Schema(description = "公司所有Model配置列表")
private List<AgentConfigInfoResponse.ModelConfigInfo> modelConfigs;
}

View File

@@ -0,0 +1,62 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "标注结果比对响应")
public record AnnotationResultCompareResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "问答对列表") List<QaRecord> qaRecords,
@Schema(description = "差异列表") List<DiffRecord> diffRecords
) {
@Schema(description = "问答记录")
public record QaRecord(
@Schema(description = "记录ID", example = "qa_001") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "运输时效是多久?") String question,
@Schema(description = "答案", example = "3天") String answer,
@Schema(description = "是否需要审核", example = "true") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegments sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") Scores scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegments(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@Schema(description = "差异记录")
public record DiffRecord(
@Schema(description = "关联的问答记录ID", example = "qa_001") String qaId,
@Schema(description = "问题", example = "运输时效是多久?") String question,
@Schema(description = "提取模型答案", example = "3天") String extractAnswer,
@Schema(description = "校验模型答案", example = "72小时") String verifyAnswer,
@Schema(description = "差异原因", example = "时间单位不一致") String diffReason,
@Schema(description = "合并后的最终答案", example = "72小时3天") String mergedAnswer,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") Scores scores
) {
}
@Schema(description = "评分结构")
public record Scores(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -0,0 +1,87 @@
package com.labelsys.backend.dto.response;
import com.labelsys.backend.enums.AnnotationResultStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "标注结果详情响应(包含文件内容)")
public record AnnotationResultDetailResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "任务名称", example = "产品说明书标注") String taskName,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "资源名称", example = "产品A说明书.pdf") String resourceName,
@Schema(description = "标注结果状态", example = "MANUAL_REVIEW_PENDING") AnnotationResultStatus runtimeStatus,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
// 文件路径
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/801.json") String qaContentFilePath,
@Schema(description = "差异摘要文件路径", example = "annotation-results/2/diff/801.json") String diffSummaryFilePath,
// 文件内容
@Schema(description = "问答内容") QaContentDto qaContent,
@Schema(description = "差异摘要(需要审核时有值)") DiffContentDto diffSummary,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
) {
@Schema(description = "问答内容结构")
public record QaContentDto(
@Schema(description = "问答记录列表") List<QaRecordDto> records
) {
}
@Schema(description = "问答记录")
public record QaRecordDto(
@Schema(description = "记录ID", example = "q1") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "产品重量是多少?") String question,
@Schema(description = "答案", example = "5kg") String answer,
@Schema(description = "是否需要审核", example = "false") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegmentsDto sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegmentsDto(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@Schema(description = "差异摘要结构")
public record DiffContentDto(
@Schema(description = "差异记录列表") List<DiffRecordDto> records
) {
}
@Schema(description = "差异记录")
public record DiffRecordDto(
@Schema(description = "关联问答ID", example = "q2") String qaId,
@Schema(description = "问题", example = "保修期多久?") String question,
@Schema(description = "抽取答案", example = "2年") String extractAnswer,
@Schema(description = "验证答案", example = "3年") String verifyAnswer,
@Schema(description = "差异原因", example = "抽取与验证结果不一致") String diffReason,
@Schema(description = "合并后答案") String mergedAnswer,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores
) {
}
@Schema(description = "评分结构")
public record ScoresDto(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -0,0 +1,63 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "归档历史详情响应")
public record AnnotationResultHistoryDetailResponse(
@Schema(description = "历史记录ID", example = "901") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "任务名称", example = "产品说明书标注") String taskName,
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "资源名称", example = "产品A说明书.pdf") String resourceName,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/802.json") String qaContentFilePath,
@Schema(description = "问答内容") QaContentDto qaContent,
@Schema(description = "归档原因", example = "审核通过后归档") String archiveReason,
@Schema(description = "归档操作人ID", example = "5") Long archivedBy,
@Schema(description = "归档时间", example = "2026-05-06T10:30:00") LocalDateTime archivedAt,
@Schema(description = "创建时间", example = "2026-05-06T10:30:00") LocalDateTime createdAt,
@Schema(description = "审核人ID自动归档时为null", example = "5") Long reviewerId,
@Schema(description = "审核人姓名自动归档时为auto", example = "张三") String reviewerName
) {
@Schema(description = "问答内容结构")
public record QaContentDto(
@Schema(description = "问答记录列表") List<QaRecordDto> records
) {
}
@Schema(description = "问答记录")
public record QaRecordDto(
@Schema(description = "记录ID", example = "q1") String id,
@Schema(description = "批次ID", example = "50") Long batchId,
@Schema(description = "问题", example = "产品重量是多少?") String question,
@Schema(description = "答案", example = "5kg") String answer,
@Schema(description = "是否需要审核", example = "false") Boolean requiresReview,
@Schema(description = "源片段信息") SourceSegmentsDto sourceSegments,
@Schema(description = "问题分类") String questionCategory,
@Schema(description = "评分") ScoresDto scores,
@Schema(description = "审核评论") String reviewComment
) {
}
@Schema(description = "源片段信息")
public record SourceSegmentsDto(
@Schema(description = "片段内容") String segment,
@Schema(description = "块索引", example = "0") Integer chunkIndex,
@Schema(description = "块标题") String chunkTitle,
@Schema(description = "块内容") String chunkContent
) {
}
@Schema(description = "评分结构")
public record ScoresDto(
@Schema(description = "相似度", example = "0.7") Double similarity,
@Schema(description = "置信度1", example = "0.9") Double confidence1,
@Schema(description = "置信度2", example = "0.85") Double confidence2,
@Schema(description = "幻觉检测", example = "0.9") Double hallucination,
@Schema(description = "信任度", example = "0.64") Double trust
) {
}
}

View File

@@ -0,0 +1,25 @@
// AnnotationResultHistoryResponse.java
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "归档历史响应")
public record AnnotationResultHistoryResponse(
@Schema(description = "历史记录ID, 不显示", example = "901") Long id,
//@Schema(description = "来源结果ID", example = "802") Long sourceResultId,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "任务名称", example = "产品说明书标注") String taskName, // 新增
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "资源名称", example = "产品A说明书.pdf") String resourceName, // 新增
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/802.json") String qaContentFilePath,
@Schema(description = "归档原因", example = "审核通过后归档") String archiveReason,
@Schema(description = "归档操作人ID", example = "5") Long archivedBy,
@Schema(description = "归档时间", example = "2026-05-06T10:30:00") LocalDateTime archivedAt,
@Schema(description = "创建时间", example = "2026-05-06T10:30:00") LocalDateTime createdAt,
@Schema(description = "审核人ID自动归档时为null", example = "5") Long reviewerId,
@Schema(description = "审核人姓名自动归档时为auto", example = "张三") String reviewerName
) {
}

View File

@@ -0,0 +1,22 @@
package com.labelsys.backend.dto.response;
import com.labelsys.backend.enums.AnnotationResultStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "标注结果响应")
public record AnnotationResultResponse(
@Schema(description = "结果ID", example = "191000000000000401") Long id,
@Schema(description = "任务ID", example = "191000000000000301") Long taskId,
@Schema(description = "任务名称", example = "产品说明书标注") String taskName, // 新增
@Schema(description = "资源ID", example = "191000000000000101") Long resourceId,
@Schema(description = "资源名称", example = "产品A说明书.pdf") String resourceName, // 新增
@Schema(description = "标注结果状态", example = "MANUAL_REVIEW_PENDING") AnnotationResultStatus runtimeStatus,
@Schema(description = "是否需要人工审核", example = "true") Boolean requiresManualReview,
@Schema(description = "是否已删除", example = "false") Boolean isDeleted,
@Schema(description = "问答内容文件路径", example = "annotation-results/2/qa/801.json") String qaContentFilePath,
@Schema(description = "差异摘要文件路径", example = "annotation-results/2/diff/801.json") String diffSummaryFilePath,
@Schema(description = "创建时间", example = "2026-04-27T10:40:00") LocalDateTime createdAt
) {
}

View File

@@ -0,0 +1,19 @@
package com.labelsys.backend.dto.response;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "标注任务响应")
public record AnnotationTaskResponse(
@Schema(description = "任务ID", example = "191000000000000301") Long id,
@Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds,
@Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt
) {
}

View File

@@ -5,9 +5,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "可登录公司选项") @Schema(description = "可登录公司选项")
public record CompanyOptionResponse( public record CompanyOptionResponse(
@Schema(description = "公司ID") Long companyId, @Schema(description = "公司ID", example = "191000000000000001") Long companyId,
@Schema(description = "公司编码") String companyCode, @Schema(description = "公司编码", example = "ALPHA") String companyCode,
@Schema(description = "公司名称") String companyName @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName
) { ) {
public static CompanyOptionResponse from(SysCompany company) { public static CompanyOptionResponse from(SysCompany company) {
return new CompanyOptionResponse(company.getId(), company.getCompanyCode(), company.getCompanyName()); return new CompanyOptionResponse(company.getId(), company.getCompanyCode(), company.getCompanyName());

View File

@@ -6,10 +6,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "公司响应") @Schema(description = "公司响应")
public record CompanyResponse( public record CompanyResponse(
@Schema(description = "公司ID") Long companyId, @Schema(description = "公司ID", example = "191000000000000001") Long companyId,
@Schema(description = "公司编码") String companyCode, @Schema(description = "公司编码", example = "ALPHA") String companyCode,
@Schema(description = "公司名称") String companyName, @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName,
@Schema(description = "公司状态枚举值ENABLED启用、DISABLED禁用") CompanyStatus status @Schema(description = "公司状态枚举值ENABLED启用、DISABLED禁用", example = "ENABLED") CompanyStatus status
) { ) {
public static CompanyResponse from(SysCompany company) { public static CompanyResponse from(SysCompany company) {
return new CompanyResponse(company.getId(), company.getCompanyCode(), company.getCompanyName(), company.getStatus()); return new CompanyResponse(company.getId(), company.getCompanyCode(), company.getCompanyName(), company.getStatus());

View File

@@ -7,16 +7,16 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "当前登录用户响应") @Schema(description = "当前登录用户响应")
public record CurrentUserResponse( public record CurrentUserResponse(
@Schema(description = "用户ID") Long userId, @Schema(description = "用户ID", example = "191000000000000021") Long userId,
@Schema(description = "公司ID") Long companyId, @Schema(description = "公司ID", example = "191000000000000001") Long companyId,
@Schema(description = "公司编码") String companyCode, @Schema(description = "公司编码", example = "ALPHA") String companyCode,
@Schema(description = "公司名称") String companyName, @Schema(description = "公司名称", example = "阿尔法标注有限公司") String companyName,
@Schema(description = "手机号") String phone, @Schema(description = "手机号", example = "13800138002") String phone,
@Schema(description = "用户名,可为空", example = "alpha-admin") String username, @Schema(description = "用户名,可为空", example = "alpha-admin") String username,
@Schema(description = "真实姓名") String realName, @Schema(description = "真实姓名", example = "张审核") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role,
@Schema(description = "岗位,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, @Schema(description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position,
@Schema(description = "是否必须修改密码") boolean mustChangePassword @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword
) { ) {
public static CurrentUserResponse from(LoginUser loginUser) { public static CurrentUserResponse from(LoginUser loginUser) {
return new CurrentUserResponse( return new CurrentUserResponse(

View File

@@ -1,24 +0,0 @@
package com.labelsys.backend.dto.response;
import com.labelsys.backend.entity.BizDataRecord;
import com.labelsys.backend.enums.UserRole;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "数据记录响应")
public record DataRecordResponse(
@Schema(description = "记录ID") Long id,
@Schema(description = "公司ID") Long companyId,
@Schema(description = "创建人ID") Long creatorId,
@Schema(description = "创建人角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole creatorRole,
@Schema(description = "记录名称") String recordName
) {
public static DataRecordResponse from(BizDataRecord record) {
return new DataRecordResponse(
record.getId(),
record.getCompanyId(),
record.getCreatorId(),
record.getCreatorRole(),
record.getRecordName()
);
}
}

View File

@@ -0,0 +1,11 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "文件内容响应")
public record FileContentResponse(
@Schema(description = "文件路径", example = "annotation-results/2/qa/801.json") String filePath,
@Schema(description = "文件内容") String content,
@Schema(description = "文件大小(字节)", example = "1024") Integer size
) {
}

View File

@@ -0,0 +1,31 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "图片BBOX响应")
public record ImageBboxResponse(
@Schema(description = "bbox标识ID", example = "191000000000000101") Long id,
@Schema(description = "资源ID", example = "191000000000000102") Long resourceId,
@Schema(description = "资源文件路径", example = "/data/images/car.jpg") String filepath,
@Schema(description = "BBOX坐标列表") List<BboxCoordinateResponse> bboxes,
@Schema(description = "备注", example = "车辆检测标注") String remark,
@Schema(description = "创建人名称", example = "张审核") String creatorName,
@Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:05:00") LocalDateTime updatedAt
) {
@Schema(description = "BBOX坐标响应对象")
public record BboxCoordinateResponse(
@Schema(description = "BBOX标识", example = "bbox_001") String id,
@Schema(description = "左上角X坐标", example = "100") Integer x,
@Schema(description = "左上角Y坐标", example = "50") Integer y,
@Schema(description = "宽度", example = "200") Integer width,
@Schema(description = "高度", example = "150") Integer height,
@Schema(description = "标注标签", example = "车辆") String label
// @Schema(description = "置信度", example = "0.95") Double confidence
) {
}
}

View File

@@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "登录响应") @Schema(description = "登录响应")
public record LoginResponse( public record LoginResponse(
@Schema(description = "访问令牌") String token, @Schema(description = "访问令牌", example = "eyJhbGciOiJIUzI1NiJ9.demo.token") String token,
@Schema(description = "当前公司信息") CompanyOptionResponse company, @Schema(description = "当前公司信息", example = "{\"companyId\":191000000000000001,\"companyCode\":\"ALPHA\",\"companyName\":\"阿尔法标注有限公司\"}") CompanyOptionResponse company,
@Schema(description = "手机号") String phone, @Schema(description = "手机号", example = "13800138002") String phone,
@Schema(description = "用户名,可为空", example = "alpha-admin") String username, @Schema(description = "用户名,可为空", example = "alpha-admin") String username,
@Schema(description = "真实姓名") String realName, @Schema(description = "真实姓名", example = "张审核") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role,
@Schema(description = "岗位,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, @Schema(description = "岗位枚举枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN公司管理员、SUPER_ADMIN超级管理员", example = "REVIEWER") UserPosition position,
@Schema(description = "是否必须修改密码") boolean mustChangePassword @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword
) { ) {
public static LoginResponse from(String token, LoginUser loginUser, SysCompany company) { public static LoginResponse from(String token, LoginUser loginUser, SysCompany company) {
return new LoginResponse( return new LoginResponse(

View File

@@ -5,10 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "菜单响应") @Schema(description = "菜单响应")
public record MenuResponse( public record MenuResponse(
@Schema(description = "菜单编码") String menuCode, @Schema(description = "菜单编码", example = "annotation.task.list") String menuCode,
@Schema(description = "菜单名称") String menuName, @Schema(description = "菜单名称", example = "标注任务") String menuName,
@Schema(description = "菜单路径") String path, @Schema(description = "菜单路径", example = "/annotation/tasks") String path,
@Schema(description = "排序") Integer sortOrder @Schema(description = "排序", example = "10") Integer sortOrder
) { ) {
public static MenuResponse from(SysMenu menu) { public static MenuResponse from(SysMenu menu) {
return new MenuResponse(menu.getMenuCode(), menu.getMenuName(), menu.getPath(), menu.getSortOrder()); return new MenuResponse(menu.getMenuCode(), menu.getMenuName(), menu.getPath(), menu.getSortOrder());

View File

@@ -0,0 +1,13 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "合并审核结果响应")
public record MergeReviewResultResponse(
@Schema(description = "运行态结果ID", example = "191000000000000401") Long resultId,
@Schema(description = "历史结果ID", example = "191000000000000451") Long historyId,
@Schema(description = "归档原因", example = "MANUAL_REVIEW_MERGED") String archiveReason,
@Schema(description = "归档时间", example = "2026-04-27T11:05:00") LocalDateTime archivedAt
) {
}

View File

@@ -0,0 +1,21 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "资源响应")
public record SourceResourceResponse(
@Schema(description = "资源ID", example = "191000000000000101") Long id,
@Schema(description = "资源名称", example = "2026年4月运输巡检记录") String resourceName,
@Schema(description = "资源类型", example = "TEXT") String resourceType,
@Schema(description = "桶名称", example = "annotation-source") String bucketName,
@Schema(description = "文件路径", example = "company/191000000000000001/text/191000000000000101.txt") String filePath,
@Schema(description = "文件大小", example = "20480") Long fileSize,
@Schema(description = "存储提供方", example = "rustfs") String storageProvider,
@Schema(description = "是否有BBOX标注,不显示", example = "false") Boolean hasBbox,
@Schema(description = "备注", example = "第一批导入样本") String remark,
@Schema(description = "创建人名称", example = "张审核") String creatorName,
@Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:05:00") LocalDateTime updatedAt
) {
}

View File

@@ -0,0 +1,16 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "资源上传响应")
public record SourceUploadResponse(
@Schema(description = "资源ID", example = "191000000000000101") Long id,
@Schema(description = "资源名称", example = "2026年4月运输巡检记录") String resourceName,
@Schema(description = "资源类型", example = "TEXT") String resourceType,
@Schema(description = "桶名称", example = "annotation-source") String bucketName,
@Schema(description = "文件路径", example = "company/191000000000000001/text/191000000000000101.txt") String filePath,
@Schema(description = "文件大小", example = "20480") Long fileSize,
@Schema(description = "创建时间", example = "2026-04-27T10:00:00") LocalDateTime createdAt
) {
}

View File

@@ -0,0 +1,20 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Schema(description = "系统配置响应")
public record SysConfigResponse(
@Schema(description = "配置ID", example = "191000000000000501") Long id,
@Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项", example = "MODEL") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status,
@Schema(description = "创建人ID", example = "191000000000000021") Long creatorId,
@Schema(description = "创建时间", example = "2026-04-27T09:50:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:15:00") LocalDateTime updatedAt
) {
}

View File

@@ -8,15 +8,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户响应") @Schema(description = "用户响应")
public record UserResponse( public record UserResponse(
@Schema(description = "用户ID") Long userId, @Schema(description = "用户ID", example = "191000000000000021") Long userId,
@Schema(description = "公司ID") Long companyId, @Schema(description = "公司ID", example = "191000000000000001") Long companyId,
@Schema(description = "手机号") String phone, @Schema(description = "手机号", example = "13800138002") String phone,
@Schema(description = "用户名,可为空", example = "alpha-admin") String username, @Schema(description = "用户名,可为空", example = "alpha-admin") String username,
@Schema(description = "真实姓名") String realName, @Schema(description = "真实姓名", example = "张审核") String realName,
@Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师") UserRole role, @Schema(description = "角色枚举值EMPLOYEE员工、MANAGER部门经理、ENGINEER总工程师", example = "EMPLOYEE") UserRole role,
@Schema(description = "岗位,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") UserPosition position, @Schema(description = "岗位枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员", example = "REVIEWER") UserPosition position,
@Schema(description = "用户状态枚举值ENABLED启用、DISABLED禁用") UserStatus status, @Schema(description = "用户状态枚举值ENABLED启用、DISABLED禁用", example = "ENABLED") UserStatus status,
@Schema(description = "是否必须修改密码") boolean mustChangePassword @Schema(description = "是否必须修改密码", example = "false") boolean mustChangePassword
) { ) {
public static UserResponse from(SysUser user) { public static UserResponse from(SysUser user) {
return new UserResponse( return new UserResponse(

View File

@@ -0,0 +1,160 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.labelsys.backend.util.SM4Util;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_agent_configs")
public class AnnotationAgentConfig {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("company_id")
private Long companyId; // 所属公司ID来源于annotation_task.company_id
@TableField("agent_type")
private String agentType; // Agent角色: extract(抽取/Analyzer+Regenerator) / verify(校验/Reviewer) / industry(行业识别) / hallucination(幻觉检测)
@TableField("model_config_id")
private Long modelConfigId; // 模型配置来源ID外键引用sys_config.id任务启动时从sys_config(config_type=MODEL, status=ENABLED)获取
@TableField("model_name")
private String modelName; // 模型名称快照,如: qwen-max / glm-4 / claude-3-opus
@TableField("model_url")
private String modelUrl; // 模型调用地址快照
@TableField("model_api_key")
private String modelApiKey; // 模型调用密钥快照(需要加密存储)
@TableField("prompt_config_id")
private Long promptConfigId; // Prompt配置来源ID外键引用sys_config.id任务启动时从sys_config(config_type=PROMPT, status=ENABLED)获取
@TableField("prompt_text")
private String promptText; // Prompt文本快照任务执行期间实际使用的提示词
@TableField(value = "config_snapshot", typeHandler = JacksonTypeHandler.class)
private JsonNode configSnapshot; // 完整配置快照JSON包含temperature、max_tokens等运行时参数
@TableField("created_at")
private LocalDateTime createdAt; // 快照创建时间
@TableField("llm_type")
private String llmType; // LLM类型text, audio, video, image等默认为'text'
// Getter和Setter方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCompanyId() {
return companyId;
}
public void setCompanyId(Long companyId) {
this.companyId = companyId;
}
public String getAgentType() {
return agentType;
}
public void setAgentType(String agentType) {
this.agentType = agentType;
}
public Long getModelConfigId() {
return modelConfigId;
}
public void setModelConfigId(Long modelConfigId) {
this.modelConfigId = modelConfigId;
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getModelUrl() {
return modelUrl;
}
public void setModelUrl(String modelUrl) {
this.modelUrl = modelUrl;
}
public String getModelApiKey() {
if (this.modelApiKey != null) {
return SM4Util.decryptSafe(this.modelApiKey); // 解密返回
}
return null;
}
public void setModelApiKey(String modelApiKey) {
if (modelApiKey != null) {
this.modelApiKey = SM4Util.encrypt(modelApiKey); // 加密存储
}
}
public Long getPromptConfigId() {
return promptConfigId;
}
public void setPromptConfigId(Long promptConfigId) {
this.promptConfigId = promptConfigId;
}
public String getPromptText() {
return promptText;
}
public void setPromptText(String promptText) {
this.promptText = promptText;
}
public JsonNode getConfigSnapshot() {
return configSnapshot;
}
public void setConfigSnapshot(JsonNode configSnapshot) {
this.configSnapshot = configSnapshot;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getLlmType() {
return llmType;
}
public void setLlmType(String llmType) {
this.llmType = llmType;
}
}

View File

@@ -0,0 +1,67 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_result")
public class AnnotationResult {
@TableId(type = IdType.INPUT)
private Long id;
@TableField("company_id")
private Long companyId;
@TableField("creator_id")
private Long creatorId;
@TableField("creator_role")
private String creatorRole;
@TableField("task_id")
private Long taskId;
@TableField("resource_id")
private Long resourceId;
// 添加字段
@TableField("task_name")
private String taskName;
@TableField("resource_name")
private String resourceName;
@TableField("qa_content_file_path")
private String qaContentFilePath;
@TableField("diff_summary_file_path")
private String diffSummaryFilePath;
@TableField("requires_manual_review")
private Boolean requiresManualReview;
@TableLogic(value = "false", delval = "true")
@TableField("is_deleted")
private Boolean isDeleted;
@TableField("reviewer_id")
private Long reviewerId;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_at")
private LocalDateTime updatedAt;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,40 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_result_history")
public class AnnotationResultHistory {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private Long creatorId;
private String creatorRole;
private Long sourceResultId;
private Long taskId;
private Long resourceId;
private String taskName;
private String resourceName;
//private String qaContentJson;
// private String qaContentStorageMode;
private String qaContentFilePath;
private String archiveReason;
private Long archivedBy;
private LocalDateTime archivedAt;
private LocalDateTime createdAt;
// 新增审核人相关字段
private Long reviewerId;
private String reviewerName;
}

View File

@@ -0,0 +1,37 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.enums.UserRole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_task")
public class AnnotationTask {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private Long creatorId;
private UserRole creatorRole;
private String taskName;
private TaskType taskType;
private String taskStatus;
@TableLogic(value = "false", delval = "true")
private Boolean isDeleted;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private String errorMessage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,24 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_task_resource")
public class AnnotationTaskResource {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private Long taskId;
private Long resourceId;
private LocalDateTime createdAt;
}

View File

@@ -1,8 +1,5 @@
package com.labelsys.backend.entity; package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.UserRole; import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -14,9 +11,7 @@ import lombok.NoArgsConstructor;
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@TableName("biz_data_record")
public class BizDataRecord { public class BizDataRecord {
@TableId(type = IdType.INPUT)
private Long id; private Long id;
private Long companyId; private Long companyId;
private Long creatorId; private Long creatorId;

View File

@@ -0,0 +1,80 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 图片BBOX标注表实体类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("image_bbox_annotation")
public class ImageBboxAnnotation {
/**
* 主键ID
*/
@TableId(type = IdType.INPUT)
private Long id;
/**
* 所属公司ID
*/
private Long companyId;
/**
* 关联的图片资源ID
*/
private Long resourceId;
/**
* bbox坐标信息JSON数组
*/
private String bboxJson;
/**
* 备注说明
*/
private String remark;
/**
* 创建人用户ID
*/
private Long creatorId;
/**
* 创建人数据权限角色,默认 EMPLOYEE
*/
private UserRole creatorRole;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
/**
* 软删除标记
*/
@TableLogic(value = "false", delval = "true")
private Boolean deleted;
/**
* 软删除时间
*/
private LocalDateTime deletedAt;
}

View File

@@ -0,0 +1,38 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("source_resource")
public class SourceResource {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private Long creatorId;
private UserRole creatorRole;
private String resourceName;
private String resourceType;
private String bucketName;
private String filePath;
private Long fileSize;
private String storageProvider;
private Boolean hasBbox;
private String remark;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic(value = "false", delval = "true")
private Boolean deleted;
private LocalDateTime deletedAt;
}

View File

@@ -0,0 +1,29 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_config")
public class SysConfig {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private String configType;
private String configName;
private String configValue;
private String status;
private Long creatorId;
private String creatorRole;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -18,10 +18,10 @@ public class SysMenu {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private Long companyId; private Long companyId;
private String permissionCode;
private String menuCode; private String menuCode;
private String menuName; private String menuName;
private String path; private String path;
private String visiblePositions;
private Integer sortOrder; private Integer sortOrder;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;

View File

@@ -0,0 +1,43 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Agent类型枚举")
public enum AgentType {
@Schema(description = "红黑对抗")
RED_TEAM("RedTeamAgent"),
@Schema(description = "分析")
ANALYZER("AnalyzerAgent"),
@Schema(description = "行业识别")
INDUSTRY_CLASSIFIER("IndustryClassifierAgent"),
@Schema(description = "幻觉检测")
HALLUCINATION_DETECTOR("HallucinationDetectorAgent"),
@Schema(description = "审查")
REVIEWER("ReviewerAgent"),
@Schema(description = "再次生成")
REGENERATOR("regenerator");
private final String code;
AgentType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static boolean isValid(String value) {
for (AgentType type : AgentType.values()) {
if (type.name().equalsIgnoreCase(value) || type.getCode().equals(value)) {
return true;
}
}
return false;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
package com.labelsys.backend.enums;
import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "系统配置类型枚举值model:大模型配置、prompt提示词配置, system:其他配置项")
public enum ConfigType {
MODEL,
PROMPT,
SYSTEM;
public static boolean isValid(String value) {
return Arrays.stream(values()).anyMatch(type -> type.name().equals(value));
}
}

View File

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

View File

@@ -0,0 +1,37 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "多模态类型枚举")
public enum MultiModalType {
@Schema(description = "文本")
TEXT("text"),
@Schema(description = "音频")
AUDIO("audio"),
@Schema(description = "视频")
VIDEO("video"),
@Schema(description = "图片")
IMAGE("image");
private final String code;
MultiModalType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static boolean isValid(String value) {
for (MultiModalType type : MultiModalType.values()) {
if (type.name().equalsIgnoreCase(value) || type.getCode().equals(value)) {
return true;
}
}
return false;
}
}

View File

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

View File

@@ -0,0 +1,16 @@
package com.labelsys.backend.enums;
import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源类型枚举值TEXT:文本、IMAGE:图片、VIDEO:视频")
public enum ResourceType {
TEXT,
IMAGE,
VIDEO;
public static boolean isValid(String value) {
return Arrays.stream(values()).anyMatch(type -> type.name().equals(value));
}
}

View File

@@ -0,0 +1,17 @@
package com.labelsys.backend.enums;
import java.util.Arrays;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "任务状态枚举值PENDING:待执行、RUNNING:处理中、COMPLETED:已完成、FAILED:失败")
public enum TaskStatus {
PENDING,
RUNNING,
COMPLETED,
FAILED;
public static boolean isValid(String value) {
return Arrays.stream(values()).anyMatch(status -> status.name().equals(value));
}
}

View File

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

View File

@@ -6,13 +6,9 @@ import lombok.RequiredArgsConstructor;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@Schema(description = "岗位枚举,枚举值:UPLOADER上传者、ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN超级管理员") @Schema(description = "岗位枚举枚举值ANNOTATOR标注员、DATA_TRAINER数据训练师、REVIEWER审核员、ADMIN公司管理员、SUPER_ADMIN超级管理员")
public enum UserPosition { public enum UserPosition {
UPLOADER(1), ANNOTATOR(1), DATA_TRAINER(2), REVIEWER(3), ADMIN(4), SUPER_ADMIN(5);
ANNOTATOR(2),
DATA_TRAINER(3),
REVIEWER(4),
ADMIN(5);
private final int level; private final int level;

View File

@@ -8,98 +8,180 @@ import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.entity.SysCompany; import com.labelsys.backend.entity.SysCompany;
import com.labelsys.backend.entity.SysUser; import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.CompanyStatus; import com.labelsys.backend.enums.CompanyStatus;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.enums.UserStatus; import com.labelsys.backend.enums.UserStatus;
import com.labelsys.backend.mapper.SysCompanyMapper; import com.labelsys.backend.mapper.SysCompanyMapper;
import com.labelsys.backend.mapper.SysUserMapper; import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.service.session.TokenSessionRepository; import com.labelsys.backend.service.session.TokenSessionRepository;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Set;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.time.Duration;
import java.util.Set;
/**
* 认证拦截器
*
* <p>实现 HandlerInterceptor 接口,用于在请求处理前进行身份认证和权限校验。
* 负责验证用户登录状态、会话有效性、密码修改强制要求以及岗位权限检查。
*/
@Component @Component
public class AuthInterceptor implements HandlerInterceptor { public class AuthInterceptor implements HandlerInterceptor {
/**
* 公开路径集合(无需登录即可访问)
* 包括认证相关接口和 Swagger 文档接口
*/
private static final Set<String> OPEN_PATHS = Set.of( private static final Set<String> OPEN_PATHS = Set.of(
"/label/api/auth/companies", "/api/auth/companies", "/label/api/auth/companies",
"/label/api/auth/login", "/api/auth/login", "/label/api/auth/login",
"/label/swagger-ui.html", "/swagger-ui.html", "/label/swagger-ui.html",
"/label/v3/api-docs", "/v3/api-docs", "/label/v3/api-docs",
"/label/v3/api-docs/swagger-config" "/v3/api-docs/swagger-config", "/label/v3/api-docs/swagger-config");
);
/**
* 强制修改密码时允许访问的路径集合
* 包括修改密码、登出和获取当前用户信息接口
*/
private static final Set<String> ALLOWED_WHEN_MUST_CHANGE_PASSWORD = Set.of( private static final Set<String> ALLOWED_WHEN_MUST_CHANGE_PASSWORD = Set.of(
"/label/api/auth/change-password", "/api/auth/change-password", "/label/api/auth/change-password",
"/label/api/auth/logout", "/api/auth/logout", "/label/api/auth/logout",
"/label/api/auth/me" "/api/auth/me", "/label/api/auth/me");
);
/**
* Token会话存储仓库
*/
private final TokenSessionRepository tokenSessionRepository; private final TokenSessionRepository tokenSessionRepository;
/**
* 用户数据访问层
*/
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
/**
* 公司数据访问层
*/
private final SysCompanyMapper sysCompanyMapper; private final SysCompanyMapper sysCompanyMapper;
/**
* 会话有效期
*/
private final Duration sessionTtl; private final Duration sessionTtl;
public AuthInterceptor( /**
TokenSessionRepository tokenSessionRepository, * 构造函数
SysUserMapper sysUserMapper, *
* @param tokenSessionRepository Token会话存储仓库
* @param sysUserMapper 用户数据访问层
* @param sysCompanyMapper 公司数据访问层
* @param sessionTtl 会话有效期默认2小时
*/
public AuthInterceptor(TokenSessionRepository tokenSessionRepository, SysUserMapper sysUserMapper,
SysCompanyMapper sysCompanyMapper, SysCompanyMapper sysCompanyMapper,
@Value("${labelsys.session.ttl:PT2H}") Duration sessionTtl @Value("${labelsys.session.ttl:PT2H}") Duration sessionTtl) {
) {
this.tokenSessionRepository = tokenSessionRepository; this.tokenSessionRepository = tokenSessionRepository;
this.sysUserMapper = sysUserMapper; this.sysUserMapper = sysUserMapper;
this.sysCompanyMapper = sysCompanyMapper; this.sysCompanyMapper = sysCompanyMapper;
this.sessionTtl = sessionTtl; this.sessionTtl = sessionTtl;
} }
/**
* 请求处理前的认证拦截
*
* <p>执行以下校验逻辑:
* 1. OPTIONS 请求直接放行
* 2. 公开路径和 Swagger 路径直接放行
* 3. 非 HandlerMethod 请求直接放行
* 4. 验证 Token 并获取登录用户
* 5. 验证用户和公司状态
* 6. 验证会话版本(密码修改后会话失效)
* 7. 检查强制修改密码状态
* 8. 设置用户上下文并刷新会话
* 9. 检查岗位权限
*
* @param request HTTP请求
* @param response HTTP响应
* @param handler 处理器
* @return 是否放行
*/
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// OPTIONS 请求直接放行CORS预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true; return true;
} }
String path = request.getRequestURI(); String path = request.getRequestURI();
// Swagger 相关路径和公开路径直接放行
if (path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || OPEN_PATHS.contains(path)) { if (path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || OPEN_PATHS.contains(path)) {
return true; return true;
} }
// 非 HandlerMethod 请求直接放行
if (!(handler instanceof HandlerMethod handlerMethod)) { if (!(handler instanceof HandlerMethod handlerMethod)) {
return true; return true;
} }
String token = extractToken(request.getHeader("Authorization"));
LoginUser loginUser = tokenSessionRepository.find(token)
.orElseThrow(() -> new UnauthorizedException("未登录或登录已过期"));
// 提取 Token 并验证会话
String token = extractToken(request.getHeader("Authorization"));
LoginUser loginUser =
tokenSessionRepository.find(token).orElseThrow(() -> new UnauthorizedException("未登录或登录已过期"));
// 验证用户和公司状态
SysUser user = sysUserMapper.findByIdAndCompanyId(loginUser.userId(), loginUser.companyId()); SysUser user = sysUserMapper.findByIdAndCompanyId(loginUser.userId(), loginUser.companyId());
SysCompany company = sysCompanyMapper.findById(loginUser.companyId()); SysCompany company = sysCompanyMapper.selectById(loginUser.companyId());
if (user == null || company == null || user.getStatus() != UserStatus.ENABLED || company.getStatus() != CompanyStatus.ENABLED) { if (user == null || company == null || user.getStatus() != UserStatus.ENABLED
|| company.getStatus() != CompanyStatus.ENABLED) {
throw new UnauthorizedException("未登录或登录已过期"); throw new UnauthorizedException("未登录或登录已过期");
} }
// 验证会话版本(密码修改后会话失效)
if (!user.getSessionVersion().equals(loginUser.sessionVersion())) { if (!user.getSessionVersion().equals(loginUser.sessionVersion())) {
throw new UnauthorizedException("登录状态已失效,请重新登录"); throw new UnauthorizedException("登录状态已失效,请重新登录");
} }
// 检查强制修改密码状态
if (Boolean.TRUE.equals(user.getMustChangePassword()) && !ALLOWED_WHEN_MUST_CHANGE_PASSWORD.contains(path)) { if (Boolean.TRUE.equals(user.getMustChangePassword()) && !ALLOWED_WHEN_MUST_CHANGE_PASSWORD.contains(path)) {
throw new ForbiddenException("首次登录后请先修改密码"); throw new ForbiddenException("首次登录后请先修改密码");
} }
// 设置用户上下文并刷新会话
LoginUser refreshedUser = LoginUser.from(user, company); LoginUser refreshedUser = LoginUser.from(user, company);
UserContext.set(refreshedUser); UserContext.set(refreshedUser);
tokenSessionRepository.refresh(token, sessionTtl); tokenSessionRepository.refresh(token, sessionTtl);
// 检查岗位权限
RequirePosition requirePosition = resolveRequirePosition(handlerMethod); RequirePosition requirePosition = resolveRequirePosition(handlerMethod);
if (requirePosition != null && !refreshedUser.position().canAccess(requirePosition.value())) { if (requirePosition != null && !refreshedUser.position().canAccess(requirePosition.value())) {
throw new ForbiddenException("当前岗位无权限访问"); throw new ForbiddenException("当前岗位无权限访问");
} }
return true; return true;
} }
/**
* 请求处理完成后清理用户上下文
*
* @param request HTTP请求
* @param response HTTP响应
* @param handler 处理器
* @param ex 异常(如果有)
*/
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
UserContext.clear(); UserContext.clear();
} }
/**
* 从 Authorization 头中提取 Token
*
* @param authorization Authorization 头值
* @return Token
*/
private String extractToken(String authorization) { private String extractToken(String authorization) {
if (authorization == null || !authorization.startsWith("Bearer ")) { if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new UnauthorizedException("未登录或登录已过期"); throw new UnauthorizedException("未登录或登录已过期");
@@ -107,6 +189,14 @@ public class AuthInterceptor implements HandlerInterceptor {
return authorization.substring(7).trim(); return authorization.substring(7).trim();
} }
/**
* 解析方法或类上的 @RequirePosition 注解
*
* <p>优先从方法上获取注解,若方法上没有则从类上获取。
*
* @param handlerMethod 处理器方法
* @return RequirePosition 注解可能为null
*/
private RequirePosition resolveRequirePosition(HandlerMethod handlerMethod) { private RequirePosition resolveRequirePosition(HandlerMethod handlerMethod) {
RequirePosition methodAnnotation = handlerMethod.getMethodAnnotation(RequirePosition.class); RequirePosition methodAnnotation = handlerMethod.getMethodAnnotation(RequirePosition.class);
if (methodAnnotation != null) { if (methodAnnotation != null) {
@@ -114,4 +204,5 @@ public class AuthInterceptor implements HandlerInterceptor {
} }
return handlerMethod.getBeanType().getAnnotation(RequirePosition.class); return handlerMethod.getBeanType().getAnnotation(RequirePosition.class);
} }
} }

View File

@@ -0,0 +1,11 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationAgentConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AnnotationAgentConfigMapper extends BaseMapper<AnnotationAgentConfig> {
// 基于BaseMapper已提供基本的CRUD操作
// 如需特殊查询,可在此添加自定义方法
}

View File

@@ -0,0 +1,7 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationResultHistory;
public interface AnnotationResultHistoryMapper extends BaseMapper<AnnotationResultHistory> {
}

View File

@@ -0,0 +1,47 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationResult;
import org.apache.ibatis.annotations.Param;
/**
* 标注结果数据访问层
*
* 继承 BaseMapper<AnnotationResult>,提供标注结果表的基础 CRUD 操作,
* 并扩展自定义的查询和更新方法。
*/
public interface AnnotationResultMapper extends BaseMapper<AnnotationResult> {
/**
* 根据ID和公司ID查询活跃的标注结果
*
* @param id 结果ID
* @param companyId 公司ID
* @return 标注结果信息,不存在或已归档返回 null
*/
AnnotationResult findActiveByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
/**
* 标记标注结果为已归档
*
* @param id 结果ID
* @param companyId 公司ID
* @param reviewerId 审核人ID
* @return 更新影响的行数
*/
int markArchived(@Param("id") Long id,
@Param("companyId") Long companyId,
@Param("reviewerId") Long reviewerId);
/**
* 标记标注结果为已审核并归档
*
* @param id 结果ID
* @param companyId 公司ID
* @param reviewerId 审核人ID
* @return 更新影响的行数
*/
int markReviewedAndArchived(@Param("id") Long id,
@Param("companyId") Long companyId,
@Param("reviewerId") Long reviewerId);
}

View File

@@ -0,0 +1,24 @@
package com.labelsys.backend.mapper;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationTask;
/**
* 标注任务数据访问层
*
* 继承 BaseMapper<AnnotationTask>,提供标注任务表的基础 CRUD 操作,
* 并扩展自定义的查询方法。
*/
public interface AnnotationTaskMapper extends BaseMapper<AnnotationTask> {
/**
* 根据ID和公司ID查询标注任务
*
* @param id 任务ID
* @param companyId 公司ID
* @return 标注任务信息,不存在返回 null
*/
AnnotationTask findByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
}

View File

@@ -0,0 +1,39 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationTaskResource;
import java.util.List;
import org.apache.ibatis.annotations.Param;
/**
* 标注任务资源关联数据访问层
*
* 继承 BaseMapper<AnnotationTaskResource>,提供标注任务资源关联表的基础 CRUD 操作,
* 并扩展自定义的查询和删除方法。
*/
public interface AnnotationTaskResourceMapper extends BaseMapper<AnnotationTaskResource> {
/**
* 根据任务ID查询关联的资源ID列表
*
* @param taskId 任务ID
* @return 资源ID列表
*/
List<Long> listResourceIdsByTaskId(@Param("taskId") Long taskId);
/**
* 根据任务ID删除关联的资源记录
*
* @param taskId 任务ID
* @return 删除影响的行数
*/
int deleteByTaskId(@Param("taskId") Long taskId);
/**
* 统计资源被任务关联的数量
*
* @param resourceId 资源ID
* @return 关联数量
*/
int countByResourceId(@Param("resourceId") Long resourceId);
}

View File

@@ -1,15 +0,0 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.BizDataRecord;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface BizDataRecordMapper extends BaseMapper<BizDataRecord> {
List<BizDataRecord> listVisibleByEmployee(@Param("companyId") Long companyId, @Param("creatorId") Long creatorId);
List<BizDataRecord> listVisibleByManager(@Param("companyId") Long companyId);
List<BizDataRecord> listVisibleByEngineer(@Param("companyId") Long companyId);
}

View File

@@ -0,0 +1,32 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.ImageBboxAnnotation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 图像边界框标注数据访问层
*
* 继承 BaseMapper<ImageBboxAnnotation>,提供图像边界框标注表的基础 CRUD 操作,
* 并扩展自定义的查询和删除方法。
*/
@Mapper
public interface ImageBboxAnnotationMapper extends BaseMapper<ImageBboxAnnotation> {
/**
* 根据资源ID查询图像边界框标注
*
* @param resourceId 资源ID
* @return 图像边界框标注,不存在返回 null
*/
ImageBboxAnnotation selectByResourceId(@Param("resourceId") Long resourceId);
/**
* 根据资源ID删除图像边界框标注
*
* @param resourceId 资源ID
* @return 删除影响的行数
*/
int deleteByResourceId(@Param("resourceId") Long resourceId);
}

View File

@@ -0,0 +1,33 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.SourceResource;
import java.util.List;
import org.apache.ibatis.annotations.Param;
/**
* 源资源数据访问层
*
* 继承 BaseMapper<SourceResource>,提供源资源表的基础 CRUD 操作,
* 并扩展自定义的查询方法。
*/
public interface SourceResourceMapper extends BaseMapper<SourceResource> {
/**
* 根据公司ID和资源ID列表查询资源
*
* @param companyId 公司ID
* @param resourceIds 资源ID列表
* @return 资源列表
*/
List<SourceResource> selectByCompanyIdAndIds(@Param("companyId") Long companyId, @Param("resourceIds") List<Long> resourceIds);
/**
* 根据公司ID和资源名称查询资源
*
* @param companyId 公司ID
* @param resourceName 资源名称
* @return 资源信息,不存在返回 null
*/
SourceResource selectByCompanyIdAndResourceName(@Param("companyId") Long companyId, @Param("resourceName") String resourceName);
}

Some files were not shown because too many files have changed in this diff Show More