This commit is contained in:
ran.shang
2026-01-08 14:33:33 +08:00
parent 2b4458dc1b
commit 5405012faa
45 changed files with 1716 additions and 209 deletions

View File

@@ -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 '代理人';

View File

@@ -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='文件风险';

View File

@@ -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<OrgUserDTO> 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

View File

@@ -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

View File

@@ -53,14 +53,15 @@ public class DynamicTableServiceImpl implements DynamicTableService {
List<DynamicTableColumnDTO> 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();

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -311,7 +311,7 @@ public class RedissonUtils {
return null;
}
/**
* 校验上锁保存(有事务幂等)
* @param <T>
@@ -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 <T extends IBaseDTO> Long lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<Long> saveSupplier) {
public static <T extends IBaseDTO> T lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<T> saveSupplier) {
return lockAndSave(lockDTO, existSupplier, saveSupplier, true, null);
}
public static <T extends IBaseDTO> Long lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<Long> saveSupplier, String existMsg) {
public static <T extends IBaseDTO> T lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<T> saveSupplier, String existMsg) {
return lockAndSave(lockDTO, existSupplier, saveSupplier, true, existMsg);
}
/**
* 校验上锁保存
* @param <T>
@@ -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 <T extends IBaseDTO> Long lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<Long> saveSupplier, boolean hasTransaction, String existMsg) throws ExistException{
public static <T extends IBaseDTO> T lockAndSave(LockDTO lockDTO, Supplier<T> existSupplier,
Supplier<T> 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);
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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<String, Object> inputs;
/** 查询内容 */
private String query;
/** 响应模式: blocking/streaming */
@Builder.Default
private String responseMode = "blocking";
/** 会话ID */
private String conversationId;
/** 用户标识 */
private String user;
/** 文件列表 */
private List<DifyFileInfo> 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;
}
}

View File

@@ -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<RetrieverResource> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<DifyFileUploadResponse> 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<DifyFileUploadResponse> 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<String, Object> 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<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
log.info("dify uploadFile request url:{}, user:{}, fileName:{}",
url, request.getUser(), request.getFileName());
// 发送请求
ResponseEntity<DifyFileUploadResponse> 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<DifyChatResponse> 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<DifyChatResponse> 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<DifyChatRequest> requestEntity = new HttpEntity<>(request, headers);
log.info("dify chatMessages request url:{}, body:{}", url, JsonUtils.toJsonString(request));
// 发送请求
ResponseEntity<DifyChatResponse> 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());
}
}
}

View File

@@ -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<String> {
/**
* 重度风险
*/
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);
}
}

View File

@@ -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<DifyFileUploadResponse> 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<DifyChatResponse> chatMessages(
@RequestParam("apiKey") String apiKey,
@Validated @RequestBody DifyChatRequest request) {
return difyService.chatMessages(apiKey, request);
}
}

View File

@@ -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<FileRiskDTO> create(@Valid @RequestBody FileRiskCreateDTO createDTO) {
FileRiskDTO fileRiskDTO = fileRiskService.create(createDTO);
return AlitaResult.success(fileRiskDTO);
}
@GetMapping("/file_risk/rule")
@ApiOperation("获取风险规则")
public AlitaResult<String> getRiskRule() {
return AlitaResult.success(fileRiskService.getRiskRule(UserContextHolder.getExistLoginOrgId()));
}
@PostMapping("/file_risk/rule")
@ApiOperation("保存风险规则")
public AlitaResult<Void> saveRiskRule(@RequestBody String riskRule) {
fileRiskService.saveRiskRule(riskRule);
return AlitaResult.success();
}
@GetMapping("/file_risk/min_audit_risk_level")
@ApiOperation("获取最低审核风险等级")
public AlitaResult<String> getMinAuditRiskLevel() {
return AlitaResult.success(fileRiskService.getMinAuditRiskLevel(UserContextHolder.getExistLoginOrgId()));
}
@PostMapping("/file_risk/min_audit_risk_level")
@ApiOperation("保存最低审核风险等级")
public AlitaResult<Void> saveMinAuditRiskLevel(@RequestBody String minAuditRiskLevel) {
fileRiskService.saveMinAuditRiskLevel(minAuditRiskLevel);
return AlitaResult.success();
}
@GetMapping("/file_risk/scan")
@ApiOperation("扫描文件风险")
public AlitaResult<FileRiskDTO> 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<FileRiskDTO>> list(FileRiskQueryDTO queryDTO) {
List<FileRiskDTO> list = fileRiskService.list(queryDTO);
return AlitaResult.success(list);
}
@GetMapping("/file_risk/page")
@ApiOperation("分页查询文件风险")
public AlitaResult<Page<FileRiskDTO>> page(FileRiskQueryDTO queryDTO) {
fileRiskService.page(queryDTO);
return AlitaResult.success(queryDTO.getPageResult());
}
}

