Merge branch 'dev54'

This commit is contained in:
wh
2026-05-11 21:34:44 +08:00
19 changed files with 1142 additions and 72 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ CLAUDE.md
*.iws *.iws
.agents/ .agents/
.history/ .history/
.trae/
logs/ logs/
# ========================================== # ==========================================

View File

@@ -84,6 +84,11 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency> <dependency>
<groupId>software.amazon.awssdk</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId> <artifactId>s3</artifactId>

View File

@@ -0,0 +1,46 @@
package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
import com.labelsys.backend.common.Result;
import com.labelsys.backend.context.UserContext;
import com.labelsys.backend.dto.request.SaveAgentConfigRequest;
import com.labelsys.backend.dto.response.AgentConfigListResponse;
import com.labelsys.backend.entity.AnnotationAgentConfig;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.AnnotationAgentConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
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;
@Tag(name = "Agent配置管理")
@RestController
@RequestMapping("/api/agent-configs")
@RequiredArgsConstructor
public class AnnotationAgentConfigController {
private final AnnotationAgentConfigService annotationAgentConfigService;
@Operation(summary = "保存Agent配置")
@PostMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<AgentConfigListResponse> save(@Valid @RequestBody SaveAgentConfigRequest request) {
return Result.success(
annotationAgentConfigService.saveAgentConfigs(UserContext.requireUser(), request));
}
@Operation(summary = "获取公司Agent对应配置列表模型配置和提示词配置")
@GetMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<AgentConfigListResponse> list() {
var user = UserContext.requireUser();
return Result.success(
annotationAgentConfigService.getAgentConfigsForCompany(user.companyId()));
}
}

View File

@@ -1,5 +1,20 @@
package com.labelsys.backend.controller; package com.labelsys.backend.controller;
import com.labelsys.backend.annotation.RequirePosition;
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.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery;
import com.labelsys.backend.dto.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.service.SysConfigService;
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.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -9,21 +24,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
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.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery;
import com.labelsys.backend.dto.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.service.SysConfigService;
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;
@Tag(name = "系统配置管理") @Tag(name = "系统配置管理")
@RestController @RestController
@RequestMapping("/api/sys-configs") @RequestMapping("/api/sys-configs")
@@ -33,16 +33,16 @@ public class SysConfigController {
private final SysConfigService sysConfigService; private final SysConfigService sysConfigService;
@Operation(summary = "创建系统配置") @Operation(summary = "创建系统配置")
// @RequirePosition(UserPosition.ADMIN)
@PostMapping @PostMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<SysConfigResponse> create(@Valid @RequestBody SaveSysConfigRequest request) { public Result<SysConfigResponse> create(@Valid @RequestBody SaveSysConfigRequest request) {
return Result return Result
.success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request))); .success(sysConfigService.toResponse(sysConfigService.saveConfig(UserContext.requireUser(), request)));
} }
@Operation(summary = "更新系统配置") @Operation(summary = "更新系统配置")
// @RequirePosition(UserPosition.ADMIN)
@PutMapping("/{id}") @PutMapping("/{id}")
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<SysConfigResponse> update( public Result<SysConfigResponse> update(
@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id, @Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id,
@Valid @RequestBody UpdateSysConfigRequest request) { @Valid @RequestBody UpdateSysConfigRequest request) {
@@ -52,14 +52,16 @@ public class SysConfigController {
@Operation(summary = "分页查询系统配置") @Operation(summary = "分页查询系统配置")
@GetMapping @GetMapping
@RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
public Result<PageResult<SysConfigResponse>> page(@ParameterObject SysConfigPageQuery query) { public Result<PageResult<SysConfigResponse>> page(@ParameterObject SysConfigPageQuery query) {
return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query)); return Result.success(sysConfigService.pageConfigs(UserContext.requireUser(), query));
} }
@Operation(summary = "查询系统配置详情") @Operation(summary = "查询配置详情")
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<SysConfigResponse> @RequirePosition(UserPosition.ADMIN) // 仅限ADMIN及以上岗位访问
detail(@Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id) { public Result<SysConfigResponse> detail(
return Result.success(sysConfigService.getConfig(UserContext.requireUser(), id)); @Parameter(description = "配置ID", example = "191000000000000501") @PathVariable Long id) {
return Result.success(sysConfigService.getConfigDetail(UserContext.requireUser(), id));
} }
} }

View File

@@ -0,0 +1,20 @@
package com.labelsys.backend.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "大模型配置模型")
@Data
public class LlmConfigModel {
@Schema(description = "多模态类型枚举")
private String llmType;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "模型调用地址")
private String modelUrl;
@Schema(description = "API密钥加密存储")
private String apiKey;
}

View File

