From 5405012faac433ccd09b13706815c8d86cb25020 Mon Sep 17 00:00:00 2001 From: "ran.shang" <> Date: Thu, 8 Jan 2026 14:33:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/file-postgres.sql | 32 +++ db/file.sql | 17 ++ .../chat/service/impl/ChatServiceImpl.java | 5 +- .../mapstruct/DynamicTableMapMapper.java | 15 +- .../service/impl/DynamicTableServiceImpl.java | 5 +- .../component/file/config/FileProperties.java | 25 +- .../senmesh/config/service/ConfigService.java | 10 + .../service/impl/ConfigServiceImpl.java | 22 ++ .../frame/redis/redisson/RedissonUtils.java | 24 +- .../senmesh/sdk/dify/config/DifyConfig.java | 23 ++ .../sdk/dify/config/DifyProperties.java | 32 +++ .../senmesh/sdk/dify/dto/DifyChatRequest.java | 69 +++++ .../sdk/dify/dto/DifyChatResponse.java | 144 ++++++++++ .../sdk/dify/dto/DifyFileUploadRequest.java | 36 +++ .../sdk/dify/dto/DifyFileUploadResponse.java | 44 +++ .../senmesh/sdk/dify/service/DifyClient.java | 206 ++++++++++++++ .../senmesh/sfile/constant/RiskLevelEnum.java | 100 +++++++ .../sfile/controller/DifyController.java | 60 ++++ .../sfile/controller/FileRiskController.java | 89 ++++++ .../sfile/controller/RiskFileController.java | 36 --- .../sfile/controller/UserFileController.java | 8 + .../sfile/controller/UserSpaceController.java | 17 +- .../sfile/convert/FileRiskConvert.java | 23 ++ .../com/senmesh/sfile/dao/FileRiskMapper.java | 26 ++ .../com/senmesh/sfile/dao/UserFileMapper.java | 9 + .../senmesh/sfile/dto/FileRiskCreateDTO.java | 31 ++ .../com/senmesh/sfile/dto/FileRiskDTO.java | 37 +++ .../senmesh/sfile/dto/FileRiskQueryDTO.java | 34 +++ .../sfile/dto/UserFileStatisticsDTO.java | 33 +++ .../com/senmesh/sfile/entity/FileRisk.java | 44 +++ .../senmesh/sfile/mapper/FileRiskMapper.xml | 26 ++ .../senmesh/sfile/mapper/UserFileMapper.xml | 20 ++ .../senmesh/sfile/mapper/UserSpaceMapper.xml | 25 +- .../sfile/service/FileRiskService.java | 91 ++++++ .../sfile/service/RiskFileService.java | 22 -- .../sfile/service/UserFileService.java | 9 + .../service/impl/FileRiskServiceImpl.java | 266 ++++++++++++++++++ .../service/impl/RiskFileServiceImpl.java | 61 ---- .../impl/TransferFileMonitorServiceImpl.java | 66 ++++- .../impl/TransferUserFileServiceImpl.java | 7 +- .../service/impl/UserFileServiceImpl.java | 11 +- .../service/impl/UserSpaceServiceImpl.java | 5 +- .../org/mapstruct/OrgMapMapper.java | 17 +- .../org/service/impl/OrgServiceImpl.java | 5 +- src/main/resources/application.properties | 38 +-- 45 files changed, 1716 insertions(+), 209 deletions(-) create mode 100644 src/main/java/com/senmesh/sdk/dify/config/DifyConfig.java create mode 100644 src/main/java/com/senmesh/sdk/dify/config/DifyProperties.java create mode 100644 src/main/java/com/senmesh/sdk/dify/dto/DifyChatRequest.java create mode 100644 src/main/java/com/senmesh/sdk/dify/dto/DifyChatResponse.java create mode 100644 src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadRequest.java create mode 100644 src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadResponse.java create mode 100644 src/main/java/com/senmesh/sdk/dify/service/DifyClient.java create mode 100644 src/main/java/com/senmesh/sfile/constant/RiskLevelEnum.java create mode 100644 src/main/java/com/senmesh/sfile/controller/DifyController.java create mode 100644 src/main/java/com/senmesh/sfile/controller/FileRiskController.java delete mode 100644 src/main/java/com/senmesh/sfile/controller/RiskFileController.java create mode 100644 src/main/java/com/senmesh/sfile/convert/FileRiskConvert.java create mode 100644 src/main/java/com/senmesh/sfile/dao/FileRiskMapper.java create mode 100644 src/main/java/com/senmesh/sfile/dto/FileRiskCreateDTO.java create mode 100644 src/main/java/com/senmesh/sfile/dto/FileRiskDTO.java create mode 100644 src/main/java/com/senmesh/sfile/dto/FileRiskQueryDTO.java create mode 100644 src/main/java/com/senmesh/sfile/dto/UserFileStatisticsDTO.java create mode 100644 src/main/java/com/senmesh/sfile/entity/FileRisk.java create mode 100644 src/main/java/com/senmesh/sfile/mapper/FileRiskMapper.xml create mode 100644 src/main/java/com/senmesh/sfile/service/FileRiskService.java delete mode 100644 src/main/java/com/senmesh/sfile/service/RiskFileService.java create mode 100644 src/main/java/com/senmesh/sfile/service/impl/FileRiskServiceImpl.java delete mode 100644 src/main/java/com/senmesh/sfile/service/impl/RiskFileServiceImpl.java diff --git a/db/file-postgres.sql b/db/file-postgres.sql index 61f1eaf..252f9de 100644 --- a/db/file-postgres.sql +++ b/db/file-postgres.sql @@ -98,4 +98,36 @@ COMMENT ON COLUMN x_user_file.update_time IS '修改时间'; COMMENT ON COLUMN x_user_file.proxy_by IS '代理人'; +-- ---------------------------- +-- Table structure for x_file_risk +-- ---------------------------- +DROP TABLE IF EXISTS x_file_risk; +CREATE TABLE x_file_risk ( + id BIGSERIAL PRIMARY KEY, + file_code VARCHAR(32) DEFAULT NULL, + max_risk_level VARCHAR(2) DEFAULT NULL, + risk_json TEXT DEFAULT NULL, + dify_file_json TEXT DEFAULT NULL, + deleted BOOLEAN DEFAULT FALSE, + create_by BIGINT DEFAULT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_by BIGINT DEFAULT NULL, + update_time TIMESTAMP DEFAULT NULL, + proxy_by BIGINT DEFAULT NULL, + CONSTRAINT uk_file_code UNIQUE (file_code) +); +COMMENT ON TABLE x_file_risk IS '文件风险'; +COMMENT ON COLUMN x_file_risk.id IS '主键ID'; +COMMENT ON COLUMN x_file_risk.file_code IS '文件编码'; +COMMENT ON COLUMN x_file_risk.max_risk_level IS '最高风险等级:1重度/2中度/3轻度'; +COMMENT ON COLUMN x_file_risk.risk_json IS '风险详情JSON'; +COMMENT ON COLUMN x_file_risk.dify_file_json IS 'Dify文件上传响应JSON'; +COMMENT ON COLUMN x_file_risk.deleted IS '是否删除'; +COMMENT ON COLUMN x_file_risk.create_by IS '创建人'; +COMMENT ON COLUMN x_file_risk.create_time IS '创建时间'; +COMMENT ON COLUMN x_file_risk.update_by IS '修改人'; +COMMENT ON COLUMN x_file_risk.update_time IS '修改时间'; +COMMENT ON COLUMN x_file_risk.proxy_by IS '代理人'; + + diff --git a/db/file.sql b/db/file.sql index 4094b39..278f48f 100644 --- a/db/file.sql +++ b/db/file.sql @@ -48,3 +48,20 @@ CREATE TABLE `x_user_file` ( `proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户文件'; + +CREATE TABLE `x_file_risk` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `file_code` varchar(32) DEFAULT NULL COMMENT '文件编码', + `max_risk_level` varchar(2) DEFAULT NULL COMMENT '最高风险等级:1重度/2中度/3轻度', + `risk_json` text DEFAULT NULL COMMENT '风险详情JSON', + `dify_file_json` text DEFAULT NULL COMMENT 'Dify文件上传响应JSON', + + `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + `proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_file_code` (`file_code`) USING BTREE COMMENT '文件编码唯一索引' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件风险'; diff --git a/src/main/java/com/senmesh/component/chat/service/impl/ChatServiceImpl.java b/src/main/java/com/senmesh/component/chat/service/impl/ChatServiceImpl.java index bd0850e..7d559b4 100644 --- a/src/main/java/com/senmesh/component/chat/service/impl/ChatServiceImpl.java +++ b/src/main/java/com/senmesh/component/chat/service/impl/ChatServiceImpl.java @@ -55,7 +55,7 @@ public class ChatServiceImpl implements ChatService { ChatQueryDTO queryDTO = ChatQueryDTO.builder().orgId(orgId).personUserId(personUserId).status( ChatStatusEnum.ACTIVE.getStatus()).build(); - return RedissonUtils.lockAndSave(getLockDTO(orgId, personUserId), + ChatDTO chatDTO = RedissonUtils.lockAndSave(getLockDTO(orgId, personUserId), () -> this.find(queryDTO), () -> { List orgUserDTOs = orgUserMapper.listOrgUserDTO( OrgUserQueryDTO.builder().orgId(orgId).administrator(Boolean.TRUE).build()); @@ -65,8 +65,9 @@ public class ChatServiceImpl implements ChatService { chat.setOrgUserId(orgUserDTOs.get(0).getUserId()); chat.setStatus(ChatStatusEnum.ACTIVE.getStatus()); chatMapper.saveEntity(chat); - return chat.getId(); + return ChatMapMapper.INSTANCE.toDTO(chat); }); + return chatDTO.getId(); } @Override diff --git a/src/main/java/com/senmesh/component/dynamictable/mapstruct/DynamicTableMapMapper.java b/src/main/java/com/senmesh/component/dynamictable/mapstruct/DynamicTableMapMapper.java index 2c8cd0f..742e8ff 100644 --- a/src/main/java/com/senmesh/component/dynamictable/mapstruct/DynamicTableMapMapper.java +++ b/src/main/java/com/senmesh/component/dynamictable/mapstruct/DynamicTableMapMapper.java @@ -19,15 +19,24 @@ public interface DynamicTableMapMapper { /** * dto to entity - * + * * @param dynamicTableDto * @return * @author sr on 2024-04-09 */ DynamicTable toEntity( DynamicTableDTO dynamicTableDto); - - + + /** + * entity to dto + * + * @param dynamicTable + * @return + * @author sr on 2024-04-09 + */ + DynamicTableDTO toDto(DynamicTable dynamicTable); + + /** * @param dynamicTableCreateDTO * @return diff --git a/src/main/java/com/senmesh/component/dynamictable/service/impl/DynamicTableServiceImpl.java b/src/main/java/com/senmesh/component/dynamictable/service/impl/DynamicTableServiceImpl.java index 5a19de8..23d38ef 100644 --- a/src/main/java/com/senmesh/component/dynamictable/service/impl/DynamicTableServiceImpl.java +++ b/src/main/java/com/senmesh/component/dynamictable/service/impl/DynamicTableServiceImpl.java @@ -53,14 +53,15 @@ public class DynamicTableServiceImpl implements DynamicTableService { List columnDTOs = dynamicTableCreateDTO.getColumnDTOs(); checkColumn(columnDTOs); - return RedissonUtils.lockAndSave(getLockDTO(userId, tableType), + DynamicTableDTO dynamicTableDTO = RedissonUtils.lockAndSave(getLockDTO(userId, tableType), () -> this.find(dynamicTableQueryDTO), () -> { DynamicTable dynamicTable = DynamicTableMapMapper.INSTANCE.toEntity(dynamicTableCreateDTO); dynamicTable.setColumnData(JsonUtils.toDefaultJsonString(columnDTOs)); dynamicTableMapper.saveEntity(dynamicTable); - return dynamicTable.getId(); + return DynamicTableMapMapper.INSTANCE.toDto(dynamicTable); }); + return dynamicTableDTO.getId(); // Long userId = dynamicTableCreateDTO.getUserId(); // String tableType = dynamicTableCreateDTO.getTableType(); diff --git a/src/main/java/com/senmesh/component/file/config/FileProperties.java b/src/main/java/com/senmesh/component/file/config/FileProperties.java index 1003fe6..f5f2286 100644 --- a/src/main/java/com/senmesh/component/file/config/FileProperties.java +++ b/src/main/java/com/senmesh/component/file/config/FileProperties.java @@ -14,16 +14,25 @@ public class FileProperties { /** 文件上传路径 **/ private String uploadDir; - /** 传输发送目录 **/ - private String transferSendDir; - - /** 传输接收目录 **/ - private String transferReceiveDir; - private final Storage storage = new Storage(); - /** 文件位置:1内网2外网 **/ - private String location; + private final Transfer transfer = new Transfer(); + + @Data + @NoArgsConstructor + public static class Transfer { + /** 文件位置:1内网2外网 **/ + private String location; + + /** 传输发送目录 **/ + private String sendDir; + + /** 传输接收目录 **/ + private String receiveDir; + + /** 解包风险校验开关 **/ + private Boolean unpackRiskCheck = false; + } @Data @NoArgsConstructor diff --git a/src/main/java/com/senmesh/config/service/ConfigService.java b/src/main/java/com/senmesh/config/service/ConfigService.java index 009b5a4..c1db565 100644 --- a/src/main/java/com/senmesh/config/service/ConfigService.java +++ b/src/main/java/com/senmesh/config/service/ConfigService.java @@ -129,4 +129,14 @@ public interface ConfigService { */ ConfigDTO getConfigDto(String code, Long orgId, boolean enable); + /** + * 创建或修改配置 + * 如果配置已存在则修改,不存在则创建 + * + * @param orgId 机构ID + * @param configCode 配置编码 + * @param configValue 配置值 + */ + void createOrModify(Long orgId, String configCode, String configValue); + } diff --git a/src/main/java/com/senmesh/config/service/impl/ConfigServiceImpl.java b/src/main/java/com/senmesh/config/service/impl/ConfigServiceImpl.java index b3c6f6d..4c01ac4 100644 --- a/src/main/java/com/senmesh/config/service/impl/ConfigServiceImpl.java +++ b/src/main/java/com/senmesh/config/service/impl/ConfigServiceImpl.java @@ -154,4 +154,26 @@ public class ConfigServiceImpl implements ConfigService { return dto; } + @Override + @Transactional + public void createOrModify(Long orgId, String configCode, String configValue) { + // 查询是否已存在配置 + ConfigDTO existConfig = queryOrgConfigByCode(orgId, configCode); + + if (existConfig != null) { + // 存在则修改 + configMapper.updateEntitySelective( + Config.builder().id(existConfig.getId()).configValue(configValue).build()); + } else { + // 不存在则创建 + Config config = Config.builder() + .configCode(configCode) + .configValue(configValue) + .orgId(orgId) + .enable(true) + .build(); + configMapper.saveEntity(config); + } + } + } diff --git a/src/main/java/com/senmesh/frame/redis/redisson/RedissonUtils.java b/src/main/java/com/senmesh/frame/redis/redisson/RedissonUtils.java index 2946b56..1541d7f 100644 --- a/src/main/java/com/senmesh/frame/redis/redisson/RedissonUtils.java +++ b/src/main/java/com/senmesh/frame/redis/redisson/RedissonUtils.java @@ -311,7 +311,7 @@ public class RedissonUtils { return null; } - + /** * 校验上锁保存(有事务幂等) * @param @@ -319,19 +319,19 @@ public class RedissonUtils { * @param existSupplier * @param saveSupplier * @return - * @author sr on 2024-04-18 + * @author sr on 2024-04-18 */ - public static Long lockAndSave(LockDTO lockDTO, Supplier existSupplier, - Supplier saveSupplier) { + public static T lockAndSave(LockDTO lockDTO, Supplier existSupplier, + Supplier saveSupplier) { return lockAndSave(lockDTO, existSupplier, saveSupplier, true, null); } - public static Long lockAndSave(LockDTO lockDTO, Supplier existSupplier, - Supplier saveSupplier, String existMsg) { + public static T lockAndSave(LockDTO lockDTO, Supplier existSupplier, + Supplier saveSupplier, String existMsg) { return lockAndSave(lockDTO, existSupplier, saveSupplier, true, existMsg); } - + /** * 校验上锁保存 * @param @@ -341,14 +341,14 @@ public class RedissonUtils { * @param hasTransaction 当前是否有事务 * @param existMsg 存在抛的异常消息,空则不抛异常返回id * @return - * @author sr on 2024-04-18 + * @author sr on 2024-04-18 */ - public static Long lockAndSave(LockDTO lockDTO, Supplier existSupplier, - Supplier saveSupplier, boolean hasTransaction, String existMsg) throws ExistException{ + public static T lockAndSave(LockDTO lockDTO, Supplier existSupplier, + Supplier saveSupplier, boolean hasTransaction, String existMsg) throws ExistException { IBaseDTO baseDTO = existSupplier.get(); if (baseDTO != null) { if (StringUtils.isBlank(existMsg)) { - return baseDTO.getId(); + return (T) baseDTO; } else { throw new ExistException(existMsg); } @@ -358,7 +358,7 @@ public class RedissonUtils { baseDTO = existSupplier.get(); if (baseDTO != null) { if (StringUtils.isBlank(existMsg)) { - return baseDTO.getId(); + return (T) baseDTO; } else { throw new ExistException(existMsg); } diff --git a/src/main/java/com/senmesh/sdk/dify/config/DifyConfig.java b/src/main/java/com/senmesh/sdk/dify/config/DifyConfig.java new file mode 100644 index 0000000..fbe48ed --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/config/DifyConfig.java @@ -0,0 +1,23 @@ +package com.senmesh.sdk.dify.config; + +import lombok.Data; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Dify配置 + * + * @author sr + */ +@Data +@Configuration +@EnableConfigurationProperties(DifyProperties.class) +@ConditionalOnProperty(name = DifyConfig.DIFY_ENABLE, havingValue = "true") +public class DifyConfig { + + static final String DIFY_ENABLE = "dify.enable"; + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/config/DifyProperties.java b/src/main/java/com/senmesh/sdk/dify/config/DifyProperties.java new file mode 100644 index 0000000..5716f90 --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/config/DifyProperties.java @@ -0,0 +1,32 @@ +package com.senmesh.sdk.dify.config; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Dify配置属性 + * + * @author sr + */ +@ConfigurationProperties(prefix = DifyProperties.PREFIX) +@Data +@Component +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DifyProperties implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final String PREFIX = "dify"; + + /** Dify服务地址 */ + private String host; + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/dto/DifyChatRequest.java b/src/main/java/com/senmesh/sdk/dify/dto/DifyChatRequest.java new file mode 100644 index 0000000..7098c18 --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/dto/DifyChatRequest.java @@ -0,0 +1,69 @@ +package com.senmesh.sdk.dify.dto; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dify对话请求 + * + * @author sr + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DifyChatRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 输入参数 */ + private Map inputs; + + /** 查询内容 */ + private String query; + + /** 响应模式: blocking/streaming */ + @Builder.Default + private String responseMode = "blocking"; + + /** 会话ID */ + private String conversationId; + + /** 用户标识 */ + private String user; + + /** 文件列表 */ + private List files; + + /** + * 文件信息 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class DifyFileInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 文件类型: image/document */ + private String type; + + /** 传输方式: remote_url/local_file */ + @Builder.Default + private String transferMethod = "local_file"; + + /** 远程URL */ + private String url; + + /** 上传文件ID */ + private String uploadFileId; + } + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/dto/DifyChatResponse.java b/src/main/java/com/senmesh/sdk/dify/dto/DifyChatResponse.java new file mode 100644 index 0000000..b057ab0 --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/dto/DifyChatResponse.java @@ -0,0 +1,144 @@ +package com.senmesh.sdk.dify.dto; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dify对话响应 + * + * @author sr + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DifyChatResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 消息ID */ + private String messageId; + + /** 会话ID */ + private String conversationId; + + /** 模式 */ + private String mode; + + /** 回答内容 */ + private String answer; + + /** 元数据 */ + private Metadata metadata; + + /** 创建时间 */ + private Long createdAt; + + /** + * 元数据 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Metadata implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 使用量 */ + private Usage usage; + + /** 检索器资源 */ + private List retrieverResources; + } + + /** + * 使用量 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Usage implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 提示词token数 */ + private Integer promptTokens; + + /** 提示词单位价格 */ + private String promptUnitPrice; + + /** 提示词价格单位 */ + private String promptPriceUnit; + + /** 提示词价格 */ + private String promptPrice; + + /** 完成token数 */ + private Integer completionTokens; + + /** 完成单位价格 */ + private String completionUnitPrice; + + /** 完成价格单位 */ + private String completionPriceUnit; + + /** 完成价格 */ + private String completionPrice; + + /** 总token数 */ + private Integer totalTokens; + + /** 总价格 */ + private String totalPrice; + + /** 货币 */ + private String currency; + + /** 延迟(毫秒) */ + private Double latency; + } + + /** + * 检索器资源 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class RetrieverResource implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 位置 */ + private Integer position; + + /** 数据集ID */ + private String datasetId; + + /** 数据集名称 */ + private String datasetName; + + /** 文档ID */ + private String documentId; + + /** 文档名称 */ + private String documentName; + + /** 分段ID */ + private String segmentId; + + /** 分数 */ + private Double score; + + /** 内容 */ + private String content; + } + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadRequest.java b/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadRequest.java new file mode 100644 index 0000000..49dd798 --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadRequest.java @@ -0,0 +1,36 @@ +package com.senmesh.sdk.dify.dto; + +import java.io.InputStream; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dify文件上传请求 + * + * @author sr + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DifyFileUploadRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 文件输入流 */ + private InputStream inputStream; + + /** 文件名 */ + private String fileName; + + /** 文件MIME类型 */ + private String contentType; + + /** 用户标识 */ + private String user; + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadResponse.java b/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadResponse.java new file mode 100644 index 0000000..0854e3c --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/dto/DifyFileUploadResponse.java @@ -0,0 +1,44 @@ +package com.senmesh.sdk.dify.dto; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dify文件上传响应 + * + * @author sr + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DifyFileUploadResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 文件ID */ + private String id; + + /** 文件名 */ + private String name; + + /** 文件大小 */ + private Long size; + + /** 文件扩展名 */ + private String extension; + + /** MIME类型 */ + private String mimeType; + + /** 创建者 */ + private String createdBy; + + /** 创建时间 */ + private Long createdAt; + +} + diff --git a/src/main/java/com/senmesh/sdk/dify/service/DifyClient.java b/src/main/java/com/senmesh/sdk/dify/service/DifyClient.java new file mode 100644 index 0000000..d40fea9 --- /dev/null +++ b/src/main/java/com/senmesh/sdk/dify/service/DifyClient.java @@ -0,0 +1,206 @@ +package com.senmesh.sdk.dify.service; + +import com.senmesh.common.AlitaResult; +import com.senmesh.common.util.JsonUtils; +import com.senmesh.sdk.common.constant.SdkConstants; +import com.senmesh.sdk.common.util.HttpUtils; +import com.senmesh.sdk.dify.config.DifyProperties; +import com.senmesh.sdk.dify.dto.DifyChatRequest; +import com.senmesh.sdk.dify.dto.DifyChatResponse; +import com.senmesh.sdk.dify.dto.DifyFileUploadRequest; +import com.senmesh.sdk.dify.dto.DifyFileUploadResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Dify客户端 + * + * @author sr + */ +@Slf4j +@Component +public class DifyClient { + + private static DifyProperties difyProperties; + + private static final String FILE_UPLOAD_URI = "/v1/files/upload"; + private static final String CHAT_MESSAGES_URI = "/v1/chat-messages"; + + /** + * 构造方法 - 用于Spring注入,同时设置静态变量 + */ + @Autowired + public DifyClient(DifyProperties difyProperties) { + DifyClient.difyProperties = difyProperties; + } + + /** + * 上传文件到Dify(静态方法 - 只传apiKey) + * + * @param apiKey API密钥 + * @param request 文件上传请求 + * @return 上传结果 + */ + public static AlitaResult uploadFile(String apiKey, DifyFileUploadRequest request) { + if (difyProperties == null) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "DifyProperties未初始化"); + } + return uploadFile(difyProperties.getHost(), apiKey, request); + } + + /** + * 上传文件到Dify(静态方法) + * + * @param host Dify服务地址 + * @param apiKey API密钥 + * @param request 文件上传请求 + * @return 上传结果 + */ + public static AlitaResult uploadFile(String host, String apiKey, DifyFileUploadRequest request) { + try { + // 参数校验 + if (StringUtils.isBlank(host)) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "Dify服务地址不能为空"); + } + if (StringUtils.isBlank(apiKey)) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "API密钥不能为空"); + } + if (request == null || request.getInputStream() == null) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "文件输入流不能为空"); + } + if (StringUtils.isBlank(request.getFileName())) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "文件名不能为空"); + } + if (StringUtils.isBlank(request.getUser())) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "用户标识不能为空"); + } + + String url = host + FILE_UPLOAD_URI; + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.set("Authorization", "Bearer " + apiKey); + + // 构建请求体 + MultiValueMap body = new LinkedMultiValueMap<>(); + + // 将InputStream转换为ByteArrayResource + byte[] fileBytes = org.springframework.util.StreamUtils.copyToByteArray(request.getInputStream()); + ByteArrayResource fileResource = new ByteArrayResource(fileBytes) { + @Override + public String getFilename() { + return request.getFileName(); + } + }; + + body.add("file", fileResource); + body.add("user", request.getUser()); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + log.info("dify uploadFile request url:{}, user:{}, fileName:{}", + url, request.getUser(), request.getFileName()); + + // 发送请求 + ResponseEntity response = HttpUtils.restTemplate.exchange( + url, + HttpMethod.POST, + requestEntity, + DifyFileUploadResponse.class + ); + + DifyFileUploadResponse result = response.getBody(); + log.info("dify uploadFile response:{}", JsonUtils.toJsonString(result)); + + return AlitaResult.success(result); + + } catch (Exception e) { + log.error("dify uploadFile error:", e); + return AlitaResult.error(SdkConstants.SDK_HTTP_INVOKE_ERROR_CODE, e.getMessage()); + } + } + + /** + * 发送对话消息(静态方法 - 只传apiKey) + * + * @param apiKey API密钥 + * @param request 对话请求 + * @return 对话结果 + */ + public static AlitaResult chatMessages(String apiKey, DifyChatRequest request) { + if (difyProperties == null) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "DifyProperties未初始化"); + } + return chatMessages(difyProperties.getHost(), apiKey, request); + } + + /** + * 发送对话消息(静态方法) + * + * @param host Dify服务地址 + * @param apiKey API密钥 + * @param request 对话请求 + * @return 对话结果 + */ + public static AlitaResult chatMessages(String host, String apiKey, DifyChatRequest request) { + try { + // 参数校验 + if (StringUtils.isBlank(host)) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "Dify服务地址不能为空"); + } + if (StringUtils.isBlank(apiKey)) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "API密钥不能为空"); + } + if (request == null) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "请求参数不能为空"); + } + if (StringUtils.isBlank(request.getQuery())) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "查询内容不能为空"); + } + if (StringUtils.isBlank(request.getUser())) { + return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "用户标识不能为空"); + } + + String url = host + CHAT_MESSAGES_URI; + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + apiKey); + + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + log.info("dify chatMessages request url:{}, body:{}", url, JsonUtils.toJsonString(request)); + + // 发送请求 + ResponseEntity response = HttpUtils.restTemplate.exchange( + url, + HttpMethod.POST, + requestEntity, + DifyChatResponse.class + ); + + DifyChatResponse result = response.getBody(); + log.info("dify chatMessages response:{}", JsonUtils.toJsonString(result)); + + return AlitaResult.success(result); + + } catch (Exception e) { + log.error("dify chatMessages error:", e); + return AlitaResult.error(SdkConstants.SDK_HTTP_INVOKE_ERROR_CODE, e.getMessage()); + } + } + +} + diff --git a/src/main/java/com/senmesh/sfile/constant/RiskLevelEnum.java b/src/main/java/com/senmesh/sfile/constant/RiskLevelEnum.java new file mode 100644 index 0000000..4267fd9 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/constant/RiskLevelEnum.java @@ -0,0 +1,100 @@ +package com.senmesh.sfile.constant; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.senmesh.mybatis.handle.BaseEnum; + +/** + * 风险等级枚举 + */ +public enum RiskLevelEnum implements BaseEnum { + + /** + * 重度风险 + */ + SEVERE("1", "重度"), + + /** + * 中度风险 + */ + MODERATE("2", "中度"), + + /** + * 轻度风险 + */ + MILD("3", "轻度"); + + /** + * 风险等级 + */ + @EnumValue + private final String level; + + /** + * 等级描述 + */ + private final String desc; + + RiskLevelEnum(String level, String desc) { + this.level = level; + this.desc = desc; + } + + public String getLevel() { + return level; + } + + public String getDesc() { + return desc; + } + + @Override + public String getValue() { + return this.level; + } + + /** + * 根据等级获取枚举 + * + * @param level 等级 + * @return RiskLevelEnum + */ + public static RiskLevelEnum getByLevel(String level) { + for (RiskLevelEnum riskLevelEnum : RiskLevelEnum.values()) { + if (riskLevelEnum.getLevel().equals(level)) { + return riskLevelEnum; + } + } + return null; + } + + /** + * 判断是否为重度风险 + * + * @param level 等级 + * @return true-重度,false-非重度 + */ + public static boolean isSevere(String level) { + return SEVERE.getLevel().equals(level); + } + + /** + * 判断是否为中度风险 + * + * @param level 等级 + * @return true-中度,false-非中度 + */ + public static boolean isModerate(String level) { + return MODERATE.getLevel().equals(level); + } + + /** + * 判断是否为轻度风险 + * + * @param level 等级 + * @return true-轻度,false-非轻度 + */ + public static boolean isMild(String level) { + return MILD.getLevel().equals(level); + } +} + diff --git a/src/main/java/com/senmesh/sfile/controller/DifyController.java b/src/main/java/com/senmesh/sfile/controller/DifyController.java new file mode 100644 index 0000000..325a250 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/controller/DifyController.java @@ -0,0 +1,60 @@ +package com.senmesh.sfile.controller; + +import com.senmesh.common.AlitaResult; +import com.senmesh.sdk.dify.dto.DifyChatRequest; +import com.senmesh.sdk.dify.dto.DifyChatResponse; +import com.senmesh.sdk.dify.dto.DifyFileUploadRequest; +import com.senmesh.sdk.dify.dto.DifyFileUploadResponse; +import com.senmesh.sfile.service.DifyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * Dify接口Controller + * + * @author sr + */ +@RestController +@RequestMapping("/dify") +@Api(tags = "Dify接口") +@ConditionalOnBean(DifyService.class) +public class DifyController { + + @Autowired + private DifyService difyService; + + @ApiOperation("上传文件到Dify") + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public AlitaResult uploadFile( + @RequestParam("apiKey") String apiKey, + @RequestParam("file") MultipartFile file, + @RequestParam("user") String user) throws Exception { + DifyFileUploadRequest request = DifyFileUploadRequest.builder() + .inputStream(file.getInputStream()) + .fileName(file.getOriginalFilename()) + .contentType(file.getContentType()) + .user(user) + .build(); + return difyService.uploadFile(apiKey, request); + } + + @ApiOperation("发送对话消息") + @PostMapping("/chat") + public AlitaResult chatMessages( + @RequestParam("apiKey") String apiKey, + @Validated @RequestBody DifyChatRequest request) { + return difyService.chatMessages(apiKey, request); + } + +} + diff --git a/src/main/java/com/senmesh/sfile/controller/FileRiskController.java b/src/main/java/com/senmesh/sfile/controller/FileRiskController.java new file mode 100644 index 0000000..cb7c296 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/controller/FileRiskController.java @@ -0,0 +1,89 @@ +package com.senmesh.sfile.controller; + +import com.senmesh.common.AlitaResult; +import com.senmesh.common.util.UserContextHolder; +import com.senmesh.sfile.dto.FileRiskCreateDTO; +import com.senmesh.sfile.dto.FileRiskDTO; +import com.senmesh.sfile.dto.FileRiskQueryDTO; +import com.senmesh.sfile.service.FileRiskService; +import com.senmesh.tkmybatis.pojo.Page; + +import javax.validation.Valid; +import java.util.List; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 文件风险Controller + */ +@RestController +@Api(tags = "文件风险") +public class FileRiskController { + + @Autowired + private FileRiskService fileRiskService; + + @PostMapping("/file_risk/create") + @ApiOperation("创建文件风险") + public AlitaResult create(@Valid @RequestBody FileRiskCreateDTO createDTO) { + FileRiskDTO fileRiskDTO = fileRiskService.create(createDTO); + return AlitaResult.success(fileRiskDTO); + } + + @GetMapping("/file_risk/rule") + @ApiOperation("获取风险规则") + public AlitaResult getRiskRule() { + return AlitaResult.success(fileRiskService.getRiskRule(UserContextHolder.getExistLoginOrgId())); + } + + @PostMapping("/file_risk/rule") + @ApiOperation("保存风险规则") + public AlitaResult saveRiskRule(@RequestBody String riskRule) { + fileRiskService.saveRiskRule(riskRule); + return AlitaResult.success(); + } + + @GetMapping("/file_risk/min_audit_risk_level") + @ApiOperation("获取最低审核风险等级") + public AlitaResult getMinAuditRiskLevel() { + return AlitaResult.success(fileRiskService.getMinAuditRiskLevel(UserContextHolder.getExistLoginOrgId())); + } + + @PostMapping("/file_risk/min_audit_risk_level") + @ApiOperation("保存最低审核风险等级") + public AlitaResult saveMinAuditRiskLevel(@RequestBody String minAuditRiskLevel) { + fileRiskService.saveMinAuditRiskLevel(minAuditRiskLevel); + return AlitaResult.success(); + } + + @GetMapping("/file_risk/scan") + @ApiOperation("扫描文件风险") + public AlitaResult scanFileRisk( + @ApiParam(value = "文件编码", required = true) @RequestParam String fileCode) { + FileRiskDTO fileRiskDTO = fileRiskService.scanFileRisk(fileCode); + return AlitaResult.success(fileRiskDTO); + } + + @GetMapping("/file_risk/list") + @ApiOperation("列表查询文件风险") + public AlitaResult> list(FileRiskQueryDTO queryDTO) { + List list = fileRiskService.list(queryDTO); + return AlitaResult.success(list); + } + + @GetMapping("/file_risk/page") + @ApiOperation("分页查询文件风险") + public AlitaResult> page(FileRiskQueryDTO queryDTO) { + fileRiskService.page(queryDTO); + return AlitaResult.success(queryDTO.getPageResult()); + } + +} + diff --git a/src/main/java/com/senmesh/sfile/controller/RiskFileController.java b/src/main/java/com/senmesh/sfile/controller/RiskFileController.java deleted file mode 100644 index f1e4d5b..0000000 --- a/src/main/java/com/senmesh/sfile/controller/RiskFileController.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.senmesh.sfile.controller; - -import com.senmesh.common.AlitaResult; -import com.senmesh.sfile.service.RiskFileService; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import org.springframework.beans.factory.annotation.Autowired; -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.RestController; - -/** - * 风险文件Controller - */ -@RestController -@Api(tags = "风险文件") -public class RiskFileController { - - @Autowired - private RiskFileService riskFileService; - - @GetMapping("/risk_file/rule") - @ApiOperation("获取风险规则") - public AlitaResult getRiskRule() { - return AlitaResult.success(riskFileService.getRiskRule()); - } - - @PostMapping("/risk_file/rule") - @ApiOperation("保存风险规则") - public AlitaResult saveRiskRule(@RequestBody String riskRule) { - riskFileService.saveRiskRule(riskRule); - return AlitaResult.success(); - } - -} diff --git a/src/main/java/com/senmesh/sfile/controller/UserFileController.java b/src/main/java/com/senmesh/sfile/controller/UserFileController.java index 0a8aa31..fdda457 100644 --- a/src/main/java/com/senmesh/sfile/controller/UserFileController.java +++ b/src/main/java/com/senmesh/sfile/controller/UserFileController.java @@ -8,6 +8,7 @@ import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileModifyDTO; import com.senmesh.sfile.dto.UserFileModifyStatusDTO; import com.senmesh.sfile.dto.UserFileQueryDTO; +import com.senmesh.sfile.dto.UserFileStatisticsDTO; import com.senmesh.sfile.service.UserFileService; import com.senmesh.tkmybatis.pojo.Page; import io.swagger.annotations.Api; @@ -84,5 +85,12 @@ public class UserFileController { userFileService.page(queryDTO); return AlitaResult.success(queryDTO.getPageResult()); } + + @GetMapping("/statistics/category") + @ApiOperation("按文件种类统计") + public AlitaResult> statisticsByCategory(UserFileQueryDTO queryDTO) { + queryDTO.setUserId(UserContextHolder.getExistLoginUserId()); + return AlitaResult.success(userFileService.statisticsByCategory(queryDTO)); + } } diff --git a/src/main/java/com/senmesh/sfile/controller/UserSpaceController.java b/src/main/java/com/senmesh/sfile/controller/UserSpaceController.java index d4bb8a5..561f08a 100644 --- a/src/main/java/com/senmesh/sfile/controller/UserSpaceController.java +++ b/src/main/java/com/senmesh/sfile/controller/UserSpaceController.java @@ -1,6 +1,7 @@ package com.senmesh.sfile.controller; import com.senmesh.common.AlitaResult; +import com.senmesh.common.util.UserContextHolder; import com.senmesh.sfile.dto.UserSpaceCreateDTO; import com.senmesh.sfile.dto.UserSpaceDTO; import com.senmesh.sfile.dto.UserSpaceModifyDTO; @@ -29,13 +30,14 @@ public class UserSpaceController { @PostMapping("/create") @ApiOperation("创建") public AlitaResult create(@Validated @RequestBody UserSpaceCreateDTO createDTO) { - + createDTO.setUserId(UserContextHolder.getExistLoginUserId()); return AlitaResult.success(userSpaceService.create(createDTO)); } @PostMapping("/modify") @ApiOperation("修改") public AlitaResult modify(@Validated @RequestBody UserSpaceModifyDTO modifyDTO) { + modifyDTO.setUserId(UserContextHolder.getExistLoginUserId()); userSpaceService.modify(modifyDTO); return AlitaResult.success(true); } @@ -50,20 +52,9 @@ public class UserSpaceController { @GetMapping("/find") @ApiOperation("查询") public AlitaResult find(UserSpaceQueryDTO queryDTO) { + queryDTO.setUserId(UserContextHolder.getExistLoginUserId()); return AlitaResult.success(userSpaceService.find(queryDTO)); } - @GetMapping("/list") - @ApiOperation("列表查询") - public AlitaResult> list(UserSpaceQueryDTO queryDTO) { - return AlitaResult.success(userSpaceService.list(queryDTO)); - } - - @GetMapping("/page") - @ApiOperation("分页查询") - public AlitaResult> page(UserSpaceQueryDTO queryDTO) { - userSpaceService.page(queryDTO); - return AlitaResult.success(queryDTO.getPageResult()); - } } diff --git a/src/main/java/com/senmesh/sfile/convert/FileRiskConvert.java b/src/main/java/com/senmesh/sfile/convert/FileRiskConvert.java new file mode 100644 index 0000000..b582458 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/convert/FileRiskConvert.java @@ -0,0 +1,23 @@ +package com.senmesh.sfile.convert; + +import com.senmesh.sfile.dto.FileRiskCreateDTO; +import com.senmesh.sfile.dto.FileRiskDTO; +import com.senmesh.sfile.entity.FileRisk; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 文件风险转换器 + */ +@Mapper +public interface FileRiskConvert { + + FileRiskConvert INSTANCE = Mappers.getMapper(FileRiskConvert.class); + + FileRisk toEntity(FileRiskDTO dto); + + FileRiskDTO toDto(FileRisk entity); + + FileRisk toEntity(FileRiskCreateDTO dto); +} + diff --git a/src/main/java/com/senmesh/sfile/dao/FileRiskMapper.java b/src/main/java/com/senmesh/sfile/dao/FileRiskMapper.java new file mode 100644 index 0000000..73f6429 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/dao/FileRiskMapper.java @@ -0,0 +1,26 @@ +package com.senmesh.sfile.dao; + +import com.senmesh.sfile.dto.FileRiskDTO; +import com.senmesh.sfile.dto.FileRiskQueryDTO; +import com.senmesh.sfile.entity.FileRisk; +import com.senmesh.tkmybatis.mybatis.MyMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文件风险Mapper + */ +@Mapper +public interface FileRiskMapper extends MyMapper { + + /** + * 查询列表(PageHelper会根据queryDTO自动判断是否分页) + * + * @param queryDTO 查询DTO + * @return List + */ + List list(@Param("param") FileRiskQueryDTO queryDTO); +} + diff --git a/src/main/java/com/senmesh/sfile/dao/UserFileMapper.java b/src/main/java/com/senmesh/sfile/dao/UserFileMapper.java index 04b1f8e..2bacd2c 100644 --- a/src/main/java/com/senmesh/sfile/dao/UserFileMapper.java +++ b/src/main/java/com/senmesh/sfile/dao/UserFileMapper.java @@ -2,6 +2,7 @@ package com.senmesh.sfile.dao; import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileQueryDTO; +import com.senmesh.sfile.dto.UserFileStatisticsDTO; import com.senmesh.sfile.entity.UserFile; import com.senmesh.tkmybatis.mybatis.MyMapper; import org.apache.ibatis.annotations.Mapper; @@ -54,5 +55,13 @@ public interface UserFileMapper extends MyMapper { @Param("oldStatus") String oldStatus, @Param("newStatus") String newStatus, @Param("operateUserId") Long operateUserId); + + /** + * 按文件种类统计文件个数和总大小 + * + * @param queryDTO 查询DTO + * @return List + */ + List statisticsByCategory(@Param("param") UserFileQueryDTO queryDTO); } diff --git a/src/main/java/com/senmesh/sfile/dto/FileRiskCreateDTO.java b/src/main/java/com/senmesh/sfile/dto/FileRiskCreateDTO.java new file mode 100644 index 0000000..660d265 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/dto/FileRiskCreateDTO.java @@ -0,0 +1,31 @@ +package com.senmesh.sfile.dto; + +import com.senmesh.common.dto.BaseOperateDTO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +/** + * 文件风险创建DTO + */ +@ApiModel("文件风险创建DTO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileRiskCreateDTO extends BaseOperateDTO { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("文件编码") + @NotEmpty(message = "文件编码不能为空") + private String fileCode; +} + diff --git a/src/main/java/com/senmesh/sfile/dto/FileRiskDTO.java b/src/main/java/com/senmesh/sfile/dto/FileRiskDTO.java new file mode 100644 index 0000000..50117d5 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/dto/FileRiskDTO.java @@ -0,0 +1,37 @@ +package com.senmesh.sfile.dto; + +import com.senmesh.common.dto.BaseDTO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 文件风险DTO + */ +@ApiModel("文件风险") +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileRiskDTO extends BaseDTO { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("文件编码") + private String fileCode; + + @ApiModelProperty("最高风险等级:1重度/2中度/3轻度") + private String maxRiskLevel; + + @ApiModelProperty("风险详情JSON") + private String riskJson; + + @ApiModelProperty("Dify文件上传响应JSON") + private String difyFileJson; +} + diff --git a/src/main/java/com/senmesh/sfile/dto/FileRiskQueryDTO.java b/src/main/java/com/senmesh/sfile/dto/FileRiskQueryDTO.java new file mode 100644 index 0000000..386c1d5 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/dto/FileRiskQueryDTO.java @@ -0,0 +1,34 @@ +package com.senmesh.sfile.dto; + +import com.senmesh.common.dto.PageQueryDTO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 文件风险查询DTO + */ +@ApiModel("文件风险查询DTO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileRiskQueryDTO extends PageQueryDTO { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("主键ID") + private Long id; + + @ApiModelProperty("文件编码") + private String fileCode; + + @ApiModelProperty("最高风险等级:1重度/2中度/3轻度") + private String maxRiskLevel; +} + diff --git a/src/main/java/com/senmesh/sfile/dto/UserFileStatisticsDTO.java b/src/main/java/com/senmesh/sfile/dto/UserFileStatisticsDTO.java new file mode 100644 index 0000000..e609af4 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/dto/UserFileStatisticsDTO.java @@ -0,0 +1,33 @@ +package com.senmesh.sfile.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 用户文件统计DTO + */ +@ApiModel("用户文件统计DTO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserFileStatisticsDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("文件种类") + private String fileCategory; + + @ApiModelProperty("文件个数") + private Long fileCount; + + @ApiModelProperty("文件总大小(kb)") + private Long totalSize; +} + diff --git a/src/main/java/com/senmesh/sfile/entity/FileRisk.java b/src/main/java/com/senmesh/sfile/entity/FileRisk.java new file mode 100644 index 0000000..295590b --- /dev/null +++ b/src/main/java/com/senmesh/sfile/entity/FileRisk.java @@ -0,0 +1,44 @@ +package com.senmesh.sfile.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.senmesh.common.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 文件风险 + */ +@TableName("x_file_risk") +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileRisk extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 文件编码 + */ + private String fileCode; + + /** + * 最高风险等级:1重度/2中度/3轻度 + */ + private String maxRiskLevel; + + /** + * 风险详情JSON + */ + private String riskJson; + + /** + * Dify文件上传响应JSON + */ + private String difyFileJson; +} + diff --git a/src/main/java/com/senmesh/sfile/mapper/FileRiskMapper.xml b/src/main/java/com/senmesh/sfile/mapper/FileRiskMapper.xml new file mode 100644 index 0000000..33f2387 --- /dev/null +++ b/src/main/java/com/senmesh/sfile/mapper/FileRiskMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + FROM x_file_risk t + WHERE t.deleted = FALSE + + AND t.id = #{param.id} + + + AND t.file_code = #{param.fileCode} + + + AND t.max_risk_level = #{param.maxRiskLevel} + + + + + diff --git a/src/main/java/com/senmesh/sfile/mapper/UserFileMapper.xml b/src/main/java/com/senmesh/sfile/mapper/UserFileMapper.xml index 65f9d5b..854f793 100644 --- a/src/main/java/com/senmesh/sfile/mapper/UserFileMapper.xml +++ b/src/main/java/com/senmesh/sfile/mapper/UserFileMapper.xml @@ -9,6 +9,26 @@ ORDER BY t.id DESC + + FROM x_user_file t LEFT JOIN xs_file_record fr ON t.file_code = fr.file_code AND fr.deleted = FALSE diff --git a/src/main/java/com/senmesh/sfile/mapper/UserSpaceMapper.xml b/src/main/java/com/senmesh/sfile/mapper/UserSpaceMapper.xml index 724ee69..94933af 100644 --- a/src/main/java/com/senmesh/sfile/mapper/UserSpaceMapper.xml +++ b/src/main/java/com/senmesh/sfile/mapper/UserSpaceMapper.xml @@ -4,12 +4,21 @@ - - + SELECT t.id, + t.user_id, + t.total_size, + (SELECT COALESCE(SUM(fr.file_size), 0) + FROM x_user_file uf + LEFT JOIN xs_file_record fr ON uf.file_code = fr.file_code AND fr.deleted = FALSE + WHERE uf.deleted = FALSE + AND uf.user_id = t.user_id + AND uf.file_type = '1') AS used_size, + t.deleted, + t.create_by, + t.create_time, + t.update_by, + t.update_time, + t.proxy_by FROM x_user_space t WHERE t.deleted = FALSE @@ -18,6 +27,8 @@ AND t.user_id = #{param.userId} - + ORDER BY t.id DESC + + diff --git a/src/main/java/com/senmesh/sfile/service/FileRiskService.java b/src/main/java/com/senmesh/sfile/service/FileRiskService.java new file mode 100644 index 0000000..032835e --- /dev/null +++ b/src/main/java/com/senmesh/sfile/service/FileRiskService.java @@ -0,0 +1,91 @@ +package com.senmesh.sfile.service; + +import com.senmesh.sfile.dto.FileRiskCreateDTO; +import com.senmesh.sfile.dto.FileRiskDTO; +import com.senmesh.sfile.dto.FileRiskQueryDTO; + +import java.util.List; + +/** + * 文件风险Service + */ +public interface FileRiskService { + + /** + * 创建文件风险(带锁) + * 根据fileCode查询,如果存在则直接返回,不存在则创建 + * + * @param createDTO 创建DTO + * @return 文件风险DTO + */ + FileRiskDTO create(FileRiskCreateDTO createDTO); + + /** + * 获取风险规则 + * + * @param orgId 机构ID + * @return 风险规则配置值 + */ + String getRiskRule(Long orgId); + + /** + * 保存风险规则 + * + * @param riskRule 风险规则配置值 + */ + void saveRiskRule(String riskRule); + + /** + * 获取最低审核风险等级 + * + * @param orgId 机构ID + * @return 最低审核风险等级配置值 + */ + String getMinAuditRiskLevel(Long orgId); + + /** + * 保存最低审核风险等级 + * + * @param minAuditRiskLevel 最低审核风险等级配置值 + */ + void saveMinAuditRiskLevel(String minAuditRiskLevel); + + /** + * 扫描文件风险 + * 1. 根据fileCode查询,如果存在则直接返回 + * 2. 如果不存在,则走扫描逻辑: + * - 拿到文件流调dify上传文件 + * - 调用获取风险规则 + * - 调用dify chat拿风险结果 + * - 获取结果中的max_risk_level + * - 保存数据 + * + * @param fileCode 文件编码 + * @return 文件风险DTO + */ + FileRiskDTO scanFileRisk(String fileCode); + + /** + * 列表查询文件风险 + * + * @param queryDTO 查询条件 + * @return 文件风险DTO列表 + */ + List list(FileRiskQueryDTO queryDTO); + + /** + * 分页查询文件风险 + * + * @param queryDTO 查询条件 + */ + void page(FileRiskQueryDTO queryDTO); + + /** + * 单个查询文件风险 + * + * @param queryDTO 查询条件 + * @return 文件风险DTO + */ + FileRiskDTO find(FileRiskQueryDTO queryDTO); +} + diff --git a/src/main/java/com/senmesh/sfile/service/RiskFileService.java b/src/main/java/com/senmesh/sfile/service/RiskFileService.java deleted file mode 100644 index 3136552..0000000 --- a/src/main/java/com/senmesh/sfile/service/RiskFileService.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.senmesh.sfile.service; - -/** - * 风险文件Service - */ -public interface RiskFileService { - - /** - * 获取风险规则 - * - * @return 风险规则配置值 - */ - String getRiskRule(); - - /** - * 保存风险规则 - * - * @param riskRule 风险规则配置值 - */ - void saveRiskRule(String riskRule); -} - diff --git a/src/main/java/com/senmesh/sfile/service/UserFileService.java b/src/main/java/com/senmesh/sfile/service/UserFileService.java index 6c79b44..9bcf6ee 100644 --- a/src/main/java/com/senmesh/sfile/service/UserFileService.java +++ b/src/main/java/com/senmesh/sfile/service/UserFileService.java @@ -7,6 +7,7 @@ import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileModifyDTO; import com.senmesh.sfile.dto.UserFileModifyStatusDTO; import com.senmesh.sfile.dto.UserFileQueryDTO; +import com.senmesh.sfile.dto.UserFileStatisticsDTO; import java.util.List; @@ -111,5 +112,13 @@ public interface UserFileService { * @return UserFileDTO */ UserFileDTO findOne(UserFileQueryDTO queryDTO); + + /** + * 按文件种类统计文件个数和总大小 + * + * @param queryDTO 查询DTO + * @return List + */ + List statisticsByCategory(UserFileQueryDTO queryDTO); } diff --git a/src/main/java/com/senmesh/sfile/service/impl/FileRiskServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/FileRiskServiceImpl.java new file mode 100644 index 0000000..5b11cee --- /dev/null +++ b/src/main/java/com/senmesh/sfile/service/impl/FileRiskServiceImpl.java @@ -0,0 +1,266 @@ +package com.senmesh.sfile.service.impl; + +import com.senmesh.common.AlitaResult; +import com.senmesh.common.constant.CommonConstants; +import com.senmesh.common.util.JsonUtils; +import com.senmesh.common.util.ListUtil; +import com.senmesh.common.util.UserContextHolder; +import com.senmesh.component.file.service.FileService; +import com.senmesh.config.dto.ConfigDTO; +import com.senmesh.config.dto.ConfigOperatorDTO; +import com.senmesh.config.entity.Config; +import com.senmesh.config.service.ConfigService; +import com.senmesh.sdk.dify.dto.DifyChatRequest; +import com.senmesh.sdk.dify.dto.DifyChatResponse; +import com.senmesh.sdk.dify.dto.DifyFileUploadRequest; +import com.senmesh.sdk.dify.dto.DifyFileUploadResponse; +import com.senmesh.sdk.dify.service.DifyClient; +import com.senmesh.common.exception.util.Assert; +import com.senmesh.frame.redis.dto.LockDTO; +import com.senmesh.frame.redis.redisson.RedissonUtils; +import com.senmesh.sfile.convert.FileRiskConvert; +import com.senmesh.sfile.dao.FileRiskMapper; +import com.senmesh.sfile.dto.FileRiskCreateDTO; +import com.senmesh.sfile.dto.FileRiskDTO; +import com.senmesh.sfile.dto.FileRiskQueryDTO; +import com.senmesh.sfile.entity.FileRisk; +import com.senmesh.sfile.service.FileRiskService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 文件风险ServiceImpl + */ +@Slf4j +@Service +public class FileRiskServiceImpl implements FileRiskService { + + private static final String RISK_RULE_CONFIG_CODE = "risk_rule"; + private static final String MIN_AUDIT_RISK_LEVEL_CONFIG_CODE = "min_audit_risk_level"; + + /** + * 规则生成 API Key + */ + @Value("${dify.risk.rule.api-key}") + private String riskRuleApiKey; + + /** + * 文件智能风险预警 API Key + */ + @Value("${dify.risk.scan.api-key}") + private String riskScanApiKey; + + @Autowired + private ConfigService configService; + + @Autowired + private FileRiskMapper fileRiskMapper; + + @Autowired + private FileService fileService; + + /** + * 获取文件风险锁DTO + * + * @param fileCode 文件编码 + * @return LockDTO + */ + public static LockDTO getLockDTO(String fileCode) { + Assert.notBlank(fileCode, "文件编码不能为空"); + return LockDTO.builder() + .methodName("filerisk") + .tryLockWaitTime(0L) + .lockTimeout(60 * 5L) + .arg(fileCode) + .failMsg("该文件风险正在被操作") + .build(); + } + + @Override + @Transactional + public FileRiskDTO create(FileRiskCreateDTO createDTO) { + String fileCode = createDTO.getFileCode(); + FileRiskQueryDTO queryDTO = FileRiskQueryDTO.builder().fileCode(fileCode).build(); + + return RedissonUtils.lockAndSave(getLockDTO(fileCode), + () -> this.find(queryDTO), () -> { + FileRisk fileRisk = FileRiskConvert.INSTANCE.toEntity(createDTO); + fileRiskMapper.saveEntity(fileRisk); + return FileRiskConvert.INSTANCE.toDto(fileRisk); + }); + } + + @Override + public String getRiskRule(Long orgId) { + ConfigDTO configDTO = configService.queryOrgConfigByCode(orgId, RISK_RULE_CONFIG_CODE); + return configDTO == null ? null : configDTO.getConfigValue(); + } + + @Override + @Transactional + public void saveRiskRule(String riskRule) { + configService.createOrModify(UserContextHolder.getExistLoginOrgId(), RISK_RULE_CONFIG_CODE, riskRule); + } + + @Override + public String getMinAuditRiskLevel(Long orgId) { + ConfigDTO configDTO = configService.queryOrgConfigByCode(orgId, MIN_AUDIT_RISK_LEVEL_CONFIG_CODE); + return configDTO == null ? null : configDTO.getConfigValue(); + } + + @Override + @Transactional + public void saveMinAuditRiskLevel(String minAuditRiskLevel) { + configService.createOrModify(UserContextHolder.getExistLoginOrgId(), MIN_AUDIT_RISK_LEVEL_CONFIG_CODE, minAuditRiskLevel); + } + + + + @Override + @Transactional + public FileRiskDTO scanFileRisk(String fileCode) { + // 1. 查询或创建记录 + FileRiskQueryDTO queryDTO = FileRiskQueryDTO.builder().fileCode(fileCode).build(); + FileRiskDTO fileRiskDTO = this.find(queryDTO); + + if (fileRiskDTO == null) { + // 1.1 记录不存在,创建新记录 + log.info("文件风险不存在,创建新记录,fileCode: {}", fileCode); + FileRiskCreateDTO createDTO = FileRiskCreateDTO.builder() + .fileCode(fileCode) + .build(); + fileRiskDTO = this.create(createDTO); + } + + if (StringUtils.isNotBlank(fileRiskDTO.getRiskJson())) { + return fileRiskDTO; + } + + // 2. 如果 difyFileJson 为空,则上传文件 + DifyFileUploadResponse uploadResponse = null; + if (StringUtils.isBlank(fileRiskDTO.getDifyFileJson())) { + uploadResponse = uploadFileToDify(fileRiskDTO, fileCode, riskScanApiKey); + } else { + // 从 difyFileJson 中解析 uploadFileId + uploadResponse = JsonUtils.toObject(fileRiskDTO.getDifyFileJson(), DifyFileUploadResponse.class); + } + + // 3. 如果 riskJson 为空,则调用 Dify Chat 分析 + + DifyChatRequest chatRequest = DifyChatRequest.builder() + .query("请分析这个文件的风险") + .user(String.valueOf(UserContextHolder.getLoginUserId())) + .files(Collections.singletonList( + DifyChatRequest.DifyFileInfo.builder() + .type(uploadResponse.getMimeType()) + .uploadFileId(uploadResponse.getId()) + .build() + )) + .build(); + + AlitaResult chatResult = DifyClient.chatMessages(riskScanApiKey, chatRequest); + Assert.isTrue(chatResult.getSuccess() , + "调用 Dify Chat 失败,fileCode: " + fileCode + ", error: " + chatResult.getMsg()); + + DifyChatResponse chatResponse = chatResult.getData(); + String answer = chatResponse.getAnswer(); + + // 解析风险等级 + String maxRiskLevel = parseMaxRiskLevel(chatResponse); + if (StringUtils.isBlank(maxRiskLevel)) { + log.warn("未能从 Dify 返回结果中解析出 max_risk_level,fileCode: {}", fileCode); + maxRiskLevel = null; // 默认为空 + } + + // 更新 riskJson 和 maxRiskLevel + FileRisk updateFileRisk = FileRisk.builder() + .id(fileRiskDTO.getId()) + .maxRiskLevel(maxRiskLevel) + .riskJson(answer) + .build(); + fileRiskMapper.updateEntitySelective(updateFileRisk); + log.info("更新文件风险成功,fileCode: {}, maxRiskLevel: {}", fileCode, maxRiskLevel); + + // 更新 DTO + fileRiskDTO.setMaxRiskLevel(maxRiskLevel); + fileRiskDTO.setRiskJson(answer); + return fileRiskDTO; + } + + @Override + public List list(FileRiskQueryDTO queryDTO) { + queryDTO.disablePage(); + return fileRiskMapper.list(queryDTO); + } + + @Override + public void page(FileRiskQueryDTO queryDTO) { + fileRiskMapper.list(queryDTO); + } + + @Override + public FileRiskDTO find(FileRiskQueryDTO queryDTO) { + List list = this.list(queryDTO); + return ListUtil.first(list); + } + + /** + * 上传文件到 Dify + * + * @param fileRiskDTO 文件风险DTO + * @param fileCode 文件编码 + * @param apiKey API密钥 + * @return DifyFileUploadResponse + */ + private DifyFileUploadResponse uploadFileToDify(FileRiskDTO fileRiskDTO, String fileCode, String apiKey) { + // 1. 获取文件流 + InputStream fileStream = fileService.getFile(fileCode); + Assert.notNull(fileStream, "获取文件流失败,fileCode: " + fileCode); + + // 2. 上传文件到 Dify + DifyFileUploadRequest uploadRequest = DifyFileUploadRequest.builder() + .inputStream(fileStream) + .fileName(fileCode) + .user(String.valueOf(UserContextHolder.getLoginUserId())) + .build(); + + AlitaResult uploadResult = DifyClient.uploadFile(apiKey, uploadRequest); + Assert.isTrue(uploadResult.getSuccess() , + "上传文件到 Dify 失败,fileCode: " + fileCode + ", error: " + uploadResult.getMsg()); + + DifyFileUploadResponse uploadResponse = uploadResult.getData(); + String difyFileJson = JsonUtils.toJsonString(uploadResponse); + // 3. 更新 difyFileJson 字段 + FileRisk updateFileRisk = FileRisk.builder() + .id(fileRiskDTO.getId()) + .difyFileJson(difyFileJson) + .build(); + fileRiskMapper.updateEntitySelective(updateFileRisk); + + // 更新 DTO + fileRiskDTO.setDifyFileJson(difyFileJson); + + return uploadResponse; + } + + /** + * 从Dify返回的响应中解析最高风险等级 + * + * @param chatResponse Dify返回的响应 + * @return 最高风险等级:1重度/2中度/3轻度 + */ + private String parseMaxRiskLevel(DifyChatResponse chatResponse) { + Map resultMap = JsonUtils.toMap(chatResponse.getAnswer()); + return String.valueOf(resultMap.get("max_risk_level")); + } +} + diff --git a/src/main/java/com/senmesh/sfile/service/impl/RiskFileServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/RiskFileServiceImpl.java deleted file mode 100644 index f42a009..0000000 --- a/src/main/java/com/senmesh/sfile/service/impl/RiskFileServiceImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.senmesh.sfile.service.impl; - -import com.senmesh.common.constant.CommonConstants; -import com.senmesh.common.util.UserContextHolder; -import com.senmesh.config.dto.ConfigDTO; -import com.senmesh.config.dto.ConfigOperatorDTO; -import com.senmesh.config.entity.Config; -import com.senmesh.config.service.ConfigService; -import com.senmesh.sfile.service.RiskFileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * 风险文件ServiceImpl - */ -@Service -public class RiskFileServiceImpl implements RiskFileService { - - private static final String RISK_RULE_CONFIG_CODE = "risk_rule"; - - @Autowired - private ConfigService configService; - - @Override - public String getRiskRule() { - ConfigDTO configDTO = configService.queryOrgConfigByCode(UserContextHolder.getExistLoginOrgId(), RISK_RULE_CONFIG_CODE); - return configDTO == null ? null : configDTO.getConfigValue(); - } - - @Override - @Transactional - public void saveRiskRule(String riskRule) { - // 查询是否已存在配置 - ConfigDTO existConfig = configService.queryOrgConfigByCode(UserContextHolder.getExistLoginOrgId(), RISK_RULE_CONFIG_CODE); - - if (existConfig != null) { - // 存在则修改 - ConfigOperatorDTO modifyDTO = ConfigOperatorDTO.builder() - .id(existConfig.getId()) - .configCode(RISK_RULE_CONFIG_CODE) - .configValue(riskRule) - .orgId(UserContextHolder.getExistLoginOrgId()) - .enable(true) - .build(); - configService.modify(modifyDTO); - } else { - // 不存在则创建 - ConfigOperatorDTO createDTO = ConfigOperatorDTO.builder() - .configCode(RISK_RULE_CONFIG_CODE) - .configValue(riskRule) - .orgId(UserContextHolder.getExistLoginOrgId()) - .enable(true) - .build(); - configService.create(createDTO); - } - } -} - diff --git a/src/main/java/com/senmesh/sfile/service/impl/TransferFileMonitorServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/TransferFileMonitorServiceImpl.java index e6c4b6c..d89c888 100644 --- a/src/main/java/com/senmesh/sfile/service/impl/TransferFileMonitorServiceImpl.java +++ b/src/main/java/com/senmesh/sfile/service/impl/TransferFileMonitorServiceImpl.java @@ -2,6 +2,7 @@ package com.senmesh.sfile.service.impl; import com.senmesh.common.exception.util.Assert; import com.senmesh.common.util.JsonUtils; +import com.senmesh.common.util.UserContextHolder; import com.senmesh.component.file.config.FileProperties; import com.senmesh.component.file.service.FileService; import com.senmesh.sfile.constant.FileLocationEnum; @@ -9,13 +10,16 @@ import com.senmesh.sfile.constant.FileStatusEnum; import com.senmesh.sfile.constant.FileTypeEnum; import com.senmesh.sfile.constant.RootTypeEnum; import com.senmesh.sfile.dto.ExpandUserFileDTO; +import com.senmesh.sfile.dto.FileRiskDTO; import com.senmesh.sfile.dto.TransferMetadataDTO; import com.senmesh.sfile.dto.UserFileCreateDTO; import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileModifyStatusDTO; +import com.senmesh.sfile.service.FileRiskService; import com.senmesh.sfile.service.TransferFileMonitorService; import com.senmesh.sfile.service.UserFileService; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -56,9 +60,12 @@ public class TransferFileMonitorServiceImpl implements TransferFileMonitorServic @Autowired private FileService fileService; + @Autowired + private FileRiskService fileRiskService; + @Override public void scanAndProcess() { - String transferReceiveDir = fileProperties.getTransferReceiveDir(); + String transferReceiveDir = fileProperties.getTransfer().getReceiveDir(); Path receiveDir = Paths.get(transferReceiveDir); try { @@ -300,7 +307,7 @@ public class TransferFileMonitorServiceImpl implements TransferFileMonitorServic Assert.notEmpty(files, "expandUserFile 缺少 files"); // 2. 确定 rootType 并设置到 folder 中 - String location = fileProperties.getLocation(); + String location = fileProperties.getTransfer().getLocation(); String rootType; if (FileLocationEnum.INTERNAL.getLocation().equals(location)) { rootType = RootTypeEnum.EXTERNAL_TO_INTERNAL.getType(); @@ -371,7 +378,15 @@ public class TransferFileMonitorServiceImpl implements TransferFileMonitorServic } } - // 5. 调用 UserFileService 的重载方法创建文件夹和文件 + // 5. 检查是否开启解包风险校验 + String fileStatus = FileStatusEnum.NORMAL.getStatus(); + if (Boolean.TRUE.equals(fileProperties.getTransfer().getUnpackRiskCheck())) { + log.info("解包风险校验已开启,开始扫描文件: {}", fileName); + fileStatus = scanFilesForRisk(files, folder); + } + folder.setFileStatus(fileStatus); + + // 6. 调用 UserFileService 的重载方法创建文件夹和文件 ExpandUserFileDTO result = userFileService.create(folder, files); log.info("文件解包成功: {}, 创建文件夹ID: {}", fileName, result.getFolder().getId()); @@ -383,6 +398,49 @@ public class TransferFileMonitorServiceImpl implements TransferFileMonitorServic throw new RuntimeException("解包文件失败: " + e.getMessage(), e); } } + + /** + * 扫描文件风险 + * for循环文件扫描,拿到最高风险等级,跟最低审核风险等级比较,如果不小于则状态给审批中,否则给正常 + * + * @param files 文件列表 + * @param folder 文件夹信息 + * @return 文件状态 + */ + private String scanFilesForRisk(List files, UserFileDTO folder) { + // 1. 获取最低审核风险等级 + Long orgId = UserContextHolder.getExistLoginOrgId(); + String minAuditRiskLevel = fileRiskService.getMinAuditRiskLevel(orgId); + if (StringUtils.isBlank(minAuditRiskLevel)) { + log.info("未配置最低审核风险等级,默认不审核"); + return FileStatusEnum.NORMAL.getStatus(); + } + + // 2. for循环扫描文件,获取最高风险等级 + String maxRiskLevel = "3"; // 默认轻度风险 + for (UserFileDTO file : files) { + String fileCode = file.getFileCode(); + if (StringUtils.isBlank(fileCode)) { + continue; + } + + FileRiskDTO fileRiskDTO = fileRiskService.scanFileRisk(fileCode); + String riskLevel = fileRiskDTO.getMaxRiskLevel(); + if (StringUtils.isNotBlank(riskLevel) && riskLevel.compareTo(maxRiskLevel) < 0) { + maxRiskLevel = riskLevel; + } + } + + // 3. 比较最高风险等级和最低审核风险等级 + if (maxRiskLevel.compareTo(minAuditRiskLevel) <= 0) { + log.info("文件风险等级 {} 不小于最低审核风险等级 {},状态设置为待审核", maxRiskLevel, minAuditRiskLevel); + return FileStatusEnum.PENDING_REVIEW.getStatus(); + } else { + log.info("文件风险等级 {} 小于最低审核风险等级 {},状态设置为正常", maxRiskLevel, minAuditRiskLevel); + return FileStatusEnum.NORMAL.getStatus(); + } + } + /** * 创建 .finish 文件到发送目录,告诉对方处理完成 * @@ -391,7 +449,7 @@ public class TransferFileMonitorServiceImpl implements TransferFileMonitorServic private void createFinishFile(String zipFileName) { try { // 获取发送目录 - String transferSendDir = fileProperties.getTransferSendDir(); + String transferSendDir = fileProperties.getTransfer().getSendDir(); Path sendPath = Paths.get(transferSendDir); // 确保发送目录存在 diff --git a/src/main/java/com/senmesh/sfile/service/impl/TransferUserFileServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/TransferUserFileServiceImpl.java index ead09cf..01d742c 100644 --- a/src/main/java/com/senmesh/sfile/service/impl/TransferUserFileServiceImpl.java +++ b/src/main/java/com/senmesh/sfile/service/impl/TransferUserFileServiceImpl.java @@ -53,7 +53,7 @@ public class TransferUserFileServiceImpl implements TransferUserFileService { Long userId = UserContextHolder.getExistLoginUserId(); // 根据 FileProperties#location 的类型决定是内穿外还是外传内 - String location = fileProperties.getLocation(); + String location = fileProperties.getTransfer().getLocation(); String rootType; if (FileLocationEnum.INTERNAL.getLocation().equals(location)) { // 内网 -> 内穿外 @@ -63,7 +63,6 @@ public class TransferUserFileServiceImpl implements TransferUserFileService { rootType = RootTypeEnum.EXTERNAL_TO_INTERNAL.getType(); } - // TODO 扫描文件审核 // 调用UserFileService的重载create方法 ExpandUserFileCreateDTO expandCreateDTO = ExpandUserFileCreateDTO.builder() .userFileId(createDTO.getUserFileId()) @@ -95,7 +94,7 @@ public class TransferUserFileServiceImpl implements TransferUserFileService { List files = expandUserFileDTO.getFiles(); // 1. 创建传输发送目录 - String transferSendDir = fileProperties.getTransferSendDir(); + String transferSendDir = fileProperties.getTransfer().getSendDir(); Path transferPath = Paths.get(transferSendDir); if (!Files.exists(transferPath)) { Files.createDirectories(transferPath); @@ -145,7 +144,5 @@ public class TransferUserFileServiceImpl implements TransferUserFileService { log.info("传输文件打包完成: {}", finalFilePath); } - - } diff --git a/src/main/java/com/senmesh/sfile/service/impl/UserFileServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/UserFileServiceImpl.java index e5d0d0f..2ee7d8c 100644 --- a/src/main/java/com/senmesh/sfile/service/impl/UserFileServiceImpl.java +++ b/src/main/java/com/senmesh/sfile/service/impl/UserFileServiceImpl.java @@ -18,6 +18,7 @@ import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileModifyDTO; import com.senmesh.sfile.dto.UserFileModifyStatusDTO; import com.senmesh.sfile.dto.UserFileQueryDTO; +import com.senmesh.sfile.dto.UserFileStatisticsDTO; import com.senmesh.sfile.entity.UserFile; import com.senmesh.sfile.service.UserFileService; import lombok.extern.slf4j.Slf4j; @@ -175,7 +176,7 @@ public class UserFileServiceImpl implements UserFileService { .fileType(FileTypeEnum.FOLDER.getType()) .fileName(folder.getFileName()) .parentId(0L) - .fileStatus(FileStatusEnum.NORMAL.getStatus()) + .fileStatus(folder.getFileStatus()) .build(); UserFileDTO folderDTO = this.create(folderCreateDTO); Long folderId = folderDTO.getId(); @@ -196,7 +197,7 @@ public class UserFileServiceImpl implements UserFileService { .fileCode(fileInfo.getFileCode()) .parentId(folderId) .parentPath(folderPath) - .fileStatus(FileStatusEnum.NORMAL.getStatus()) + .fileStatus(folder.getFileStatus()) .build(); UserFileDTO createdFile = this.create(fileCreateDTO); @@ -327,5 +328,11 @@ public class UserFileServiceImpl implements UserFileService { Assert.notNull(userFileDTO, "文件不存在"); return userFileDTO; } + + @Override + public List statisticsByCategory(UserFileQueryDTO queryDTO) { + queryDTO.disablePage(); + return userFileMapper.statisticsByCategory(queryDTO); + } } diff --git a/src/main/java/com/senmesh/sfile/service/impl/UserSpaceServiceImpl.java b/src/main/java/com/senmesh/sfile/service/impl/UserSpaceServiceImpl.java index d23b88e..edff9e6 100644 --- a/src/main/java/com/senmesh/sfile/service/impl/UserSpaceServiceImpl.java +++ b/src/main/java/com/senmesh/sfile/service/impl/UserSpaceServiceImpl.java @@ -48,14 +48,15 @@ public class UserSpaceServiceImpl implements UserSpaceService { Long userId = createDTO.getUserId(); UserSpaceQueryDTO queryDTO = UserSpaceQueryDTO.builder().userId(userId).build(); - return RedissonUtils.lockAndSave(getLockDTO(userId), + UserSpaceDTO userSpaceDTO = RedissonUtils.lockAndSave(getLockDTO(userId), () -> this.find(queryDTO), () -> { UserSpace userSpace = UserSpaceConvert.INSTANCE.toEntity(createDTO); userSpace.setTotalSize(DEFAULT_TOTAL_SIZE); userSpace.setUsedSize(0L); userSpaceMapper.saveEntity(userSpace); - return userSpace.getId(); + return UserSpaceConvert.INSTANCE.toDto(userSpace); }); + return userSpaceDTO.getId(); } @Override diff --git a/src/main/java/com/senmesh/usercenter/org/mapstruct/OrgMapMapper.java b/src/main/java/com/senmesh/usercenter/org/mapstruct/OrgMapMapper.java index cba4054..e2d3ebf 100644 --- a/src/main/java/com/senmesh/usercenter/org/mapstruct/OrgMapMapper.java +++ b/src/main/java/com/senmesh/usercenter/org/mapstruct/OrgMapMapper.java @@ -19,17 +19,26 @@ public interface OrgMapMapper { /** * @param orgDto * @return - * @author sr on 2024-04-18 + * @author sr on 2024-04-18 */ Org toEntity( OrgDTO orgDto); - + + /** + * entity to dto + * + * @param org + * @return + * @author sr on 2024-04-18 + */ + OrgDTO toDto(Org org); + /** * @param orgCreateDTO * @return - * @author sr on 2024-04-18 + * @author sr on 2024-04-18 */ Org toEntity(OrgCreateDTO orgCreateDTO); - + } diff --git a/src/main/java/com/senmesh/usercenter/org/service/impl/OrgServiceImpl.java b/src/main/java/com/senmesh/usercenter/org/service/impl/OrgServiceImpl.java index 9a0f03f..605256c 100644 --- a/src/main/java/com/senmesh/usercenter/org/service/impl/OrgServiceImpl.java +++ b/src/main/java/com/senmesh/usercenter/org/service/impl/OrgServiceImpl.java @@ -80,14 +80,15 @@ public class OrgServiceImpl implements OrgService { Assert.notNull(orgTypeEnum, "机构类型有误"); Assert.isTrue(orgTypeEnum != OrgTypeEnum.ORG_TYPE_PLATFORM, "该机构类型不支持创建"); String orgName = orgCreateDTO.getOrgName(); - Long orgId = RedissonUtils.lockAndSave(getLockDTO(orgName), + OrgDTO orgDTO = RedissonUtils.lockAndSave(getLockDTO(orgName), () -> this.find(OrgQueryDTO.builder().orgName(orgName).build()), () -> { Org org = OrgMapMapper.INSTANCE.toEntity(orgCreateDTO); initCreateOrg(org); orgMapper.saveEntity(org); - return org.getId(); + return OrgMapMapper.INSTANCE.toDto(org); }, "该机构名称已存在"); + Long orgId = orgDTO.getId(); // 创建管理员用户 userService.createOrgUser(OrgUserCreateDTO.builder().orgId(orgId).phone(orgCreateDTO.getPhone()).realName(orgCreateDTO.getRealName()) .administrator(Boolean.TRUE).build()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 06a6359..ceb85b0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -44,12 +44,16 @@ java.io.tmpdir=/var/${spring.application.name}/upload/tmpdir #文件存储配置 file.storage.type=local + +#文件传输配置 ### 文件位置:1内网2外网 -file.location=1 +file.transfer.location=1 ### 传输发送目录 -file.transfer-send-dir=D:/work/git/demo/transfer/send +file.transfer.send-dir=D:/work/git/demo/transfer/send ### 传输接收目录 -file.transfer-receive-dir=D:/work/git/demo/transfer/receive +file.transfer.receive-dir=D:/work/git/demo/transfer/receive +### 解包风险校验 +file.transfer.unpack-risk-check=false #发号器 @@ -138,26 +142,12 @@ sms.numLimit=0 sms.phoneSms= -#讯飞配置 -xf.httpAPIPassword= -#在控制台-我的应用获取 -xf.appId= -xf.apiSecret= -xf.apiKey= -#语音听写地址 -xf.iatHostUrl=https://iat-api.xfyun.cn/v2/iat -#语音合成地址 -xf.ttsHostUrl=https://tts-api.xfyun.cn/v2/tts -#机器翻译niutrans -xf.otsHostUrl=https://ntrans.xfyun.cn/v2/ots -#实时语音转写 -xf.rtasr.APIKey= - -#deepseek配置 -deepseek.httpAPIPassword= - -#ai配置 -#对话提供者,对应配置的PREFIX -ai.chatProvider=deepseek +#dify配置 +dify.enable=false +dify.host=https://flow.senmesh.com +# 规则生成 API Key +dify.risk.rule.api-key=app-agFkKV5EsAEZfCC6wGSDraak +# 文件智能风险预警 API Key +dify.risk.scan.api-key=app-K9WlRD9hEhLbesUK8HDf3ATP