优化任务删除方法,核心代码注释

This commit is contained in:
wh
2026-05-13 23:29:43 +08:00
parent 92335758bd
commit c51f645227
24 changed files with 1728 additions and 558 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,13 +6,28 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/**
* 标注结果自动归档定时任务
* <p>
* 定期检查并归档符合条件的标注结果,
* 将已完成且超过自动归档超时时间的结果归档到历史表。
*/
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class AutoArchiveAnnotationResultJob { public class AutoArchiveAnnotationResultJob {
/**
* 标注结果归档服务
*/
private final AnnotationResultArchiveService annotationResultArchiveService; private final AnnotationResultArchiveService annotationResultArchiveService;
/**
* 自动归档符合条件的标注结果
* <p>
* 定时执行默认每5分钟执行一次可通过配置调整执行间隔。
* 归档完成后记录日志。
*/
@Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}") @Scheduled(fixedDelayString = "${labelsys.annotation.auto-archive-fixed-delay:300000}")
public void autoArchiveEligibleResults() { public void autoArchiveEligibleResults() {
int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults(); int archivedCount = annotationResultArchiveService.autoArchiveEligibleResults();
@@ -20,4 +35,4 @@ public class AutoArchiveAnnotationResultJob {
log.info("auto archived annotation results, count={}", archivedCount); log.info("auto archived annotation results, count={}", archivedCount);
} }
} }
} }

View File