View File

@@ -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<String> getRiskRule() {
return AlitaResult.success(riskFileService.getRiskRule());
}
@PostMapping("/risk_file/rule")
@ApiOperation("保存风险规则")
public AlitaResult<Void> saveRiskRule(@RequestBody String riskRule) {
riskFileService.saveRiskRule(riskRule);
return AlitaResult.success();
}
}

View File

@@ -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<List<UserFileStatisticsDTO>> statisticsByCategory(UserFileQueryDTO queryDTO) {
queryDTO.setUserId(UserContextHolder.getExistLoginUserId());
return AlitaResult.success(userFileService.statisticsByCategory(queryDTO));
}
}

View File

@@ -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<Long> create(@Validated @RequestBody UserSpaceCreateDTO createDTO) {
createDTO.setUserId(UserContextHolder.getExistLoginUserId());
return AlitaResult.success(userSpaceService.create(createDTO));
}
@PostMapping("/modify")
@ApiOperation("修改")
public AlitaResult<Boolean> 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<UserSpaceDTO> find(UserSpaceQueryDTO queryDTO) {
queryDTO.setUserId(UserContextHolder.getExistLoginUserId());
return AlitaResult.success(userSpaceService.find(queryDTO));
}
@GetMapping("/list")
@ApiOperation("列表查询")
public AlitaResult<List<UserSpaceDTO>> list(UserSpaceQueryDTO queryDTO) {
return AlitaResult.success(userSpaceService.list(queryDTO));
}
@GetMapping("/page")
@ApiOperation("分页查询")
public AlitaResult<Page<UserSpaceDTO>> page(UserSpaceQueryDTO queryDTO) {
userSpaceService.page(queryDTO);
return AlitaResult.success(queryDTO.getPageResult());
}
}

View File

@@ -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);
}

View File

@@ -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<FileRisk> {
/**
* 查询列表(PageHelper会根据queryDTO自动判断是否分页)
*
* @param queryDTO 查询DTO
* @return List<FileRiskDTO>
*/
List<FileRiskDTO> list(@Param("param") FileRiskQueryDTO queryDTO);
}

View File

@@ -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<UserFile> {
@Param("oldStatus") String oldStatus,
@Param("newStatus") String newStatus,
@Param("operateUserId") Long operateUserId);
/**
* 按文件种类统计文件个数和总大小
*
* @param queryDTO 查询DTO
* @return List<UserFileStatisticsDTO>
*/
List<UserFileStatisticsDTO> statisticsByCategory(@Param("param") UserFileQueryDTO queryDTO);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<FileRiskDTO> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("文件编码")
private String fileCode;
@ApiModelProperty("最高风险等级:1重度/2中度/3轻度")
private String maxRiskLevel;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.senmesh.sfile.dao.FileRiskMapper">
<select id="list" resultType="com.senmesh.sfile.dto.FileRiskDTO">
SELECT t.*
<include refid="queryFromSql"></include>
ORDER BY t.id DESC
</select>
<sql id="queryFromSql">
FROM x_file_risk t
WHERE t.deleted = FALSE
<if test='param.id != null'>
AND t.id = #{param.id}
</if>
<if test='param.fileCode != null and param.fileCode.trim() != ""'>
AND t.file_code = #{param.fileCode}
</if>
<if test='param.maxRiskLevel != null and param.maxRiskLevel.trim() != ""'>
AND t.max_risk_level = #{param.maxRiskLevel}
</if>
</sql>
</mapper>

