去掉任务中相关模型和提示词绑定,新增资源图片bbox处理

This commit is contained in:
wh
2026-05-05 18:40:44 +08:00
parent 74674990d8
commit fbbd73c916
25 changed files with 796 additions and 721 deletions

View File

@@ -1,11 +1,10 @@
package com.labelsys.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {

View File

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

View File

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

View File

@@ -1,24 +1,18 @@
package com.labelsys.backend.dto.request;
import java.util.List;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "创建标注任务请求")
public record CreateAnnotationTaskRequest(
@Schema(description = "任务名称", example = "运输文档问答抽取任务") @NotBlank(message = "任务名称不能为空") String taskName,
@Schema(description = "行业类型", defaultValue = "TRANSPORT", example = "TRANSPORT") @NotNull(message = "行业类型不能为空") IndustryType industryType,
@Schema(description = "任务类型", defaultValue = "EXTRACT_QA", example = "EXTRACT_QA") @NotNull(message = "任务类型不能为空") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List<Long> resourceIds,
@Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel,
@Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel,
@Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt,
@Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt) {
}
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") @NotEmpty(message = "资源列表不能为空") List<Long> resourceIds) {
}

View File

@@ -1,12 +0,0 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "手动模型配置请求")
public record ManualModelConfigRequest(
@Schema(description = "模型名称", example = "qwen-plus") @NotBlank(message = "模型名称不能为空") String modelName,
@Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") @NotBlank(message = "模型地址不能为空") String modelUrl,
@Schema(description = "模型密钥", example = "sk-demo1234") @NotBlank(message = "模型密钥不能为空") String apiKey
) {
}

View File

@@ -1,10 +0,0 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "任务提示词配置请求")
public record PromptConfigOptionRequest(
@Schema(description = "已选择的提示词配置名称", example = "qa-extract-v1") String selectedConfigName,
@Schema(description = "手动输入的提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText
) {
}

View File

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

View File

@@ -1,16 +0,0 @@
package com.labelsys.backend.dto.request;
import com.labelsys.backend.enums.ConfigMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@Schema(description = "任务模型配置请求")
public record TaskModelConfigRequest(
@Schema(description = "配置模式SELECT 或 MANUAL", example = "SELECT") @NotNull(message = "配置模式不能为空") ConfigMode mode,
@Schema(description = "已选择的配置名称", example = "qwen-plus-extract") String selectedConfigName,
@Schema(description = "手动录入的模型配置", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @Valid ManualModelConfigRequest manualConfig) {
}

View File

@@ -6,15 +6,10 @@ import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
@Schema(description = "更新标注任务请求")
public record UpdateAnnotationTaskRequest(
@Schema(description = "行业类型", example = "TRANSPORT") IndustryType industryType,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds,
@Schema(description = "抽取模型配置", example = "{\"mode\":\"SELECT\",\"selectedConfigName\":\"qwen-plus-extract\"}") @Valid TaskModelConfigRequest extractModel,
@Schema(description = "校验模型配置", example = "{\"mode\":\"MANUAL\",\"manualConfig\":{\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo5678\"}}") @Valid TaskModelConfigRequest verifyModel,
@Schema(description = "抽取提示词配置", example = "{\"selectedConfigName\":\"qa-extract-v1\"}") @Valid PromptConfigOptionRequest extractPrompt,
@Schema(description = "校验提示词配置", example = "{\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") @Valid PromptConfigOptionRequest verifyPrompt) {
}
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds) {
}

View File

@@ -1,25 +1,21 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "标注任务响应")
public record AnnotationTaskResponse(
@Schema(description = "任务ID", example = "191000000000000301") Long id,
@Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName,
@Schema(description = "行业类型:默认值transport暂不显示", example = "transport") IndustryType industryType,
@Schema(description = "任务类型:暂不显示", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds,
@Schema(description = "抽取模型配置", example = "{\"configId\":191000000000000511,\"configName\":\"qwen-plus-extract\",\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****1234\"}") TaskModelConfigResponse extractModel,
@Schema(description = "校验模型配置", example = "{\"configId\":191000000000000512,\"configName\":\"qwen-max-verify\",\"modelName\":\"qwen-max\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"maskedApiKey\":\"****5678\"}") TaskModelConfigResponse verifyModel,
@Schema(description = "抽取提示词配置", example = "{\"configId\":191000000000000521,\"configName\":\"qa-extract-v1\",\"promptText\":\"请抽取文档中的问答对,并按 JSON 数组输出。\"}") TaskPromptConfigResponse extractPrompt,
@Schema(description = "校验提示词配置", example = "{\"configId\":191000000000000522,\"configName\":\"qa-verify-v1\",\"promptText\":\"请对抽取结果进行逐项校验,并输出差异说明。\"}") TaskPromptConfigResponse verifyPrompt,
@Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt
@Schema(description = "任务ID", example = "191000000000000301") Long id,
@Schema(description = "任务名称", example = "运输文档问答抽取任务") String taskName,
@Schema(description = "行业类型", example = "transport") IndustryType industryType,
@Schema(description = "任务类型", example = "EXTRACT_QA") TaskType taskType,
@Schema(description = "任务状态", example = "PENDING") String taskStatus,
@Schema(description = "资源ID列表", example = "[191000000000000101,191000000000000102]") List<Long> resourceIds,
@Schema(description = "创建时间", example = "2026-04-27T10:20:00") LocalDateTime createdAt,
@Schema(description = "更新时间", example = "2026-04-27T10:30:00") LocalDateTime updatedAt
) {
}
}