@@ -0,0 +1,28 @@
package com.labelsys.backend.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
@Schema(description = "保存Agent配置请求: 请求体中agentConfigs为map结构key为AgentType枚举值值为用户选择的模型配置ID和提示词配置ID")
@Data
public class SaveAgentConfigRequest {
@Schema(description = "Agent配置映射key为AgentType枚举类型codevalue为配置信息。支持的Agent类型RedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
@NotNull(message = "Agent配置不能为空")
private Map<String, AgentConfigItem> agentConfigs;
@Schema(description = "Agent配置项")
@Data
public static class AgentConfigItem {
@Schema(description = "模型配置ID", example = "191000000000000501")
@NotNull(message = "模型配置ID不能为空")
private Long modelConfigId;
@Schema(description = "提示词配置ID", example = "191000000000000502")
private Long promptConfigId;
}
}

View File

@@ -5,9 +5,10 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "保存系统配置请求") @Schema(description = "保存系统配置请求")
public record SaveSysConfigRequest( public record SaveSysConfigRequest(
@Schema(description = "配置类型", example = "MODEL") @NotBlank(message = "配置类型不能为空") String configType, @Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项", example = "MODEL") @NotBlank(message = "配置类型不能为空") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") @NotBlank(message = "配置名称不能为空") String configName, @Schema(description = "配置名称", example = "qwen-plus-extract") @NotBlank(message = "配置名称不能为空") String configName,
@Schema(description = "配置值", @Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank( example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank(
message = "配置值不能为空") String configValue, message = "配置值不能为空") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status) {} @Schema(description = "配置状态", example = "ENABLED") String status) {
}

View File

@@ -6,9 +6,9 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "更新系统配置请求") @Schema(description = "更新系统配置请求")
public record UpdateSysConfigRequest( public record UpdateSysConfigRequest(
@Schema(description = "配置类型", example = "MODEL") String configType, @Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项\", example = \"MODEL\"", example = "MODEL") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName, @Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "配置值", @Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank( example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") @NotBlank(
message = "配置值不能为空") String configValue, message = "配置值不能为空") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status) {} @Schema(description = "配置状态", example = "ENABLED") String status) {}

View File

@@ -0,0 +1,57 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
@Schema(description = "Agent配置信息响应")
@Data
@Builder
public class AgentConfigInfoResponse {
@Schema(description = "配置ID")
private Long id;
@Schema(description = "Agent类型。支持的Agent类型RedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
private String agentType;
@Schema(description = "模型配置信息")
private ModelConfigInfo modelConfig;
@Schema(description = "Prompt配置信息")
private PromptConfigInfo promptConfig;
@Schema(description = "创建时间")
private java.time.LocalDateTime createdAt;
@Data
@Builder
@Schema(description = "模型配置信息")
public static class ModelConfigInfo {
@Schema(description = "模型配置ID")
private Long id;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "模型URL")
private String modelUrl;
@Schema(description = "API密钥")
private String apiKey;
@Schema(description = "LLM类型")
private String llmType;
}
@Data
@Builder
@Schema(description = "Prompt配置信息")
public static class PromptConfigInfo {
@Schema(description = "Prompt配置ID")
private Long id;
@Schema(description = "Prompt文本")
private String promptText;
}
}

View File

@@ -0,0 +1,23 @@
package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "Agent配置列表响应:返回结果agentConfigs为公司Agent配置Mapkey为AgentType枚举值固定六个Agent类型key, 值为Agent配置对象")
@Data
@Builder
public class AgentConfigListResponse {
@Schema(description = "Agent配置映射key为AgentType枚举类型codevalue为Agent配置信息。AgentType类型codeRedTeamAgent(红黑对抗)、AnalyzerAgent(分析)、IndustryClassifierAgent(行业识别)、HallucinationDetectorAgent(幻觉检测)、ReviewerAgent(审查)、regenerator(再次生成)")
private Map<String, AgentConfigInfoResponse> agentConfigs;
@Schema(description = "公司所有Prompt配置列表")
private List<AgentConfigInfoResponse.PromptConfigInfo> promptConfigs;
@Schema(description = "公司所有Model配置列表")
private List<AgentConfigInfoResponse.ModelConfigInfo> modelConfigs;
}

View File