@@ -26,19 +26,36 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j /**
@Service * 标注代理配置服务
@Transactional *
@RequiredArgsConstructor * <p>提供标注代理配置的保存、查询等核心业务操作,
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配置 * 保存多个Agent配置
public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) { *
* @param user 当前登录用户
* @param request 保存代理配置请求
* @return 代理配置列表响应
*/
public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
// 遍历所有Agent配置项 // 遍历所有Agent配置项
for (Map.Entry<String, SaveAgentConfigRequest.AgentConfigItem> entry : request.getAgentConfigs().entrySet()) { for (Map.Entry<String, SaveAgentConfigRequest.AgentConfigItem> entry : request.getAgentConfigs().entrySet()) {
String agentType = entry.getKey(); String agentType = entry.getKey();
@@ -109,48 +126,67 @@ public class AnnotationAgentConfigService {
return getAgentConfigsForCompany(user.companyId()); return getAgentConfigsForCompany(user.companyId());
} }
// 从模型配置值中提取API密钥 /**
private String extractApiKeyFromConfig(String configValue) { * 从模型配置值中提取API密钥
try { *
ObjectMapper objectMapper = new ObjectMapper(); * @param configValue 配置值
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class); * @return API密钥
return llmConfig != null ? llmConfig.getApiKey() : null; */
} catch (Exception e) { private String extractApiKeyFromConfig(String configValue) {
return null; try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
return llmConfig != null ? llmConfig.getApiKey() : null;
} catch (Exception e) {
return null;
}
} }
}
/**
// 从模型配置值中提取模型URL * 从模型配置值中提取模型URL
private String extractModelUrlFromConfig(String configValue) { *
try { * @param configValue 配置值
ObjectMapper objectMapper = new ObjectMapper(); * @return 模型URL
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class); */
return llmConfig != null ? llmConfig.getModelUrl() : null; private String extractModelUrlFromConfig(String configValue) {
} catch (Exception e) { try {
return null; 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) { * 从模型配置值中提取LLM类型
try { *
ObjectMapper objectMapper = new ObjectMapper(); * @param configValue 配置值
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class); * @return LLM类型
return llmConfig != null ? llmConfig.getLlmType() : null; */
} catch (Exception e) { private String extractLlmTypeFromConfig(String configValue) {
return null; 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类型的配置实体 // 获取特定Agent类型的配置实体
public AnnotationAgentConfig getAgentConfigEntity(Long companyId, String agentType) { public AnnotationAgentConfig getAgentConfigEntity(Long companyId, String agentType) {

View File

@@ -32,56 +32,94 @@ import java.util.List;
import static org.springframework.util.StringUtils.hasText; import static org.springframework.util.StringUtils.hasText;
@Slf4j /**
@Service * 标注结果归档服务
@RequiredArgsConstructor *
public class AnnotationResultArchiveService { * <p>提供标注结果历史记录的分页查询、详情获取、自动归档等核心业务操作,
* 支持从对象存储加载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 AnnotationResultMapper annotationResultMapper;
private final DataPermissionService dataPermissionService;
@Value("${labelsys.annotation.auto-archive-timeout:PT2H}") /**
private Duration autoArchiveTimeout; * 标注结果历史数据访问层
*/
private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
public PageResult<AnnotationResultHistoryResponse> pageHistory(LoginUser currentUser, /**
AnnotationResultHistoryPageQuery query) { * 对象存储服务
try { */
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); private final ObjectStorageService objectStorageService;
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
var wrapper = new LambdaQueryWrapper<AnnotationResultHistory>() /**
.eq(AnnotationResultHistory::getCompanyId, currentUser.companyId()) * JSON 对象映射器
.eq(query.taskId() != null, AnnotationResultHistory::getTaskId, query.taskId()) */
.eq(query.resourceId() != null, AnnotationResultHistory::getResourceId, query.resourceId()) private final ObjectMapper objectMapper;
.orderByDesc(AnnotationResultHistory::getCreatedAt);
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<AnnotationResultHistoryResponse> pageHistory(LoginUser currentUser,
AnnotationResultHistoryPageQuery query) {
try {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
var wrapper = new LambdaQueryWrapper<AnnotationResultHistory>()
.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<AnnotationResultHistory>(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<AnnotationResultHistory>(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) { public AnnotationResultHistoryDetailResponse getHistory(LoginUser currentUser, Long historyId) {
try { try {
@@ -366,4 +404,4 @@ public class AnnotationResultArchiveService {
} }
} }
} }

View File

@@ -32,324 +32,401 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@Slf4j /**
@Service * 标注结果服务
@RequiredArgsConstructor *
public class AnnotationResultService { * <p>提供标注结果的分页查询、详情获取、审核对比、审核结果合并等核心业务操作,
* 支持标注结果的归档管理和权限控制。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationResultService {
private final AnnotationResultMapper annotationResultMapper; /**
private final AnnotationResultHistoryMapper annotationResultHistoryMapper; * 标注结果数据访问层
private final SourceResourceMapper sourceResourceMapper; */
private final DataPermissionService dataPermissionService; private final AnnotationResultMapper annotationResultMapper;
private final ObjectStorageService objectStorageService;
private final ObjectMapper objectMapper;
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) { /**
try { * 标注结果历史数据访问层
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); */
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); private final AnnotationResultHistoryMapper annotationResultHistoryMapper;
var wrapper = new LambdaQueryWrapper<AnnotationResult>() /**
.eq(AnnotationResult::getIsDeleted, Boolean.FALSE) * 源资源数据访问层
.eq(AnnotationResult::getCompanyId, currentUser.companyId()) */
.eq(query.taskId() != null, AnnotationResult::getTaskId, query.taskId()) private final SourceResourceMapper sourceResourceMapper;
.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); private final DataPermissionService dataPermissionService;
/**
* 对象存储服务
*/
private final ObjectStorageService objectStorageService;
/**
* JSON 对象映射器
*/
private final ObjectMapper objectMapper;
/**
* 分页查询标注结果列表
*
* @param currentUser 当前登录用户
* @param query 分页查询条件
* @return 分页结果列表
*/
public PageResult<AnnotationResultResponse> pageResults(LoginUser currentUser, AnnotationResultPageQuery query) {
try {
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
var wrapper = new LambdaQueryWrapper<AnnotationResult>()
.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<AnnotationResult>(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<AnnotationResult>(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()); * @param currentUser 当前登录用户
if (result == null) { * @param resultId 结果ID
log.warn("Result not found or cross-tenant access attempt: resultId={}, companyId={}, userId={}", * @return 结果详情响应
resultId, */
currentUser.companyId(), currentUser.userId()); public AnnotationResultDetailResponse getResult(LoginUser currentUser, Long resultId) {
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); 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 // 转换 QA 内容(仅保留 records
AnnotationResultDetailResponse.QaContentDto qaContentDto = new AnnotationResultDetailResponse.QaContentDto( AnnotationResultDetailResponse.QaContentDto qaContentDto = new AnnotationResultDetailResponse.QaContentDto(
qaContent.records().stream() qaContent.records().stream()
.map(r -> new AnnotationResultDetailResponse.QaRecordDto( .map(r -> new AnnotationResultDetailResponse.QaRecordDto(
r.id(), r.id(),
r.batchId(), r.batchId(),
r.question(), r.question(),
r.answer(), r.answer(),
r.requiresReview(), r.requiresReview(),
r.sourceSegments() != null ? new AnnotationResultDetailResponse.SourceSegmentsDto( r.sourceSegments() != null ? new AnnotationResultDetailResponse.SourceSegmentsDto(
r.sourceSegments().segment(), r.sourceSegments().segment(),
r.sourceSegments().chunkIndex(), r.sourceSegments().chunkIndex(),
r.sourceSegments().chunkTitle(), r.sourceSegments().chunkTitle(),
r.sourceSegments().chunkContent()) : null, 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(),
r.questionCategory(), r.questionCategory(),
r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto( r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
r.scores().similarity(), r.scores().similarity(),
r.scores().confidence1(), r.scores().confidence1(),
r.scores().confidence2(), r.scores().confidence2(),
r.scores().hallucination(), r.scores().hallucination(),
r.scores().trust()) : null)) r.scores().trust()) : null,
r.reviewComment()))
.toList() .toList()
); );
}
return new AnnotationResultDetailResponse( // 转换差异内容(仅保留 records
result.getId(), AnnotationResultDetailResponse.DiffContentDto diffContentDto = null;
result.getTaskId(), if (diffContent != null) {
result.getTaskName(), diffContentDto = new AnnotationResultDetailResponse.DiffContentDto(
result.getResourceId(), diffContent.records().stream()
result.getResourceName(), .map(r -> new AnnotationResultDetailResponse.DiffRecordDto(
deriveStatus(result), r.qaId(), r.question(), r.extractAnswer(),
result.getRequiresManualReview(), r.verifyAnswer(), r.diffReason(), r.mergedAnswer(),
result.getIsDeleted(), r.questionCategory(),
result.getQaContentFilePath(), r.scores() != null ? new AnnotationResultDetailResponse.ScoresDto(
result.getDiffSummaryFilePath(), r.scores().similarity(),
qaContentDto, r.scores().confidence1(),
diffContentDto, r.scores().confidence2(),
result.getCreatedAt() r.scores().hallucination(),
); r.scores().trust()) : null))
} .toList()
);
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); return new AnnotationResultDetailResponse(
DiffContent diffContent = Boolean.TRUE.equals(result.getRequiresManualReview()) ?
loadDiffSummary(result) : null;
SourceResource resource = sourceResourceMapper.selectById(result.getResourceId());
// 转换 QA 记录
List<AnnotationResultCompareResponse.QaRecord> 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<AnnotationResultCompareResponse.DiffRecord> 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.getId(),
result.getTaskId(), result.getTaskId(),
result.getTaskName(),
result.getResourceId(), result.getResourceId(),
qaRecords, result.getResourceName(),
diffRecords 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, * @param currentUser 当前登录用户
currentUser.companyId()); * @param resultId 结果ID
if (result == null) { * @return 对比响应对象
throw new BusinessException(ResultCode.NOT_FOUND, "结果不存在"); */
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<AnnotationResultCompareResponse.QaRecord> 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<AnnotationResultCompareResponse.DiffRecord> 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 // 读取当前 qa.json
List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream() QaContent qaContent = loadQaContent(result);
.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();
QaContent updatedQaContent = new QaContent( // 更新 qa.json 的 answer 字段和 reviewComment
qaContent.taskId(), List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream()
qaContent.resourceId(), .map(record -> {
updatedQaRecords, String mergedAnswer = request.mergedAnswers().get(record.id());
new QaContent.Metadata( String reviewComment =
qaContent.metadata().createdAt(), request.reviewComments() != null ? request.reviewComments().get(record.id()) : null;
LocalDateTime.now().toString() if (mergedAnswer != null || reviewComment != null) {
) return new QaContent.QaRecord(
); record.id(),
saveQaContent(result, updatedQaContent); 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 原子完成人工审核归档,避免状态部分更新后再次被自动归档扫描到。 QaContent updatedQaContent = new QaContent(
int updated = annotationResultMapper.markReviewedAndArchived( 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(), result.getId(),
currentUser.companyId(), result.getTaskId(),
currentUser.userId()); 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;
} }
if (Boolean.TRUE.equals(result.getRequiresManualReview())) {
result.setRequiresManualReview(false); return AnnotationResultStatus.MANUAL_REVIEW_PENDING;
result.setIsDeleted(true); }
result.setReviewerId(currentUser.userId()); return AnnotationResultStatus.AUTO_ARCHIVE_PENDING;
// 归档到历史表(人工审核后归档)
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;
} }
}
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) { private QaContent loadQaContent(AnnotationResult result) {
try { try {

View File

@@ -10,11 +10,13 @@ import com.labelsys.backend.dto.request.AnnotationTaskPageQuery;
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest; import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest; import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
import com.labelsys.backend.dto.response.AnnotationTaskResponse; import com.labelsys.backend.dto.response.AnnotationTaskResponse;
import com.labelsys.backend.entity.AnnotationResult;
import com.labelsys.backend.entity.AnnotationTask; import com.labelsys.backend.entity.AnnotationTask;
import com.labelsys.backend.entity.AnnotationTaskResource; import com.labelsys.backend.entity.AnnotationTaskResource;
import com.labelsys.backend.entity.SourceResource; import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.enums.TaskStatus; import com.labelsys.backend.enums.TaskStatus;
import com.labelsys.backend.enums.TaskType; import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.mapper.AnnotationResultMapper;
import com.labelsys.backend.mapper.AnnotationTaskMapper; import com.labelsys.backend.mapper.AnnotationTaskMapper;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper; import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper; import com.labelsys.backend.mapper.SourceResourceMapper;
@@ -31,21 +33,56 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/**
* 标注任务服务
*
* <p>提供标注任务的创建、更新、查询、删除等核心业务操作,
* 管理任务与资源的关联关系,并处理任务的状态流转。
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AnnotationTaskService { public class AnnotationTaskService {
private final AnnotationTaskMapper annotationTaskMapper; /**
private final 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 @Transactional
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) { public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
try { try {
// 加载并验证资源列表
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds()); List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
// 构建任务实体
AnnotationTask task = AnnotationTask.builder() AnnotationTask task = AnnotationTask.builder()
.id(IdGenerator.nextId()) .id(IdGenerator.nextId())
.companyId(currentUser.companyId()) .companyId(currentUser.companyId())
@@ -57,7 +94,9 @@ public class AnnotationTaskService {
.isDeleted(false) .isDeleted(false)
.build(); .build();
// 插入任务记录
annotationTaskMapper.insert(task); annotationTaskMapper.insert(task);
// 保存任务与资源的关联关系
saveTaskBindings(task.getId(), currentUser.companyId(), resources); saveTaskBindings(task.getId(), currentUser.companyId(), resources);
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}", log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
@@ -71,9 +110,18 @@ public class AnnotationTaskService {
} }
} }
/**
* 更新标注任务
*
* @param currentUser 当前登录用户
* @param taskId 任务ID
* @param request 更新任务请求
* @return 更新后的任务响应对象
*/
@Transactional @Transactional
public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) { public AnnotationTaskResponse updateTask(LoginUser currentUser, Long taskId, UpdateAnnotationTaskRequest request) {
try { try {
// 查询任务并验证权限
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId()); AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
if (task == null) { if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
@@ -83,24 +131,29 @@ public class AnnotationTaskService {
boolean resourcesChanged = false; boolean resourcesChanged = false;
List<SourceResource> resources = null; List<SourceResource> resources = null;
// 处理资源变更
if (request.resourceIds() != null && !request.resourceIds().isEmpty()) { if (request.resourceIds() != null && !request.resourceIds().isEmpty()) {
List<Long> currentResourceIds = normalizeIds( List<Long> currentResourceIds = normalizeIds(
annotationTaskResourceMapper.listResourceIdsByTaskId(taskId)); annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List<Long> targetResourceIds = normalizeIds(request.resourceIds()); List<Long> targetResourceIds = normalizeIds(request.resourceIds());
resourcesChanged = !currentResourceIds.equals(targetResourceIds); resourcesChanged = !currentResourceIds.equals(targetResourceIds);
// 运行中的任务不允许修改资源
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) { if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源"); throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
} }
resources = loadAndValidateResources(currentUser, request.resourceIds()); resources = loadAndValidateResources(currentUser, request.resourceIds());
} }
// 更新任务类型
if (request.taskType() != null) { if (request.taskType() != null) {
task.setTaskType(request.taskType()); task.setTaskType(request.taskType());
} }
// 保存任务更新
annotationTaskMapper.updateById(task); annotationTaskMapper.updateById(task);
// 更新资源绑定关系
if (resourcesChanged && resources != null) { if (resourcesChanged && resources != null) {
annotationTaskResourceMapper.deleteByTaskId(taskId); annotationTaskResourceMapper.deleteByTaskId(taskId);
saveTaskBindings(taskId, currentUser.companyId(), resources); saveTaskBindings(taskId, currentUser.companyId(), resources);
@@ -120,6 +173,13 @@ public class AnnotationTaskService {
} }
} }
/**
* 查询单个标注任务详情
*
* @param currentUser 当前登录用户
* @param taskId 任务ID
* @return 任务响应对象
*/
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) { public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
try { try {
AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId()); AnnotationTask task = annotationTaskMapper.findByIdAndCompanyId(taskId, currentUser.companyId());
@@ -135,11 +195,20 @@ public class AnnotationTaskService {
} }
} }
/**
* 分页查询标注任务列表
*
* @param currentUser 当前登录用户
* @param query 分页查询条件
* @return 分页任务列表
*/
public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) { public PageResult<AnnotationTaskResponse> pageTasks(LoginUser currentUser, AnnotationTaskPageQuery query) {
try { try {
// 获取数据权限过滤条件
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
// 构建查询条件
LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>() LambdaQueryWrapper<AnnotationTask> wrapper = new LambdaQueryWrapper<AnnotationTask>()
.eq(AnnotationTask::getCompanyId, currentUser.companyId()) .eq(AnnotationTask::getCompanyId, currentUser.companyId())
.eq(query.taskType() != null, AnnotationTask::getTaskType, query.taskType()) .eq(query.taskType() != null, AnnotationTask::getTaskType, query.taskType())
@@ -147,17 +216,21 @@ public class AnnotationTaskService {
.eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted()) .eq(query.isDeleted() != null, AnnotationTask::getIsDeleted, query.isDeleted())
.like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword()); .like(StringUtils.hasText(query.keyword()), AnnotationTask::getTaskName, query.keyword());
// 应用数据权限过滤
if (shouldFilterByUserId) { if (shouldFilterByUserId) {
wrapper.eq(AnnotationTask::getCreatorId, currentUser.userId()); wrapper.eq(AnnotationTask::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) { } else if (!allowedRoles.isEmpty()) {
wrapper.in(AnnotationTask::getCreatorRole, allowedRoles); wrapper.in(AnnotationTask::getCreatorRole, allowedRoles);
} }
// 按创建时间降序排序
wrapper.orderByDesc(AnnotationTask::getCreatedAt); wrapper.orderByDesc(AnnotationTask::getCreatedAt);
// 执行分页查询
Page<AnnotationTask> page = new Page<>(query.pageNo(), query.pageSize()); Page<AnnotationTask> page = new Page<>(query.pageNo(), query.pageSize());
Page<AnnotationTask> resultPage = annotationTaskMapper.selectPage(page, wrapper); Page<AnnotationTask> resultPage = annotationTaskMapper.selectPage(page, wrapper);
// 转换为响应对象并支持按资源ID过滤
List<AnnotationTaskResponse> records = resultPage.getRecords().stream() List<AnnotationTaskResponse> records = resultPage.getRecords().stream()
.filter(task -> query.resourceId() == null .filter(task -> query.resourceId() == null
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()) || annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
@@ -175,6 +248,18 @@ public class AnnotationTaskService {
} }
} }
/**
* 删除标注任务(软删除)
*
* <p>删除前会检查:
* <ul>
* <li>任务是否处于运行状态(运行中不允许删除)</li>
* <li>是否存在关联的标注结果(有结果不允许删除)</li>
* </ul>
*
* @param currentUser 当前登录用户
* @param taskId 任务ID
*/
@Transactional @Transactional
public void deleteTask(LoginUser currentUser, Long taskId) { public void deleteTask(LoginUser currentUser, Long taskId) {
try { try {
@@ -183,11 +268,29 @@ public class AnnotationTaskService {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
} }
assertTaskPermission(currentUser, task); assertTaskPermission(currentUser, task);
// 检查运行状态
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) { if (TaskStatus.RUNNING.name().equals(task.getTaskStatus())) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除"); throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许删除");
} }
task.setIsDeleted(true);
annotationTaskMapper.updateById(task); // 检查是否存在关联的标注结果(防止误删有价值数据)
long resultCount = annotationResultMapper.selectCount(
new LambdaQueryWrapper<AnnotationResult>()
.eq(AnnotationResult::getTaskId, taskId)
.eq(AnnotationResult::getIsDeleted, false)
);
if (resultCount > 0) {
throw new BusinessException(ResultCode.CONFLICT,
String.format("任务存在 %d 条标注结果,不允许删除", resultCount));
}
// 软删除任务(使用 deleteById 触发 @TableLogic 注解的逻辑删除)
annotationTaskMapper.deleteById(taskId);
// 清理关联的任务资源记录
annotationTaskResourceMapper.deleteByTaskId(taskId);
log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}", currentUser.companyId(), log.info("deleted annotation task logically, companyId={}, userId={}, taskId={}", currentUser.companyId(),
currentUser.userId(), taskId); currentUser.userId(), taskId);
} catch (Exception e) { } catch (Exception e) {
@@ -197,6 +300,13 @@ public class AnnotationTaskService {
} }
} }
/**
* 加载并验证资源列表
*
* @param currentUser 当前登录用户
* @param resourceIds 资源ID列表
* @return 验证通过的资源列表
*/
private List<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) { private List<SourceResource> loadAndValidateResources(LoginUser currentUser, List<Long> resourceIds) {
if (resourceIds == null || resourceIds.isEmpty()) { if (resourceIds == null || resourceIds.isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空"); throw new BusinessException(ResultCode.BAD_REQUEST, "任务资源不能为空");
@@ -217,6 +327,13 @@ public class AnnotationTaskService {
return resources; return resources;
} }
/**
* 保存任务与资源的绑定关系
*
* @param taskId 任务ID
* @param companyId 公司ID
* @param resources 资源列表
*/
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) { private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
for (SourceResource resource : resources) { for (SourceResource resource : resources) {
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder() annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
@@ -228,6 +345,13 @@ public class AnnotationTaskService {
} }
} }
/**
* 构建任务响应对象
*
* @param task 任务实体
* @param resourceIds 资源ID列表
* @return 任务响应对象
*/
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) { private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
return new AnnotationTaskResponse( return new AnnotationTaskResponse(
task.getId(), task.getId(),
@@ -240,10 +364,22 @@ public class AnnotationTaskService {
); );
} }
/**
* 从资源列表提取ID列表
*
* @param resources 资源列表
* @return 排序后的资源ID列表
*/
private List<Long> resourceIds(List<SourceResource> resources) { private List<Long> resourceIds(List<SourceResource> resources) {
return resources.stream().map(SourceResource::getId).sorted().toList(); return resources.stream().map(SourceResource::getId).sorted().toList();
} }
/**
* 标准化ID列表去重并排序
*
* @param resourceIds 原始ID列表
* @return 去重排序后的ID列表
*/
private List<Long> normalizeIds(List<Long> resourceIds) { private List<Long> normalizeIds(List<Long> resourceIds) {
Set<Long> uniqueIds = new HashSet<>(resourceIds); Set<Long> uniqueIds = new HashSet<>(resourceIds);
List<Long> sortedIds = new ArrayList<>(uniqueIds); List<Long> sortedIds = new ArrayList<>(uniqueIds);
@@ -251,12 +387,24 @@ public class AnnotationTaskService {
return sortedIds; return sortedIds;
} }
/**
* 断言任务操作权限
*
* @param currentUser 当前登录用户
* @param task 任务实体
*/
private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) { private void assertTaskPermission(LoginUser currentUser, AnnotationTask task) {
if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) { if (!dataPermissionService.canAccessCreator(currentUser, task.getCreatorId(), task.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务"); throw new BusinessException(ResultCode.FORBIDDEN, "无权操作任务");
} }
} }
/**
* 获取默认任务类型
*
* @param taskType 任务类型可为null
* @return 任务类型若为null则返回默认类型 EXTRACT_QA
*/
private TaskType defaultTaskType(TaskType taskType) { private TaskType defaultTaskType(TaskType taskType) {
return taskType != null ? taskType : TaskType.EXTRACT_QA; return taskType != null ? taskType : TaskType.EXTRACT_QA;
} }

View File

@@ -26,19 +26,48 @@ import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* 认证服务
*
* <p>提供用户登录、登出、密码修改、获取当前用户等核心认证业务操作。
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthService { public class AuthService {
private final SysCompanyMapper sysCompanyMapper; /**
private final SysUserMapper sysUserMapper; * 公司数据访问层
private final PasswordEncoder passwordEncoder; */
private final SysCompanyMapper sysCompanyMapper;
/**
* 用户数据访问层
*/
private final SysUserMapper sysUserMapper;
/**
* 密码编码器
*/
private final PasswordEncoder passwordEncoder;
/**
* Token会话存储仓库
*/
private final TokenSessionRepository tokenSessionRepository; private final TokenSessionRepository tokenSessionRepository;
/**
* 会话有效期默认2小时
*/
@Value("${labelsys.session.ttl:PT2H}") @Value("${labelsys.session.ttl:PT2H}")
private Duration sessionTtl; private Duration sessionTtl;
/**
* 获取可用公司列表
*
* @param phone 用户手机号
* @return 可用公司选项列表
*/
public List<CompanyOptionResponse> listAvailableCompanies(String phone) { public List<CompanyOptionResponse> listAvailableCompanies(String phone) {
try { try {
return sysCompanyMapper.findEnabledCompaniesByPhone(phone).stream() return sysCompanyMapper.findEnabledCompaniesByPhone(phone).stream()
@@ -50,6 +79,12 @@ public class AuthService {
} }
} }
/**
* 用户登录
*
* @param request 登录请求
* @return 登录响应
*/
public LoginResponse login(LoginRequest request) { public LoginResponse login(LoginRequest request) {
try { try {
SysCompany company = loadEnabledCompany(request.companyCode()); SysCompany company = loadEnabledCompany(request.companyCode());
@@ -70,6 +105,12 @@ public class AuthService {
} }
} }
/**
* 获取当前登录用户
*
* @param token 登录令牌
* @return 当前登录用户
*/
public LoginUser getCurrentUser(String token) { public LoginUser getCurrentUser(String token) {
try { try {
return tokenSessionRepository.find(token) return tokenSessionRepository.find(token)
@@ -82,6 +123,12 @@ public class AuthService {
} }
} }
/**
* 修改密码
*
* @param currentUser 当前登录用户
* @param request 修改密码请求
*/
@Transactional @Transactional
public void changePassword(LoginUser currentUser, ChangePasswordRequest request) { public void changePassword(LoginUser currentUser, ChangePasswordRequest request) {
try { try {
@@ -108,6 +155,11 @@ public class AuthService {
} }
} }
/**
* 用户登出
*
* @param token 登录令牌
*/
public void logout(String token) { public void logout(String token) {
try { try {
tokenSessionRepository.remove(token); tokenSessionRepository.remove(token);
@@ -117,6 +169,12 @@ public class AuthService {
} }
} }
/**
* 加载启用状态的公司
*
* @param companyCode 公司编码
* @return 公司实体
*/
private SysCompany loadEnabledCompany(String companyCode) { private SysCompany loadEnabledCompany(String companyCode) {
SysCompany company = sysCompanyMapper.findByCompanyCode(companyCode); SysCompany company = sysCompanyMapper.findByCompanyCode(companyCode);
if (company == null || company.getStatus() != CompanyStatus.ENABLED) { if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
@@ -125,6 +183,13 @@ public class AuthService {
return company; return company;
} }
/**
* 加载启用状态的用户
*
* @param companyId 公司ID
* @param phone 用户手机号
* @return 用户实体
*/
private SysUser loadEnabledUser(Long companyId, String phone) { private SysUser loadEnabledUser(Long companyId, String phone) {
SysUser user = sysUserMapper.findByCompanyIdAndPhone(companyId, phone); SysUser user = sysUserMapper.findByCompanyIdAndPhone(companyId, phone);
if (user == null || user.getStatus() != UserStatus.ENABLED) { if (user == null || user.getStatus() != UserStatus.ENABLED) {
@@ -132,4 +197,5 @@ public class AuthService {
} }
return user; return user;
} }
}
}

View File

@@ -15,67 +15,102 @@ import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
@Slf4j /**
@Service * 公司服务
@RequiredArgsConstructor *
public class CompanyService { * <p>提供公司的创建、查询、状态更新等核心业务操作,
* 所有操作仅限系统管理员执行。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CompanyService {
private final SysCompanyMapper sysCompanyMapper; /**
* 公司数据访问层
*/
private final SysCompanyMapper sysCompanyMapper;
public List<SysCompany> listCompanies(LoginUser currentUser) { /**
try { * 查询所有公司列表
assertPlatformAdmin(currentUser); *
return sysCompanyMapper.selectList(null); * @param currentUser 当前登录用户(必须是系统管理员)
} catch (ForbiddenException e) { * @return 公司列表
throw e; */
} catch (Exception e) { public List<SysCompany> listCompanies(LoginUser currentUser) {
log.error("listCompanies failed, companyId={}, userId={}, error={}", try {
currentUser.companyId(), currentUser.userId(), e.getMessage(), e); assertPlatformAdmin(currentUser);
throw e; return sysCompanyMapper.selectList(null);
} } catch (ForbiddenException e) {
} throw e;
} catch (Exception e) {
public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) { log.error("listCompanies failed, companyId={}, userId={}, error={}",
try { currentUser.companyId(), currentUser.userId(), e.getMessage(), e);
assertPlatformAdmin(currentUser); throw e;
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;
} }
}
public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) { /**
try { * 创建公司
assertPlatformAdmin(currentUser); *
if (sysCompanyMapper.updateStatus(companyId, request.status()) == 0) { * @param currentUser 当前登录用户(必须是系统管理员)
throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在"); * @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("仅系统管理员可操作");
}
}
}

View File

@@ -12,26 +12,43 @@ import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j /**
@Service * 数据权限服务
@RequiredArgsConstructor *
public class DataPermissionService { * <p>提供数据访问权限控制,基于用户角色实现数据可见性过滤。
* 支持 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); * @param currentUser 当前登录用户
case MANAGER -> creatorRole == UserRole.EMPLOYEE || creatorRole == UserRole.MANAGER; * @param creatorId 创建者ID
case ENGINEER -> true; * @param creatorRole 创建者角色
}; * @return 是否有权限
} catch (Exception e) { */
log.error("canAccessCreator failed, companyId={}, userId={}, creatorId={}, error={}", public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
currentUser.companyId(), currentUser.userId(), creatorId, e.getMessage(), e); try {
throw e; 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 <T> List<T> filterByRole( public <T> List<T> filterByRole(
LoginUser currentUser, LoginUser currentUser,
@@ -119,4 +136,4 @@ public class DataPermissionService {
throw e; throw e;
} }
} }
} }

View File

@@ -1,34 +1,47 @@
package com.labelsys.backend.service; package com.labelsys.backend.service;
import com.labelsys.backend.config.ObjectStorageProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.labelsys.backend.config.ObjectStorageProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest; import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException; import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
/**
* 对象存储初始化器
*
* <p>在应用启动时自动初始化所需的对象存储桶,包括源文件桶、产物桶和导出桶。
*/
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class ObjectStorageInitializer { public class ObjectStorageInitializer {
/**
* S3 客户端
*/
private final S3Client s3Client; private final S3Client s3Client;
/**
* 对象存储配置属性
*/
private final ObjectStorageProperties properties; private final ObjectStorageProperties properties;
/**
* 应用启动完成后初始化存储桶
*/
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void initializeBuckets() { public void initializeBuckets() {
log.info("开始初始化对象存储桶..."); log.info("开始初始化对象存储桶...");
String[] buckets = { String[] buckets = {
properties.getSourceBucket(), properties.getSourceBucket(),
properties.getArtifactBucket(), properties.getArtifactBucket(),
properties.getExportBucket() properties.getExportBucket()
}; };
for (String bucketName : buckets) { for (String bucketName : buckets) {
@@ -43,10 +56,16 @@ public class ObjectStorageInitializer {
log.error("初始化存储桶 {} 失败: {}", bucketName, e.getMessage()); log.error("初始化存储桶 {} 失败: {}", bucketName, e.getMessage());
} }
} }
log.info("对象存储桶初始化完成"); log.info("对象存储桶初始化完成");
} }
/**
* 检查存储桶是否存在
*
* @param bucketName 存储桶名称
* @return 是否存在
*/
private boolean bucketExists(String bucketName) { private boolean bucketExists(String bucketName) {
try { try {
s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
@@ -56,7 +75,13 @@ public class ObjectStorageInitializer {
} }
} }
/**
* 创建存储桶
*
* @param bucketName 存储桶名称
*/
private void createBucket(String bucketName) { private void createBucket(String bucketName) {
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()); s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
} }
} }

View File

@@ -2,12 +2,39 @@ package com.labelsys.backend.service;
import java.time.Duration; import java.time.Duration;
/**
* 对象存储服务接口
*
* <p>定义对象存储的核心操作包括上传、下载、删除和生成预签名URL。
*/
public interface ObjectStorageService { public interface ObjectStorageService {
/**
* 上传文件到对象存储
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
* @param content 文件内容
* @param contentType 内容类型
* @return 对象键
*/
String upload(String bucketName, String objectKey, byte[] content, String contentType); String upload(String bucketName, String objectKey, byte[] content, String contentType);
/**
* 从对象存储删除文件
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
*/
void delete(String bucketName, String objectKey); void delete(String bucketName, String objectKey);
/**
* 从对象存储下载文件
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
* @return 文件内容字节数组
*/
byte[] download(String bucketName, String objectKey); byte[] download(String bucketName, String objectKey);
/** /**

View File

@@ -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.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
/**
* Rustfs 对象存储服务实现
*
* 基于 AWS S3 SDK 实现的对象存储服务提供上传、下载、删除和生成预签名URL等功能。
*/
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class RustfsObjectStorageService implements ObjectStorageService { public class RustfsObjectStorageService implements ObjectStorageService {
/**
* S3 客户端
*/
private final S3Client s3Client; private final S3Client s3Client;
/**
* 对象存储配置属性
*/
private final ObjectStorageProperties objectStorageProperties; private final ObjectStorageProperties objectStorageProperties;
/**
* 上传文件到对象存储
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
* @param content 文件内容
* @param contentType 内容类型
* @return 对象键
*/
@Override @Override
public String upload(String bucketName, String objectKey, byte[] content, String contentType) { public String upload(String bucketName, String objectKey, byte[] content, String contentType) {
try { try {
PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder() PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder()
.bucket(bucketName) .bucket(bucketName)
.key(objectKey); .key(objectKey);
if (contentType != null && !contentType.isBlank()) { if (contentType != null && !contentType.isBlank()) {
requestBuilder.contentType(contentType); requestBuilder.contentType(contentType);
} }
@@ -43,31 +64,52 @@ public class RustfsObjectStorageService implements ObjectStorageService {
} }
} }
/**
* 从对象存储删除文件
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
*/
@Override @Override
public void delete(String bucketName, String objectKey) { public void delete(String bucketName, String objectKey) {
try { try {
s3Client.deleteObject(DeleteObjectRequest.builder() s3Client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucketName) .bucket(bucketName)
.key(objectKey) .key(objectKey)
.build()); .build());
} catch (Exception ex) { } catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储删除失败"); throw new BusinessException(ResultCode.ERROR, "对象存储删除失败");
} }
} }
/**
* 从对象存储下载文件
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
* @return 文件内容字节数组
*/
@Override @Override
public byte[] download(String bucketName, String objectKey) { public byte[] download(String bucketName, String objectKey) {
try { try {
GetObjectRequest request = GetObjectRequest.builder() GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucketName) .bucket(bucketName)
.key(objectKey) .key(objectKey)
.build(); .build();
return s3Client.getObject(request, ResponseTransformer.toBytes()).asByteArray(); return s3Client.getObject(request, ResponseTransformer.toBytes()).asByteArray();
} catch (Exception ex) { } catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "对象存储下载失败"); throw new BusinessException(ResultCode.ERROR, "对象存储下载失败");
} }
} }
/**
* 生成预签名URL
*
* @param bucketName 存储桶名称
* @param objectKey 对象键
* @param duration 有效期
* @return 预签名URL
*/
@Override @Override
public String generatePresignedUrl(String bucketName, String objectKey, Duration duration) { public String generatePresignedUrl(String bucketName, String objectKey, Duration duration) {
try (S3Presigner presigner = S3Presigner.builder() try (S3Presigner presigner = S3Presigner.builder()
@@ -75,22 +117,23 @@ public class RustfsObjectStorageService implements ObjectStorageService {
.region(Region.of(objectStorageProperties.getRegion())) .region(Region.of(objectStorageProperties.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create( .credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create( AwsBasicCredentials.create(
objectStorageProperties.getAccessKey(), objectStorageProperties.getAccessKey(),
objectStorageProperties.getSecretKey()))) objectStorageProperties.getSecretKey())))
.build()) { .build()) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder() GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucketName) .bucket(bucketName)
.key(objectKey) .key(objectKey)
.build(); .build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(duration) .signatureDuration(duration)
.getObjectRequest(getObjectRequest) .getObjectRequest(getObjectRequest)
.build(); .build();
return presigner.presignGetObject(presignRequest).url().toString(); return presigner.presignGetObject(presignRequest).url().toString();
} catch (Exception ex) { } catch (Exception ex) {
throw new BusinessException(ResultCode.ERROR, "生成预签名URL失败"); throw new BusinessException(ResultCode.ERROR, "生成预签名URL失败");
} }
} }
} }

View File

@@ -45,23 +45,79 @@ import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/**
* 源资源服务
*
* <p>提供资源的上传、下载、查询、删除等核心业务操作,
* 支持图片BBOX标注功能并处理资源与任务的关联关系。
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SourceResourceService { public class SourceResourceService {
private final SourceResourceMapper sourceResourceMapper; /**
private final AnnotationResultMapper annotationResultMapper; * 源资源数据访问层
private final AnnotationResultHistoryMapper annotationResultHistoryMapper; */
private final AnnotationTaskResourceMapper annotationTaskResourceMapper; private final SourceResourceMapper sourceResourceMapper;
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 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 @Transactional
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) { public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
try { try {
@@ -72,21 +128,25 @@ public class SourceResourceService {
if (!ResourceType.isValid(request.getResourceType())) { if (!ResourceType.isValid(request.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法"); throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
} }
// 确定资源名称
String resourceName = String resourceName =
StringUtils.hasText(request.getResourceName()) ? StringUtils.hasText(request.getResourceName()) ?
request.getResourceName() : request.getResourceName() :
file.getOriginalFilename(); file.getOriginalFilename();
// 检查资源名称是否重复
SourceResource existingResource = SourceResource existingResource =
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName); sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
if (existingResource != null) { if (existingResource != null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName); throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName);
} }
// 生成资源ID和存储路径
long resourceId = IdGenerator.nextId(); long resourceId = IdGenerator.nextId();
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType()); String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(), String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(),
request.getResourceType(), request.getResourceType(),
resourceId, extension); resourceId, extension);
// 上传文件到对象存储
try { try {
objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(), objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(),
file.getContentType()); file.getContentType());
@@ -94,6 +154,7 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败"); throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
} }
// 保存资源记录
SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId()) SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId())
.creatorId(currentUser.userId()).creatorRole(currentUser.role()) .creatorId(currentUser.userId()).creatorRole(currentUser.role())
.resourceName( .resourceName(
@@ -118,11 +179,20 @@ public class SourceResourceService {
} }
} }
/**
* 分页查询资源列表
*
* @param currentUser 当前登录用户
* @param query 分页查询条件
* @return 分页资源列表
*/
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) { public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
try { try {
// 获取数据权限过滤条件
List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
// 构建查询条件
LambdaQueryWrapper<SourceResource> wrapper = LambdaQueryWrapper<SourceResource> wrapper =
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId()) new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, .eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType,
@@ -130,12 +200,14 @@ public class SourceResourceService {
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, .like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName,
query.keyword()); query.keyword());
// 应用数据权限过滤
if (shouldFilterByUserId) { if (shouldFilterByUserId) {
wrapper.eq(SourceResource::getCreatorId, currentUser.userId()); wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) { } else if (!allowedRoles.isEmpty()) {
wrapper.in(SourceResource::getCreatorRole, allowedRoles); wrapper.in(SourceResource::getCreatorRole, allowedRoles);
} }
// 按创建时间降序排序
wrapper.orderByDesc(SourceResource::getCreatedAt); wrapper.orderByDesc(SourceResource::getCreatedAt);
// 判断是否需要分页 // 判断是否需要分页
@@ -158,6 +230,13 @@ public class SourceResourceService {
} }
} }
/**
* 查询单个资源详情
*
* @param currentUser 当前登录用户
* @param resourceId 资源ID
* @return 资源响应对象
*/
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) { public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
try { try {
SourceResource resource = sourceResourceMapper.selectById(resourceId); SourceResource resource = sourceResourceMapper.selectById(resourceId);
@@ -191,6 +270,19 @@ public class SourceResourceService {
} }
} }
/**
* 删除资源(软删除)
*
* <p>删除前会检查:
* <ul>
* <li>资源是否被标注任务引用(活跃任务不允许删除)</li>
* <li>资源是否存在标注历史记录(有历史不允许删除)</li>
* <li>资源是否有未删除的标注结果(有结果不允许删除)</li>
* </ul>
*
* @param currentUser 当前登录用户
* @param resourceId 资源ID
*/
@Transactional @Transactional
public void deleteResource(LoginUser currentUser, Long resourceId) { public void deleteResource(LoginUser currentUser, Long resourceId) {
try { try {
@@ -198,6 +290,7 @@ public class SourceResourceService {
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
} }
// 检查操作权限
if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(), if (!dataPermissionService.canAccessCreator(currentUser, resource.getCreatorId(),
resource.getCreatorRole())) { resource.getCreatorRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源"); throw new BusinessException(ResultCode.FORBIDDEN, "无权删除资源");
@@ -209,10 +302,8 @@ public class SourceResourceService {
// 删除关联资源 // 删除关联资源
deleteAssociatedRecords(resourceId); deleteAssociatedRecords(resourceId);
// 执行软删除 // 执行软删除(触发 @TableLogic 注解)
resource.setDeleted(true); sourceResourceMapper.deleteById(resourceId);
resource.setDeletedAt(LocalDateTime.now());
sourceResourceMapper.updateById(resource);
// 删除对象存储中的文件 // 删除对象存储中的文件
objectStorageService.delete(resource.getBucketName(), resource.getFilePath()); objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
@@ -271,6 +362,13 @@ public class SourceResourceService {
} }
} }
/**
* 获取图片BBOX标注信息
*
* @param currentUser 当前登录用户
* @param resourceId 资源ID
* @return BBOX标注响应对象
*/
public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) { public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) {
try { try {
SourceResource resource = sourceResourceMapper.selectById(resourceId); SourceResource resource = sourceResourceMapper.selectById(resourceId);
@@ -314,6 +412,14 @@ public class SourceResourceService {
} }
} }
/**
* 保存图片BBOX标注
*
* @param currentUser 当前登录用户
* @param resourceId 资源ID
* @param request BBOX标注请求
* @return BBOX标注响应对象
*/
@Transactional @Transactional
public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) { public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) {
try { try {
@@ -325,6 +431,7 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注"); throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注");
} }
// 序列化BBOX数据
String bboxJson; String bboxJson;
try { try {
bboxJson = objectMapper.writeValueAsString(request.bboxes()); bboxJson = objectMapper.writeValueAsString(request.bboxes());
@@ -334,6 +441,7 @@ public class SourceResourceService {
boolean isNewAnnotation = imageBboxAnnotationMapper.selectByResourceId(resourceId) == null; boolean isNewAnnotation = imageBboxAnnotationMapper.selectByResourceId(resourceId) == null;
// 更新或插入BBOX标注记录
ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId); ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId);
if (existing != null) { if (existing != null) {
existing.setBboxJson(bboxJson); existing.setBboxJson(bboxJson);
@@ -370,6 +478,12 @@ public class SourceResourceService {
} }
} }
/**
* 删除图片BBOX标注
*
* @param currentUser 当前登录用户
* @param resourceId 资源ID
*/
@Transactional @Transactional
public void deleteImageBbox(LoginUser currentUser, Long resourceId) { public void deleteImageBbox(LoginUser currentUser, Long resourceId) {
try { try {
@@ -377,6 +491,7 @@ public class SourceResourceService {
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) { if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
} }
// 删除BBOX标注记录
imageBboxAnnotationMapper.deleteByResourceId(resourceId); imageBboxAnnotationMapper.deleteByResourceId(resourceId);
// 更新资源表的has_bbox字段为false // 更新资源表的has_bbox字段为false
@@ -393,6 +508,12 @@ public class SourceResourceService {
} }
} }
/**
* 解析BBOX JSON数据
*
* @param bboxJson BBOX坐标JSON字符串
* @return BBOX坐标响应列表
*/
private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) { private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {
if (!StringUtils.hasText(bboxJson)) { if (!StringUtils.hasText(bboxJson)) {
return List.of(); return List.of();
@@ -407,6 +528,12 @@ public class SourceResourceService {
} }
} }
/**
* 转换为资源响应对象
*
* @param resource 资源实体
* @return 资源响应对象
*/
private SourceResourceResponse toResponse(SourceResource resource) { private SourceResourceResponse toResponse(SourceResource resource) {
SysUser creator = sysUserMapper.selectById(resource.getCreatorId()); SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(), return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
@@ -415,6 +542,13 @@ public class SourceResourceService {
creator == null ? null : creator.getRealName(), resource.getCreatedAt(), resource.getUpdatedAt()); creator == null ? null : creator.getRealName(), resource.getCreatedAt(), resource.getUpdatedAt());
} }
/**
* 解析文件扩展名
*
* @param originalFilename 原始文件名
* @param resourceType 资源类型
* @return 文件扩展名
*/
private String resolveExtension(String originalFilename, String resourceType) { private String resolveExtension(String originalFilename, String resourceType) {
if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) { if (StringUtils.hasText(originalFilename) && originalFilename.contains(".")) {
return originalFilename.substring(originalFilename.lastIndexOf('.') + 1); return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);

View File

@@ -30,19 +30,42 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/**
* 系统配置服务
*
* <p>提供系统配置的创建、更新、查询等核心业务操作,
* 支持模型配置、提示词配置和系统参数配置的管理,
* 并负责配置更新时同步更新标注代理配置。
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysConfigService { public class SysConfigService {
private final SysConfigMapper sysConfigMapper; /**
* 系统配置数据访问层
*/
private final SysConfigMapper sysConfigMapper;
/**
* 标注代理配置数据访问层
*/
private final AnnotationAgentConfigMapper agentConfigMapper; private final AnnotationAgentConfigMapper agentConfigMapper;
//private final DataPermissionService dataPermissionService; //private final DataPermissionService dataPermissionService;
/**
* 保存系统配置
*
* @param currentUser 当前登录用户
* @param request 保存配置请求
* @return 保存的配置实体
*/
@Transactional @Transactional
public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) { public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) {
try { try {
// 验证配置类型
validateConfigType(request.configType()); validateConfigType(request.configType());
// 检查配置名称是否重复
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(),
request.configName()); request.configName());
if (existing != null) { if (existing != null) {
@@ -55,6 +78,7 @@ public class SysConfigService {
processedConfigValue = processModelConfigValue(processedConfigValue, true); processedConfigValue = processModelConfigValue(processedConfigValue, true);
} }
// 构建配置实体
SysConfig config = SysConfig.builder() SysConfig config = SysConfig.builder()
.id(IdGenerator.nextId()) .id(IdGenerator.nextId())
.companyId(currentUser.companyId()) .companyId(currentUser.companyId())
@@ -79,6 +103,14 @@ public class SysConfigService {
} }
} }
/**
* 更新系统配置
*
* @param currentUser 当前登录用户
* @param configId 配置ID
* @param request 更新配置请求
* @return 更新后的配置实体
*/
@Transactional @Transactional
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) { public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) {
try { try {
@@ -99,18 +131,20 @@ public class SysConfigService {
existing.setConfigName(request.configName()); existing.setConfigName(request.configName());
} }
// 更新配置类型
if (StringUtils.hasText(request.configType())) { if (StringUtils.hasText(request.configType())) {
validateConfigType(request.configType()); validateConfigType(request.configType());
existing.setConfigType(request.configType()); existing.setConfigType(request.configType());
} }
// 更新配置值模型类型需要加密API密钥
if (StringUtils.hasText(request.configValue())) { if (StringUtils.hasText(request.configValue())) {
// 如果是model类型对apiKey进行加密处理
if (ConfigType.MODEL.name().equalsIgnoreCase(existing.getConfigType())) { if (ConfigType.MODEL.name().equalsIgnoreCase(existing.getConfigType())) {
existing.setConfigValue(processModelConfigValue(request.configValue(), true)); existing.setConfigValue(processModelConfigValue(request.configValue(), true));
} else { } else {
existing.setConfigValue(request.configValue()); existing.setConfigValue(request.configValue());
} }
} }
// 更新配置状态
if (StringUtils.hasText(request.status())) { if (StringUtils.hasText(request.status())) {
existing.setStatus(request.status()); existing.setStatus(request.status());
} }
@@ -131,6 +165,13 @@ public class SysConfigService {
} }
} }
/**
* 分页查询系统配置列表
*
* @param currentUser 当前登录用户
* @param query 分页查询条件
* @return 分页配置列表
*/
public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) { public PageResult<SysConfigResponse> pageConfigs(LoginUser currentUser, SysConfigPageQuery query) {
try { try {
// List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser); // List<String> allowedRoles = dataPermissionService.getAllowedRoles(currentUser);
@@ -166,6 +207,12 @@ public class SysConfigService {
} }
} }
/**
* 转换为配置响应对象
*
* @param config 配置实体
* @return 配置响应对象
*/
public SysConfigResponse toResponse(SysConfig config) { public SysConfigResponse toResponse(SysConfig config) {
// 如果是模型配置需要解密API密钥 // 如果是模型配置需要解密API密钥
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType()) && config.getConfigValue() != null) { if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType()) && config.getConfigValue() != null) {
@@ -208,6 +255,13 @@ public class SysConfigService {
); );
} }
/**
* 获取配置实体(内部方法)
*
* @param currentUser 当前登录用户
* @param configId 配置ID
* @return 配置实体
*/
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) { private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
SysConfig config = sysConfigMapper.selectById(configId); SysConfig config = sysConfigMapper.selectById(configId);
if (config == null || !currentUser.companyId().equals(config.getCompanyId())) { if (config == null || !currentUser.companyId().equals(config.getCompanyId())) {
@@ -216,6 +270,11 @@ public class SysConfigService {
return config; return config;
} }
/**
* 验证配置类型
*
* @param configType 配置类型
*/
private void validateConfigType(String configType) { private void validateConfigType(String configType) {
if (!ConfigType.isValid(configType)) { if (!ConfigType.isValid(configType)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法"); throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
@@ -253,18 +312,27 @@ public class SysConfigService {
/** /**
* 根据ID获取配置实体 * 根据ID获取配置实体
*
* @param configId 配置ID
* @return 配置实体
*/ */
public SysConfig getById(Long configId) { public SysConfig getById(Long configId) {
return sysConfigMapper.selectById(configId); return sysConfigMapper.selectById(configId);
} }
/** /**
* 获取配置详情根据配置ID查询数据库判断类型并返回SysConfigResponse格式 * 获取配置详情
*
* <p>根据配置ID查询数据库对于模型配置会解密API密钥后返回。
*
* @param currentUser 当前登录用户
* @param configId 配置ID
* @return 配置响应对象
*/ */
public SysConfigResponse getConfigDetail(LoginUser currentUser, Long configId) { public SysConfigResponse getConfigDetail(LoginUser currentUser, Long configId) {
SysConfig config = getConfigEntity(currentUser, configId); SysConfig config = getConfigEntity(currentUser, configId);
// 如果是模型配置,我们需要在返回前处理API密钥解密 // 如果是模型配置需要在返回前处理API密钥解密
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType())) { if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType())) {
if (config.getConfigValue() != null) { if (config.getConfigValue() != null) {
try { try {
@@ -275,7 +343,7 @@ public class SysConfigService {
String decryptedApiKey = SM4Util.decryptSafe(model.getApiKey()); String decryptedApiKey = SM4Util.decryptSafe(model.getApiKey());
model.setApiKey(decryptedApiKey); model.setApiKey(decryptedApiKey);
// 将更新后的对象转回JSON字符串但这次API密钥是解密的 // 将更新后的对象转回JSON字符串
String updatedConfigValue = objectMapper.writeValueAsString(model); String updatedConfigValue = objectMapper.writeValueAsString(model);
config.setConfigValue(updatedConfigValue); config.setConfigValue(updatedConfigValue);
} }
@@ -286,12 +354,15 @@ public class SysConfigService {
} }
} }
// 总是返回SysConfigResponse对象
return toResponse(config); return toResponse(config);
} }
/** /**
* 获取公司特定类型的配置实体列表 * 获取公司特定类型的配置实体列表
*
* @param companyId 公司ID
* @param configType 配置类型
* @return 配置列表
*/ */
public List<SysConfig> getCompanyConfigsByType(Long companyId, String configType) { public List<SysConfig> getCompanyConfigsByType(Long companyId, String configType) {
LambdaQueryWrapper<SysConfig> wrapper = LambdaQueryWrapper<SysConfig> wrapper =
@@ -358,5 +429,6 @@ public class SysConfigService {
log.error("syncUpdateAgentConfigs failed, configId={}, error={}", configId, e.getMessage(), e); log.error("syncUpdateAgentConfigs failed, configId={}, error={}", configId, e.getMessage(), e);
throw e; throw e;
} }
} }
} }

View File

@@ -28,18 +28,48 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
/**
* 用户服务
*
* 提供用户的创建、查询、更新等核心业务操作,
* 支持不同角色的用户管理,包括公司管理员和系统管理员。
*/
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserService { public class UserService {
/**
* 默认初始密码
*/
private static final String DEFAULT_PASSWORD = "123456"; private static final String DEFAULT_PASSWORD = "123456";
private final SysUserMapper sysUserMapper; /**
private final SysCompanyMapper sysCompanyMapper; * 用户数据访问层
private final PasswordEncoder passwordEncoder; */
private final SysUserMapper sysUserMapper;
/**
* 公司数据访问层
*/
private final SysCompanyMapper sysCompanyMapper;
/**
* 密码加密器
*/
private final PasswordEncoder passwordEncoder;
/**
* Token会话存储库
*/
private final TokenSessionRepository tokenSessionRepository; private final TokenSessionRepository tokenSessionRepository;
/**
* 查询所有管理员用户(超级管理员专用)
*
* @param currentUser 当前登录用户
* @return 用户列表
*/
public List<SysUser> listAllUsers(LoginUser currentUser) { public List<SysUser> listAllUsers(LoginUser currentUser) {
try { try {
assertSystemAdmin(currentUser); assertSystemAdmin(currentUser);
@@ -57,6 +87,12 @@ public class UserService {
} }
} }
/**
* 查询公司用户列表(公司管理员专用)
*
* @param currentUser 当前登录用户
* @return 用户列表
*/
public List<SysUser> listCompanyUsers(LoginUser currentUser) { public List<SysUser> listCompanyUsers(LoginUser currentUser) {
try { try {
assertCompanyAdmin(currentUser); assertCompanyAdmin(currentUser);
@@ -72,6 +108,13 @@ public class UserService {
} }
} }
/**
* 查询指定公司的管理员列表(超级管理员专用)
*
* @param currentUser 当前登录用户
* @param companyId 公司ID
* @return 用户列表
*/
public List<SysUser> listCompanyAdmins(LoginUser currentUser, Long companyId) { public List<SysUser> listCompanyAdmins(LoginUser currentUser, Long companyId) {
try { try {
assertSystemAdmin(currentUser); assertSystemAdmin(currentUser);
@@ -85,6 +128,13 @@ public class UserService {
} }
} }
/**
* 创建公司管理员(超级管理员专用)
*
* @param currentUser 当前登录用户
* @param request 创建请求
* @return 创建的用户
*/
public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) { public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) {
try { try {
assertSystemAdmin(currentUser); assertSystemAdmin(currentUser);
@@ -101,6 +151,13 @@ public class UserService {
} }
} }
/**
* 创建系统工程师管理员(超级管理员专用)
*
* @param currentUser 当前登录用户
* @param request 创建请求
* @return 创建的用户
*/
public SysUser createSystemEngineerAdmin(LoginUser currentUser, CreateSystemEngineerAdminRequest request) { public SysUser createSystemEngineerAdmin(LoginUser currentUser, CreateSystemEngineerAdminRequest request) {
try { try {
assertSystemAdmin(currentUser); assertSystemAdmin(currentUser);
@@ -126,6 +183,13 @@ public class UserService {
} }
} }
/**
* 创建公司用户(公司管理员专用)
*
* @param currentUser 当前登录用户
* @param request 创建请求
* @return 创建的用户
*/
public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) { public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) {
try { try {
assertCompanyAdmin(currentUser); assertCompanyAdmin(currentUser);
@@ -139,6 +203,14 @@ public class UserService {
} }
} }
/**
* 创建指定公司的用户(超级管理员专用)
*
* @param currentUser 当前登录用户
* @param companyId 公司ID
* @param request 创建请求
* @return 创建的用户
*/
public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) { public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) {
try { try {
assertSystemAdmin(currentUser); assertSystemAdmin(currentUser);
@@ -153,6 +225,13 @@ public class UserService {
} }
} }
/**
* 更新用户角色和岗位(公司管理员专用)
*
* @param currentUser 当前登录用户
* @param userId 用户ID
* @param request 更新请求
*/
@Transactional @Transactional
public void updateAssignment(LoginUser currentUser, Long userId, UpdateUserAssignmentRequest request) { public void updateAssignment(LoginUser currentUser, Long userId, UpdateUserAssignmentRequest request) {
try { try {
@@ -161,6 +240,7 @@ public class UserService {
== 0) { == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
} }
// 强制用户重新登录
tokenSessionRepository.removeAll(userId); tokenSessionRepository.removeAll(userId);
} catch (BusinessException e) { } catch (BusinessException e) {
throw e; throw e;
@@ -171,6 +251,13 @@ public class UserService {
} }
} }
/**
* 更新用户状态(公司管理员专用)
*
* @param currentUser 当前登录用户
* @param userId 用户ID
* @param request 更新请求
*/
@Transactional @Transactional
public void updateStatus(LoginUser currentUser, Long userId, UpdateUserStatusRequest request) { public void updateStatus(LoginUser currentUser, Long userId, UpdateUserStatusRequest request) {
try { try {
@@ -178,6 +265,7 @@ public class UserService {
if (sysUserMapper.updateStatus(userId, currentUser.companyId(), request.status()) == 0) { if (sysUserMapper.updateStatus(userId, currentUser.companyId(), request.status()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
} }
// 强制用户重新登录
tokenSessionRepository.removeAll(userId); tokenSessionRepository.removeAll(userId);
} catch (BusinessException e) { } catch (BusinessException e) {
throw e; throw e;
@@ -188,6 +276,14 @@ public class UserService {
} }
} }
/**
* 更新公司管理员状态(超级管理员专用)
*
* @param currentUser 当前登录用户
* @param companyId 公司ID
* @param userId 用户ID
* @param request 更新请求
*/
@Transactional @Transactional
public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId, public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId,
UpdateUserStatusRequest request) { UpdateUserStatusRequest request) {
@@ -196,6 +292,7 @@ public class UserService {
if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) { if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在"); throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
} }
// 强制用户重新登录
tokenSessionRepository.removeAll(userId); tokenSessionRepository.removeAll(userId);
} catch (BusinessException e) { } catch (BusinessException e) {
throw e; throw e;
@@ -207,6 +304,13 @@ public class UserService {
} }
} }
/**
* 创建用户(内部方法)
*
* @param companyId 公司ID
* @param request 创建请求
* @return 创建的用户
*/
private SysUser createUser(Long companyId, CreateUserRequest request) { private SysUser createUser(Long companyId, CreateUserRequest request) {
if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) { if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) {
throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在"); throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在");
@@ -221,6 +325,11 @@ public class UserService {
return user; return user;
} }
/**
* 确保公司存在且已启用
*
* @param companyId 公司ID
*/
private void ensureEnabledCompany(Long companyId) { private void ensureEnabledCompany(Long companyId) {
SysCompany company = sysCompanyMapper.selectById(companyId); SysCompany company = sysCompanyMapper.selectById(companyId);
if (company == null || company.getStatus() != CompanyStatus.ENABLED) { if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
@@ -234,15 +343,26 @@ public class UserService {
// } // }
// } // }
/**
* 断言超级管理员权限
*
* @param currentUser 当前登录用户
*/
private void assertSystemAdmin(LoginUser currentUser) { private void assertSystemAdmin(LoginUser currentUser) {
if (!currentUser.isSuperAdmin()) { if (!currentUser.isSuperAdmin()) {
throw new ForbiddenException("仅超级管理员可操作"); throw new ForbiddenException("仅超级管理员可操作");
} }
} }
/**
* 断言公司管理员权限
*
* @param currentUser 当前登录用户
*/
private void assertCompanyAdmin(LoginUser currentUser) { private void assertCompanyAdmin(LoginUser currentUser) {
if (currentUser.isSuperAdmin() || currentUser.position() != UserPosition.ADMIN) { if (currentUser.isSuperAdmin() || currentUser.position() != UserPosition.ADMIN) {
throw new ForbiddenException("仅公司管理员可操作"); throw new ForbiddenException("仅公司管理员可操作");
} }
} }
}
}

View File

@@ -2,15 +2,34 @@ package com.labelsys.backend.util;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/**
* ID 生成器
* 使用时间戳 + 序列号的方式生成唯一ID
* 确保在同一毫秒内生成多个ID时不会重复。
*/
public final class IdGenerator { public final class IdGenerator {
/**
* 序列号,范围 0-999超过后自动归零
*/
private static final AtomicInteger SEQUENCE = new AtomicInteger(); private static final AtomicInteger SEQUENCE = new AtomicInteger();
/**
* 私有构造函数,防止实例化
*/
private IdGenerator() { private IdGenerator() {
} }
/**
* 生成下一个唯一ID
* ID 结构:时间戳(毫秒) * 1000 + 序列号(0-999)
* 序列号在同一毫秒内递增,超过 999 时归零。
*
* @return 唯一ID
*/
public static long nextId() { public static long nextId() {
int sequence = SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1); int sequence = SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1);
return System.currentTimeMillis() * 1000 + sequence; return System.currentTimeMillis() * 1000 + sequence;
} }
}
}

View File

@@ -3,36 +3,79 @@ package com.labelsys.backend.util;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
/**
* 对象存储路径构建器
*
* 提供对象存储中文件路径的统一构建方法,
* 按照固定格式生成源文件和结果文件的存储路径。
*/
public final class ObjectStoragePathBuilder { public final class ObjectStoragePathBuilder {
/**
* 年月格式化器,格式为 yyyyMM如 202401
*/
private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM"); private static final DateTimeFormatter YEAR_MONTH = DateTimeFormatter.ofPattern("yyyyMM");
/**
* 私有构造函数,防止实例化
*/
private ObjectStoragePathBuilder() { private ObjectStoragePathBuilder() {
} }
/**
* 构建源文件对象键
*
* 路径格式source/{companyId}/{category}/{yearMonth}/{resourceId}/original.{extension}
*
* @param companyId 公司ID
* @param resourceType 资源类型
* @param resourceId 资源ID
* @param extension 文件扩展名
* @return 对象键
*/
public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) { public static String sourceObjectKey(Long companyId, String resourceType, Long resourceId, String extension) {
String category = resourceType.toLowerCase(); String category = resourceType.toLowerCase();
return "source/%d/%s/%s/%d/original.%s".formatted( return "source/%d/%s/%s/%d/original.%s".formatted(
companyId, companyId,
category, category,
YEAR_MONTH.format(LocalDateTime.now()), YEAR_MONTH.format(LocalDateTime.now()),
resourceId, resourceId,
extension.toLowerCase()); extension.toLowerCase());
} }
/**
* 构建标注结果 QA 文件对象键
*
* 路径格式result/{companyId}/{yearMonth}/{taskId}/{resultId}/qa.json
*
* @param companyId 公司ID
* @param taskId 任务ID
* @param resultId 结果ID
* @return 对象键
*/
public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) { public static String resultQaObjectKey(Long companyId, Long taskId, Long resultId) {
return "result/%d/%s/%d/%d/qa.json".formatted( return "result/%d/%s/%d/%d/qa.json".formatted(
companyId, companyId,
YEAR_MONTH.format(LocalDateTime.now()), YEAR_MONTH.format(LocalDateTime.now()),
taskId, taskId,
resultId); resultId);
} }
/**
* 构建标注结果差异文件对象键
*
* 路径格式result/{companyId}/{yearMonth}/{taskId}/{resultId}/diff.json
*
* @param companyId 公司ID
* @param taskId 任务ID
* @param resultId 结果ID
* @return 对象键
*/
public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) { public static String resultDiffObjectKey(Long companyId, Long taskId, Long resultId) {
return "result/%d/%s/%d/%d/diff.json".formatted( return "result/%d/%s/%d/%d/diff.json".formatted(
companyId, companyId,
YEAR_MONTH.format(LocalDateTime.now()), YEAR_MONTH.format(LocalDateTime.now()),
taskId, taskId,
resultId); resultId);
} }
} }