View File

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

View File

@@ -1,13 +0,0 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "任务模型配置响应")
public record TaskModelConfigResponse(
@Schema(description = "配置ID", example = "191000000000000511") Long configId,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "模型名称", example = "qwen-plus") String modelName,
@Schema(description = "模型地址", example = "https://dashscope.aliyuncs.com/compatible-mode/v1") String modelUrl,
@Schema(description = "脱敏后的模型密钥", example = "****1234") String maskedApiKey
) {
}

View File

@@ -1,11 +0,0 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "任务提示词配置响应")
public record TaskPromptConfigResponse(
@Schema(description = "配置ID", example = "191000000000000521") Long configId,
@Schema(description = "配置名称", example = "qa-extract-v1") String configName,
@Schema(description = "提示词内容", example = "请抽取文档中的问答对,并按 JSON 数组输出。") String promptText
) {
}

View File

@@ -6,12 +6,13 @@ import com.baomidou.mybatisplus.annotation.TableName;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.enums.UserRole;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@@ -19,30 +20,18 @@ import lombok.NoArgsConstructor;
@TableName("annotation_task")
public class AnnotationTask {
@TableId(type = IdType.INPUT)
private Long id;
private Long companyId;
private Long creatorId;
private UserRole creatorRole;
private String taskName;
private IndustryType industryType;
private TaskType taskType;
private Long extractModelConfigId;
private String extractModelName;
private String extractModelUrl;
private String extractModelApiKey;
private Long verifyModelConfigId;
private String verifyModelName;
private String verifyModelUrl;
private String verifyModelApiKey;
private Long extractPromptConfigId;
private String extractPrompt;
private Long verifyPromptConfigId;
private String verifyPrompt;
private String taskStatus;
private Boolean isDeleted;
private Long id;
private Long companyId;
private Long creatorId;
private UserRole creatorRole;
private String taskName;
private IndustryType industryType;
private TaskType taskType;
private String taskStatus;
private Boolean isDeleted;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private String errorMessage;
private String errorMessage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.ImageBboxAnnotation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ImageBboxAnnotationMapper extends BaseMapper<ImageBboxAnnotation> {
ImageBboxAnnotation selectByResourceId(@Param("resourceId") Long resourceId);
int deleteByResourceId(@Param("resourceId") Long resourceId);
}

View File

@@ -1,15 +1,5 @@
package com.labelsys.backend.service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.labelsys.backend.common.ResultCode;
@@ -20,12 +10,9 @@ import com.labelsys.backend.dto.request.AnnotationTaskPageQuery;
import com.labelsys.backend.dto.request.CreateAnnotationTaskRequest;
import com.labelsys.backend.dto.request.UpdateAnnotationTaskRequest;
import com.labelsys.backend.dto.response.AnnotationTaskResponse;
import com.labelsys.backend.dto.response.TaskModelConfigResponse;
import com.labelsys.backend.dto.response.TaskPromptConfigResponse;
import com.labelsys.backend.entity.AnnotationTask;
import com.labelsys.backend.entity.AnnotationTaskResource;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.IndustryType;
import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.enums.TaskStatus;
@@ -33,51 +20,52 @@ import com.labelsys.backend.enums.TaskType;
import com.labelsys.backend.mapper.AnnotationTaskMapper;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationTaskService {
private final AnnotationTaskMapper annotationTaskMapper;
private final AnnotationTaskMapper annotationTaskMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SourceResourceMapper sourceResourceMapper;
private final SysConfigMapper sysConfigMapper;
private final SysConfigService sysConfigService;
private final DataPermissionService dataPermissionService;
private final SourceResourceMapper sourceResourceMapper;
private final DataPermissionService dataPermissionService;
@Transactional
public AnnotationTaskResponse createTask(LoginUser currentUser, CreateAnnotationTaskRequest request) {
List<SourceResource> resources = loadAndValidateResources(currentUser, request.resourceIds());
SysConfigService.ResolvedModelConfig extractModel = sysConfigService.resolveModelConfig(currentUser,
request.extractModel());
SysConfigService.ResolvedModelConfig verifyModel = sysConfigService.resolveModelConfig(currentUser,
request.verifyModel());
SysConfigService.ResolvedPromptConfig extractPrompt = sysConfigService.resolvePromptConfig(currentUser,
request.extractPrompt());
SysConfigService.ResolvedPromptConfig verifyPrompt = sysConfigService.resolvePromptConfig(currentUser,
request.verifyPrompt());
AnnotationTask task = AnnotationTask.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId())
.creatorId(currentUser.userId()).creatorRole(currentUser.role()).taskName(request.taskName())
.industryType(defaultIndustryType(request.industryType())).taskType(defaultTaskType(request.taskType()))
.extractModelConfigId(extractModel.configId()).extractModelName(extractModel.modelName())
.extractModelUrl(extractModel.modelUrl()).extractModelApiKey(extractModel.apiKey())
.verifyModelConfigId(verifyModel.configId()).verifyModelName(verifyModel.modelName())
.verifyModelUrl(verifyModel.modelUrl()).verifyModelApiKey(verifyModel.apiKey())
.extractPromptConfigId(extractPrompt.configId()).extractPrompt(extractPrompt.promptText())
.verifyPromptConfigId(verifyPrompt.configId()).verifyPrompt(verifyPrompt.promptText())
.taskStatus(TaskStatus.PENDING.name()).isDeleted(false).build();
AnnotationTask task = AnnotationTask.builder()
.id(IdGenerator.nextId())
.companyId(currentUser.companyId())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role())
.taskName(request.taskName())
.industryType(defaultIndustryType(request.industryType()))
.taskType(defaultTaskType(request.taskType()))
.taskStatus(TaskStatus.PENDING.name())
.isDeleted(false)
.build();
annotationTaskMapper.insert(task);
saveTaskBindings(task.getId(), currentUser.companyId(), resources);
log.info("created annotation task, companyId={}, userId={}, taskId={}, resourceCount={}",
currentUser.companyId(), currentUser.userId(), task.getId(), resources.size());
return buildTaskResponse(task, resourceIds(resources), extractModel, verifyModel, extractPrompt, verifyPrompt);
return buildTaskResponse(task, resourceIds(resources));
}
@Transactional
@@ -95,45 +83,13 @@ public class AnnotationTaskService {
List<Long> currentResourceIds = normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
List<Long> targetResourceIds = normalizeIds(request.resourceIds());
resourcesChanged = !currentResourceIds.equals(targetResourceIds);
if (TaskStatus.RUNNING.name().equals(task.getTaskStatus()) && resourcesChanged) {
throw new BusinessException(ResultCode.CONFLICT, "运行中的任务不允许修改资源");
}
resources = loadAndValidateResources(currentUser, request.resourceIds());
}
SysConfigService.ResolvedModelConfig extractModel = null;
SysConfigService.ResolvedModelConfig verifyModel = null;
SysConfigService.ResolvedPromptConfig extractPrompt = null;
SysConfigService.ResolvedPromptConfig verifyPrompt = null;
if (request.extractModel() != null) {
extractModel = sysConfigService.resolveModelConfig(currentUser, request.extractModel());
task.setExtractModelConfigId(extractModel.configId());
task.setExtractModelName(extractModel.modelName());
task.setExtractModelUrl(extractModel.modelUrl());
task.setExtractModelApiKey(extractModel.apiKey());
}
if (request.verifyModel() != null) {
verifyModel = sysConfigService.resolveModelConfig(currentUser, request.verifyModel());
task.setVerifyModelConfigId(verifyModel.configId());
task.setVerifyModelName(verifyModel.modelName());
task.setVerifyModelUrl(verifyModel.modelUrl());
task.setVerifyModelApiKey(verifyModel.apiKey());
}
if (request.extractPrompt() != null) {
extractPrompt = sysConfigService.resolvePromptConfig(currentUser, request.extractPrompt());
task.setExtractPromptConfigId(extractPrompt.configId());
task.setExtractPrompt(extractPrompt.promptText());
}
if (request.verifyPrompt() != null) {
verifyPrompt = sysConfigService.resolvePromptConfig(currentUser, request.verifyPrompt());
task.setVerifyPromptConfigId(verifyPrompt.configId());
task.setVerifyPrompt(verifyPrompt.promptText());
}
if (request.industryType() != null) {
task.setIndustryType(request.industryType());
}
@@ -155,10 +111,7 @@ public class AnnotationTaskService {
List<Long> finalResourceIds = resources != null ? resourceIds(resources)
: normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(taskId));
if (extractModel == null || verifyModel == null || extractPrompt == null || verifyPrompt == null) {
return buildTaskResponse(task, finalResourceIds);
}
return buildTaskResponse(task, finalResourceIds, extractModel, verifyModel, extractPrompt, verifyPrompt);
return buildTaskResponse(task, finalResourceIds);
}
public AnnotationTaskResponse getTask(LoginUser currentUser, Long taskId) {
@@ -195,7 +148,7 @@ public class AnnotationTaskService {
List<AnnotationTaskResponse> records = resultPage.getRecords().stream()
.filter(task -> query.resourceId() == null
|| annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId())
.contains(query.resourceId()))
.contains(query.resourceId()))
.map(task -> buildTaskResponse(task,
normalizeIds(annotationTaskResourceMapper.listResourceIdsByTaskId(task.getId()))))
.toList();
@@ -245,39 +198,26 @@ public class AnnotationTaskService {
private void saveTaskBindings(Long taskId, Long companyId, List<SourceResource> resources) {
for (SourceResource resource : resources) {
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder().id(IdGenerator.nextId())
.companyId(companyId).taskId(taskId).resourceId(resource.getId()).build());
annotationTaskResourceMapper.insert(AnnotationTaskResource.builder()
.id(IdGenerator.nextId())
.companyId(companyId)
.taskId(taskId)
.resourceId(resource.getId())
.build());
}
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds,
SysConfigService.ResolvedModelConfig extractModel, SysConfigService.ResolvedModelConfig verifyModel,
SysConfigService.ResolvedPromptConfig extractPrompt, SysConfigService.ResolvedPromptConfig verifyPrompt) {
return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(),
task.getTaskStatus(), resourceIds, sysConfigService.toResponse(extractModel),
sysConfigService.toResponse(verifyModel), sysConfigService.toResponse(extractPrompt),
sysConfigService.toResponse(verifyPrompt), task.getCreatedAt(), task.getUpdatedAt());
}
private AnnotationTaskResponse buildTaskResponse(AnnotationTask task, List<Long> resourceIds) {
String extractModelConfigName = resolveConfigName(task.getExtractModelConfigId());
String verifyModelConfigName = resolveConfigName(task.getVerifyModelConfigId());
String extractPromptConfigName = resolveConfigName(task.getExtractPromptConfigId());
String verifyPromptConfigName = resolveConfigName(task.getVerifyPromptConfigId());
return new AnnotationTaskResponse(task.getId(), task.getTaskName(), task.getIndustryType(), task.getTaskType(),
task.getTaskStatus(), resourceIds,
new TaskModelConfigResponse(task.getExtractModelConfigId(), extractModelConfigName,
task.getExtractModelName(),
task.getExtractModelUrl(), maskSecret(task.getExtractModelApiKey())),
new TaskModelConfigResponse(task.getVerifyModelConfigId(), verifyModelConfigName,
task.getVerifyModelName(),
task.getVerifyModelUrl(), maskSecret(task.getVerifyModelApiKey())),
new TaskPromptConfigResponse(task.getExtractPromptConfigId(), extractPromptConfigName,
task.getExtractPrompt()),
new TaskPromptConfigResponse(task.getVerifyPromptConfigId(), verifyPromptConfigName,
task.getVerifyPrompt()),
task.getCreatedAt(), task.getUpdatedAt());
return new AnnotationTaskResponse(
task.getId(),
task.getTaskName(),
task.getIndustryType(),
task.getTaskType(),
task.getTaskStatus(),
resourceIds,
task.getCreatedAt(),
task.getUpdatedAt()
);
}
private List<Long> resourceIds(List<SourceResource> resources) {
@@ -304,22 +244,4 @@ public class AnnotationTaskService {
private TaskType defaultTaskType(TaskType taskType) {
return taskType != null ? taskType : TaskType.EXTRACT_QA;
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
private String resolveConfigName(Long configId) {
if (configId == null) {
return null;
}
SysConfig config = sysConfigMapper.selectById(configId);
return config != null ? config.getConfigName() : null;
}
}
}

View File

@@ -1,47 +1,56 @@
package com.labelsys.backend.service;
import java.io.IOException;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.config.ObjectStorageProperties;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.SaveImageBboxRequest;
import com.labelsys.backend.dto.request.SourceResourcePageQuery;
import com.labelsys.backend.dto.request.SourceUploadRequest;
import com.labelsys.backend.dto.response.ImageBboxResponse;
import com.labelsys.backend.dto.response.SourceResourceResponse;
import com.labelsys.backend.dto.response.SourceUploadResponse;
import com.labelsys.backend.entity.ImageBboxAnnotation;
import com.labelsys.backend.entity.SourceResource;
import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.ResourceType;
import com.labelsys.backend.enums.SourceStatus;
import com.labelsys.backend.mapper.AnnotationTaskResourceMapper;
import com.labelsys.backend.mapper.ImageBboxAnnotationMapper;
import com.labelsys.backend.mapper.SourceResourceMapper;
import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.util.IdGenerator;
import com.labelsys.backend.util.ObjectStoragePathBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SourceResourceService {
private final SourceResourceMapper sourceResourceMapper;
private final SourceResourceMapper sourceResourceMapper;
private final AnnotationTaskResourceMapper annotationTaskResourceMapper;
private final SysUserMapper sysUserMapper;
private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
private final com.labelsys.backend.config.ObjectStorageProperties objectStorageProperties;
private final SysUserMapper sysUserMapper;
private final DataPermissionService dataPermissionService;
private final ObjectStorageService objectStorageService;
private final ObjectStorageProperties objectStorageProperties;
private final ImageBboxAnnotationMapper imageBboxAnnotationMapper;
private final ObjectMapper objectMapper;
@Transactional
public SourceUploadResponse upload(LoginUser currentUser, SourceUploadRequest request) {
@@ -53,37 +62,39 @@ public class SourceResourceService {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源类型非法");
}
String resourceName =
StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename();
StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename();
SourceResource existingResource =
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
sourceResourceMapper.selectByCompanyIdAndResourceName(currentUser.companyId(), resourceName);
if (existingResource != null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源名称已存在:" + resourceName);
}
long resourceId = IdGenerator.nextId();
String extension = resolveExtension(file.getOriginalFilename(), request.getResourceType());
String objectKey = ObjectStoragePathBuilder.sourceObjectKey(currentUser.companyId(), request.getResourceType(),
resourceId, extension);
resourceId, extension);
try {
objectStorageService.upload(objectStorageProperties.getSourceBucket(), objectKey, file.getBytes(),
file.getContentType());
file.getContentType());
} catch (IOException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "读取上传文件失败");
}
SourceResource resource = SourceResource.builder().id(resourceId).companyId(currentUser.companyId())
.creatorId(currentUser.userId()).creatorRole(currentUser.role())
.resourceName(
StringUtils.hasText(request.getResourceName()) ? request.getResourceName() : file.getOriginalFilename())
.resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket())
.filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name())
.storageProvider("rustfs").remark(request.getRemark()).build();
.creatorId(currentUser.userId()).creatorRole(currentUser.role())
.resourceName(
StringUtils.hasText(request.getResourceName()) ?
request.getResourceName() :
file.getOriginalFilename())
.resourceType(request.getResourceType()).bucketName(objectStorageProperties.getSourceBucket())
.filePath(objectKey).fileSize(file.getSize()).sourceStatus(SourceStatus.READY.name())
.storageProvider("rustfs").remark(request.getRemark()).build();
sourceResourceMapper.insert(resource);
log.info("uploaded source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(),
currentUser.userId(), resourceId);
currentUser.userId(), resourceId);
return new SourceUploadResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getCreatedAt());
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getCreatedAt());
}
public PageResult<SourceResourceResponse> pageResources(LoginUser currentUser, SourceResourcePageQuery query) {
@@ -91,10 +102,12 @@ public class SourceResourceService {
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<SourceResource> wrapper =
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType, query.resourceType())
.eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus, query.sourceStatus())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword());
new LambdaQueryWrapper<SourceResource>().eq(SourceResource::getCompanyId, currentUser.companyId())
.eq(StringUtils.hasText(query.resourceType()), SourceResource::getResourceType,
query.resourceType())
.eq(StringUtils.hasText(query.sourceStatus()), SourceResource::getSourceStatus,
query.sourceStatus())
.like(StringUtils.hasText(query.keyword()), SourceResource::getResourceName, query.keyword());
if (shouldFilterByUserId) {
wrapper.eq(SourceResource::getCreatorId, currentUser.userId());
@@ -109,20 +122,30 @@ public class SourceResourceService {
List<SourceResourceResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
return new PageResult<>(records, resultPage.getTotal(), (int)resultPage.getCurrent(),
(int)resultPage.getSize());
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
}
public SourceResourceResponse getResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
log.warn("Resource not found or cross-tenant access attempt: resourceId={}, companyId={}, userId={}",
resourceId, currentUser.companyId(), currentUser.userId());
resourceId, currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
return toResponse(resource);
}
/**
* 获取资源实体(内部使用)
*
* @param resourceId 资源ID
* @return 资源实体
*/
public SourceResource getResourceEntity(Long resourceId) {
return sourceResourceMapper.selectById(resourceId);
}
@Transactional
public void deleteResource(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
@@ -137,21 +160,21 @@ public class SourceResourceService {
resource.setSourceStatus(SourceStatus.ARCHIVED.name());
sourceResourceMapper.updateById(resource);
log.info("archived referenced source resource, companyId={}, userId={}, resourceId={}",
currentUser.companyId(), currentUser.userId(), resourceId);
currentUser.companyId(), currentUser.userId(), resourceId);
return;
}
objectStorageService.delete(resource.getBucketName(), resource.getFilePath());
sourceResourceMapper.deleteById(resourceId);
log.info("deleted source resource, companyId={}, userId={}, resourceId={}", currentUser.companyId(),
currentUser.userId(), resourceId);
currentUser.userId(), resourceId);
}
private SourceResourceResponse toResponse(SourceResource resource) {
SysUser creator = sysUserMapper.selectById(resource.getCreatorId());
return new SourceResourceResponse(resource.getId(), resource.getResourceName(), resource.getResourceType(),
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(),
resource.getCreatedAt(), resource.getUpdatedAt());
resource.getBucketName(), resource.getFilePath(), resource.getFileSize(), resource.getSourceStatus(),
resource.getStorageProvider(), resource.getRemark(), creator == null ? null : creator.getRealName(),
resource.getCreatedAt(), resource.getUpdatedAt());
}
private String resolveExtension(String originalFilename, String resourceType) {
@@ -165,4 +188,140 @@ public class SourceResourceService {
default -> "bin";
};
}
}
/**
* 下载图片资源
*
* @param currentUser 当前用户
* @param resourceId 资源ID
* @return 图片字节数组
*/
public byte[] downloadImage(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
log.warn("Resource not found or cross-tenant access attempt: resourceId={}, companyId={}, userId={}",
resourceId, currentUser.companyId(), currentUser.userId());
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!"IMAGE".equals(resource.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持下载");
}
if (!"READY".equals(resource.getSourceStatus())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "资源未就绪");
}
return objectStorageService.download(resource.getBucketName(), resource.getFilePath());
}
/**
* 获取图片资源的Content-Type
*
* @param resource 资源实体
* @return Content-Type
*/
public String getImageContentType(SourceResource resource) {
String filePath = resource.getFilePath();
if (filePath != null && filePath.contains(".")) {
String extension = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase();
return switch (extension) {
case "jpg", "jpeg" -> "image/jpeg";
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
default -> "application/octet-stream";
};
}
return "application/octet-stream";
}
public ImageBboxResponse getImageBbox(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!"IMAGE".equals(resource.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注");
}
ImageBboxAnnotation annotation = imageBboxAnnotationMapper.selectByResourceId(resourceId);
if (annotation == null) {
return new ImageBboxResponse(null, resourceId, List.of(), null, null, null, null);
}
List<ImageBboxResponse.BboxCoordinateResponse> bboxes = parseBboxJson(annotation.getBboxJson());
SysUser creator = sysUserMapper.selectById(annotation.getCreatorId());
return new ImageBboxResponse(
annotation.getId(),
annotation.getResourceId(),
bboxes,
annotation.getRemark(),
creator == null ? null : creator.getRealName(),
annotation.getCreatedAt(),
annotation.getUpdatedAt()
);
}
@Transactional
public ImageBboxResponse saveImageBbox(LoginUser currentUser, Long resourceId, SaveImageBboxRequest request) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
if (!"IMAGE".equals(resource.getResourceType())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "仅图片资源支持BBOX标注");
}
String bboxJson;
try {
bboxJson = objectMapper.writeValueAsString(request.bboxes());
} catch (JsonProcessingException e) {
throw new BusinessException(ResultCode.BAD_REQUEST, "BBOX数据序列化失败");
}
ImageBboxAnnotation existing = imageBboxAnnotationMapper.selectByResourceId(resourceId);
if (existing != null) {
existing.setBboxJson(bboxJson);
existing.setRemark(request.remark());
existing.setUpdatedAt(LocalDateTime.now());
imageBboxAnnotationMapper.updateById(existing);
} else {
long annotationId = IdGenerator.nextId();
ImageBboxAnnotation annotation = ImageBboxAnnotation.builder()
.id(annotationId)
.companyId(currentUser.companyId())
.resourceId(resourceId)
.bboxJson(bboxJson)
.remark(request.remark())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role())
.build();
imageBboxAnnotationMapper.insert(annotation);
}
return getImageBbox(currentUser, resourceId);
}
@Transactional
public void deleteImageBbox(LoginUser currentUser, Long resourceId) {
SourceResource resource = sourceResourceMapper.selectById(resourceId);
if (resource == null || !currentUser.companyId().equals(resource.getCompanyId())) {
throw new BusinessException(ResultCode.NOT_FOUND, "资源不存在");
}
imageBboxAnnotationMapper.deleteByResourceId(resourceId);
}
private List<ImageBboxResponse.BboxCoordinateResponse> parseBboxJson(String bboxJson) {
if (!StringUtils.hasText(bboxJson)) {
return List.of();
}
try {
return objectMapper.readValue(bboxJson,
new TypeReference<List<ImageBboxResponse.BboxCoordinateResponse>>() {
});
} catch (JsonProcessingException e) {
log.warn("Failed to parse bbox json: {}", e.getMessage());
return List.of();
}
}
}

