Merge branch 'dev54'
This commit is contained in:
@@ -6,17 +6,38 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyBatis Plus 配置类
|
||||||
|
*
|
||||||
|
* <p>配置 MyBatis Plus 的增强功能,包括分页插件等。
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
public class MybatisPlusConfig {
|
public class MybatisPlusConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 MyBatis Plus 拦截器
|
||||||
|
*
|
||||||
|
* <p>注册分页拦截器,支持 PostgreSQL 数据库的分页查询。
|
||||||
|
* 设置分页溢出处理为 false(超出页数返回空页),最大分页限制为 200 条。
|
||||||
|
*
|
||||||
|
* @return MyBatis Plus 拦截器
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||||
|
|
||||||
|
// 创建分页拦截器,指定数据库类型为 PostgreSQL
|
||||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL);
|
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL);
|
||||||
|
|
||||||
|
// 设置分页溢出处理:false 表示超出页数时返回空页,不进行页码修正
|
||||||
paginationInnerInterceptor.setOverflow(false);
|
paginationInnerInterceptor.setOverflow(false);
|
||||||
|
|
||||||
|
// 设置最大分页限制:200 条,防止一次性查询过多数据
|
||||||
paginationInnerInterceptor.setMaxLimit(200L);
|
paginationInnerInterceptor.setMaxLimit(200L);
|
||||||
|
|
||||||
interceptor.addInnerInterceptor(paginationInnerInterceptor);
|
interceptor.addInnerInterceptor(paginationInnerInterceptor);
|
||||||
|
|
||||||
return interceptor;
|
return interceptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.labelsys.backend.config;
|
package com.labelsys.backend.config;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -11,21 +10,50 @@ import software.amazon.awssdk.regions.Region;
|
|||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置类
|
||||||
|
*
|
||||||
|
* <p>配置 AWS S3 客户端,用于与对象存储服务(如 MinIO、AWS S3)进行交互。
|
||||||
|
* 通过配置属性读取 endpoint、region、accessKey、secretKey 等参数。
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@EnableConfigurationProperties(ObjectStorageProperties.class)
|
@EnableConfigurationProperties(ObjectStorageProperties.class)
|
||||||
public class ObjectStorageConfig {
|
public class ObjectStorageConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置属性
|
||||||
|
*/
|
||||||
private final ObjectStorageProperties properties;
|
private final ObjectStorageProperties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并配置 S3 客户端
|
||||||
|
*
|
||||||
|
* 配置内容包括:
|
||||||
|
* - endpoint: 对象存储服务地址
|
||||||
|
* - region: 区域标识
|
||||||
|
* - credentials: 访问密钥(accessKey 和 secretKey)
|
||||||
|
* - pathStyleAccess: 是否启用路径风格访问
|
||||||
|
*
|
||||||
|
* @return 配置完成的 S3 客户端
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public S3Client s3Client() {
|
public S3Client s3Client() {
|
||||||
return S3Client.builder()
|
return S3Client.builder()
|
||||||
|
// 设置自定义 endpoint(支持非 AWS S3 服务如 MinIO)
|
||||||
.endpointOverride(URI.create(properties.getEndpoint()))
|
.endpointOverride(URI.create(properties.getEndpoint()))
|
||||||
|
// 设置区域
|
||||||
.region(Region.of(properties.getRegion()))
|
.region(Region.of(properties.getRegion()))
|
||||||
|
// 设置凭证提供者
|
||||||
.credentialsProvider(StaticCredentialsProvider.create(
|
.credentialsProvider(StaticCredentialsProvider.create(
|
||||||
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
|
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
|
||||||
.serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(properties.isPathStyleAccess()).build())
|
// 配置路径风格访问(对于 MinIO 等自建存储服务通常需要启用)
|
||||||
|
.serviceConfiguration(S3Configuration.builder()
|
||||||
|
.pathStyleAccessEnabled(properties.isPathStyleAccess())
|
||||||
|
.build())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,15 +3,54 @@ package com.labelsys.backend.config;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置属性类
|
||||||
|
*
|
||||||
|
* 用于读取 `labelsys.object-storage` 前缀的配置项,
|
||||||
|
* 包括连接信息和存储桶配置。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties(prefix = "labelsys.object-storage")
|
@ConfigurationProperties(prefix = "labelsys.object-storage")
|
||||||
public class ObjectStorageProperties {
|
public class ObjectStorageProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储服务地址
|
||||||
|
*/
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域标识
|
||||||
|
*/
|
||||||
private String region;
|
private String region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问密钥 ID
|
||||||
|
*/
|
||||||
private String accessKey;
|
private String accessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问密钥
|
||||||
|
*/
|
||||||
private String secretKey;
|
private String secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用路径风格访问(默认 true,适用于 MinIO 等自建存储)
|
||||||
|
*/
|
||||||
private boolean pathStyleAccess = true;
|
private boolean pathStyleAccess = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源文件存储桶名称
|
||||||
|
*/
|
||||||
private String sourceBucket;
|
private String sourceBucket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产物存储桶名称
|
||||||
|
*/
|
||||||
private String artifactBucket;
|
private String artifactBucket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出文件存储桶名称
|
||||||
|
*/
|
||||||
private String exportBucket;
|
private String exportBucket;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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("公司、员工、认证、岗位权限和数据权限接口"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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/**");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package com.labelsys.backend.interceptor;
|
package com.labelsys.backend.interceptor;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.method.HandlerMethod;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import com.labelsys.backend.annotation.RequirePosition;
|
import com.labelsys.backend.annotation.RequirePosition;
|
||||||
import com.labelsys.backend.common.exception.ForbiddenException;
|
import com.labelsys.backend.common.exception.ForbiddenException;
|
||||||
import com.labelsys.backend.common.exception.UnauthorizedException;
|
import com.labelsys.backend.common.exception.UnauthorizedException;
|
||||||
@@ -20,13 +12,29 @@ 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 org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
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(
|
||||||
"/api/auth/companies", "/label/api/auth/companies",
|
"/api/auth/companies", "/label/api/auth/companies",
|
||||||
"/api/auth/login", "/label/api/auth/login",
|
"/api/auth/login", "/label/api/auth/login",
|
||||||
@@ -34,70 +42,146 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
"/v3/api-docs", "/label/v3/api-docs",
|
"/v3/api-docs", "/label/v3/api-docs",
|
||||||
"/v3/api-docs/swagger-config", "/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(
|
||||||
"/api/auth/change-password", "/label/api/auth/change-password",
|
"/api/auth/change-password", "/label/api/auth/change-password",
|
||||||
"/api/auth/logout", "/label/api/auth/logout",
|
"/api/auth/logout", "/label/api/auth/logout",
|
||||||
"/api/auth/me", "/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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param tokenSessionRepository Token会话存储仓库
|
||||||
|
* @param sysUserMapper 用户数据访问层
|
||||||
|
* @param sysCompanyMapper 公司数据访问层
|
||||||
|
* @param sessionTtl 会话有效期(默认2小时)
|
||||||
|
*/
|
||||||
public AuthInterceptor(TokenSessionRepository tokenSessionRepository, SysUserMapper sysUserMapper,
|
public AuthInterceptor(TokenSessionRepository tokenSessionRepository, SysUserMapper sysUserMapper,
|
||||||
SysCompanyMapper sysCompanyMapper, @Value("${labelsys.session.ttl:PT2H}") Duration sessionTtl) {
|
SysCompanyMapper sysCompanyMapper,
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取 Token 并验证会话
|
||||||
String token = extractToken(request.getHeader("Authorization"));
|
String token = extractToken(request.getHeader("Authorization"));
|
||||||
LoginUser loginUser =
|
LoginUser loginUser =
|
||||||
tokenSessionRepository.find(token).orElseThrow(() -> new UnauthorizedException("未登录或登录已过期"));
|
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.selectById(loginUser.companyId());
|
SysCompany company = sysCompanyMapper.selectById(loginUser.companyId());
|
||||||
if (user == null || company == null || user.getStatus() != UserStatus.ENABLED
|
if (user == null || company == null || user.getStatus() != UserStatus.ENABLED
|
||||||
|| company.getStatus() != CompanyStatus.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,
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||||
Exception ex) {
|
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("未登录或登录已过期");
|
||||||
@@ -105,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) {
|
||||||
@@ -112,4 +204,5 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
return handlerMethod.getBeanType().getAnnotation(RequirePosition.class);
|
return handlerMethod.getBeanType().getAnnotation(RequirePosition.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,28 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果自动归档定时任务
|
||||||
|
* <p>
|
||||||
|
* 定期检查并归档符合条件的标注结果,
|
||||||
|
* 将已完成且超过自动归档超时时间的结果归档到历史表。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AutoArchiveAnnotationResultJob {
|
public class AutoArchiveAnnotationResultJob {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果归档服务
|
||||||
|
*/
|
||||||
private final AnnotationResultArchiveService annotationResultArchiveService;
|
private final AnnotationResultArchiveService annotationResultArchiveService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动归档符合条件的标注结果
|
||||||
|
* <p>
|
||||||
|
* 定时执行,默认每5分钟执行一次,可通过配置调整执行间隔。
|
||||||
|
* 归档完成后记录日志。
|
||||||
|
*/
|
||||||
@Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}")
|
@Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}")
|
||||||
public void autoArchiveEligibleResults() {
|
public void autoArchiveEligibleResults() {
|
||||||
int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults();
|
int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults();
|
||||||
|
|||||||
@@ -26,18 +26,35 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注代理配置服务
|
||||||
|
*
|
||||||
|
* <p>提供标注代理配置的保存、查询等核心业务操作,
|
||||||
|
* 支持多种Agent类型的配置管理,关联模型配置和提示词配置。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AnnotationAgentConfigService {
|
public class AnnotationAgentConfigService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注代理配置数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationAgentConfigMapper agentConfigMapper;
|
private final AnnotationAgentConfigMapper agentConfigMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置服务
|
||||||
|
*/
|
||||||
private final SysConfigService sysConfigService;
|
private final SysConfigService sysConfigService;
|
||||||
|
|
||||||
// 保存多个Agent配置
|
/**
|
||||||
// 保存多个Agent配置
|
* 保存多个Agent配置
|
||||||
|
*
|
||||||
|
* @param user 当前登录用户
|
||||||
|
* @param request 保存代理配置请求
|
||||||
|
* @return 代理配置列表响应
|
||||||
|
*/
|
||||||
public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
|
public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
|
||||||
// 遍历所有Agent配置项
|
// 遍历所有Agent配置项
|
||||||
for (Map.Entry<String, SaveAgentConfigRequest.AgentConfigItem> entry : request.getAgentConfigs().entrySet()) {
|
for (Map.Entry<String, SaveAgentConfigRequest.AgentConfigItem> entry : request.getAgentConfigs().entrySet()) {
|
||||||
@@ -109,7 +126,12 @@ public class AnnotationAgentConfigService {
|
|||||||
return getAgentConfigsForCompany(user.companyId());
|
return getAgentConfigsForCompany(user.companyId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从模型配置值中提取API密钥
|
/**
|
||||||
|
* 从模型配置值中提取API密钥
|
||||||
|
*
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return API密钥
|
||||||
|
*/
|
||||||
private String extractApiKeyFromConfig(String configValue) {
|
private String extractApiKeyFromConfig(String configValue) {
|
||||||
try {
|
try {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -120,8 +142,12 @@ public class AnnotationAgentConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
// 从模型配置值中提取模型URL
|
* 从模型配置值中提取模型URL
|
||||||
|
*
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return 模型URL
|
||||||
|
*/
|
||||||
private String extractModelUrlFromConfig(String configValue) {
|
private String extractModelUrlFromConfig(String configValue) {
|
||||||
try {
|
try {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -132,7 +158,12 @@ public class AnnotationAgentConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从模型配置值中提取LLM类型
|
/**
|
||||||
|
* 从模型配置值中提取LLM类型
|
||||||
|
*
|
||||||
|
* @param configValue 配置值
|
||||||
|
* @return LLM类型
|
||||||
|
*/
|
||||||
private String extractLlmTypeFromConfig(String configValue) {
|
private String extractLlmTypeFromConfig(String configValue) {
|
||||||
try {
|
try {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -143,7 +174,12 @@ public class AnnotationAgentConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取提示词文本
|
/**
|
||||||
|
* 获取提示词文本
|
||||||
|
*
|
||||||
|
* @param promptConfigId 提示词配置ID
|
||||||
|
* @return 提示词文本
|
||||||
|
*/
|
||||||
private String getPromptText(Long promptConfigId) {
|
private String getPromptText(Long promptConfigId) {
|
||||||
if (promptConfigId == null) {
|
if (promptConfigId == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -32,22 +32,60 @@ import java.util.List;
|
|||||||
|
|
||||||
import static org.springframework.util.StringUtils.hasText;
|
import static org.springframework.util.StringUtils.hasText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果归档服务
|
||||||
|
*
|
||||||
|
* <p>提供标注结果历史记录的分页查询、详情获取、自动归档等核心业务操作,
|
||||||
|
* 支持从对象存储加载QA内容并归档到历史表。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AnnotationResultArchiveService {
|
public class AnnotationResultArchiveService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人工归档原因标识
|
||||||
|
*/
|
||||||
private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW";
|
private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultMapper annotationResultMapper;
|
private final AnnotationResultMapper annotationResultMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果历史数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储服务
|
||||||
|
*/
|
||||||
private final ObjectStorageService objectStorageService;
|
private final ObjectStorageService objectStorageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 对象映射器
|
||||||
|
*/
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限服务
|
||||||
|
*/
|
||||||
private final DataPermissionService dataPermissionService;
|
private final DataPermissionService dataPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动归档超时时间(默认2小时)
|
||||||
|
*/
|
||||||
@Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
|
@Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
|
||||||
private Duration autoArchiveTimeout;
|
private Duration autoArchiveTimeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询归档历史记录
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param query 分页查询条件
|
||||||
|
* @return 分页历史记录列表
|
||||||
|
*/
|
||||||
public PageResult<AnnotationResultHistoryResponse> pageHistory(LoginUser currentUser,
|
public PageResult<AnnotationResultHistoryResponse> pageHistory(LoginUser currentUser,
|
||||||
AnnotationResultHistoryPageQuery query) {
|
AnnotationResultHistoryPageQuery query) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -32,18 +32,54 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果服务
|
||||||
|
*
|
||||||
|
* <p>提供标注结果的分页查询、详情获取、审核对比、审核结果合并等核心业务操作,
|
||||||
|
* 支持标注结果的归档管理和权限控制。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AnnotationResultService {
|
public class AnnotationResultService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultMapper annotationResultMapper;
|
private final AnnotationResultMapper annotationResultMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果历史数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源资源数据访问层
|
||||||
|
*/
|
||||||
private final SourceResourceMapper sourceResourceMapper;
|
private final SourceResourceMapper sourceResourceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限服务
|
||||||
|
*/
|
||||||
private final DataPermissionService dataPermissionService;
|
private final DataPermissionService dataPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储服务
|
||||||
|
*/
|
||||||
private final ObjectStorageService objectStorageService;
|
private final ObjectStorageService objectStorageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 对象映射器
|
||||||
|
*/
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询标注结果列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param query 分页查询条件
|
||||||
|
* @return 分页结果列表
|
||||||
|
*/
|
||||||
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
|
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
|
||||||
try {
|
try {
|
||||||
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
||||||
@@ -82,6 +118,13 @@ public class AnnotationResultService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标注结果详情
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resultId 结果ID
|
||||||
|
* @return 结果详情响应
|
||||||
|
*/
|
||||||
public AnnotationResultDetailResponse getResult(LoginUser currentUser, Long resultId) {
|
public AnnotationResultDetailResponse getResult(LoginUser currentUser, Long resultId) {
|
||||||
try {
|
try {
|
||||||
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
|
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
|
||||||
@@ -109,6 +152,14 @@ public class AnnotationResultService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为详情响应对象
|
||||||
|
*
|
||||||
|
* @param result 标注结果实体
|
||||||
|
* @param qaContent QA内容
|
||||||
|
* @param diffContent 差异内容
|
||||||
|
* @return 详情响应对象
|
||||||
|
*/
|
||||||
private AnnotationResultDetailResponse toDetailResponse(AnnotationResult result,
|
private AnnotationResultDetailResponse toDetailResponse(AnnotationResult result,
|
||||||
QaContent qaContent, DiffContent diffContent) {
|
QaContent qaContent, DiffContent diffContent) {
|
||||||
|
|
||||||
@@ -173,6 +224,13 @@ public class AnnotationResultService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标注结果对比数据
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resultId 结果ID
|
||||||
|
* @return 对比响应对象
|
||||||
|
*/
|
||||||
public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
|
public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
|
||||||
try {
|
try {
|
||||||
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
|
AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
|
||||||
@@ -249,6 +307,13 @@ public class AnnotationResultService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并审核结果
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resultId 结果ID
|
||||||
|
* @param request 合并审核请求
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void mergeReviewResult(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
|
public void mergeReviewResult(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -325,13 +390,19 @@ public class AnnotationResultService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为响应对象
|
||||||
|
*
|
||||||
|
* @param result 标注结果实体
|
||||||
|
* @return 响应对象
|
||||||
|
*/
|
||||||
private AnnotationResultResponse toResponse(AnnotationResult result) {
|
private AnnotationResultResponse toResponse(AnnotationResult result) {
|
||||||
return new AnnotationResultResponse(
|
return new AnnotationResultResponse(
|
||||||
result.getId(),
|
result.getId(),
|
||||||
result.getTaskId(),
|
result.getTaskId(),
|
||||||
result.getTaskName(), // 新增
|
result.getTaskName(),
|
||||||
result.getResourceId(),
|
result.getResourceId(),
|
||||||
result.getResourceName(), // 新增
|
result.getResourceName(),
|
||||||
deriveStatus(result),
|
deriveStatus(result),
|
||||||
result.getRequiresManualReview(),
|
result.getRequiresManualReview(),
|
||||||
result.getIsDeleted(),
|
result.getIsDeleted(),
|
||||||
@@ -341,6 +412,12 @@ public class AnnotationResultService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推导标注结果状态
|
||||||
|
*
|
||||||
|
* @param result 标注结果实体
|
||||||
|
* @return 标注结果状态
|
||||||
|
*/
|
||||||
private AnnotationResultStatus deriveStatus(AnnotationResult result) {
|
private AnnotationResultStatus deriveStatus(AnnotationResult result) {
|
||||||
if (Boolean.TRUE.equals(result.getIsDeleted())) {
|
if (Boolean.TRUE.equals(result.getIsDeleted())) {
|
||||||
return AnnotationResultStatus.ARCHIVED;
|
return AnnotationResultStatus.ARCHIVED;
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import com.labelsys.backend.dto.request.AnnotationTaskPageQuery;
|
|||||||
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
|
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
|
||||||
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
|
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
|
||||||
import com.labelsys.backend.dto.response.AnnotationTaskResponse;
|
import com.labelsys.backend.dto.response.AnnotationTaskResponse;
|
||||||
|
import com.labelsys.backend.entity.AnnotationResult;
|
||||||
import com.labelsys.backend.entity.AnnotationTask;
|
import com.labelsys.backend.entity.AnnotationTask;
|
||||||
import com.labelsys.backend.entity.AnnotationTaskResource;
|
import com.labelsys.backend.entity.AnnotationTaskResource;
|
||||||
import com.labelsys.backend.entity.SourceResource;
|
import com.labelsys.backend.entity.SourceResource;
|
||||||
import com.labelsys.backend.enums.TaskStatus;
|
import com.labelsys.backend.enums.TaskStatus;
|
||||||
import com.labelsys.backend.enums.TaskType;
|
import com.labelsys.backend.enums.TaskType;
|
||||||
|
import com.labelsys.backend.mapper.AnnotationResultMapper;
|
||||||
import com.labelsys.backend.mapper.AnnotationTaskMapper;
|
import com.labelsys.backend.mapper.AnnotationTaskMapper;
|
||||||
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
|
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
|
||||||
import com.labelsys.backend.mapper.SourceResourceMapper;
|
import com.labelsys.backend.mapper.SourceResourceMapper;
|
||||||
@@ -31,21 +33,56 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注任务服务
|
||||||
|
*
|
||||||
|
* <p>提供标注任务的创建、更新、查询、删除等核心业务操作,
|
||||||
|
* 管理任务与资源的关联关系,并处理任务的状态流转。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AnnotationTaskService {
|
public class AnnotationTaskService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注任务数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationTaskMapper annotationTaskMapper;
|
private final AnnotationTaskMapper annotationTaskMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务资源关联数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
|
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果数据访问层
|
||||||
|
*/
|
||||||
|
private final AnnotationResultMapper annotationResultMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源资源数据访问层
|
||||||
|
*/
|
||||||
private final SourceResourceMapper sourceResourceMapper;
|
private final SourceResourceMapper sourceResourceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限服务
|
||||||
|
*/
|
||||||
private final DataPermissionService dataPermissionService;
|
private final DataPermissionService dataPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建标注任务
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 创建任务请求,包含任务名称、任务类型和资源ID列表
|
||||||
|
* @return 创建的任务响应对象
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
|
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
|
||||||
try {
|
try {
|
||||||
|
// 加载并验证资源列表
|
||||||
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
|
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
|
||||||
|
|
||||||
|
// 构建任务实体
|
||||||
AnnotationTask task = AnnotationTask.builder()
|
AnnotationTask task = AnnotationTask.builder()
|
||||||
.id(IdGenerator.nextId())
|
.id(IdGenerator.nextId())
|
||||||
.companyId(currentUser.companyId())
|
.companyId(currentUser.companyId())
|
||||||
@@ -57,7 +94,9 @@ public class AnnotationTaskService {
|
|||||||
.isDeleted(false)
|
.isDeleted(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// 插入任务记录
|
||||||
annotationTaskMapper.insert(task);
|
annotationTaskMapper.insert(task);
|
||||||
|
// 保存任务与资源的关联关系
|
||||||
saveTaskBindings(task.getId(), currentUser.companyId(), resources);
|
saveTaskBindings(task.getId(), currentUser.companyId(), resources);
|
||||||
|
|
||||||
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
|
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
|
||||||
@@ -71,9 +110,18 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新标注任务
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param request 更新任务请求
|
||||||
|
* @return 更新后的任务响应对象
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) {
|
public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) {
|
||||||
try {
|
try {
|
||||||
|
// 查询任务并验证权限
|
||||||
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
|
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
|
||||||
@@ -83,24 +131,29 @@ public class AnnotationTaskService {
|
|||||||
boolean resourcesChanged = false;
|
boolean resourcesChanged = false;
|
||||||
List<SourceResource> resources = null;
|
List<SourceResource> resources = null;
|
||||||
|
|
||||||
|
// 处理资源变更
|
||||||
if (request.resourceIds() != null && !request.resourceIds().isEmpty()) {
|
if (request.resourceIds() != null && !request.resourceIds().isEmpty()) {
|
||||||
List<Long> currentResourceIds = normalizeIds(
|
List<Long> currentResourceIds = normalizeIds(
|
||||||
annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
|
annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
|
||||||
List<Long> targetResourceIds = normalizeIds(request.resourceIds());
|
List<Long> targetResourceIds = normalizeIds(request.resourceIds());
|
||||||
resourcesChanged = !currentResourceIds.equals(targetResourceIds);
|
resourcesChanged = !currentResourceIds.equals(targetResourceIds);
|
||||||
|
|
||||||
|
// 运行中的任务不允许修改资源
|
||||||
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
|
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
|
||||||
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
|
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
|
||||||
}
|
}
|
||||||
resources = loadAndValidateResources(currentUser, request.resourceIds());
|
resources = loadAndValidateResources(currentUser, request.resourceIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新任务类型
|
||||||
if (request.taskType() != null) {
|
if (request.taskType() != null) {
|
||||||
task.setTaskType(request.taskType());
|
task.setTaskType(request.taskType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存任务更新
|
||||||
annotationTaskMapper.updateById(task);
|
annotationTaskMapper.updateById(task);
|
||||||
|
|
||||||
|
// 更新资源绑定关系
|
||||||
if (resourcesChanged && resources != null) {
|
if (resourcesChanged && resources != null) {
|
||||||
annotationTaskResourceMapper.deleteByTaskId(taskId);
|
annotationTaskResourceMapper.deleteByTaskId(taskId);
|
||||||
saveTaskBindings(taskId, currentUser.companyId(), resources);
|
saveTaskBindings(taskId, currentUser.companyId(), resources);
|
||||||
@@ -120,6 +173,13 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询单个标注任务详情
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 任务响应对象
|
||||||
|
*/
|
||||||
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
|
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
|
||||||
try {
|
try {
|
||||||
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
|
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
|
||||||
@@ -135,11 +195,20 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询标注任务列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param query 分页查询条件
|
||||||
|
* @return 分页任务列表
|
||||||
|
*/
|
||||||
public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
|
public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
|
||||||
try {
|
try {
|
||||||
|
// 获取数据权限过滤条件
|
||||||
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
||||||
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
|
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>()
|
LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>()
|
||||||
.eq(AnnotationTask::getCompanyId, currentUser.companyId())
|
.eq(AnnotationTask::getCompanyId, currentUser.companyId())
|
||||||
.eq(query.taskType() != null, AnnotationTask::getTaskType, query.taskType())
|
.eq(query.taskType() != null, AnnotationTask::getTaskType, query.taskType())
|
||||||
@@ -147,17 +216,21 @@ public class AnnotationTaskService {
|
|||||||
.eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted())
|
.eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted())
|
||||||
.like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword());
|
.like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword());
|
||||||
|
|
||||||
|
// 应用数据权限过滤
|
||||||
if (shouldFilterByUserId) {
|
if (shouldFilterByUserId) {
|
||||||
wrapper.eq(AnnotationTask::getCreatorId, currentUser.userId());
|
wrapper.eq(AnnotationTask::getCreatorId, currentUser.userId());
|
||||||
} else if (!allowedRoles.isEmpty()) {
|
} else if (!allowedRoles.isEmpty()) {
|
||||||
wrapper.in(AnnotationTask::getCreatorRole, allowedRoles);
|
wrapper.in(AnnotationTask::getCreatorRole, allowedRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按创建时间降序排序
|
||||||
wrapper.orderByDesc(AnnotationTask::getCreatedAt);
|
wrapper.orderByDesc(AnnotationTask::getCreatedAt);
|
||||||
|
|
||||||
|
// 执行分页查询
|
||||||
Page<AnnotationTask> page = new Page<>(query.pageNo(), query.pageSize());
|
Page<AnnotationTask> page = new Page<>(query.pageNo(), query.pageSize());
|
||||||
Page<AnnotationTask> resultPage = annotationTaskMapper.selectPage(page, wrapper);
|
Page<AnnotationTask> resultPage = annotationTaskMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
|
// 转换为响应对象,并支持按资源ID过滤
|
||||||
List<AnnotationTaskResponse> records = resultPage.getRecords().stream()
|
List<AnnotationTaskResponse> records = resultPage.getRecords().stream()
|
||||||
.filter(task -> query.resourceId() == null
|
.filter(task -> query.resourceId() == null
|
||||||
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
|
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
|
||||||
@@ -175,6 +248,18 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除标注任务(软删除)
|
||||||
|
*
|
||||||
|
* <p>删除前会检查:
|
||||||
|
* <ul>
|
||||||
|
* <li>任务是否处于运行状态(运行中不允许删除)</li>
|
||||||
|
* <li>是否存在关联的标注结果(有结果不允许删除)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param taskId 任务ID
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTask(LoginUser currentUser, Long taskId) {
|
public void deleteTask(LoginUser currentUser, Long taskId) {
|
||||||
try {
|
try {
|
||||||
@@ -183,11 +268,29 @@ public class AnnotationTaskService {
|
|||||||
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
|
||||||
}
|
}
|
||||||
assertTaskPermission(currentUser, task);
|
assertTaskPermission(currentUser, task);
|
||||||
|
|
||||||
|
// 检查运行状态
|
||||||
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) {
|
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) {
|
||||||
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除");
|
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除");
|
||||||
}
|
}
|
||||||
task.setIsDeleted(true);
|
|
||||||
annotationTaskMapper.updateById(task);
|
// 检查是否存在关联的标注结果(防止误删有价值数据)
|
||||||
|
long resultCount = annotationResultMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<AnnotationResult>()
|
||||||
|
.eq(AnnotationResult::getTaskId, taskId)
|
||||||
|
.eq(AnnotationResult::getIsDeleted, false)
|
||||||
|
);
|
||||||
|
if (resultCount > 0) {
|
||||||
|
throw new BusinessException(ResultCode.CONFLICT,
|
||||||
|
String.format("任务存在 %d 条标注结果,不允许删除", resultCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除任务(使用 deleteById 触发 @TableLogic 注解的逻辑删除)
|
||||||
|
annotationTaskMapper.deleteById(taskId);
|
||||||
|
|
||||||
|
// 清理关联的任务资源记录
|
||||||
|
annotationTaskResourceMapper.deleteByTaskId(taskId);
|
||||||
|
|
||||||
log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}", currentUser.companyId(),
|
log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}", currentUser.companyId(),
|
||||||
currentUser.userId(), taskId);
|
currentUser.userId(), taskId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -197,6 +300,13 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载并验证资源列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceIds 资源ID列表
|
||||||
|
* @return 验证通过的资源列表
|
||||||
|
*/
|
||||||
private List<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) {
|
private List<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) {
|
||||||
if (resourceIds == null || resourceIds.isEmpty()) {
|
if (resourceIds == null || resourceIds.isEmpty()) {
|
||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空");
|
throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空");
|
||||||
@@ -217,6 +327,13 @@ public class AnnotationTaskService {
|
|||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存任务与资源的绑定关系
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param resources 资源列表
|
||||||
|
*/
|
||||||
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
|
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
|
||||||
for (SourceResource resource : resources) {
|
for (SourceResource resource : resources) {
|
||||||
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
|
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
|
||||||
@@ -228,6 +345,13 @@ public class AnnotationTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建任务响应对象
|
||||||
|
*
|
||||||
|
* @param task 任务实体
|
||||||
|
* @param resourceIds 资源ID列表
|
||||||
|
* @return 任务响应对象
|
||||||
|
*/
|
||||||
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
|
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
|
||||||
return new AnnotationTaskResponse(
|
return new AnnotationTaskResponse(
|
||||||
task.getId(),
|
task.getId(),
|
||||||
@@ -240,10 +364,22 @@ public class AnnotationTaskService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从资源列表提取ID列表
|
||||||
|
*
|
||||||
|
* @param resources 资源列表
|
||||||
|
* @return 排序后的资源ID列表
|
||||||
|
*/
|
||||||
private List<Long> resourceIds(List<SourceResource> resources) {
|
private List<Long> resourceIds(List<SourceResource> resources) {
|
||||||
return resources.stream().map(SourceResource::getId).sorted().toList();
|
return resources.stream().map(SourceResource::getId).sorted().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化ID列表(去重并排序)
|
||||||
|
*
|
||||||
|
* @param resourceIds 原始ID列表
|
||||||
|
* @return 去重排序后的ID列表
|
||||||
|
*/
|
||||||
private List<Long> normalizeIds(List<Long> resourceIds) {
|
private List<Long> normalizeIds(List<Long> resourceIds) {
|
||||||
Set<Long> uniqueIds = new HashSet<>(resourceIds);
|
Set<Long> uniqueIds = new HashSet<>(resourceIds);
|
||||||
List<Long> sortedIds = new ArrayList<>(uniqueIds);
|
List<Long> sortedIds = new ArrayList<>(uniqueIds);
|
||||||
@@ -251,12 +387,24 @@ public class AnnotationTaskService {
|
|||||||
return sortedIds;
|
return sortedIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言任务操作权限
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param task 任务实体
|
||||||
|
*/
|
||||||
private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) {
|
private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) {
|
||||||
if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) {
|
if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) {
|
||||||
throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务");
|
throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认任务类型
|
||||||
|
*
|
||||||
|
* @param taskType 任务类型(可为null)
|
||||||
|
* @return 任务类型,若为null则返回默认类型 EXTRACT_QA
|
||||||
|
*/
|
||||||
private TaskType defaultTaskType(TaskType taskType) {
|
private TaskType defaultTaskType(TaskType taskType) {
|
||||||
return taskType != null ? taskType : TaskType.EXTRACT_QA;
|
return taskType != null ? taskType : TaskType.EXTRACT_QA;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,19 +26,48 @@ import java.time.Duration;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证服务
|
||||||
|
*
|
||||||
|
* <p>提供用户登录、登出、密码修改、获取当前用户等核心认证业务操作。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司数据访问层
|
||||||
|
*/
|
||||||
private final SysCompanyMapper sysCompanyMapper;
|
private final SysCompanyMapper sysCompanyMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据访问层
|
||||||
|
*/
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码编码器
|
||||||
|
*/
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token会话存储仓库
|
||||||
|
*/
|
||||||
private final TokenSessionRepository tokenSessionRepository;
|
private final TokenSessionRepository tokenSessionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话有效期(默认2小时)
|
||||||
|
*/
|
||||||
@Value("${labelsys.session.ttl:PT2H}")
|
@Value("${labelsys.session.ttl:PT2H}")
|
||||||
private Duration sessionTtl;
|
private Duration sessionTtl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用公司列表
|
||||||
|
*
|
||||||
|
* @param phone 用户手机号
|
||||||
|
* @return 可用公司选项列表
|
||||||
|
*/
|
||||||
public List<CompanyOptionResponse> listAvailableCompanies(String phone) {
|
public List<CompanyOptionResponse> listAvailableCompanies(String phone) {
|
||||||
try {
|
try {
|
||||||
return sysCompanyMapper.findEnabledCompaniesByPhone(phone).stream()
|
return sysCompanyMapper.findEnabledCompaniesByPhone(phone).stream()
|
||||||
@@ -50,6 +79,12 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*
|
||||||
|
* @param request 登录请求
|
||||||
|
* @return 登录响应
|
||||||
|
*/
|
||||||
public LoginResponse login(LoginRequest request) {
|
public LoginResponse login(LoginRequest request) {
|
||||||
try {
|
try {
|
||||||
SysCompany company = loadEnabledCompany(request.companyCode());
|
SysCompany company = loadEnabledCompany(request.companyCode());
|
||||||
@@ -70,6 +105,12 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户
|
||||||
|
*
|
||||||
|
* @param token 登录令牌
|
||||||
|
* @return 当前登录用户
|
||||||
|
*/
|
||||||
public LoginUser getCurrentUser(String token) {
|
public LoginUser getCurrentUser(String token) {
|
||||||
try {
|
try {
|
||||||
return tokenSessionRepository.find(token)
|
return tokenSessionRepository.find(token)
|
||||||
@@ -82,6 +123,12 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 修改密码请求
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void changePassword(LoginUser currentUser, ChangePasswordRequest request) {
|
public void changePassword(LoginUser currentUser, ChangePasswordRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -108,6 +155,11 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登出
|
||||||
|
*
|
||||||
|
* @param token 登录令牌
|
||||||
|
*/
|
||||||
public void logout(String token) {
|
public void logout(String token) {
|
||||||
try {
|
try {
|
||||||
tokenSessionRepository.remove(token);
|
tokenSessionRepository.remove(token);
|
||||||
@@ -117,6 +169,12 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载启用状态的公司
|
||||||
|
*
|
||||||
|
* @param companyCode 公司编码
|
||||||
|
* @return 公司实体
|
||||||
|
*/
|
||||||
private SysCompany loadEnabledCompany(String companyCode) {
|
private SysCompany loadEnabledCompany(String companyCode) {
|
||||||
SysCompany company = sysCompanyMapper.findByCompanyCode(companyCode);
|
SysCompany company = sysCompanyMapper.findByCompanyCode(companyCode);
|
||||||
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
|
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
|
||||||
@@ -125,6 +183,13 @@ public class AuthService {
|
|||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载启用状态的用户
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param phone 用户手机号
|
||||||
|
* @return 用户实体
|
||||||
|
*/
|
||||||
private SysUser loadEnabledUser(Long companyId, String phone) {
|
private SysUser loadEnabledUser(Long companyId, String phone) {
|
||||||
SysUser user = sysUserMapper.findByCompanyIdAndPhone(companyId, phone);
|
SysUser user = sysUserMapper.findByCompanyIdAndPhone(companyId, phone);
|
||||||
if (user == null || user.getStatus() != UserStatus.ENABLED) {
|
if (user == null || user.getStatus() != UserStatus.ENABLED) {
|
||||||
@@ -132,4 +197,5 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,13 +15,28 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司服务
|
||||||
|
*
|
||||||
|
* <p>提供公司的创建、查询、状态更新等核心业务操作,
|
||||||
|
* 所有操作仅限系统管理员执行。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CompanyService {
|
public class CompanyService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司数据访问层
|
||||||
|
*/
|
||||||
private final SysCompanyMapper sysCompanyMapper;
|
private final SysCompanyMapper sysCompanyMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有公司列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户(必须是系统管理员)
|
||||||
|
* @return 公司列表
|
||||||
|
*/
|
||||||
public List<SysCompany> listCompanies(LoginUser currentUser) {
|
public List<SysCompany> listCompanies(LoginUser currentUser) {
|
||||||
try {
|
try {
|
||||||
assertPlatformAdmin(currentUser);
|
assertPlatformAdmin(currentUser);
|
||||||
@@ -35,6 +50,13 @@ public class CompanyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建公司
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户(必须是系统管理员)
|
||||||
|
* @param request 创建公司请求
|
||||||
|
* @return 创建的公司实体
|
||||||
|
*/
|
||||||
public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
|
public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
|
||||||
try {
|
try {
|
||||||
assertPlatformAdmin(currentUser);
|
assertPlatformAdmin(currentUser);
|
||||||
@@ -58,6 +80,13 @@ public class CompanyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新公司状态
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户(必须是系统管理员)
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param request 更新状态请求
|
||||||
|
*/
|
||||||
public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) {
|
public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) {
|
||||||
try {
|
try {
|
||||||
assertPlatformAdmin(currentUser);
|
assertPlatformAdmin(currentUser);
|
||||||
@@ -73,9 +102,15 @@ public class CompanyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证是否为系统管理员
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
*/
|
||||||
private void assertPlatformAdmin(LoginUser currentUser) {
|
private void assertPlatformAdmin(LoginUser currentUser) {
|
||||||
if (!currentUser.isSuperAdmin()) {
|
if (!currentUser.isSuperAdmin()) {
|
||||||
throw new ForbiddenException("仅系统管理员可操作");
|
throw new ForbiddenException("仅系统管理员可操作");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,30 @@ import java.util.List;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限服务
|
||||||
|
*
|
||||||
|
* <p>提供数据访问权限控制,基于用户角色实现数据可见性过滤。
|
||||||
|
* 支持 EMPLOYEE(员工)、MANAGER(管理者)、ENGINEER(工程师)三种角色的权限控制。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DataPermissionService {
|
public class DataPermissionService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDBC模板
|
||||||
|
*/
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否有权限访问创建者的数据
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param creatorId 创建者ID
|
||||||
|
* @param creatorRole 创建者角色
|
||||||
|
* @return 是否有权限
|
||||||
|
*/
|
||||||
public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
|
public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
|
||||||
try {
|
try {
|
||||||
return switch (currentUser.role()) {
|
return switch (currentUser.role()) {
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
package com.labelsys.backend.service;
|
package com.labelsys.backend.service;
|
||||||
|
|
||||||
|
import com.labelsys.backend.config.ObjectStorageProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.labelsys.backend.config.ObjectStorageProperties;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
|
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
|
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储初始化器
|
||||||
|
*
|
||||||
|
* <p>在应用启动时自动初始化所需的对象存储桶,包括源文件桶、产物桶和导出桶。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ObjectStorageInitializer {
|
public class ObjectStorageInitializer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 客户端
|
||||||
|
*/
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置属性
|
||||||
|
*/
|
||||||
private final ObjectStorageProperties properties;
|
private final ObjectStorageProperties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用启动完成后初始化存储桶
|
||||||
|
*/
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void initializeBuckets() {
|
public void initializeBuckets() {
|
||||||
log.info("开始初始化对象存储桶...");
|
log.info("开始初始化对象存储桶...");
|
||||||
@@ -47,6 +60,12 @@ public class ObjectStorageInitializer {
|
|||||||
log.info("对象存储桶初始化完成");
|
log.info("对象存储桶初始化完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查存储桶是否存在
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
private boolean bucketExists(String bucketName) {
|
private boolean bucketExists(String bucketName) {
|
||||||
try {
|
try {
|
||||||
s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
|
s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
|
||||||
@@ -56,7 +75,13 @@ public class ObjectStorageInitializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建存储桶
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
*/
|
||||||
private void createBucket(String bucketName) {
|
private void createBucket(String bucketName) {
|
||||||
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
|
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,39 @@ package com.labelsys.backend.service;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储服务接口
|
||||||
|
*
|
||||||
|
* <p>定义对象存储的核心操作,包括上传、下载、删除和生成预签名URL。
|
||||||
|
*/
|
||||||
public interface ObjectStorageService {
|
public interface ObjectStorageService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到对象存储
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
* @param content 文件内容
|
||||||
|
* @param contentType 内容类型
|
||||||
|
* @return 对象键
|
||||||
|
*/
|
||||||
String upload(String bucketName, String objectKey, byte[] content, String contentType);
|
String upload(String bucketName, String objectKey, byte[] content, String contentType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从对象存储删除文件
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
*/
|
||||||
void delete(String bucketName, String objectKey);
|
void delete(String bucketName, String objectKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从对象存储下载文件
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
* @return 文件内容字节数组
|
||||||
|
*/
|
||||||
byte[] download(String bucketName, String objectKey);
|
byte[] download(String bucketName, String objectKey);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,17 +16,38 @@ import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
|||||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rustfs 对象存储服务实现
|
||||||
|
*
|
||||||
|
* 基于 AWS S3 SDK 实现的对象存储服务,提供上传、下载、删除和生成预签名URL等功能。
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RustfsObjectStorageService implements ObjectStorageService {
|
public class RustfsObjectStorageService implements ObjectStorageService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 客户端
|
||||||
|
*/
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置属性
|
||||||
|
*/
|
||||||
private final ObjectStorageProperties objectStorageProperties;
|
private final ObjectStorageProperties objectStorageProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到对象存储
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
* @param content 文件内容
|
||||||
|
* @param contentType 内容类型
|
||||||
|
* @return 对象键
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String upload(String bucketName, String objectKey, byte[] content, String contentType) {
|
public String upload(String bucketName, String objectKey, byte[] content, String contentType) {
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +64,12 @@ public class RustfsObjectStorageService implements ObjectStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从对象存储删除文件
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void delete(String bucketName, String objectKey) {
|
public void delete(String bucketName, String objectKey) {
|
||||||
try {
|
try {
|
||||||
@@ -55,6 +82,13 @@ public class RustfsObjectStorageService implements ObjectStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从对象存储下载文件
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
* @return 文件内容字节数组
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public byte[] download(String bucketName, String objectKey) {
|
public byte[] download(String bucketName, String objectKey) {
|
||||||
try {
|
try {
|
||||||
@@ -68,6 +102,14 @@ public class RustfsObjectStorageService implements ObjectStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成预签名URL
|
||||||
|
*
|
||||||
|
* @param bucketName 存储桶名称
|
||||||
|
* @param objectKey 对象键
|
||||||
|
* @param duration 有效期
|
||||||
|
* @return 预签名URL
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String generatePresignedUrl(String bucketName, String objectKey, Duration duration) {
|
public String generatePresignedUrl(String bucketName, String objectKey, Duration duration) {
|
||||||
try (S3Presigner presigner = S3Presigner.builder()
|
try (S3Presigner presigner = S3Presigner.builder()
|
||||||
@@ -93,4 +135,5 @@ public class RustfsObjectStorageService implements ObjectStorageService {
|
|||||||
throw new BusinessException(ResultCode.ERROR, "生成预签名URL失败");
|
throw new BusinessException(ResultCode.ERROR, "生成预签名URL失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,23 +45,79 @@ import java.time.Duration;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源资源服务
|
||||||
|
*
|
||||||
|
* <p>提供资源的上传、下载、查询、删除等核心业务操作,
|
||||||
|
* 支持图片BBOX标注功能,并处理资源与任务的关联关系。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SourceResourceService {
|
public class SourceResourceService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源资源数据访问层
|
||||||
|
*/
|
||||||
private final SourceResourceMapper sourceResourceMapper;
|
private final SourceResourceMapper sourceResourceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注结果数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultMapper annotationResultMapper;
|
private final AnnotationResultMapper annotationResultMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注历史数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务资源关联数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
|
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据访问层
|
||||||
|
*/
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限服务
|
||||||
|
*/
|
||||||
private final DataPermissionService dataPermissionService;
|
private final DataPermissionService dataPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储服务
|
||||||
|
*/
|
||||||
private final ObjectStorageService objectStorageService;
|
private final ObjectStorageService objectStorageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储配置
|
||||||
|
*/
|
||||||
private final ObjectStorageProperties objectStorageProperties;
|
private final ObjectStorageProperties objectStorageProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片BBOX标注数据访问层
|
||||||
|
*/
|
||||||
private final ImageBboxAnnotationMapper imageBboxAnnotationMapper;
|
private final ImageBboxAnnotationMapper imageBboxAnnotationMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON序列化工具
|
||||||
|
*/
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注任务数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationTaskMapper annotationTaskMapper;
|
private final AnnotationTaskMapper annotationTaskMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传资源
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 上传请求,包含文件和资源类型
|
||||||
|
* @return 上传响应,包含资源信息
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
|
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -72,21 +128,25 @@ public class SourceResourceService {
|
|||||||
if (!ResourceType.isValid(request.getResourceType())) {
|
if (!ResourceType.isValid(request.getResourceType())) {
|
||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
|
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
|
||||||
}
|
}
|
||||||
|
// 确定资源名称
|
||||||
String resourceName =
|
String resourceName =
|
||||||
StringUtils.hasText(request.getResourceName()) ?
|
StringUtils.hasText(request.getResourceName()) ?
|
||||||
request.getResourceName() :
|
request.getResourceName() :
|
||||||
file.getOriginalFilename();
|
file.getOriginalFilename();
|
||||||
|
// 检查资源名称是否重复
|
||||||
SourceResource existingResource =
|
SourceResource existingResource =
|
||||||
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
|
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
|
||||||
if (existingResource != null) {
|
if (existingResource != null) {
|
||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName);
|
throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成资源ID和存储路径
|
||||||
long resourceId = IdGenerator.nextId();
|
long resourceId = IdGenerator.nextId();
|
||||||
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
|
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
|
||||||
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(),
|
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(),
|
||||||
request.getResourceType(),
|
request.getResourceType(),
|
||||||
resourceId, extension);
|
resourceId, extension);
|
||||||
|
// 上传文件到对象存储
|
||||||
try {
|
try {
|
||||||
objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(),
|
objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(),
|
||||||
file.getContentType());
|
file.getContentType());
|
||||||
@@ -94,6 +154,7 @@ public class SourceResourceService {
|
|||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
|
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存资源记录
|
||||||
SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId())
|
SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId())
|
||||||
.creatorId(currentUser.userId()).creatorRole(currentUser.role())
|
.creatorId(currentUser.userId()).creatorRole(currentUser.role())
|
||||||
.resourceName(
|
.resourceName(
|
||||||
@@ -118,11 +179,20 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询资源列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param query 分页查询条件
|
||||||
|
* @return 分页资源列表
|
||||||
|
*/
|
||||||
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
|
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
|
||||||
try {
|
try {
|
||||||
|
// 获取数据权限过滤条件
|
||||||
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
||||||
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
|
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
LambdaQueryWrapper<SourceResource> wrapper =
|
LambdaQueryWrapper<SourceResource> wrapper =
|
||||||
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
|
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
|
||||||
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType,
|
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType,
|
||||||
@@ -130,12 +200,14 @@ public class SourceResourceService {
|
|||||||
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName,
|
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName,
|
||||||
query.keyword());
|
query.keyword());
|
||||||
|
|
||||||
|
// 应用数据权限过滤
|
||||||
if (shouldFilterByUserId) {
|
if (shouldFilterByUserId) {
|
||||||
wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
|
wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
|
||||||
} else if (!allowedRoles.isEmpty()) {
|
} else if (!allowedRoles.isEmpty()) {
|
||||||
wrapper.in(SourceResource::getCreatorRole, allowedRoles);
|
wrapper.in(SourceResource::getCreatorRole, allowedRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按创建时间降序排序
|
||||||
wrapper.orderByDesc(SourceResource::getCreatedAt);
|
wrapper.orderByDesc(SourceResource::getCreatedAt);
|
||||||
|
|
||||||
// 判断是否需要分页
|
// 判断是否需要分页
|
||||||
@@ -158,6 +230,13 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询单个资源详情
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
* @return 资源响应对象
|
||||||
|
*/
|
||||||
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
|
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
|
||||||
try {
|
try {
|
||||||
SourceResource resource = sourceResourceMapper.selectById(resourceId);
|
SourceResource resource = sourceResourceMapper.selectById(resourceId);
|
||||||
@@ -191,6 +270,19 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除资源(软删除)
|
||||||
|
*
|
||||||
|
* <p>删除前会检查:
|
||||||
|
* <ul>
|
||||||
|
* <li>资源是否被标注任务引用(活跃任务不允许删除)</li>
|
||||||
|
* <li>资源是否存在标注历史记录(有历史不允许删除)</li>
|
||||||
|
* <li>资源是否有未删除的标注结果(有结果不允许删除)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteResource(LoginUser currentUser, Long resourceId) {
|
public void deleteResource(LoginUser currentUser, Long resourceId) {
|
||||||
try {
|
try {
|
||||||
@@ -198,6 +290,7 @@ public class SourceResourceService {
|
|||||||
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
|
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
|
||||||
}
|
}
|
||||||
|
// 检查操作权限
|
||||||
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(),
|
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(),
|
||||||
resource.getCreatorRole())) {
|
resource.getCreatorRole())) {
|
||||||
throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源");
|
throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源");
|
||||||
@@ -209,10 +302,8 @@ public class SourceResourceService {
|
|||||||
// 删除关联资源
|
// 删除关联资源
|
||||||
deleteAssociatedRecords(resourceId);
|
deleteAssociatedRecords(resourceId);
|
||||||
|
|
||||||
// 执行软删除
|
// 执行软删除(触发 @TableLogic 注解)
|
||||||
resource.setDeleted(true);
|
sourceResourceMapper.deleteById(resourceId);
|
||||||
resource.setDeletedAt(LocalDateTime.now());
|
|
||||||
sourceResourceMapper.updateById(resource);
|
|
||||||
|
|
||||||
// 删除对象存储中的文件
|
// 删除对象存储中的文件
|
||||||
objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
|
objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
|
||||||
@@ -271,6 +362,13 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图片BBOX标注信息
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
* @return BBOX标注响应对象
|
||||||
|
*/
|
||||||
public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) {
|
public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) {
|
||||||
try {
|
try {
|
||||||
SourceResource resource = sourceResourceMapper.selectById(resourceId);
|
SourceResource resource = sourceResourceMapper.selectById(resourceId);
|
||||||
@@ -314,6 +412,14 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片BBOX标注
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
* @param request BBOX标注请求
|
||||||
|
* @return BBOX标注响应对象
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) {
|
public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -325,6 +431,7 @@ public class SourceResourceService {
|
|||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注");
|
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 序列化BBOX数据
|
||||||
String bboxJson;
|
String bboxJson;
|
||||||
try {
|
try {
|
||||||
bboxJson = objectMapper.writeValueAsString(request.bboxes());
|
bboxJson = objectMapper.writeValueAsString(request.bboxes());
|
||||||
@@ -334,6 +441,7 @@ public class SourceResourceService {
|
|||||||
|
|
||||||
boolean isNewAnnotation = imageBboxAnnotationMapper.selectByResourceId(resourceId) == null;
|
boolean isNewAnnotation = imageBboxAnnotationMapper.selectByResourceId(resourceId) == null;
|
||||||
|
|
||||||
|
// 更新或插入BBOX标注记录
|
||||||
ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId);
|
ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.setBboxJson(bboxJson);
|
existing.setBboxJson(bboxJson);
|
||||||
@@ -370,6 +478,12 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除图片BBOX标注
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteImageBbox(LoginUser currentUser, Long resourceId) {
|
public void deleteImageBbox(LoginUser currentUser, Long resourceId) {
|
||||||
try {
|
try {
|
||||||
@@ -377,6 +491,7 @@ public class SourceResourceService {
|
|||||||
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
|
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
|
||||||
}
|
}
|
||||||
|
// 删除BBOX标注记录
|
||||||
imageBboxAnnotationMapper.deleteByResourceId(resourceId);
|
imageBboxAnnotationMapper.deleteByResourceId(resourceId);
|
||||||
|
|
||||||
// 更新资源表的has_bbox字段为false
|
// 更新资源表的has_bbox字段为false
|
||||||
@@ -393,6 +508,12 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析BBOX JSON数据
|
||||||
|
*
|
||||||
|
* @param bboxJson BBOX坐标JSON字符串
|
||||||
|
* @return BBOX坐标响应列表
|
||||||
|
*/
|
||||||
private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {
|
private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {
|
||||||
if (!StringUtils.hasText(bboxJson)) {
|
if (!StringUtils.hasText(bboxJson)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
@@ -407,6 +528,12 @@ public class SourceResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为资源响应对象
|
||||||
|
*
|
||||||
|
* @param resource 资源实体
|
||||||
|
* @return 资源响应对象
|
||||||
|
*/
|
||||||
private SourceResourceResponse toResponse(SourceResource resource) {
|
private SourceResourceResponse toResponse(SourceResource resource) {
|
||||||
SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
|
SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
|
||||||
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
|
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
|
||||||
@@ -415,6 +542,13 @@ public class SourceResourceService {
|
|||||||
creator == null ? null : creator.getRealName(), resource.getCreatedAt(), resource.getUpdatedAt());
|
creator == null ? null : creator.getRealName(), resource.getCreatedAt(), resource.getUpdatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文件扩展名
|
||||||
|
*
|
||||||
|
* @param originalFilename 原始文件名
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @return 文件扩展名
|
||||||
|
*/
|
||||||
private String resolveExtension(String originalFilename, String resourceType) {
|
private String resolveExtension(String originalFilename, String resourceType) {
|
||||||
if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) {
|
if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) {
|
||||||
return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
|
return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
|
||||||
|
|||||||
@@ -30,19 +30,42 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置服务
|
||||||
|
*
|
||||||
|
* <p>提供系统配置的创建、更新、查询等核心业务操作,
|
||||||
|
* 支持模型配置、提示词配置和系统参数配置的管理,
|
||||||
|
* 并负责配置更新时同步更新标注代理配置。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SysConfigService {
|
public class SysConfigService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置数据访问层
|
||||||
|
*/
|
||||||
private final SysConfigMapper sysConfigMapper;
|
private final SysConfigMapper sysConfigMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注代理配置数据访问层
|
||||||
|
*/
|
||||||
private final AnnotationAgentConfigMapper agentConfigMapper;
|
private final AnnotationAgentConfigMapper agentConfigMapper;
|
||||||
//private final DataPermissionService dataPermissionService;
|
//private final DataPermissionService dataPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存系统配置
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 保存配置请求
|
||||||
|
* @return 保存的配置实体
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) {
|
public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) {
|
||||||
try {
|
try {
|
||||||
|
// 验证配置类型
|
||||||
validateConfigType(request.configType());
|
validateConfigType(request.configType());
|
||||||
|
// 检查配置名称是否重复
|
||||||
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(),
|
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(),
|
||||||
request.configName());
|
request.configName());
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -55,6 +78,7 @@ public class SysConfigService {
|
|||||||
processedConfigValue = processModelConfigValue(processedConfigValue, true);
|
processedConfigValue = processModelConfigValue(processedConfigValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建配置实体
|
||||||
SysConfig config = SysConfig.builder()
|
SysConfig config = SysConfig.builder()
|
||||||
.id(IdGenerator.nextId())
|
.id(IdGenerator.nextId())
|
||||||
.companyId(currentUser.companyId())
|
.companyId(currentUser.companyId())
|
||||||
@@ -79,6 +103,14 @@ public class SysConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新系统配置
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param configId 配置ID
|
||||||
|
* @param request 更新配置请求
|
||||||
|
* @return 更新后的配置实体
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) {
|
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -99,18 +131,20 @@ public class SysConfigService {
|
|||||||
existing.setConfigName(request.configName());
|
existing.setConfigName(request.configName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新配置类型
|
||||||
if (StringUtils.hasText(request.configType())) {
|
if (StringUtils.hasText(request.configType())) {
|
||||||
validateConfigType(request.configType());
|
validateConfigType(request.configType());
|
||||||
existing.setConfigType(request.configType());
|
existing.setConfigType(request.configType());
|
||||||
}
|
}
|
||||||
|
// 更新配置值(模型类型需要加密API密钥)
|
||||||
if (StringUtils.hasText(request.configValue())) {
|
if (StringUtils.hasText(request.configValue())) {
|
||||||
// 如果是model类型,对apiKey进行加密处理
|
|
||||||
if (ConfigType.MODEL.name().equalsIgnoreCase(existing.getConfigType())) {
|
if (ConfigType.MODEL.name().equalsIgnoreCase(existing.getConfigType())) {
|
||||||
existing.setConfigValue(processModelConfigValue(request.configValue(), true));
|
existing.setConfigValue(processModelConfigValue(request.configValue(), true));
|
||||||
} else {
|
} else {
|
||||||
existing.setConfigValue(request.configValue());
|
existing.setConfigValue(request.configValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 更新配置状态
|
||||||
if (StringUtils.hasText(request.status())) {
|
if (StringUtils.hasText(request.status())) {
|
||||||
existing.setStatus(request.status());
|
existing.setStatus(request.status());
|
||||||
}
|
}
|
||||||
@@ -131,6 +165,13 @@ public class SysConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询系统配置列表
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param query 分页查询条件
|
||||||
|
* @return 分页配置列表
|
||||||
|
*/
|
||||||
public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
|
public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
|
||||||
try {
|
try {
|
||||||
// List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
// List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
|
||||||
@@ -166,6 +207,12 @@ public class SysConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为配置响应对象
|
||||||
|
*
|
||||||
|
* @param config 配置实体
|
||||||
|
* @return 配置响应对象
|
||||||
|
*/
|
||||||
public SysConfigResponse toResponse(SysConfig config) {
|
public SysConfigResponse toResponse(SysConfig config) {
|
||||||
// 如果是模型配置,需要解密API密钥
|
// 如果是模型配置,需要解密API密钥
|
||||||
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType()) && config.getConfigValue() != null) {
|
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType()) && config.getConfigValue() != null) {
|
||||||
@@ -208,6 +255,13 @@ public class SysConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置实体(内部方法)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param configId 配置ID
|
||||||
|
* @return 配置实体
|
||||||
|
*/
|
||||||
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
|
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
|
||||||
SysConfig config = sysConfigMapper.selectById(configId);
|
SysConfig config = sysConfigMapper.selectById(configId);
|
||||||
if (config == null || !currentUser.companyId().equals(config.getCompanyId())) {
|
if (config == null || !currentUser.companyId().equals(config.getCompanyId())) {
|
||||||
@@ -216,6 +270,11 @@ public class SysConfigService {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置类型
|
||||||
|
*
|
||||||
|
* @param configType 配置类型
|
||||||
|
*/
|
||||||
private void validateConfigType(String configType) {
|
private void validateConfigType(String configType) {
|
||||||
if (!ConfigType.isValid(configType)) {
|
if (!ConfigType.isValid(configType)) {
|
||||||
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
|
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
|
||||||
@@ -253,18 +312,27 @@ public class SysConfigService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取配置实体
|
* 根据ID获取配置实体
|
||||||
|
*
|
||||||
|
* @param configId 配置ID
|
||||||
|
* @return 配置实体
|
||||||
*/
|
*/
|
||||||
public SysConfig getById(Long configId) {
|
public SysConfig getById(Long configId) {
|
||||||
return sysConfigMapper.selectById(configId);
|
return sysConfigMapper.selectById(configId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取配置详情(根据配置ID查询数据库判断类型并返回SysConfigResponse格式)
|
* 获取配置详情
|
||||||
|
*
|
||||||
|
* <p>根据配置ID查询数据库,对于模型配置会解密API密钥后返回。
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param configId 配置ID
|
||||||
|
* @return 配置响应对象
|
||||||
*/
|
*/
|
||||||
public SysConfigResponse getConfigDetail(LoginUser currentUser, Long configId) {
|
public SysConfigResponse getConfigDetail(LoginUser currentUser, Long configId) {
|
||||||
SysConfig config = getConfigEntity(currentUser, configId);
|
SysConfig config = getConfigEntity(currentUser, configId);
|
||||||
|
|
||||||
// 如果是模型配置,我们需要在返回前处理API密钥解密
|
// 如果是模型配置,需要在返回前处理API密钥解密
|
||||||
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType())) {
|
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType())) {
|
||||||
if (config.getConfigValue() != null) {
|
if (config.getConfigValue() != null) {
|
||||||
try {
|
try {
|
||||||
@@ -275,7 +343,7 @@ public class SysConfigService {
|
|||||||
String decryptedApiKey = SM4Util.decryptSafe(model.getApiKey());
|
String decryptedApiKey = SM4Util.decryptSafe(model.getApiKey());
|
||||||
model.setApiKey(decryptedApiKey);
|
model.setApiKey(decryptedApiKey);
|
||||||
|
|
||||||
// 将更新后的对象转回JSON字符串,但这次API密钥是解密的
|
// 将更新后的对象转回JSON字符串
|
||||||
String updatedConfigValue = objectMapper.writeValueAsString(model);
|
String updatedConfigValue = objectMapper.writeValueAsString(model);
|
||||||
config.setConfigValue(updatedConfigValue);
|
config.setConfigValue(updatedConfigValue);
|
||||||
}
|
}
|
||||||
@@ -286,12 +354,15 @@ public class SysConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总是返回SysConfigResponse对象
|
|
||||||
return toResponse(config);
|
return toResponse(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取公司特定类型的配置实体列表
|
* 获取公司特定类型的配置实体列表
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param configType 配置类型
|
||||||
|
* @return 配置列表
|
||||||
*/
|
*/
|
||||||
public List<SysConfig> getCompanyConfigsByType(Long companyId, String configType) {
|
public List<SysConfig> getCompanyConfigsByType(Long companyId, String configType) {
|
||||||
LambdaQueryWrapper<SysConfig> wrapper =
|
LambdaQueryWrapper<SysConfig> wrapper =
|
||||||
@@ -358,5 +429,6 @@ public class SysConfigService {
|
|||||||
log.error("syncUpdateAgentConfigs failed, configId={}, error={}", configId, e.getMessage(), e);
|
log.error("syncUpdateAgentConfigs failed, configId={}, error={}", configId, e.getMessage(), e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,18 +28,48 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户服务
|
||||||
|
*
|
||||||
|
* 提供用户的创建、查询、更新等核心业务操作,
|
||||||
|
* 支持不同角色的用户管理,包括公司管理员和系统管理员。
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认初始密码
|
||||||
|
*/
|
||||||
private static final String DEFAULT_PASSWORD = "123456";
|
private static final String DEFAULT_PASSWORD = "123456";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据访问层
|
||||||
|
*/
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司数据访问层
|
||||||
|
*/
|
||||||
private final SysCompanyMapper sysCompanyMapper;
|
private final SysCompanyMapper sysCompanyMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码加密器
|
||||||
|
*/
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token会话存储库
|
||||||
|
*/
|
||||||
private final TokenSessionRepository tokenSessionRepository;
|
private final TokenSessionRepository tokenSessionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有管理员用户(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @return 用户列表
|
||||||
|
*/
|
||||||
public List<SysUser> listAllUsers(LoginUser currentUser) {
|
public List<SysUser> listAllUsers(LoginUser currentUser) {
|
||||||
try {
|
try {
|
||||||
assertSystemAdmin(currentUser);
|
assertSystemAdmin(currentUser);
|
||||||
@@ -57,6 +87,12 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公司用户列表(公司管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @return 用户列表
|
||||||
|
*/
|
||||||
public List<SysUser> listCompanyUsers(LoginUser currentUser) {
|
public List<SysUser> listCompanyUsers(LoginUser currentUser) {
|
||||||
try {
|
try {
|
||||||
assertCompanyAdmin(currentUser);
|
assertCompanyAdmin(currentUser);
|
||||||
@@ -72,6 +108,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定公司的管理员列表(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @return 用户列表
|
||||||
|
*/
|
||||||
public List<SysUser> listCompanyAdmins(LoginUser currentUser, Long companyId) {
|
public List<SysUser> listCompanyAdmins(LoginUser currentUser, Long companyId) {
|
||||||
try {
|
try {
|
||||||
assertSystemAdmin(currentUser);
|
assertSystemAdmin(currentUser);
|
||||||
@@ -85,6 +128,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建公司管理员(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 创建请求
|
||||||
|
* @return 创建的用户
|
||||||
|
*/
|
||||||
public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) {
|
public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) {
|
||||||
try {
|
try {
|
||||||
assertSystemAdmin(currentUser);
|
assertSystemAdmin(currentUser);
|
||||||
@@ -101,6 +151,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建系统工程师管理员(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 创建请求
|
||||||
|
* @return 创建的用户
|
||||||
|
*/
|
||||||
public SysUser createSystemEngineerAdmin(LoginUser currentUser, CreateSystemEngineerAdminRequest request) {
|
public SysUser createSystemEngineerAdmin(LoginUser currentUser, CreateSystemEngineerAdminRequest request) {
|
||||||
try {
|
try {
|
||||||
assertSystemAdmin(currentUser);
|
assertSystemAdmin(currentUser);
|
||||||
@@ -126,6 +183,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建公司用户(公司管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param request 创建请求
|
||||||
|
* @return 创建的用户
|
||||||
|
*/
|
||||||
public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) {
|
public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) {
|
||||||
try {
|
try {
|
||||||
assertCompanyAdmin(currentUser);
|
assertCompanyAdmin(currentUser);
|
||||||
@@ -139,6 +203,14 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定公司的用户(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param request 创建请求
|
||||||
|
* @return 创建的用户
|
||||||
|
*/
|
||||||
public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) {
|
public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) {
|
||||||
try {
|
try {
|
||||||
assertSystemAdmin(currentUser);
|
assertSystemAdmin(currentUser);
|
||||||
@@ -153,6 +225,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户角色和岗位(公司管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param request 更新请求
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updateAssignment(LoginUser currentUser, Long userId, UpdateUserAssignmentRequest request) {
|
public void updateAssignment(LoginUser currentUser, Long userId, UpdateUserAssignmentRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -161,6 +240,7 @@ public class UserService {
|
|||||||
== 0) {
|
== 0) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
||||||
}
|
}
|
||||||
|
// 强制用户重新登录
|
||||||
tokenSessionRepository.removeAll(userId);
|
tokenSessionRepository.removeAll(userId);
|
||||||
} catch (BusinessException e) {
|
} catch (BusinessException e) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -171,6 +251,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户状态(公司管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param request 更新请求
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updateStatus(LoginUser currentUser, Long userId, UpdateUserStatusRequest request) {
|
public void updateStatus(LoginUser currentUser, Long userId, UpdateUserStatusRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -178,6 +265,7 @@ public class UserService {
|
|||||||
if (sysUserMapper.updateStatus(userId, currentUser.companyId(), request.status()) == 0) {
|
if (sysUserMapper.updateStatus(userId, currentUser.companyId(), request.status()) == 0) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
||||||
}
|
}
|
||||||
|
// 强制用户重新登录
|
||||||
tokenSessionRepository.removeAll(userId);
|
tokenSessionRepository.removeAll(userId);
|
||||||
} catch (BusinessException e) {
|
} catch (BusinessException e) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -188,6 +276,14 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新公司管理员状态(超级管理员专用)
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param request 更新请求
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId,
|
public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId,
|
||||||
UpdateUserStatusRequest request) {
|
UpdateUserStatusRequest request) {
|
||||||
@@ -196,6 +292,7 @@ public class UserService {
|
|||||||
if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) {
|
if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) {
|
||||||
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
|
||||||
}
|
}
|
||||||
|
// 强制用户重新登录
|
||||||
tokenSessionRepository.removeAll(userId);
|
tokenSessionRepository.removeAll(userId);
|
||||||
} catch (BusinessException e) {
|
} catch (BusinessException e) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -207,6 +304,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户(内部方法)
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param request 创建请求
|
||||||
|
* @return 创建的用户
|
||||||
|
*/
|
||||||
private SysUser createUser(Long companyId, CreateUserRequest request) {
|
private SysUser createUser(Long companyId, CreateUserRequest request) {
|
||||||
if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) {
|
if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) {
|
||||||
throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在");
|
throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在");
|
||||||
@@ -221,6 +325,11 @@ public class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保公司存在且已启用
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
*/
|
||||||
private void ensureEnabledCompany(Long companyId) {
|
private void ensureEnabledCompany(Long companyId) {
|
||||||
SysCompany company = sysCompanyMapper.selectById(companyId);
|
SysCompany company = sysCompanyMapper.selectById(companyId);
|
||||||
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
|
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
|
||||||
@@ -234,15 +343,26 @@ public class UserService {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言超级管理员权限
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
*/
|
||||||
private void assertSystemAdmin(LoginUser currentUser) {
|
private void assertSystemAdmin(LoginUser currentUser) {
|
||||||
if (!currentUser.isSuperAdmin()) {
|
if (!currentUser.isSuperAdmin()) {
|
||||||
throw new ForbiddenException("仅超级管理员可操作");
|
throw new ForbiddenException("仅超级管理员可操作");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言公司管理员权限
|
||||||
|
*
|
||||||
|
* @param currentUser 当前登录用户
|
||||||
|
*/
|
||||||
private void assertCompanyAdmin(LoginUser currentUser) {
|
private void assertCompanyAdmin(LoginUser currentUser) {
|
||||||
if (currentUser.isSuperAdmin() || currentUser.position() != UserPosition.ADMIN) {
|
if (currentUser.isSuperAdmin() || currentUser.position() != UserPosition.ADMIN) {
|
||||||
throw new ForbiddenException("仅公司管理员可操作");
|
throw new ForbiddenException("仅公司管理员可操作");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,34 @@ package com.labelsys.backend.util;
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID 生成器
|
||||||
|
* 使用时间戳 + 序列号的方式生成唯一ID,
|
||||||
|
* 确保在同一毫秒内生成多个ID时不会重复。
|
||||||
|
*/
|
||||||
public final class IdGenerator {
|
public final class IdGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列号,范围 0-999,超过后自动归零
|
||||||
|
*/
|
||||||
private static final AtomicInteger SEQUENCE = new AtomicInteger();
|
private static final AtomicInteger SEQUENCE = new AtomicInteger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造函数,防止实例化
|
||||||
|
*/
|
||||||
private IdGenerator() {
|
private IdGenerator() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成下一个唯一ID
|
||||||
|
* ID 结构:时间戳(毫秒) * 1000 + 序列号(0-999)
|
||||||
|
* 序列号在同一毫秒内递增,超过 999 时归零。
|
||||||
|
*
|
||||||
|
* @return 唯一ID
|
||||||
|
*/
|
||||||
public static long nextId() {
|
public static long nextId() {
|
||||||
int sequence = SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1);
|
int sequence = SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1);
|
||||||
return System.currentTimeMillis() * 1000 + sequence;
|
return System.currentTimeMillis() * 1000 + sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,36 @@ package com.labelsys.backend.util;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象存储路径构建器
|
||||||
|
*
|
||||||
|
* 提供对象存储中文件路径的统一构建方法,
|
||||||
|
* 按照固定格式生成源文件和结果文件的存储路径。
|
||||||
|
*/
|
||||||
public final class ObjectStoragePathBuilder {
|
public final class ObjectStoragePathBuilder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年月格式化器,格式为 yyyyMM(如 202401)
|
||||||
|
*/
|
||||||
private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM");
|
private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造函数,防止实例化
|
||||||
|
*/
|
||||||
private ObjectStoragePathBuilder() {
|
private ObjectStoragePathBuilder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建源文件对象键
|
||||||
|
*
|
||||||
|
* 路径格式:source/{companyId}/{category}/{yearMonth}/{resourceId}/original.{extension}
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
* @param extension 文件扩展名
|
||||||
|
* @return 对象键
|
||||||
|
*/
|
||||||
public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) {
|
public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) {
|
||||||
String category = resourceType.toLowerCase();
|
String category = resourceType.toLowerCase();
|
||||||
return "source/%d/%s/%s/%d/original.%s".formatted(
|
return "source/%d/%s/%s/%d/original.%s".formatted(
|
||||||
@@ -20,6 +43,16 @@ public final class ObjectStoragePathBuilder {
|
|||||||
extension.toLowerCase());
|
extension.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建标注结果 QA 文件对象键
|
||||||
|
*
|
||||||
|
* 路径格式:result/{companyId}/{yearMonth}/{taskId}/{resultId}/qa.json
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param resultId 结果ID
|
||||||
|
* @return 对象键
|
||||||
|
*/
|
||||||
public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) {
|
public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) {
|
||||||
return "result/%d/%s/%d/%d/qa.json".formatted(
|
return "result/%d/%s/%d/%d/qa.json".formatted(
|
||||||
companyId,
|
companyId,
|
||||||
@@ -28,6 +61,16 @@ public final class ObjectStoragePathBuilder {
|
|||||||
resultId);
|
resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建标注结果差异文件对象键
|
||||||
|
*
|
||||||
|
* 路径格式:result/{companyId}/{yearMonth}/{taskId}/{resultId}/diff.json
|
||||||
|
*
|
||||||
|
* @param companyId 公司ID
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param resultId 结果ID
|
||||||
|
* @return 对象键
|
||||||
|
*/
|
||||||
public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) {
|
public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) {
|
||||||
return "result/%d/%s/%d/%d/diff.json".formatted(
|
return "result/%d/%s/%d/%d/diff.json".formatted(
|
||||||
companyId,
|
companyId,
|
||||||
|
|||||||
Reference in New Issue
Block a user