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