View File

@@ -1,46 +1,34 @@
package com.labelsys.backend.service;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.ManualModelConfigRequest;
import com.labelsys.backend.dto.request.PromptConfigOptionRequest;
import com.labelsys.backend.dto.request.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery;
import com.labelsys.backend.dto.request.TaskModelConfigRequest;
import com.labelsys.backend.dto.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.dto.response.TaskModelConfigResponse;
import com.labelsys.backend.dto.response.TaskPromptConfigResponse;
import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.ConfigMode;
import com.labelsys.backend.enums.ConfigType;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigService {
private final SysConfigMapper sysConfigMapper;
private final ObjectMapper objectMapper;
private final SysConfigMapper sysConfigMapper;
private final DataPermissionService dataPermissionService;
@Transactional
@@ -51,14 +39,20 @@ public class SysConfigService {
if (existing != null) {
throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
}
SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId())
.configType(request.configType()).configName(request.configName()).configValue(request.configValue())
.status(request.status()).creatorId(currentUser.userId()).creatorRole(currentUser.role().name())
SysConfig config = SysConfig.builder()
.id(IdGenerator.nextId())
.companyId(currentUser.companyId())
.configType(request.configType())
.configName(request.configName())
.configValue(request.configValue())
.status(request.status())
.creatorId(currentUser.userId())
.creatorRole(currentUser.role().name())
.build();
sysConfigMapper.insert(config);
log.info("saved sys config, companyId={}, userId={}, userRole={}, configName={}, configType={}",
currentUser.companyId(), currentUser.userId(), currentUser.role().name(), request.configName(),
request.configType());
currentUser.companyId(), currentUser.userId(), currentUser.role().name(),
request.configName(), request.configType());
return config;
}
@@ -66,12 +60,7 @@ public class SysConfigService {
public SysConfig updateConfig(LoginUser currentUser, Long configId, UpdateSysConfigRequest request) {
validateConfigType(request.configType());
SysConfig existing = getConfigEntity(currentUser, configId);
// SysConfig duplicate =
// sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(),
// request.configName());
// if (duplicate != null && !duplicate.getId().equals(configId)) {
// throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
// }
if (StringUtils.hasText(request.configName())) {
existing.setConfigName(request.configName());
}
@@ -85,8 +74,8 @@ public class SysConfigService {
existing.setStatus(request.status());
}
sysConfigMapper.updateById(existing);
log.info("updated sys config, companyId={}, userId={}, configId={}", currentUser.companyId(),
currentUser.userId(), configId);
log.info("updated sys config, companyId={}, userId={}, configId={}",
currentUser.companyId(), currentUser.userId(), configId);
return existing;
}
@@ -120,96 +109,25 @@ public class SysConfigService {
Page<SysConfig> page = new Page<>(query.pageNo(), query.pageSize());
Page<SysConfig> resultPage = sysConfigMapper.selectPage(page, wrapper);
List<SysConfigResponse> records = resultPage.getRecords().stream().map(this::toResponse).toList();
List<SysConfigResponse> records = resultPage.getRecords().stream()
.map(this::toResponse)
.toList();
return new PageResult<>(records, resultPage.getTotal(), (int) resultPage.getCurrent(),
(int) resultPage.getSize());
}
@Transactional
public ResolvedModelConfig resolveModelConfig(LoginUser currentUser, TaskModelConfigRequest request) {
if (request == null || request.mode() == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不能为空");
}
if (request.mode() == ConfigMode.SELECT) {
return resolveSelectedModel(currentUser, request.selectedConfigName());
}
if (request.mode() == ConfigMode.MANUAL) {
return resolveManualModel(currentUser, request.manualConfig());
}
throw new BusinessException(ResultCode.BAD_REQUEST, "不支持的模型配置模式");
}
public ResolvedPromptConfig resolvePromptConfig(LoginUser currentUser, PromptConfigOptionRequest request) {
if (request == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不能为空");
}
if (StringUtils.hasText(request.selectedConfigName())) {
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(),
request.selectedConfigName(), ConfigType.PROMPT.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "提示词配置不存在");
}
return new ResolvedPromptConfig(config.getId(), config.getConfigName(), config.getConfigValue());
}
if (!StringUtils.hasText(request.promptText())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词内容不能为空");
}
return new ResolvedPromptConfig(null, null, request.promptText());
}
public TaskModelConfigResponse toResponse(ResolvedModelConfig config) {
return new TaskModelConfigResponse(config.configId(), config.configName(), config.modelName(),
config.modelUrl(), maskSecret(config.apiKey()));
}
public TaskPromptConfigResponse toResponse(ResolvedPromptConfig config) {
return new TaskPromptConfigResponse(config.configId(), config.configName(), config.promptText());
}
public SysConfigResponse toResponse(SysConfig config) {
return new SysConfigResponse(config.getId(), config.getConfigType(), config.getConfigName(),
config.getConfigValue(), config.getStatus(), config.getCreatorId(), config.getCreatedAt(),
config.getUpdatedAt());
}
private ResolvedModelConfig resolveSelectedModel(LoginUser currentUser, String configName) {
if (!StringUtils.hasText(configName)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置名称不能为空");
}
SysConfig config = sysConfigMapper.findByCompanyIdAndConfigNameAndType(currentUser.companyId(), configName,
ConfigType.MODEL.name());
if (config == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "模型配置不存在");
}
ModelConfigValue configValue = parseModelConfig(config.getConfigValue());
return new ResolvedModelConfig(config.getId(), config.getConfigName(), configValue.modelName(),
configValue.modelUrl(), configValue.apiKey());
}
private ResolvedModelConfig resolveManualModel(LoginUser currentUser, ManualModelConfigRequest request) {
if (request == null || !StringUtils.hasText(request.modelName()) || !StringUtils.hasText(request.modelUrl())
|| !StringUtils.hasText(request.apiKey())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "手动模型配置不完整");
}
SysConfig existing = sysConfigMapper.findByCompanyIdAndConfigName(currentUser.companyId(), request.modelName());
if (existing == null) {
String configValue = writeModelConfig(request);
SysConfig config = SysConfig.builder().id(IdGenerator.nextId()).companyId(currentUser.companyId())
.configType(ConfigType.MODEL.name()).configName(request.modelName()).configValue(configValue)
.status("ENABLED").creatorId(currentUser.userId()).build();
sysConfigMapper.insert(config);
log.info("auto created model config, companyId={}, userId={}, configName={}", currentUser.companyId(),
currentUser.userId(), request.modelName());
return new ResolvedModelConfig(config.getId(), config.getConfigName(), request.modelName(),
request.modelUrl(), request.apiKey());
}
if (!ConfigType.MODEL.name().equals(existing.getConfigType())) {
throw new BusinessException(ResultCode.CONFLICT, "同名配置已被其他类型占用");
}
ModelConfigValue configValue = parseModelConfig(existing.getConfigValue());
return new ResolvedModelConfig(existing.getId(), existing.getConfigName(), configValue.modelName(),
configValue.modelUrl(), configValue.apiKey());
return new SysConfigResponse(
config.getId(),
config.getConfigType(),
config.getConfigName(),
config.getConfigValue(),
config.getStatus(),
config.getCreatorId(),
config.getCreatedAt(),
config.getUpdatedAt()
);
}
private SysConfig getConfigEntity(LoginUser currentUser, Long configId) {
@@ -225,54 +143,4 @@ public class SysConfigService {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
}
}
private ModelConfigValue parseModelConfig(String value) {
try {
return objectMapper.readValue(value, ModelConfigValue.class);
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值格式非法");
}
}
private String writeModelConfig(ManualModelConfigRequest request) {
try {
return objectMapper.writeValueAsString(
Map.of("modelName", request.modelName(), "modelUrl", request.modelUrl(), "apiKey",
request.apiKey()));
} catch (JsonProcessingException ex) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置值生成失败");
}
}
private String maskSecret(String secret) {
if (!StringUtils.hasText(secret)) {
return null;
}
if (secret.length() <= 4) {
return "****";
}
return "****" + secret.substring(secret.length() - 4);
}
// private <T> PageResult<T> paginate(List<T> records, Integer pageNo, Integer
// pageSize) {
// int actualPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
// int actualPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
// int fromIndex = Math.min((actualPageNo - 1) * actualPageSize,
// records.size());
// int toIndex = Math.min(fromIndex + actualPageSize, records.size());
// return new PageResult<>(records.subList(fromIndex, toIndex),
// (long)records.size(), actualPageNo,
// actualPageSize);
// }
private record ModelConfigValue(String modelName, String modelUrl, String apiKey) {
}
public record ResolvedModelConfig(Long configId, String configName, String modelName, String modelUrl,
String apiKey) {
}
public record ResolvedPromptConfig(Long configId, String configName, String promptText) {
}
}
}