diff --git a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java
index 7e1fc34..7b5a0c8 100644
--- a/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java
+++ b/src/main/java/com/labelsys/backend/config/MybatisPlusConfig.java
@@ -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 配置类
+ *
+ *
配置 MyBatis Plus 的增强功能,包括分页插件等。
+ */
@Configuration
public class MybatisPlusConfig {
+ /**
+ * 配置 MyBatis Plus 拦截器
+ *
+ *
注册分页拦截器,支持 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;
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java
index 161698b..297f179 100644
--- a/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java
+++ b/src/main/java/com/labelsys/backend/config/ObjectStorageConfig.java
@@ -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;
+
+/**
+ * 对象存储配置类
+ *
+ *
配置 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()
- .endpointOverride(URI.create(properties.getEndpoint()))
- .region(Region.of(properties.getRegion()))
- .credentialsProvider(StaticCredentialsProvider.create(
- AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
- .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(properties.isPathStyleAccess()).build())
- .build();
+ // 设置自定义 endpoint(支持非 AWS S3 服务如 MinIO)
+ .endpointOverride(URI.create(properties.getEndpoint()))
+ // 设置区域
+ .region(Region.of(properties.getRegion()))
+ // 设置凭证提供者
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())))
+ // 配置路径风格访问(对于 MinIO 等自建存储服务通常需要启用)
+ .serviceConfiguration(S3Configuration.builder()
+ .pathStyleAccessEnabled(properties.isPathStyleAccess())
+ .build())
+ .build();
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java
index b3ca457..036e59f 100644
--- a/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java
+++ b/src/main/java/com/labelsys/backend/config/ObjectStorageProperties.java
@@ -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;
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/config/OpenApiConfig.java b/src/main/java/com/labelsys/backend/config/OpenApiConfig.java
index 783157d..9db35e8 100644
--- a/src/main/java/com/labelsys/backend/config/OpenApiConfig.java
+++ b/src/main/java/com/labelsys/backend/config/OpenApiConfig.java
@@ -5,14 +5,25 @@ 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()
- .title("LabelSys 后台认证鉴权接口")
- .version("0.0.1")
- .description("公司、员工、认证、岗位权限和数据权限接口"));
+ .title("LabelSys 后台认证鉴权接口")
+ .version("0.0.1")
+ .description("公司、员工、认证、岗位权限和数据权限接口"));
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/config/SecurityBeanConfig.java b/src/main/java/com/labelsys/backend/config/SecurityBeanConfig.java
index a3ce514..69b4690 100644
--- a/src/main/java/com/labelsys/backend/config/SecurityBeanConfig.java
+++ b/src/main/java/com/labelsys/backend/config/SecurityBeanConfig.java
@@ -5,11 +5,24 @@ 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 {
+ /**
+ * 创建密码编码器
+ *
+ *
使用 BCrypt 算法对密码进行加密,BCrypt 是一种安全的密码哈希算法,
+ * 具有自动加盐和可配置的计算复杂度特性。
+ *
+ * @return BCryptPasswordEncoder 实例
+ */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/config/WebMvcConfig.java b/src/main/java/com/labelsys/backend/config/WebMvcConfig.java
index d973380..26e3ca4 100644
--- a/src/main/java/com/labelsys/backend/config/WebMvcConfig.java
+++ b/src/main/java/com/labelsys/backend/config/WebMvcConfig.java
@@ -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 配置类
+ *
+ * 实现 WebMvcConfigurer 接口,配置 Spring MVC 拦截器等功能。
+ */
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
+ /**
+ * 认证拦截器
+ */
private final AuthInterceptor authInterceptor;
+ /**
+ * 注册拦截器
+ *
+ * 将认证拦截器注册到拦截器链中,拦截所有 `/api/**` 路径的请求。
+ *
+ * @param registry 拦截器注册器
+ */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**");
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/context/UserContext.java b/src/main/java/com/labelsys/backend/context/UserContext.java
index 201cf25..ccbfebe 100644
--- a/src/main/java/com/labelsys/backend/context/UserContext.java
+++ b/src/main/java/com/labelsys/backend/context/UserContext.java
@@ -1,28 +1,62 @@
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 HOLDER = new ThreadLocal<>();
+ /**
+ * 私有构造函数,防止实例化
+ */
private UserContext() {
}
+ /**
+ * 设置当前登录用户
+ *
+ * @param loginUser 登录用户信息
+ */
public static void set(LoginUser loginUser) {
HOLDER.set(loginUser);
}
+ /**
+ * 获取当前登录用户
+ *
+ * @return 当前登录用户的 Optional 对象,未登录时返回 empty
+ */
public static Optional 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();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java
index 8dadd0d..c326f24 100644
--- a/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java
+++ b/src/main/java/com/labelsys/backend/interceptor/AuthInterceptor.java
@@ -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,84 +12,176 @@ 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;
+
+/**
+ * 认证拦截器
+ *
+ * 实现 HandlerInterceptor 接口,用于在请求处理前进行身份认证和权限校验。
+ * 负责验证用户登录状态、会话有效性、密码修改强制要求以及岗位权限检查。
+ */
@Component
public class AuthInterceptor implements HandlerInterceptor {
+ /**
+ * 公开路径集合(无需登录即可访问)
+ * 包括认证相关接口和 Swagger 文档接口
+ */
private static final Set OPEN_PATHS = Set.of(
- "/api/auth/companies", "/label/api/auth/companies",
- "/api/auth/login", "/label/api/auth/login",
- "/swagger-ui.html", "/label/swagger-ui.html",
- "/v3/api-docs", "/label/v3/api-docs",
- "/v3/api-docs/swagger-config", "/label/v3/api-docs/swagger-config");
+ "/api/auth/companies", "/label/api/auth/companies",
+ "/api/auth/login", "/label/api/auth/login",
+ "/swagger-ui.html", "/label/swagger-ui.html",
+ "/v3/api-docs", "/label/v3/api-docs",
+ "/v3/api-docs/swagger-config", "/label/v3/api-docs/swagger-config");
+ /**
+ * 强制修改密码时允许访问的路径集合
+ * 包括修改密码、登出和获取当前用户信息接口
+ */
private static final Set ALLOWED_WHEN_MUST_CHANGE_PASSWORD = Set.of(
- "/api/auth/change-password", "/label/api/auth/change-password",
- "/api/auth/logout", "/label/api/auth/logout",
- "/api/auth/me", "/label/api/auth/me");
+ "/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;
}
+ /**
+ * 请求处理前的认证拦截
+ *
+ * 执行以下校验逻辑:
+ * 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("未登录或登录已过期"));
+ 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) {
+ || 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) {
+ 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 注解
+ *
+ *
优先从方法上获取注解,若方法上没有则从类上获取。
+ *
+ * @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);
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java
index f18bbd1..d7245f9 100644
--- a/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java
+++ b/src/main/java/com/labelsys/backend/scheduled/AutoArchiveAnnotationResultJob.java
@@ -6,13 +6,28 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
+/**
+ * 标注结果自动归档定时任务
+ *
+ * 定期检查并归档符合条件的标注结果,
+ * 将已完成且超过自动归档超时时间的结果归档到历史表。
+ */
@Slf4j
@Component
@RequiredArgsConstructor
public class AutoArchiveAnnotationResultJob {
+ /**
+ * 标注结果归档服务
+ */
private final AnnotationResultArchiveService annotationResultArchiveService;
+ /**
+ * 自动归档符合条件的标注结果
+ *
+ * 定时执行,默认每5分钟执行一次,可通过配置调整执行间隔。
+ * 归档完成后记录日志。
+ */
@Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}")
public void autoArchiveEligibleResults() {
int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults();
@@ -20,4 +35,4 @@ public class AutoArchiveAnnotationResultJob {
log.info("auto archived annotation results, count={}", archivedCount);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/AnnotationAgentConfigService.java b/src/main/java/com/labelsys/backend/service/AnnotationAgentConfigService.java
index 764a61c..2f8c890 100644
--- a/src/main/java/com/labelsys/backend/service/AnnotationAgentConfigService.java
+++ b/src/main/java/com/labelsys/backend/service/AnnotationAgentConfigService.java
@@ -26,19 +26,36 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-@Slf4j
-@Service
-@Transactional
-@RequiredArgsConstructor
-public class AnnotationAgentConfigService {
+/**
+ * 标注代理配置服务
+ *
+ *
提供标注代理配置的保存、查询等核心业务操作,
+ * 支持多种Agent类型的配置管理,关联模型配置和提示词配置。
+ */
+ @Slf4j
+ @Service
+ @Transactional
+ @RequiredArgsConstructor
+ public class AnnotationAgentConfigService {
- private final AnnotationAgentConfigMapper agentConfigMapper;
+ /**
+ * 标注代理配置数据访问层
+ */
+ private final AnnotationAgentConfigMapper agentConfigMapper;
- private final SysConfigService sysConfigService;
+ /**
+ * 系统配置服务
+ */
+ private final SysConfigService sysConfigService;
- // 保存多个Agent配置
- // 保存多个Agent配置
- public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
+ /**
+ * 保存多个Agent配置
+ *
+ * @param user 当前登录用户
+ * @param request 保存代理配置请求
+ * @return 代理配置列表响应
+ */
+ public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
// 遍历所有Agent配置项
for (Map.Entry entry : request.getAgentConfigs().entrySet()) {
String agentType = entry.getKey();
@@ -109,48 +126,67 @@ public class AnnotationAgentConfigService {
return getAgentConfigsForCompany(user.companyId());
}
- // 从模型配置值中提取API密钥
- private String extractApiKeyFromConfig(String configValue) {
- try {
- ObjectMapper objectMapper = new ObjectMapper();
- LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
- return llmConfig != null ? llmConfig.getApiKey() : null;
- } catch (Exception e) {
- return null;
+ /**
+ * 从模型配置值中提取API密钥
+ *
+ * @param configValue 配置值
+ * @return API密钥
+ */
+ private String extractApiKeyFromConfig(String configValue) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
+ return llmConfig != null ? llmConfig.getApiKey() : null;
+ } catch (Exception e) {
+ return null;
+ }
}
- }
-
- // 从模型配置值中提取模型URL
- private String extractModelUrlFromConfig(String configValue) {
- try {
- ObjectMapper objectMapper = new ObjectMapper();
- LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
- return llmConfig != null ? llmConfig.getModelUrl() : null;
- } catch (Exception e) {
- return null;
+ /**
+ * 从模型配置值中提取模型URL
+ *
+ * @param configValue 配置值
+ * @return 模型URL
+ */
+ private String extractModelUrlFromConfig(String configValue) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
+ return llmConfig != null ? llmConfig.getModelUrl() : null;
+ } catch (Exception e) {
+ return null;
+ }
}
- }
- // 从模型配置值中提取LLM类型
- private String extractLlmTypeFromConfig(String configValue) {
- try {
- ObjectMapper objectMapper = new ObjectMapper();
- LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
- return llmConfig != null ? llmConfig.getLlmType() : null;
- } catch (Exception e) {
- return null;
+ /**
+ * 从模型配置值中提取LLM类型
+ *
+ * @param configValue 配置值
+ * @return LLM类型
+ */
+ private String extractLlmTypeFromConfig(String configValue) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
+ return llmConfig != null ? llmConfig.getLlmType() : null;
+ } catch (Exception e) {
+ return null;
+ }
}
- }
- // 获取提示词文本
- private String getPromptText(Long promptConfigId) {
- if (promptConfigId == null) {
- return null;
+ /**
+ * 获取提示词文本
+ *
+ * @param promptConfigId 提示词配置ID
+ * @return 提示词文本
+ */
+ private String getPromptText(Long promptConfigId) {
+ if (promptConfigId == null) {
+ return null;
+ }
+ SysConfig promptConfig = sysConfigService.getById(promptConfigId);
+ return promptConfig != null ? promptConfig.getConfigValue() : null;
}
- SysConfig promptConfig = sysConfigService.getById(promptConfigId);
- return promptConfig != null ? promptConfig.getConfigValue() : null;
- }
// 获取特定Agent类型的配置实体
public AnnotationAgentConfig getAgentConfigEntity(Long companyId, String agentType) {
diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java
index 671ee9f..99c5a51 100644
--- a/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java
+++ b/src/main/java/com/labelsys/backend/service/AnnotationResultArchiveService.java
@@ -32,56 +32,94 @@ import java.util.List;
import static org.springframework.util.StringUtils.hasText;
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class AnnotationResultArchiveService {
+/**
+ * 标注结果归档服务
+ *
+ * 提供标注结果历史记录的分页查询、详情获取、自动归档等核心业务操作,
+ * 支持从对象存储加载QA内容并归档到历史表。
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ 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 AnnotationResultHistoryMapper annotationResultHistoryMapper;
- private final ObjectStorageService objectStorageService;
- private final ObjectMapper objectMapper;
- private final DataPermissionService dataPermissionService;
+ /**
+ * 标注结果数据访问层
+ */
+ private final AnnotationResultMapper annotationResultMapper;
- @Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
- private Duration autoArchiveTimeout;
+ /**
+ * 标注结果历史数据访问层
+ */
+ private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
- public PageResult pageHistory(LoginUser currentUser,
- AnnotationResultHistoryPageQuery query) {
- try {
- List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
- boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+ /**
+ * 对象存储服务
+ */
+ private final ObjectStorageService objectStorageService;
- var wrapper = new LambdaQueryWrapper()
- .eq(AnnotationResultHistory::getCompanyId, currentUser.companyId())
- .eq(query.taskId() != null, AnnotationResultHistory::getTaskId, query.taskId())
- .eq(query.resourceId() != null, AnnotationResultHistory::getResourceId, query.resourceId())
- .orderByDesc(AnnotationResultHistory::getCreatedAt);
+ /**
+ * JSON 对象映射器
+ */
+ private final ObjectMapper objectMapper;
- if (shouldFilterByUserId) {
- wrapper.eq(AnnotationResultHistory::getCreatorId, currentUser.userId());
- } else if (!allowedRoles.isEmpty()) {
- wrapper.in(AnnotationResultHistory::getCreatorRole, allowedRoles);
+ /**
+ * 数据权限服务
+ */
+ private final DataPermissionService dataPermissionService;
+
+ /**
+ * 自动归档超时时间(默认2小时)
+ */
+ @Value("${labelsys.annotation.auto-archive-timeout:PT2H}")
+ private Duration autoArchiveTimeout;
+
+ /**
+ * 分页查询归档历史记录
+ *
+ * @param currentUser 当前登录用户
+ * @param query 分页查询条件
+ * @return 分页历史记录列表
+ */
+ public PageResult pageHistory(LoginUser currentUser,
+ AnnotationResultHistoryPageQuery query) {
+ try {
+ List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
+ boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+
+ var wrapper = new LambdaQueryWrapper()
+ .eq(AnnotationResultHistory::getCompanyId, currentUser.companyId())
+ .eq(query.taskId() != null, AnnotationResultHistory::getTaskId, query.taskId())
+ .eq(query.resourceId() != null, AnnotationResultHistory::getResourceId, query.resourceId())
+ .orderByDesc(AnnotationResultHistory::getCreatedAt);
+
+ if (shouldFilterByUserId) {
+ wrapper.eq(AnnotationResultHistory::getCreatorId, currentUser.userId());
+ } else if (!allowedRoles.isEmpty()) {
+ wrapper.in(AnnotationResultHistory::getCreatorRole, allowedRoles);
+ }
+
+ var page = new Page(query.pageNo(), query.pageSize());
+ var resultPage = annotationResultHistoryMapper.selectPage(page, wrapper);
+
+ // 分页查询不加载 qa 内容
+ var records = resultPage.getRecords().stream()
+ .map(this::toResponse)
+ .toList();
+
+ return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
+ (int) resultPage.getSize());
+ } catch (Exception e) {
+ log.error("pageHistory failed, companyId={}, userId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
+ throw e;
}
-
- var page = new Page(query.pageNo(), query.pageSize());
- var resultPage = annotationResultHistoryMapper.selectPage(page, wrapper);
-
- // 分页查询不加载 qa 内容
- var records = resultPage.getRecords().stream()
- .map(this::toResponse)
- .toList();
-
- return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
- (int) resultPage.getSize());
- } catch (Exception e) {
- log.error("pageHistory failed, companyId={}, userId={}, error={}",
- currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
- throw e;
}
- }
public AnnotationResultHistoryDetailResponse getHistory(LoginUser currentUser, Long historyId) {
try {
@@ -366,4 +404,4 @@ public class AnnotationResultArchiveService {
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/AnnotationResultService.java b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java
index c420df4..21b9097 100644
--- a/src/main/java/com/labelsys/backend/service/AnnotationResultService.java
+++ b/src/main/java/com/labelsys/backend/service/AnnotationResultService.java
@@ -32,324 +32,401 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class AnnotationResultService {
+/**
+ * 标注结果服务
+ *
+ * 提供标注结果的分页查询、详情获取、审核对比、审核结果合并等核心业务操作,
+ * 支持标注结果的归档管理和权限控制。
+ */
+ @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;
- private final ObjectMapper objectMapper;
+ /**
+ * 标注结果数据访问层
+ */
+ private final AnnotationResultMapper annotationResultMapper;
- public PageResult pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
- try {
- List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
- boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+ /**
+ * 标注结果历史数据访问层
+ */
+ private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
- var wrapper = new LambdaQueryWrapper()
- .eq(AnnotationResult::getIsDeleted, Boolean.FALSE)
- .eq(AnnotationResult::getCompanyId, currentUser.companyId())
- .eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
- .eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
- .eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
- query.requiresManualReview())
- .orderByDesc(AnnotationResult::getCreatedAt);
+ /**
+ * 源资源数据访问层
+ */
+ private final SourceResourceMapper sourceResourceMapper;
- if (shouldFilterByUserId) {
- wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
- } else if (!allowedRoles.isEmpty()) {
- wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
+ /**
+ * 数据权限服务
+ */
+ private final DataPermissionService dataPermissionService;
+
+ /**
+ * 对象存储服务
+ */
+ private final ObjectStorageService objectStorageService;
+
+ /**
+ * JSON 对象映射器
+ */
+ private final ObjectMapper objectMapper;
+
+ /**
+ * 分页查询标注结果列表
+ *
+ * @param currentUser 当前登录用户
+ * @param query 分页查询条件
+ * @return 分页结果列表
+ */
+ public PageResult pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
+ try {
+ List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
+ boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+
+ var wrapper = new LambdaQueryWrapper()
+ .eq(AnnotationResult::getIsDeleted, Boolean.FALSE)
+ .eq(AnnotationResult::getCompanyId, currentUser.companyId())
+ .eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId())
+ .eq(query.resourceId() != null, AnnotationResult::getResourceId, query.resourceId())
+ .eq(query.requiresManualReview() != null, AnnotationResult::getRequiresManualReview,
+ query.requiresManualReview())
+ .orderByDesc(AnnotationResult::getCreatedAt);
+
+ if (shouldFilterByUserId) {
+ wrapper.eq(AnnotationResult::getCreatorId, currentUser.userId());
+ } else if (!allowedRoles.isEmpty()) {
+ wrapper.in(AnnotationResult::getCreatorRole, allowedRoles);
+ }
+
+ var page = new Page(query.pageNo(), query.pageSize());
+ var resultPage = annotationResultMapper.selectPage(page, wrapper);
+
+ var records = resultPage.getRecords().stream()
+ .map(this::toResponse)
+ .filter(response -> query.runtimeStatus() == null
+ || query.runtimeStatus().equals(response.runtimeStatus().name()))
+ .toList();
+
+ return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
+ (int) resultPage.getSize());
+ } catch (Exception e) {
+ log.error("pageResults failed, companyId={}, userId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
+ throw e;
}
-
- var page = new Page(query.pageNo(), query.pageSize());
- var resultPage = annotationResultMapper.selectPage(page, wrapper);
-
- var records = resultPage.getRecords().stream()
- .map(this::toResponse)
- .filter(response -> query.runtimeStatus() == null
- || query.runtimeStatus().equals(response.runtimeStatus().name()))
- .toList();
-
- return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
- (int) resultPage.getSize());
- } catch (Exception e) {
- log.error("pageResults failed, companyId={}, userId={}, error={}",
- currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
- throw e;
}
- }
- public AnnotationResultDetailResponse getResult(LoginUser currentUser, Long resultId) {
- try {
- AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
- currentUser.companyId());
- if (result == null) {
- log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}",
- resultId,
- currentUser.companyId(), currentUser.userId());
- throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ /**
+ * 获取标注结果详情
+ *
+ * @param currentUser 当前登录用户
+ * @param resultId 结果ID
+ * @return 结果详情响应
+ */
+ public AnnotationResultDetailResponse getResult(LoginUser currentUser, Long resultId) {
+ try {
+ AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
+ currentUser.companyId());
+ if (result == null) {
+ log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}",
+ resultId,
+ currentUser.companyId(), currentUser.userId());
+ throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ }
+ //assertResultPermission(currentUser, result);
+
+ // 加载文件内容
+ QaContent qaContent = loadQaContent(result);
+ DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
+ loadDiffSummary(result) : null;
+
+ return toDetailResponse(result, qaContent, diffContent);
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("getResult failed, companyId={}, userId={}, resultId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
+ throw e;
}
- //assertResultPermission(currentUser, result);
-
- // 加载文件内容
- QaContent qaContent = loadQaContent(result);
- DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
- loadDiffSummary(result) : null;
-
- return toDetailResponse(result, qaContent, diffContent);
- } catch (BusinessException e) {
- throw e;
- } catch (Exception e) {
- log.error("getResult failed, companyId={}, userId={}, resultId={}, error={}",
- currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
- throw e;
}
- }
- private AnnotationResultDetailResponse toDetailResponse(AnnotationResult result,
- QaContent qaContent, DiffContent diffContent) {
+ /**
+ * 转换为详情响应对象
+ *
+ * @param result 标注结果实体
+ * @param qaContent QA内容
+ * @param diffContent 差异内容
+ * @return 详情响应对象
+ */
+ private AnnotationResultDetailResponse toDetailResponse(AnnotationResult result,
+ QaContent qaContent, DiffContent diffContent) {
- // 转换 QA 内容(仅保留 records)
- AnnotationResultDetailResponse.QaContentDto qaContentDto = new AnnotationResultDetailResponse.QaContentDto(
- qaContent.records().stream()
- .map(r -> new AnnotationResultDetailResponse.QaRecordDto(
- r.id(),
- r.batchId(),
- r.question(),
- r.answer(),
- r.requiresReview(),
- r.sourceSegments() != null ? new AnnotationResultDetailResponse.SourceSegmentsDto(
- r.sourceSegments().segment(),
- r.sourceSegments().chunkIndex(),
- r.sourceSegments().chunkTitle(),
- r.sourceSegments().chunkContent()) : null,
- r.questionCategory(),
- r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
- r.scores().similarity(),
- r.scores().confidence1(),
- r.scores().confidence2(),
- r.scores().hallucination(),
- r.scores().trust()) : null,
- r.reviewComment()))
- .toList()
- );
-
- // 转换差异内容(仅保留 records)
- AnnotationResultDetailResponse.DiffContentDto diffContentDto = null;
- if (diffContent != null) {
- diffContentDto = new AnnotationResultDetailResponse.DiffContentDto(
- diffContent.records().stream()
- .map(r -> new AnnotationResultDetailResponse.DiffRecordDto(
- r.qaId(), r.question(), r.extractAnswer(),
- r.verifyAnswer(), r.diffReason(), r.mergedAnswer(),
+ // 转换 QA 内容(仅保留 records)
+ AnnotationResultDetailResponse.QaContentDto qaContentDto = new AnnotationResultDetailResponse.QaContentDto(
+ qaContent.records().stream()
+ .map(r -> new AnnotationResultDetailResponse.QaRecordDto(
+ r.id(),
+ r.batchId(),
+ r.question(),
+ r.answer(),
+ r.requiresReview(),
+ r.sourceSegments() != null ? new AnnotationResultDetailResponse.SourceSegmentsDto(
+ r.sourceSegments().segment(),
+ r.sourceSegments().chunkIndex(),
+ r.sourceSegments().chunkTitle(),
+ r.sourceSegments().chunkContent()) : null,
r.questionCategory(),
r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
r.scores().similarity(),
r.scores().confidence1(),
r.scores().confidence2(),
r.scores().hallucination(),
- r.scores().trust()) : null))
+ r.scores().trust()) : null,
+ r.reviewComment()))
.toList()
);
- }
- return new AnnotationResultDetailResponse(
- result.getId(),
- result.getTaskId(),
- result.getTaskName(),
- result.getResourceId(),
- result.getResourceName(),
- deriveStatus(result),
- result.getRequiresManualReview(),
- result.getIsDeleted(),
- result.getQaContentFilePath(),
- result.getDiffSummaryFilePath(),
- qaContentDto,
- diffContentDto,
- result.getCreatedAt()
- );
- }
-
- public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
- try {
- AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
- currentUser.companyId());
- if (result == null) {
- log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}",
- resultId,
- currentUser.companyId(), currentUser.userId());
- throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ // 转换差异内容(仅保留 records)
+ AnnotationResultDetailResponse.DiffContentDto diffContentDto = null;
+ if (diffContent != null) {
+ diffContentDto = new AnnotationResultDetailResponse.DiffContentDto(
+ diffContent.records().stream()
+ .map(r -> new AnnotationResultDetailResponse.DiffRecordDto(
+ r.qaId(), r.question(), r.extractAnswer(),
+ r.verifyAnswer(), r.diffReason(), r.mergedAnswer(),
+ r.questionCategory(),
+ r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
+ r.scores().similarity(),
+ r.scores().confidence1(),
+ r.scores().confidence2(),
+ r.scores().hallucination(),
+ r.scores().trust()) : null))
+ .toList()
+ );
}
- //assertResultPermission(currentUser, result);
- QaContent qaContent = loadQaContent(result);
- DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
- loadDiffSummary(result) : null;
-
- SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
-
- // 转换 QA 记录
- List qaRecords = qaContent.records().stream()
- .map(qa -> new AnnotationResultCompareResponse.QaRecord(
- qa.id(),
- qa.batchId(),
- qa.question(),
- qa.answer(),
- qa.requiresReview(),
- qa.sourceSegments() != null ? new AnnotationResultCompareResponse.SourceSegments(
- qa.sourceSegments().segment(),
- qa.sourceSegments().chunkIndex(),
- qa.sourceSegments().chunkTitle(),
- qa.sourceSegments().chunkContent()) : null,
- qa.questionCategory(),
- qa.scores() != null ? new AnnotationResultCompareResponse.Scores(
- qa.scores().similarity(),
- qa.scores().confidence1(),
- qa.scores().confidence2(),
- qa.scores().hallucination(),
- qa.scores().trust()) : null,
- qa.reviewComment()
- )).toList();
-
- // 转换差异记录
- List diffRecords = diffContent != null ?
- diffContent.records().stream()
- .map(diff -> new AnnotationResultCompareResponse.DiffRecord(
- diff.qaId(),
- diff.question(),
- diff.extractAnswer(),
- diff.verifyAnswer(),
- diff.diffReason(),
- diff.mergedAnswer(),
- diff.questionCategory(),
- diff.scores() != null ? new AnnotationResultCompareResponse.Scores(
- diff.scores().similarity(),
- diff.scores().confidence1(),
- diff.scores().confidence2(),
- diff.scores().hallucination(),
- diff.scores().trust()) : null
- )).toList() : List.of();
-
- return new AnnotationResultCompareResponse(
+ return new AnnotationResultDetailResponse(
result.getId(),
result.getTaskId(),
+ result.getTaskName(),
result.getResourceId(),
- qaRecords,
- diffRecords
+ result.getResourceName(),
+ deriveStatus(result),
+ result.getRequiresManualReview(),
+ result.getIsDeleted(),
+ result.getQaContentFilePath(),
+ result.getDiffSummaryFilePath(),
+ qaContentDto,
+ diffContentDto,
+ result.getCreatedAt()
);
- } catch (BusinessException e) {
- throw e;
- } catch (Exception e) {
- log.error("compareResult failed, companyId={}, userId={}, resultId={}, error={}",
- currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
- throw e;
}
- }
- @Transactional
- public void mergeReviewResult(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
- try {
- AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
- currentUser.companyId());
- if (result == null) {
- throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ /**
+ * 获取标注结果对比数据
+ *
+ * @param currentUser 当前登录用户
+ * @param resultId 结果ID
+ * @return 对比响应对象
+ */
+ public AnnotationResultCompareResponse compareResult(LoginUser currentUser, Long resultId) {
+ try {
+ AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
+ currentUser.companyId());
+ if (result == null) {
+ log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}",
+ resultId,
+ currentUser.companyId(), currentUser.userId());
+ throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ }
+ //assertResultPermission(currentUser, result);
+
+ QaContent qaContent = loadQaContent(result);
+ DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
+ loadDiffSummary(result) : null;
+
+ SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
+
+ // 转换 QA 记录
+ List qaRecords = qaContent.records().stream()
+ .map(qa -> new AnnotationResultCompareResponse.QaRecord(
+ qa.id(),
+ qa.batchId(),
+ qa.question(),
+ qa.answer(),
+ qa.requiresReview(),
+ qa.sourceSegments() != null ? new AnnotationResultCompareResponse.SourceSegments(
+ qa.sourceSegments().segment(),
+ qa.sourceSegments().chunkIndex(),
+ qa.sourceSegments().chunkTitle(),
+ qa.sourceSegments().chunkContent()) : null,
+ qa.questionCategory(),
+ qa.scores() != null ? new AnnotationResultCompareResponse.Scores(
+ qa.scores().similarity(),
+ qa.scores().confidence1(),
+ qa.scores().confidence2(),
+ qa.scores().hallucination(),
+ qa.scores().trust()) : null,
+ qa.reviewComment()
+ )).toList();
+
+ // 转换差异记录
+ List diffRecords = diffContent != null ?
+ diffContent.records().stream()
+ .map(diff -> new AnnotationResultCompareResponse.DiffRecord(
+ diff.qaId(),
+ diff.question(),
+ diff.extractAnswer(),
+ diff.verifyAnswer(),
+ diff.diffReason(),
+ diff.mergedAnswer(),
+ diff.questionCategory(),
+ diff.scores() != null ? new AnnotationResultCompareResponse.Scores(
+ diff.scores().similarity(),
+ diff.scores().confidence1(),
+ diff.scores().confidence2(),
+ diff.scores().hallucination(),
+ diff.scores().trust()) : null
+ )).toList() : List.of();
+
+ return new AnnotationResultCompareResponse(
+ result.getId(),
+ result.getTaskId(),
+ result.getResourceId(),
+ qaRecords,
+ diffRecords
+ );
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("compareResult failed, companyId={}, userId={}, resultId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
+ throw e;
}
- //assertResultPermission(currentUser, result);
+ }
- // 读取当前 qa.json
- QaContent qaContent = loadQaContent(result);
+ /**
+ * 合并审核结果
+ *
+ * @param currentUser 当前登录用户
+ * @param resultId 结果ID
+ * @param request 合并审核请求
+ */
+ @Transactional
+ public void mergeReviewResult(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) {
+ try {
+ AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId,
+ currentUser.companyId());
+ if (result == null) {
+ throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在");
+ }
+ //assertResultPermission(currentUser, result);
- // 更新 qa.json 的 answer 字段和 reviewComment
- List updatedQaRecords = qaContent.records().stream()
- .map(record -> {
- String mergedAnswer = request.mergedAnswers().get(record.id());
- String reviewComment =
- request.reviewComments() != null ? request.reviewComments().get(record.id()) : null;
- if (mergedAnswer != null || reviewComment != null) {
- return new QaContent.QaRecord(
- record.id(),
- record.batchId(),
- record.question(),
- mergedAnswer != null ? mergedAnswer : record.answer(),
- false,
- record.sourceSegments(),
- record.questionCategory(),
- record.scores(),
- reviewComment != null ? reviewComment : record.reviewComment()
- );
- }
- return record;
- })
- .toList();
+ // 读取当前 qa.json
+ QaContent qaContent = loadQaContent(result);
- QaContent updatedQaContent = new QaContent(
- qaContent.taskId(),
- qaContent.resourceId(),
- updatedQaRecords,
- new QaContent.Metadata(
- qaContent.metadata().createdAt(),
- LocalDateTime.now().toString()
- )
- );
- saveQaContent(result, updatedQaContent);
+ // 更新 qa.json 的 answer 字段和 reviewComment
+ List updatedQaRecords = qaContent.records().stream()
+ .map(record -> {
+ String mergedAnswer = request.mergedAnswers().get(record.id());
+ String reviewComment =
+ request.reviewComments() != null ? request.reviewComments().get(record.id()) : null;
+ if (mergedAnswer != null || reviewComment != null) {
+ return new QaContent.QaRecord(
+ record.id(),
+ record.batchId(),
+ record.question(),
+ mergedAnswer != null ? mergedAnswer : record.answer(),
+ false,
+ record.sourceSegments(),
+ record.questionCategory(),
+ record.scores(),
+ reviewComment != null ? reviewComment : record.reviewComment()
+ );
+ }
+ return record;
+ })
+ .toList();
- // 用单条 SQL 原子完成人工审核归档,避免状态部分更新后再次被自动归档扫描到。
- int updated = annotationResultMapper.markReviewedAndArchived(
+ QaContent updatedQaContent = new QaContent(
+ qaContent.taskId(),
+ qaContent.resourceId(),
+ updatedQaRecords,
+ new QaContent.Metadata(
+ qaContent.metadata().createdAt(),
+ LocalDateTime.now().toString()
+ )
+ );
+ saveQaContent(result, updatedQaContent);
+
+ // 用单条 SQL 原子完成人工审核归档,避免状态部分更新后再次被自动归档扫描到。
+ int updated = annotationResultMapper.markReviewedAndArchived(
+ result.getId(),
+ currentUser.companyId(),
+ currentUser.userId());
+
+ if (updated == 0) {
+ // 记录已被其他进程归档
+ throw new BusinessException(ResultCode.CONFLICT, "记录已被归档");
+ }
+
+ result.setRequiresManualReview(false);
+ result.setIsDeleted(true);
+ result.setReviewerId(currentUser.userId());
+
+ // 归档到历史表(人工审核后归档)
+ archiveToHistory(result, currentUser, "审核通过后归档", false);
+
+ log.info("merged review result, companyId={}, userId={}, resultId={}",
+ currentUser.companyId(), currentUser.userId(), resultId);
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("mergeReviewResult failed, companyId={}, userId={}, resultId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ /**
+ * 转换为响应对象
+ *
+ * @param result 标注结果实体
+ * @return 响应对象
+ */
+ private AnnotationResultResponse toResponse(AnnotationResult result) {
+ return new AnnotationResultResponse(
result.getId(),
- currentUser.companyId(),
- currentUser.userId());
+ result.getTaskId(),
+ result.getTaskName(),
+ result.getResourceId(),
+ result.getResourceName(),
+ deriveStatus(result),
+ result.getRequiresManualReview(),
+ result.getIsDeleted(),
+ result.getQaContentFilePath(),
+ result.getDiffSummaryFilePath(),
+ result.getCreatedAt()
+ );
+ }
- if (updated == 0) {
- // 记录已被其他进程归档
- throw new BusinessException(ResultCode.CONFLICT, "记录已被归档");
+ /**
+ * 推导标注结果状态
+ *
+ * @param result 标注结果实体
+ * @return 标注结果状态
+ */
+ private AnnotationResultStatus deriveStatus(AnnotationResult result) {
+ if (Boolean.TRUE.equals(result.getIsDeleted())) {
+ return AnnotationResultStatus.ARCHIVED;
}
-
- result.setRequiresManualReview(false);
- result.setIsDeleted(true);
- result.setReviewerId(currentUser.userId());
-
- // 归档到历史表(人工审核后归档)
- archiveToHistory(result, currentUser, "审核通过后归档", false);
-
- log.info("merged review result, companyId={}, userId={}, resultId={}",
- currentUser.companyId(), currentUser.userId(), resultId);
- } catch (BusinessException e) {
- throw e;
- } catch (Exception e) {
- log.error("mergeReviewResult failed, companyId={}, userId={}, resultId={}, error={}",
- currentUser.companyId(), currentUser.userId(), resultId, e.getMessage(), e);
- throw e;
+ if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
+ return AnnotationResultStatus.MANUAL_REVIEW_PENDING;
+ }
+ return AnnotationResultStatus.AUTO_ARCHIVE_PENDING;
}
- }
-
- private AnnotationResultResponse toResponse(AnnotationResult result) {
- return new AnnotationResultResponse(
- result.getId(),
- result.getTaskId(),
- result.getTaskName(), // 新增
- result.getResourceId(),
- result.getResourceName(), // 新增
- deriveStatus(result),
- result.getRequiresManualReview(),
- result.getIsDeleted(),
- result.getQaContentFilePath(),
- result.getDiffSummaryFilePath(),
- result.getCreatedAt()
- );
- }
-
- private AnnotationResultStatus deriveStatus(AnnotationResult result) {
- if (Boolean.TRUE.equals(result.getIsDeleted())) {
- return AnnotationResultStatus.ARCHIVED;
- }
- if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
- return AnnotationResultStatus.MANUAL_REVIEW_PENDING;
- }
- return AnnotationResultStatus.AUTO_ARCHIVE_PENDING;
- }
private QaContent loadQaContent(AnnotationResult result) {
try {
diff --git a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java
index 5c9d121..8d29678 100644
--- a/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java
+++ b/src/main/java/com/labelsys/backend/service/AnnotationTaskService.java
@@ -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;
+/**
+ * 标注任务服务
+ *
+ * 提供标注任务的创建、更新、查询、删除等核心业务操作,
+ * 管理任务与资源的关联关系,并处理任务的状态流转。
+ */
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationTaskService {
- private final AnnotationTaskMapper annotationTaskMapper;
- private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
- private final SourceResourceMapper sourceResourceMapper;
- private final DataPermissionService dataPermissionService;
+ /**
+ * 标注任务数据访问层
+ */
+ 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 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 resources = null;
+ // 处理资源变更
if (request.resourceIds() != null && !request.resourceIds().isEmpty()) {
List currentResourceIds = normalizeIds(
annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List 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 pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
try {
+ // 获取数据权限过滤条件
List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+ // 构建查询条件
LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
.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 page = new Page<>(query.pageNo(), query.pageSize());
Page resultPage = annotationTaskMapper.selectPage(page, wrapper);
+ // 转换为响应对象,并支持按资源ID过滤
List records = resultPage.getRecords().stream()
.filter(task -> query.resourceId() == null
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
@@ -175,6 +248,18 @@ public class AnnotationTaskService {
}
}
+ /**
+ * 删除标注任务(软删除)
+ *
+ * 删除前会检查:
+ *
+ * - 任务是否处于运行状态(运行中不允许删除)
+ * - 是否存在关联的标注结果(有结果不允许删除)
+ *
+ *
+ * @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()
+ .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 loadAndValidateResources(LoginUser currentUser, List 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 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 resourceIds) {
return new AnnotationTaskResponse(
task.getId(),
@@ -240,10 +364,22 @@ public class AnnotationTaskService {
);
}
+ /**
+ * 从资源列表提取ID列表
+ *
+ * @param resources 资源列表
+ * @return 排序后的资源ID列表
+ */
private List resourceIds(List resources) {
return resources.stream().map(SourceResource::getId).sorted().toList();
}
+ /**
+ * 标准化ID列表(去重并排序)
+ *
+ * @param resourceIds 原始ID列表
+ * @return 去重排序后的ID列表
+ */
private List normalizeIds(List resourceIds) {
Set uniqueIds = new HashSet<>(resourceIds);
List 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;
}
diff --git a/src/main/java/com/labelsys/backend/service/AuthService.java b/src/main/java/com/labelsys/backend/service/AuthService.java
index 4fc36fd..8186aa3 100644
--- a/src/main/java/com/labelsys/backend/service/AuthService.java
+++ b/src/main/java/com/labelsys/backend/service/AuthService.java
@@ -26,19 +26,48 @@ import java.time.Duration;
import java.util.List;
import java.util.UUID;
+/**
+ * 认证服务
+ *
+ * 提供用户登录、登出、密码修改、获取当前用户等核心认证业务操作。
+ */
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
- private final SysCompanyMapper sysCompanyMapper;
- private final SysUserMapper sysUserMapper;
- private final PasswordEncoder passwordEncoder;
+ /**
+ * 公司数据访问层
+ */
+ 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 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;
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/CompanyService.java b/src/main/java/com/labelsys/backend/service/CompanyService.java
index 4cd8765..604469c 100644
--- a/src/main/java/com/labelsys/backend/service/CompanyService.java
+++ b/src/main/java/com/labelsys/backend/service/CompanyService.java
@@ -15,67 +15,102 @@ import org.springframework.stereotype.Service;
import java.util.List;
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class CompanyService {
+/**
+ * 公司服务
+ *
+ * 提供公司的创建、查询、状态更新等核心业务操作,
+ * 所有操作仅限系统管理员执行。
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class CompanyService {
- private final SysCompanyMapper sysCompanyMapper;
+ /**
+ * 公司数据访问层
+ */
+ private final SysCompanyMapper sysCompanyMapper;
- public List listCompanies(LoginUser currentUser) {
- try {
- assertPlatformAdmin(currentUser);
- return sysCompanyMapper.selectList(null);
- } catch (ForbiddenException e) {
- throw e;
- } catch (Exception e) {
- log.error("listCompanies failed, companyId={}, userId={}, error={}",
- currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
- throw e;
- }
- }
-
- public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
- try {
- assertPlatformAdmin(currentUser);
- if (sysCompanyMapper.findByCompanyCode(request.companyCode()) != null) {
- throw new BusinessException(ResultCode.CONFLICT, "公司编码已存在");
+ /**
+ * 查询所有公司列表
+ *
+ * @param currentUser 当前登录用户(必须是系统管理员)
+ * @return 公司列表
+ */
+ public List listCompanies(LoginUser currentUser) {
+ try {
+ assertPlatformAdmin(currentUser);
+ return sysCompanyMapper.selectList(null);
+ } catch (ForbiddenException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("listCompanies failed, companyId={}, userId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
+ throw e;
}
- SysCompany company = SysCompany.builder()
- .id(IdGenerator.nextId())
- .companyCode(request.companyCode())
- .companyName(request.companyName())
- .status(com.labelsys.backend.enums.CompanyStatus.ENABLED)
- .build();
- sysCompanyMapper.insert(company);
- return company;
- } catch (BusinessException e) {
- throw e;
- } catch (Exception e) {
- log.error("createCompany failed, companyId={}, userId={}, companyCode={}, error={}",
- currentUser.companyId(), currentUser.userId(), request.companyCode(), e.getMessage(), e);
- throw e;
}
- }
- public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) {
- try {
- assertPlatformAdmin(currentUser);
- if (sysCompanyMapper.updateStatus(companyId, request.status()) == 0) {
- throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在");
+ /**
+ * 创建公司
+ *
+ * @param currentUser 当前登录用户(必须是系统管理员)
+ * @param request 创建公司请求
+ * @return 创建的公司实体
+ */
+ public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
+ try {
+ assertPlatformAdmin(currentUser);
+ if (sysCompanyMapper.findByCompanyCode(request.companyCode()) != null) {
+ throw new BusinessException(ResultCode.CONFLICT, "公司编码已存在");
+ }
+ SysCompany company = SysCompany.builder()
+ .id(IdGenerator.nextId())
+ .companyCode(request.companyCode())
+ .companyName(request.companyName())
+ .status(com.labelsys.backend.enums.CompanyStatus.ENABLED)
+ .build();
+ sysCompanyMapper.insert(company);
+ return company;
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("createCompany failed, companyId={}, userId={}, companyCode={}, error={}",
+ currentUser.companyId(), currentUser.userId(), request.companyCode(), e.getMessage(), e);
+ throw e;
}
- } catch (BusinessException e) {
- throw e;
- } catch (Exception e) {
- log.error("updateStatus failed, companyId={}, userId={}, targetCompanyId={}, error={}",
- currentUser.companyId(), currentUser.userId(), companyId, e.getMessage(), e);
- throw e;
}
- }
- private void assertPlatformAdmin(LoginUser currentUser) {
- if (!currentUser.isSuperAdmin()) {
- throw new ForbiddenException("仅系统管理员可操作");
+ /**
+ * 更新公司状态
+ *
+ * @param currentUser 当前登录用户(必须是系统管理员)
+ * @param companyId 公司ID
+ * @param request 更新状态请求
+ */
+ public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) {
+ try {
+ assertPlatformAdmin(currentUser);
+ if (sysCompanyMapper.updateStatus(companyId, request.status()) == 0) {
+ throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在");
+ }
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("updateStatus failed, companyId={}, userId={}, targetCompanyId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), companyId, e.getMessage(), e);
+ throw e;
+ }
}
- }
-}
+
+ /**
+ * 验证是否为系统管理员
+ *
+ * @param currentUser 当前登录用户
+ */
+ private void assertPlatformAdmin(LoginUser currentUser) {
+ if (!currentUser.isSuperAdmin()) {
+ throw new ForbiddenException("仅系统管理员可操作");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/DataPermissionService.java b/src/main/java/com/labelsys/backend/service/DataPermissionService.java
index 243dda8..26e0d6a 100644
--- a/src/main/java/com/labelsys/backend/service/DataPermissionService.java
+++ b/src/main/java/com/labelsys/backend/service/DataPermissionService.java
@@ -12,26 +12,43 @@ import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class DataPermissionService {
+/**
+ * 数据权限服务
+ *
+ * 提供数据访问权限控制,基于用户角色实现数据可见性过滤。
+ * 支持 EMPLOYEE(员工)、MANAGER(管理者)、ENGINEER(工程师)三种角色的权限控制。
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class DataPermissionService {
- private final JdbcTemplate jdbcTemplate;
+ /**
+ * JDBC模板
+ */
+ private final JdbcTemplate jdbcTemplate;
- public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
- try {
- return switch (currentUser.role()) {
- case EMPLOYEE -> currentUser.userId().equals(creatorId);
- case MANAGER -> creatorRole == UserRole.EMPLOYEE || creatorRole == UserRole.MANAGER;
- case ENGINEER -> true;
- };
- } catch (Exception e) {
- log.error("canAccessCreator failed, companyId={}, userId={}, creatorId={}, error={}",
- currentUser.companyId(), currentUser.userId(), creatorId, e.getMessage(), e);
- throw e;
+ /**
+ * 判断当前用户是否有权限访问创建者的数据
+ *
+ * @param currentUser 当前登录用户
+ * @param creatorId 创建者ID
+ * @param creatorRole 创建者角色
+ * @return 是否有权限
+ */
+ public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
+ try {
+ return switch (currentUser.role()) {
+ case EMPLOYEE -> currentUser.userId().equals(creatorId);
+ case MANAGER -> creatorRole == UserRole.EMPLOYEE || creatorRole == UserRole.MANAGER;
+ case ENGINEER -> true;
+ };
+ } catch (Exception e) {
+ log.error("canAccessCreator failed, companyId={}, userId={}, creatorId={}, error={}",
+ currentUser.companyId(), currentUser.userId(), creatorId, e.getMessage(), e);
+ throw e;
+ }
}
- }
public List filterByRole(
LoginUser currentUser,
@@ -119,4 +136,4 @@ public class DataPermissionService {
throw e;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java b/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java
index ead66ce..c4a1f17 100644
--- a/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java
+++ b/src/main/java/com/labelsys/backend/service/ObjectStorageInitializer.java
@@ -1,34 +1,47 @@
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;
+/**
+ * 对象存储初始化器
+ *
+ * 在应用启动时自动初始化所需的对象存储桶,包括源文件桶、产物桶和导出桶。
+ */
@Slf4j
@Component
@RequiredArgsConstructor
public class ObjectStorageInitializer {
+ /**
+ * S3 客户端
+ */
private final S3Client s3Client;
+
+ /**
+ * 对象存储配置属性
+ */
private final ObjectStorageProperties properties;
+ /**
+ * 应用启动完成后初始化存储桶
+ */
@EventListener(ApplicationReadyEvent.class)
public void initializeBuckets() {
log.info("开始初始化对象存储桶...");
-
+
String[] buckets = {
- properties.getSourceBucket(),
- properties.getArtifactBucket(),
- properties.getExportBucket()
+ properties.getSourceBucket(),
+ properties.getArtifactBucket(),
+ properties.getExportBucket()
};
for (String bucketName : buckets) {
@@ -43,10 +56,16 @@ public class ObjectStorageInitializer {
log.error("初始化存储桶 {} 失败: {}", bucketName, e.getMessage());
}
}
-
+
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());
}
+
}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/ObjectStorageService.java b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java
index d2ceef5..c1e2064 100644
--- a/src/main/java/com/labelsys/backend/service/ObjectStorageService.java
+++ b/src/main/java/com/labelsys/backend/service/ObjectStorageService.java
@@ -2,12 +2,39 @@ package com.labelsys.backend.service;
import java.time.Duration;
+/**
+ * 对象存储服务接口
+ *
+ *
定义对象存储的核心操作,包括上传、下载、删除和生成预签名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);
/**
diff --git a/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java
index 30f5675..f2192d5 100644
--- a/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java
+++ b/src/main/java/com/labelsys/backend/service/RustfsObjectStorageService.java
@@ -16,23 +16,44 @@ 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 {
PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder()
- .bucket(bucketName)
- .key(objectKey);
+ .bucket(bucketName)
+ .key(objectKey);
if (contentType != null && !contentType.isBlank()) {
requestBuilder.contentType(contentType);
}
@@ -43,31 +64,52 @@ public class RustfsObjectStorageService implements ObjectStorageService {
}
}
+ /**
+ * 从对象存储删除文件
+ *
+ * @param bucketName 存储桶名称
+ * @param objectKey 对象键
+ */
@Override
public void delete(String bucketName, String objectKey) {
try {
s3Client.deleteObject(DeleteObjectRequest.builder()
- .bucket(bucketName)
- .key(objectKey)
- .build());
+ .bucket(bucketName)
+ .key(objectKey)
+ .build());
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储删除失败");
}
}
+ /**
+ * 从对象存储下载文件
+ *
+ * @param bucketName 存储桶名称
+ * @param objectKey 对象键
+ * @return 文件内容字节数组
+ */
@Override
public byte[] download(String bucketName, String objectKey) {
try {
GetObjectRequest request = GetObjectRequest.builder()
- .bucket(bucketName)
- .key(objectKey)
- .build();
+ .bucket(bucketName)
+ .key(objectKey)
+ .build();
return s3Client.getObject(request, ResponseTransformer.toBytes()).asByteArray();
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储下载失败");
}
}
+ /**
+ * 生成预签名URL
+ *
+ * @param bucketName 存储桶名称
+ * @param objectKey 对象键
+ * @param duration 有效期
+ * @return 预签名URL
+ */
@Override
public String generatePresignedUrl(String bucketName, String objectKey, Duration duration) {
try (S3Presigner presigner = S3Presigner.builder()
@@ -75,22 +117,23 @@ public class RustfsObjectStorageService implements ObjectStorageService {
.region(Region.of(objectStorageProperties.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(
- objectStorageProperties.getAccessKey(),
+ objectStorageProperties.getAccessKey(),
objectStorageProperties.getSecretKey())))
.build()) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
- .bucket(bucketName)
- .key(objectKey)
- .build();
+ .bucket(bucketName)
+ .key(objectKey)
+ .build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
- .signatureDuration(duration)
- .getObjectRequest(getObjectRequest)
- .build();
+ .signatureDuration(duration)
+ .getObjectRequest(getObjectRequest)
+ .build();
return presigner.presignGetObject(presignRequest).url().toString();
} catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "生成预签名URL失败");
}
}
+
}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/SourceResourceService.java b/src/main/java/com/labelsys/backend/service/SourceResourceService.java
index 7b0c726..05f6772 100644
--- a/src/main/java/com/labelsys/backend/service/SourceResourceService.java
+++ b/src/main/java/com/labelsys/backend/service/SourceResourceService.java
@@ -45,23 +45,79 @@ import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
+/**
+ * 源资源服务
+ *
+ *
提供资源的上传、下载、查询、删除等核心业务操作,
+ * 支持图片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;
- private final ImageBboxAnnotationMapper imageBboxAnnotationMapper;
- private final ObjectMapper objectMapper;
- private final AnnotationTaskMapper annotationTaskMapper;
+ /**
+ * 源资源数据访问层
+ */
+ 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 pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
try {
+ // 获取数据权限过滤条件
List allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
+ // 构建查询条件
LambdaQueryWrapper wrapper =
new LambdaQueryWrapper().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 {
}
}
+ /**
+ * 删除资源(软删除)
+ *
+ * 删除前会检查:
+ *
+ * - 资源是否被标注任务引用(活跃任务不允许删除)
+ * - 资源是否存在标注历史记录(有历史不允许删除)
+ * - 资源是否有未删除的标注结果(有结果不允许删除)
+ *
+ *
+ * @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 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);
diff --git a/src/main/java/com/labelsys/backend/service/SysConfigService.java b/src/main/java/com/labelsys/backend/service/SysConfigService.java
index 4b01af7..22b3105 100644
--- a/src/main/java/com/labelsys/backend/service/SysConfigService.java
+++ b/src/main/java/com/labelsys/backend/service/SysConfigService.java
@@ -30,19 +30,42 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
+/**
+ * 系统配置服务
+ *
+ * 提供系统配置的创建、更新、查询等核心业务操作,
+ * 支持模型配置、提示词配置和系统参数配置的管理,
+ * 并负责配置更新时同步更新标注代理配置。
+ */
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigService {
- private final SysConfigMapper sysConfigMapper;
+ /**
+ * 系统配置数据访问层
+ */
+ 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 pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
try {
// List 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格式)
+ * 获取配置详情
+ *
+ * 根据配置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 getCompanyConfigsByType(Long companyId, String configType) {
LambdaQueryWrapper wrapper =
@@ -358,5 +429,6 @@ public class SysConfigService {
log.error("syncUpdateAgentConfigs failed, configId={}, error={}", configId, e.getMessage(), e);
throw e;
}
+
}
}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/service/UserService.java b/src/main/java/com/labelsys/backend/service/UserService.java
index 86f1ee4..f295710 100644
--- a/src/main/java/com/labelsys/backend/service/UserService.java
+++ b/src/main/java/com/labelsys/backend/service/UserService.java
@@ -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;
+ /**
+ * 用户数据访问层
+ */
+ private final SysUserMapper sysUserMapper;
+
+ /**
+ * 公司数据访问层
+ */
+ private final SysCompanyMapper sysCompanyMapper;
+
+ /**
+ * 密码加密器
+ */
+ private final PasswordEncoder passwordEncoder;
+
+ /**
+ * Token会话存储库
+ */
private final TokenSessionRepository tokenSessionRepository;
+ /**
+ * 查询所有管理员用户(超级管理员专用)
+ *
+ * @param currentUser 当前登录用户
+ * @return 用户列表
+ */
public List listAllUsers(LoginUser currentUser) {
try {
assertSystemAdmin(currentUser);
@@ -57,6 +87,12 @@ public class UserService {
}
}
+ /**
+ * 查询公司用户列表(公司管理员专用)
+ *
+ * @param currentUser 当前登录用户
+ * @return 用户列表
+ */
public List listCompanyUsers(LoginUser currentUser) {
try {
assertCompanyAdmin(currentUser);
@@ -72,6 +108,13 @@ public class UserService {
}
}
+ /**
+ * 查询指定公司的管理员列表(超级管理员专用)
+ *
+ * @param currentUser 当前登录用户
+ * @param companyId 公司ID
+ * @return 用户列表
+ */
public List 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("仅公司管理员可操作");
}
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/util/IdGenerator.java b/src/main/java/com/labelsys/backend/util/IdGenerator.java
index 4097e2d..5cf665a 100644
--- a/src/main/java/com/labelsys/backend/util/IdGenerator.java
+++ b/src/main/java/com/labelsys/backend/util/IdGenerator.java
@@ -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;
}
-}
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java
index 30574a1..efbc4f3 100644
--- a/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java
+++ b/src/main/java/com/labelsys/backend/util/ObjectStoragePathBuilder.java
@@ -3,36 +3,79 @@ 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(
- companyId,
- category,
- YEAR_MONTH.format(LocalDateTime.now()),
- resourceId,
- extension.toLowerCase());
+ companyId,
+ category,
+ YEAR_MONTH.format(LocalDateTime.now()),
+ resourceId,
+ 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,
- YEAR_MONTH.format(LocalDateTime.now()),
- taskId,
- resultId);
+ companyId,
+ YEAR_MONTH.format(LocalDateTime.now()),
+ taskId,
+ 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,
- YEAR_MONTH.format(LocalDateTime.now()),
- taskId,
- resultId);
+ companyId,
+ YEAR_MONTH.format(LocalDateTime.now()),
+ taskId,
+ resultId);
}
-}
+}
\ No newline at end of file