@@ -1,14 +1,17 @@
package com.labelsys.backend.dto.response; package com.labelsys.backend.dto.response;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Schema(description = "系统配置响应") @Schema(description = "系统配置响应")
public record SysConfigResponse( public record SysConfigResponse(
@Schema(description = "配置ID", example = "191000000000000501") Long id, @Schema(description = "配置ID", example = "191000000000000501") Long id,
@Schema(description = "配置类型", example = "MODEL") String configType, @Schema(description = "配置类型: MODEL-大模型配置、PROMPT-提示词配置、SYSTEM-其他配置项", example = "MODEL") String configType,
@Schema(description = "配置名称", example = "qwen-plus-extract") String configName, @Schema(description = "配置名称", example = "qwen-plus-extract") String configName,
@Schema(description = "配置值", example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") String configValue, @Schema(description = "配置值, 当configType为model时候configValue为大模型配置的json字符串configType为其他值时configValue为普通字符串",
example = "{\"modelName\":\"qwen-plus\",\"modelUrl\":\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\"apiKey\":\"sk-demo1234\"}") String configValue,
@Schema(description = "配置状态", example = "ENABLED") String status, @Schema(description = "配置状态", example = "ENABLED") String status,
@Schema(description = "创建人ID", example = "191000000000000021") Long creatorId, @Schema(description = "创建人ID", example = "191000000000000021") Long creatorId,
@Schema(description = "创建时间", example = "2026-04-27T09:50:00") LocalDateTime createdAt, @Schema(description = "创建时间", example = "2026-04-27T09:50:00") LocalDateTime createdAt,

View File

@@ -0,0 +1,160 @@
package com.labelsys.backend.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.labelsys.backend.util.SM4Util;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("annotation_agent_configs")
public class AnnotationAgentConfig {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("company_id")
private Long companyId; // 所属公司ID来源于annotation_task.company_id
@TableField("agent_type")
private String agentType; // Agent角色: extract(抽取/Analyzer+Regenerator) / verify(校验/Reviewer) / industry(行业识别) / hallucination(幻觉检测)
@TableField("model_config_id")
private Long modelConfigId; // 模型配置来源ID外键引用sys_config.id任务启动时从sys_config(config_type=MODEL, status=ENABLED)获取
@TableField("model_name")
private String modelName; // 模型名称快照,如: qwen-max / glm-4 / claude-3-opus
@TableField("model_url")
private String modelUrl; // 模型调用地址快照
@TableField("model_api_key")
private String modelApiKey; // 模型调用密钥快照(需要加密存储)
@TableField("prompt_config_id")
private Long promptConfigId; // Prompt配置来源ID外键引用sys_config.id任务启动时从sys_config(config_type=PROMPT, status=ENABLED)获取
@TableField("prompt_text")
private String promptText; // Prompt文本快照任务执行期间实际使用的提示词
@TableField(value = "config_snapshot", typeHandler = JacksonTypeHandler.class)
private JsonNode configSnapshot; // 完整配置快照JSON包含temperature、max_tokens等运行时参数
@TableField("created_at")
private LocalDateTime createdAt; // 快照创建时间
@TableField("llm_type")
private String llmType; // LLM类型text, audio, video, image等默认为'text'
// Getter和Setter方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCompanyId() {
return companyId;
}
public void setCompanyId(Long companyId) {
this.companyId = companyId;
}
public String getAgentType() {
return agentType;
}
public void setAgentType(String agentType) {
this.agentType = agentType;
}
public Long getModelConfigId() {
return modelConfigId;
}
public void setModelConfigId(Long modelConfigId) {
this.modelConfigId = modelConfigId;
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getModelUrl() {
return modelUrl;
}
public void setModelUrl(String modelUrl) {
this.modelUrl = modelUrl;
}
public String getModelApiKey() {
if (this.modelApiKey != null) {
return SM4Util.decryptSafe(this.modelApiKey); // 解密返回
}
return null;
}
public void setModelApiKey(String modelApiKey) {
if (modelApiKey != null) {
this.modelApiKey = SM4Util.encrypt(modelApiKey); // 加密存储
}
}
public Long getPromptConfigId() {
return promptConfigId;
}
public void setPromptConfigId(Long promptConfigId) {
this.promptConfigId = promptConfigId;
}
public String getPromptText() {
return promptText;
}
public void setPromptText(String promptText) {
this.promptText = promptText;
}
public JsonNode getConfigSnapshot() {
return configSnapshot;
}
public void setConfigSnapshot(JsonNode configSnapshot) {
this.configSnapshot = configSnapshot;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getLlmType() {
return llmType;
}
public void setLlmType(String llmType) {
this.llmType = llmType;
}
}

View File

@@ -0,0 +1,43 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Agent类型枚举")
public enum AgentType {
@Schema(description = "红黑对抗")
RED_TEAM("RedTeamAgent"),
@Schema(description = "分析")
ANALYZER("AnalyzerAgent"),
@Schema(description = "行业识别")
INDUSTRY_CLASSIFIER("IndustryClassifierAgent"),
@Schema(description = "幻觉检测")
HALLUCINATION_DETECTOR("HallucinationDetectorAgent"),
@Schema(description = "审查")
REVIEWER("ReviewerAgent"),
@Schema(description = "再次生成")
REGENERATOR("regenerator");
private final String code;
AgentType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static boolean isValid(String value) {
for (AgentType type : AgentType.values()) {
if (type.name().equalsIgnoreCase(value) || type.getCode().equals(value)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
package com.labelsys.backend.enums;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "多模态类型枚举")
public enum MultiModalType {
@Schema(description = "文本")
TEXT("text"),
@Schema(description = "音频")
AUDIO("audio"),
@Schema(description = "视频")
VIDEO("video"),
@Schema(description = "图片")
IMAGE("image");
private final String code;
MultiModalType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static boolean isValid(String value) {
for (MultiModalType type : MultiModalType.values()) {
if (type.name().equalsIgnoreCase(value) || type.getCode().equals(value)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
package com.labelsys.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.labelsys.backend.entity.AnnotationAgentConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AnnotationAgentConfigMapper extends BaseMapper<AnnotationAgentConfig> {
// 基于BaseMapper已提供基本的CRUD操作
// 如需特殊查询,可在此添加自定义方法
}

View File

@@ -0,0 +1,299 @@
package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.LlmConfigModel;
import com.labelsys.backend.dto.request.SaveAgentConfigRequest;
import com.labelsys.backend.dto.response.AgentConfigInfoResponse;
import com.labelsys.backend.dto.response.AgentConfigListResponse;
import com.labelsys.backend.entity.AnnotationAgentConfig;
import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.AgentType;
import com.labelsys.backend.mapper.AnnotationAgentConfigMapper;
import com.labelsys.backend.util.SM4Util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AnnotationAgentConfigService {
private final AnnotationAgentConfigMapper agentConfigMapper;
private final SysConfigService sysConfigService;
// 保存多个Agent配置
// 保存多个Agent配置
public AgentConfigListResponse saveAgentConfigs(LoginUser user, SaveAgentConfigRequest request) {
// 遍历所有Agent配置项
for (Map.Entry<String, SaveAgentConfigRequest.AgentConfigItem> entry : request.getAgentConfigs().entrySet()) {
String agentType = entry.getKey();
SaveAgentConfigRequest.AgentConfigItem configItem = entry.getValue();
// 验证Agent类型是否有效
if (!AgentType.isValid(agentType)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "无效的Agent类型: " + agentType);
}
// 验证模型配置是否存在且属于该公司
SysConfig modelConfig = sysConfigService.getById(configItem.getModelConfigId());
if (modelConfig == null || modelConfig.getCompanyId() == null || !modelConfig.getCompanyId()
.equals(user.companyId())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置不存在或不属于当前公司");
}
// 验证提示词配置是否存在且属于该公司(如果提供了的话)
if (configItem.getPromptConfigId() != null) {
SysConfig promptConfig = sysConfigService.getById(configItem.getPromptConfigId());
if (promptConfig == null || promptConfig.getCompanyId() == null || !promptConfig.getCompanyId()
.equals(user.companyId())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "提示词配置不存在或不属于当前公司");
}
}
// 从模型配置中提取API密钥
String apiKey = extractApiKeyFromConfig(modelConfig.getConfigValue());
// 检查是否已存在配置
LambdaQueryWrapper<AnnotationAgentConfig> wrapper = Wrappers.<AnnotationAgentConfig>lambdaQuery()
.eq(AnnotationAgentConfig::getCompanyId, user.companyId())
.eq(AnnotationAgentConfig::getAgentType, agentType);
AnnotationAgentConfig existing = agentConfigMapper.selectOne(wrapper);
if (existing != null) {
// 更新现有配置
existing.setModelConfigId(configItem.getModelConfigId());
existing.setPromptConfigId(configItem.getPromptConfigId());
// 从模型配置中获取其他字段
existing.setModelName(modelConfig.getConfigName());
existing.setModelUrl(extractModelUrlFromConfig(modelConfig.getConfigValue()));
existing.setModelApiKey(apiKey); // 保存API密钥
existing.setLlmType(extractLlmTypeFromConfig(modelConfig.getConfigValue()));
existing.setCreatedAt(LocalDateTime.now());
agentConfigMapper.updateById(existing);
} else {
// 创建新配置
AnnotationAgentConfig config = AnnotationAgentConfig.builder()
.companyId(user.companyId())
.agentType(agentType)
.modelConfigId(configItem.getModelConfigId())
.modelName(modelConfig.getConfigName())
.modelUrl(extractModelUrlFromConfig(modelConfig.getConfigValue()))
.modelApiKey(apiKey) // 保存API密钥
.promptConfigId(configItem.getPromptConfigId())
.promptText(getPromptText(configItem.getPromptConfigId()))
.llmType(extractLlmTypeFromConfig(modelConfig.getConfigValue()))
.createdAt(LocalDateTime.now())
.build();
agentConfigMapper.insert(config);
}
}
// 返回更新后的完整配置列表
return getAgentConfigsForCompany(user.companyId());
}
// 从模型配置值中提取API密钥
private String extractApiKeyFromConfig(String configValue) {
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
return llmConfig != null ? llmConfig.getApiKey() : null;
} catch (Exception e) {
return null;
}
}
// 从模型配置值中提取模型URL
private String extractModelUrlFromConfig(String configValue) {
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
return llmConfig != null ? llmConfig.getModelUrl() : null;
} catch (Exception e) {
return null;
}
}
// 从模型配置值中提取LLM类型
private String extractLlmTypeFromConfig(String configValue) {
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(configValue, LlmConfigModel.class);
return llmConfig != null ? llmConfig.getLlmType() : null;
} catch (Exception e) {
return null;
}
}
// 获取提示词文本
private String getPromptText(Long promptConfigId) {
if (promptConfigId == null) {
return null;
}
SysConfig promptConfig = sysConfigService.getById(promptConfigId);
return promptConfig != null ? promptConfig.getConfigValue() : null;
}
// 获取特定Agent类型的配置实体
public AnnotationAgentConfig getAgentConfigEntity(Long companyId, String agentType) {
LambdaQueryWrapper<AnnotationAgentConfig> wrapper = Wrappers.<AnnotationAgentConfig>lambdaQuery()
.eq(AnnotationAgentConfig::getCompanyId, companyId)
.eq(AnnotationAgentConfig::getAgentType, agentType);
return agentConfigMapper.selectOne(wrapper);
}
// 获取公司所有Agent配置的完整信息
public AgentConfigListResponse getAgentConfigsForCompany(Long companyId) {
// 获取所有固定Agent类型
Map<String, AgentConfigInfoResponse> agentConfigs = new HashMap<>();
// 获取该公司所有的Prompt配置
List<AgentConfigInfoResponse.PromptConfigInfo> promptConfigs = getCompanyPromptConfigs(companyId);
// 获取该公司所有的Model配置
List<AgentConfigInfoResponse.ModelConfigInfo> modelConfigs = getCompanyModelConfigs(companyId);
// 遍历所有Agent类型
for (AgentType agentType : AgentType.values()) {
String agentTypeCode = agentType.getCode();
// 查询该Agent类型的配置
AnnotationAgentConfig agentConfig = getAgentConfigByType(companyId, agentTypeCode);
if (agentConfig != null) {
// 如果存在配置,构建完整配置信息
AgentConfigInfoResponse.AgentConfigInfoResponseBuilder builder = AgentConfigInfoResponse.builder()
.id(agentConfig.getId())
.agentType(agentConfig.getAgentType())
.createdAt(agentConfig.getCreatedAt());
// 设置模型配置信息
if (agentConfig.getModelConfigId() != null) {
SysConfig modelConfig = sysConfigService.getById(agentConfig.getModelConfigId());
if (modelConfig != null) {
AgentConfigInfoResponse.ModelConfigInfo modelConfigInfo = AgentConfigInfoResponse.ModelConfigInfo.builder()
.id(modelConfig.getId())
.modelName(modelConfig.getConfigName())
.build();
// 解析模型配置值
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(modelConfig.getConfigValue(),
LlmConfigModel.class);
if (llmConfig != null) {
modelConfigInfo.setModelUrl(llmConfig.getModelUrl());
modelConfigInfo.setApiKey(SM4Util.decryptSafe(llmConfig.getApiKey()));
modelConfigInfo.setLlmType(llmConfig.getLlmType());
}
} catch (Exception e) {
// 解析失败,忽略
}
builder.modelConfig(modelConfigInfo);
}
}
// 设置Prompt配置信息
if (agentConfig.getPromptConfigId() != null) {
SysConfig promptConfig = sysConfigService.getById(agentConfig.getPromptConfigId());
if (promptConfig != null) {
AgentConfigInfoResponse.PromptConfigInfo promptConfigInfo = AgentConfigInfoResponse.PromptConfigInfo.builder()
.id(promptConfig.getId())
.promptText(promptConfig.getConfigValue())
.build();
builder.promptConfig(promptConfigInfo);
}
}
agentConfigs.put(agentTypeCode, builder.build());
} else {
// 如果没有配置,返回空的配置信息
agentConfigs.put(agentTypeCode, AgentConfigInfoResponse.builder()
.agentType(agentTypeCode)
.build());
}
}
return AgentConfigListResponse.builder()
.agentConfigs(agentConfigs)
.promptConfigs(promptConfigs)
.modelConfigs(modelConfigs)
.build();
}
// 获取公司所有Prompt配置
private List<AgentConfigInfoResponse.PromptConfigInfo> getCompanyPromptConfigs(Long companyId) {
List<SysConfig> configs = sysConfigService.getCompanyConfigsByType(companyId, "PROMPT");
return configs.stream().map(config -> AgentConfigInfoResponse.PromptConfigInfo.builder()
.id(config.getId())
.promptText(config.getConfigValue())
.build()).collect(Collectors.toList());
}
// 获取公司所有Model配置
private List<AgentConfigInfoResponse.ModelConfigInfo> getCompanyModelConfigs(Long companyId) {
List<SysConfig> configs = sysConfigService.getCompanyConfigsByType(companyId, "MODEL");
return configs.stream().map(config -> {
AgentConfigInfoResponse.ModelConfigInfo.ModelConfigInfoBuilder builder =
AgentConfigInfoResponse.ModelConfigInfo.builder()
.id(config.getId())
.modelName(config.getConfigName());
// 解析模型配置值
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel llmConfig = objectMapper.readValue(config.getConfigValue(), LlmConfigModel.class);
if (llmConfig != null) {
builder.modelUrl(llmConfig.getModelUrl())
.apiKey(SM4Util.decryptSafe(llmConfig.getApiKey()))
.llmType(llmConfig.getLlmType());
}
} catch (Exception e) {
// 解析失败,忽略
}
return builder.build();
}).collect(Collectors.toList());
}
// 根据类型获取Agent配置
private AnnotationAgentConfig getAgentConfigByType(Long companyId, String agentType) {
LambdaQueryWrapper<AnnotationAgentConfig> wrapper = Wrappers.<AnnotationAgentConfig>lambdaQuery()
.eq(AnnotationAgentConfig::getCompanyId, companyId)
.eq(AnnotationAgentConfig::getAgentType, agentType);
return agentConfigMapper.selectOne(wrapper);
}
// private AgentConfigDetailResponse toDetailResponse(AnnotationAgentConfig config) {
// return AgentConfigDetailResponse.builder()
// .id(config.getId())
// .agentType(config.getAgentType())
// .llmType(config.getLlmType())
// .modelName(config.getModelName())
// .modelUrl(config.getModelUrl())
// .promptText(config.getPromptText())
// .configSnapshot(config.getConfigSnapshot())
// .build();
// }
}

View File

@@ -266,7 +266,8 @@ public class AnnotationResultService {
List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream() List<QaContent.QaRecord> updatedQaRecords = qaContent.records().stream()
.map(record -> { .map(record -> {
String mergedAnswer = request.mergedAnswers().get(record.id()); String mergedAnswer = request.mergedAnswers().get(record.id());
String reviewComment = request.reviewComments() != null ? request.reviewComments().get(record.id()) : null; String reviewComment =
request.reviewComments() != null ? request.reviewComments().get(record.id()) : null;
if (mergedAnswer != null || reviewComment != null) { if (mergedAnswer != null || reviewComment != null) {
return new QaContent.QaRecord( return new QaContent.QaRecord(
record.id(), record.id(),

View File

@@ -2,9 +2,12 @@ package com.labelsys.backend.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.ResultCode;
import com.labelsys.backend.common.exception.BusinessException; import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.LlmConfigModel;
import com.labelsys.backend.dto.common.PageResult; import com.labelsys.backend.dto.common.PageResult;
import com.labelsys.backend.dto.request.SaveSysConfigRequest; import com.labelsys.backend.dto.request.SaveSysConfigRequest;
import com.labelsys.backend.dto.request.SysConfigPageQuery; import com.labelsys.backend.dto.request.SysConfigPageQuery;
@@ -12,9 +15,9 @@ import com.labelsys.backend.dto.request.UpdateSysConfigRequest;
import com.labelsys.backend.dto.response.SysConfigResponse; import com.labelsys.backend.dto.response.SysConfigResponse;
import com.labelsys.backend.entity.SysConfig; import com.labelsys.backend.entity.SysConfig;
import com.labelsys.backend.enums.ConfigType; import com.labelsys.backend.enums.ConfigType;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.SysConfigMapper; import com.labelsys.backend.mapper.SysConfigMapper;
import com.labelsys.backend.util.IdGenerator; import com.labelsys.backend.util.IdGenerator;
import com.labelsys.backend.util.SM4Util;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -29,7 +32,7 @@ import java.util.List;
public class SysConfigService { public class SysConfigService {
private final SysConfigMapper sysConfigMapper; private final SysConfigMapper sysConfigMapper;
private final DataPermissionService dataPermissionService; //private final DataPermissionService dataPermissionService;
@Transactional @Transactional
public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) { public SysConfig saveConfig(LoginUser currentUser, SaveSysConfigRequest request) {
@@ -40,12 +43,19 @@ public class SysConfigService {
if (existing != null) { if (existing != null) {
throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在"); throw new BusinessException(ResultCode.CONFLICT, "配置名称已存在");
} }
String processedConfigValue = request.configValue();
// 如果是model类型对apiKey进行加密处理
if (ConfigType.MODEL.name().equalsIgnoreCase(request.configType())) {
processedConfigValue = processModelConfigValue(processedConfigValue, true);
}
SysConfig config = SysConfig.builder() SysConfig config = SysConfig.builder()
.id(IdGenerator.nextId()) .id(IdGenerator.nextId())
.companyId(currentUser.companyId()) .companyId(currentUser.companyId())
.configType(request.configType()) .configType(request.configType())
.configName(request.configName()) .configName(request.configName())
.configValue(request.configValue()) .configValue(processedConfigValue)
.status(request.status()) .status(request.status())
.creatorId(currentUser.userId()) .creatorId(currentUser.userId())
.creatorRole(currentUser.role().name()) .creatorRole(currentUser.role().name())
@@ -77,8 +87,13 @@ public class SysConfigService {
existing.setConfigType(request.configType()); existing.setConfigType(request.configType());
} }
if (StringUtils.hasText(request.configValue())) { if (StringUtils.hasText(request.configValue())) {
// 如果是model类型对apiKey进行加密处理
if (ConfigType.MODEL.name().equalsIgnoreCase(request.configType())) {
existing.setConfigValue(processModelConfigValue(request.configValue(), true));
} 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());
} }
@@ -95,27 +110,10 @@ public class SysConfigService {
} }
} }
public SysConfigResponse getConfig(LoginUser currentUser, Long configId) {
try {
SysConfig config = getConfigEntity(currentUser, configId);
if (!dataPermissionService.canAccessCreator(currentUser, config.getCreatorId(),
UserRole.valueOf(config.getCreatorRole()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "无权访问配置");
}
return toResponse(config);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("getConfig failed, companyId={}, userId={}, configId={}, error={}",
currentUser.companyId(), currentUser.userId(), configId, e.getMessage(), e);
throw e;
}
}
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);
boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser); // boolean shouldFilterByUserId = dataPermissionService.shouldFilterByUserId(currentUser);
LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<SysConfig>() LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getCompanyId, currentUser.companyId()) .eq(SysConfig::getCompanyId, currentUser.companyId())
@@ -123,11 +121,11 @@ public class SysConfigService {
.eq(StringUtils.hasText(query.status()), SysConfig::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), SysConfig::getStatus, query.status())
.like(StringUtils.hasText(query.configName()), SysConfig::getConfigName, query.configName()); .like(StringUtils.hasText(query.configName()), SysConfig::getConfigName, query.configName());
if (shouldFilterByUserId) { // if (shouldFilterByUserId) {
wrapper.eq(SysConfig::getCreatorId, currentUser.userId()); // wrapper.eq(SysConfig::getCreatorId, currentUser.userId());
} else if (!allowedRoles.isEmpty()) { // } else if (!allowedRoles.isEmpty()) {
wrapper.in(SysConfig::getCreatorRole, allowedRoles); // wrapper.in(SysConfig::getCreatorRole, allowedRoles);
} // }
wrapper.orderByDesc(SysConfig::getCreatedAt); wrapper.orderByDesc(SysConfig::getCreatedAt);
@@ -173,4 +171,84 @@ public class SysConfigService {
throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法"); throw new BusinessException(ResultCode.BAD_REQUEST, "配置类型非法");
} }
} }
/**
* 处理模型配置值加密或解密API密钥
*
* @param configValue 配置值
* @param encrypt true表示加密false表示解密
* @return 处理后的配置值
*/
private String processModelConfigValue(String configValue, boolean encrypt) {
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel configModel = objectMapper.readValue(configValue, LlmConfigModel.class);
if (configModel.getApiKey() != null && !configModel.getApiKey().isEmpty()) {
if (encrypt) {
// 加密API密钥
configModel.setApiKey(SM4Util.encrypt(configModel.getApiKey()));
} else {
// 解密API密钥
configModel.setApiKey(SM4Util.decryptSafe(configModel.getApiKey()));
}
}
return objectMapper.writeValueAsString(configModel);
} catch (JsonProcessingException e) {
log.error("处理模型配置值失败: {}", e.getMessage(), e);
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置格式错误");
}
}
/**
* 根据ID获取配置实体
*/
public SysConfig getById(Long configId) {
return sysConfigMapper.selectById(configId);
}
/**
* 获取配置详情根据配置ID查询数据库判断类型并返回SysConfigResponse格式
*/
public SysConfigResponse getConfigDetail(LoginUser currentUser, Long configId) {
SysConfig config = getConfigEntity(currentUser, configId);
// 如果是模型配置我们需要在返回前处理API密钥解密
if (ConfigType.MODEL.name().equalsIgnoreCase(config.getConfigType())) {
if (config.getConfigValue() != null) {
try {
ObjectMapper objectMapper = new ObjectMapper();
LlmConfigModel model = objectMapper.readValue(config.getConfigValue(), LlmConfigModel.class);
if (model != null && model.getApiKey() != null) {
// 解密API密钥并更新配置值
String decryptedApiKey = SM4Util.decryptSafe(model.getApiKey());
model.setApiKey(decryptedApiKey);
// 将更新后的对象转回JSON字符串但这次API密钥是解密的
String updatedConfigValue = objectMapper.writeValueAsString(model);
config.setConfigValue(updatedConfigValue);
}
} catch (JsonProcessingException e) {
log.error("解析模型配置失败: {}", e.getMessage(), e);
throw new BusinessException(ResultCode.BAD_REQUEST, "模型配置格式错误");
}
}
}
// 总是返回SysConfigResponse对象
return toResponse(config);
}
/**
* 获取公司特定类型的配置实体列表
*/
public List<SysConfig> getCompanyConfigsByType(Long companyId, String configType) {
LambdaQueryWrapper<SysConfig> wrapper =
new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getCompanyId, companyId)
.eq(SysConfig::getConfigType, configType);
return sysConfigMapper.selectList(wrapper);
}
} }

View File

@@ -0,0 +1,255 @@
package com.labelsys.backend.util;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Arrays;
/**
* SM4 国密加解密工具类
* SM4-CBC 模式,密钥/IV 从环境变量读取hex编码32字符
*/
public class SM4Util {
private static final String ALGORITHM = "SM4";
private static final String TRANSFORMATION = "SM4/CBC/NoPadding";
private static final int BLOCK_SIZE = 16;
private static final String SM4_SECRET_KEY = "a5c9b98d8042ff99a5befedd1bcdc78a";
private static final String SM4_IV = "83e3d80962fea60369fb7ebaac9b2285";
static {
Security.addProvider(new BouncyCastleProvider());
}
// ---- 密钥 / IV ----
private static byte[] getKey() {
//String hex = System.getenv("SM4_SECRET_KEY");
if (SM4_SECRET_KEY == null || SM4_SECRET_KEY.isEmpty())
throw new SM4CryptoException("环境变量 SM4_SECRET_KEY 未配置");
byte[] key = hexToBytes(SM4_SECRET_KEY);
if (key.length != 16)
throw new SM4CryptoException("SM4 密钥长度必须为16字节当前为 " + key.length + " 字节");
return key;
}
private static byte[] getIV() {
//String hex = System.getenv("SM4_IV");
if (SM4_IV != null && !SM4_IV.isEmpty()) {
byte[] iv = hexToBytes(SM4_IV);
if (iv.length != 16)
throw new SM4CryptoException("SM4 IV 长度必须为16字节当前为 " + iv.length + " 字节");
return iv;
}
return new byte[16]; // 默认全零 IV
}
// ---- PKCS7 填充 ----
private static byte[] pad(byte[] data) {
int n = BLOCK_SIZE - (data.length % BLOCK_SIZE);
byte[] out = Arrays.copyOf(data, data.length + n);
Arrays.fill(out, data.length, out.length, (byte) n);
return out;
}
private static byte[] unpad(byte[] data) {
if (data.length == 0)
throw new SM4CryptoException("数据为空,无法去填充");
int n = data[data.length - 1] & 0xFF;
if (n < 1 || n > 16)
throw new SM4CryptoException("无效的填充长度: " + n);
for (int i = data.length - n; i < data.length; i++) {
if ((data[i] & 0xFF) != n)
throw new SM4CryptoException("填充校验失败");
}
return Arrays.copyOf(data, data.length - n);
}
// ---- 加解密方法 ----
/**
* SM4-CBC 加密,返回 hex 密文
*/
public static String encrypt(String plainText, String key, String iv) {
try {
if (key == null || key.length() != 32) {
throw new IllegalArgumentException("SM4密钥长度必须为32位十六进制字符");
}
if (iv == null || iv.length() != 32) {
throw new IllegalArgumentException("SM4 IV长度必须为32位十六进制字符");
}
byte[] keyBytes = hexToBytes(key);
byte[] ivBytes = hexToBytes(iv);
byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
// 手动进行PKCS7填充与Python版本保持一致
byte[] paddedData = pkcs7Pad(plainBytes, 16);
SM4Engine engine = new SM4Engine();
CBCBlockCipher blockCipher = new CBCBlockCipher(engine);
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(blockCipher);
KeyParameter keyParameter = new KeyParameter(keyBytes);
ParametersWithIV parametersWithIV = new ParametersWithIV(keyParameter, ivBytes);
cipher.init(true, parametersWithIV);
byte[] encrypted = new byte[cipher.getOutputSize(paddedData.length)];
int len = cipher.processBytes(paddedData, 0, paddedData.length, encrypted, 0);
len += cipher.doFinal(encrypted, len);
byte[] result = new byte[len];
System.arraycopy(encrypted, 0, result, 0, len);
return bytesToHex(result);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败", e);
}
}
/**
* SM4-CBC 解密,传入 hex 密文
*/
public static String decrypt(String cipherText, String key, String iv) {
try {
if (key == null || key.length() != 32) {
throw new IllegalArgumentException("SM4密钥长度必须为32位十六进制字符");
}
if (iv == null || iv.length() != 32) {
throw new IllegalArgumentException("SM4 IV长度必须为32位十六进制字符");
}
byte[] keyBytes = hexToBytes(key);
byte[] ivBytes = hexToBytes(iv);
byte[] cipherBytes = hexToBytes(cipherText);
SM4Engine engine = new SM4Engine();
CBCBlockCipher blockCipher = new CBCBlockCipher(engine);
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(blockCipher);
KeyParameter keyParameter = new KeyParameter(keyBytes);
ParametersWithIV parametersWithIV = new ParametersWithIV(keyParameter, ivBytes);
cipher.init(false, parametersWithIV);
byte[] decrypted = new byte[cipher.getOutputSize(cipherBytes.length)];
int len = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decrypted, 0);
len += cipher.doFinal(decrypted, len);
// 手动去PKCS7填充与Python版本保持一致
byte[] unpadded = pkcs7Unpad(Arrays.copyOf(decrypted, len));
return new String(unpadded, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4解密失败", e);
}
}
/**
* 使用默认密钥和IV进行加密
*/
public static String encrypt(String plainText) {
return encrypt(plainText, SM4_SECRET_KEY, SM4_IV);
}
/**
* 使用默认密钥和IV进行解密
*/
public static String decrypt(String cipherText) {
return decrypt(cipherText, SM4_SECRET_KEY, SM4_IV);
}
/**
* 解密,非有效密文时原样返回(兼容未加密旧数据)
*/
public static String decryptSafe(String value) {
try {
return decrypt(value);
} catch (Exception e) {
return value;
}
}
// ---- PKCS7 填充/去填充方法 ----
private static byte[] pkcs7Pad(byte[] data, int blockSize) {
int paddingLen = blockSize - (data.length % blockSize);
byte[] paddedData = new byte[data.length + paddingLen];
System.arraycopy(data, 0, paddedData, 0, data.length);
for (int i = 0; i < paddingLen; i++) {
paddedData[data.length + i] = (byte) paddingLen;
}
return paddedData;
}
private static byte[] pkcs7Unpad(byte[] data) {
if (data == null || data.length == 0) {
throw new RuntimeException("数据为空,无法去填充");
}
int padLen = data[data.length - 1] & 0xFF; // 转换为无符号整数
if (padLen < 1 || padLen > 16) {
throw new RuntimeException("无效的填充长度: " + padLen);
}
if (data.length < padLen) {
throw new RuntimeException("数据长度小于填充长度");
}
// 检查填充是否正确
for (int i = 0; i < padLen; i++) {
if (data[data.length - padLen + i] != (byte) padLen) {
throw new RuntimeException("填充校验失败");
}
}
byte[] result = new byte[data.length - padLen];
System.arraycopy(data, 0, result, 0, data.length - padLen);
return result;
}
// ---- Hex 工具 ----
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte v : b)
sb.append(String.format("%02x", v & 0xFF));
return sb.toString();
}
private static byte[] hexToBytes(String hex) {
byte[] out = new byte[hex.length() / 2];
for (int i = 0; i < out.length; i++)
out[i] = (byte) ((Character.digit(hex.charAt(2 * i), 16) << 4)
+ Character.digit(hex.charAt(2 * i + 1), 16));
return out;
}
// ---- 异常 ----
public static class SM4CryptoException extends RuntimeException {
public SM4CryptoException(String msg) {
super(msg);
}
public SM4CryptoException(String msg, Throwable cause) {
super(msg, cause);
}
}
static void main() {
String key = "a5c9b98d8042ff99a5befedd1bcdc78a";
String iv = "83e3d80962fea60369fb7ebaac9b2285";
String plainText = "extract-api-key-demo";
String encrypted = SM4Util.encrypt(plainText);
String decrypted = SM4Util.decrypt(encrypted);
System.out.println("原文: " + plainText);
System.out.println("密文: " + encrypted);
System.out.println("解密: " + decrypted);
}
}