View File

@@ -9,6 +9,26 @@
ORDER BY t.id DESC
</select>
<select id="statisticsByCategory" resultType="com.senmesh.sfile.dto.UserFileStatisticsDTO">
SELECT
COALESCE(fc.file_category, '未分类') AS file_category,
COUNT(t.id) AS file_count,
COALESCE(SUM(fr.file_size), 0) AS total_size
FROM x_user_file t
LEFT JOIN xs_file_record fr ON t.file_code = fr.file_code AND fr.deleted = FALSE
LEFT JOIN x_file_category fc ON fr.file_suffix = fc.file_suffix AND fc.deleted = FALSE
WHERE t.deleted = FALSE
AND t.file_type = '1'
<if test='param.userId != null'>
AND t.user_id = #{param.userId}
</if>
<if test='param.rootType != null and param.rootType.trim() != ""'>
AND t.root_type = #{param.rootType}
</if>
GROUP BY fc.file_category
ORDER BY total_size DESC
</select>
<sql id="queryFromSql">
FROM x_user_file t
LEFT JOIN xs_file_record fr ON t.file_code = fr.file_code AND fr.deleted = FALSE

View File

@@ -4,12 +4,21 @@
<mapper namespace="com.senmesh.sfile.dao.UserSpaceMapper">
<select id="list" resultType="com.senmesh.sfile.dto.UserSpaceDTO">
SELECT t.*
<include refid="queryFromSql"></include>
ORDER BY t.id DESC
</select>
<sql id="queryFromSql">
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
<if test='param.id != null'>
@@ -18,6 +27,8 @@
<if test='param.userId != null'>
AND t.user_id = #{param.userId}
</if>
</sql>
ORDER BY t.id DESC
</select>
</mapper>

View File

@@ -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<FileRiskDTO> list(FileRiskQueryDTO queryDTO);
/**
* 分页查询文件风险
*
* @param queryDTO 查询条件
*/
void page(FileRiskQueryDTO queryDTO);
/**
* 单个查询文件风险
*
* @param queryDTO 查询条件
* @return 文件风险DTO
*/
FileRiskDTO find(FileRiskQueryDTO queryDTO);
}

View File

@@ -1,22 +0,0 @@
package com.senmesh.sfile.service;
/**
* 风险文件Service
*/
public interface RiskFileService {
/**
* 获取风险规则
*
* @return 风险规则配置值
*/
String getRiskRule();
/**
* 保存风险规则
*
* @param riskRule 风险规则配置值
*/
void saveRiskRule(String riskRule);
}

View File

@@ -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<UserFileStatisticsDTO>
*/
List<UserFileStatisticsDTO> statisticsByCategory(UserFileQueryDTO queryDTO);
}

View File

@@ -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<DifyChatResponse> 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<FileRiskDTO> 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<FileRiskDTO> 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<DifyFileUploadResponse> 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<String, Object> resultMap = JsonUtils.toMap(chatResponse.getAnswer());
return String.valueOf(resultMap.get("max_risk_level"));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<UserFileDTO> 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);
// 确保发送目录存在

View File

@@ -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<UserFileDTO> 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);
}
}

View File

@@ -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<UserFileStatisticsDTO> statisticsByCategory(UserFileQueryDTO queryDTO) {
queryDTO.disablePage();
return userFileMapper.statisticsByCategory(queryDTO);
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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