deepdata: core product code (backend, frontend, runtime, deepflow module)

This commit is contained in:
figmar
2026-08-30 19:23:38 +08:00
commit bd3db30be9
1405 changed files with 271727 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
# 共享库
## 概述
共享库包含所有后端服务共用的代码和工具,包括领域构建块、异常处理、JWT 工具等。
## 架构
```
backend/shared/
├── domain-common/ # DDD 构建块、异常处理
│ └── src/main/java/com/deepdata/common/
│ ├── infrastructure/exception/ # BusinessException, ErrorCode
│ ├── setting/ # 系统参数、模型配置
│ └── domain/ # Base entities, repositories
└── security-common/ # JWT 工具、认证辅助
└── src/main/java/com/deepdata/security/
```
## 库
### 1. domain-common
#### BusinessException
统一的业务异常处理机制:
```java
// 抛出业务异常
throw BusinessException.of(ErrorCode.DATASET_NOT_FOUND)
.withDetail("dataset_id", datasetId);
// 带上下文的异常
throw BusinessException.of(ErrorCode.VALIDATION_FAILED)
.withDetail("field", "email")
.withDetail("reason", "Invalid format");
```
#### ErrorCode
错误码枚举接口:
```java
public interface ErrorCode {
String getCode();
String getMessage();
HttpStatus getHttpStatus();
}
// 示例
public enum CommonErrorCode implements ErrorCode {
SUCCESS("0000", "Success", HttpStatus.OK),
DATABASE_NOT_FOUND("4001", "Database not found", HttpStatus.NOT_FOUND);
}
```
#### BaseEntity
所有实体的基类,包含审计字段:
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseEntity<T> implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private String id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableField(fill = FieldFill.INSERT)
private String createdBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
}
```
### 2. security-common
#### JWT 工具
JWT Token 生成和验证:
```java
// 生成 Token
String token = JwtUtil.generateToken(userId, secret, expiration);
// 验证 Token
Claims claims = JwtUtil.validateToken(token, secret);
String userId = claims.getSubject();
```
## 使用
### 在服务中使用共享库
#### Maven 依赖
```xml
<dependency>
<groupId>com.deepdata</groupId>
<artifactId>domain-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.deepdata</groupId>
<artifactId>security-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
#### 使用 BusinessException
```java
@RestController
@RequiredArgsConstructor
public class DatasetController {
public ResponseEntity<DatasetResponse> getDataset(String id) {
Dataset dataset = datasetService.findById(id);
if (dataset == null) {
throw BusinessException.of(ErrorCode.DATASET_NOT_FOUND);
}
return ResponseEntity.ok(DatasetResponse.from(dataset));
}
}
```
## 快速开始
### 构建共享库
```bash
cd backend
mvn clean install
```
### 在服务中使用
共享库会自动被所有后端服务继承。
## 文档
- [AGENTS.md](./AGENTS.md)
## 相关链接
- [后端 README](../README.md)
+144
View File
@@ -0,0 +1,144 @@
# Shared Libraries
## Overview
Shared Libraries contain code and utilities shared across all backend services, including domain building blocks, exception handling, JWT utilities, and more.
## Architecture
```
backend/shared/
├── domain-common/ # DDD building blocks, exception handling
│ └── src/main/java/com/deepdata/common/
│ ├── infrastructure/exception/ # BusinessException, ErrorCode
│ ├── setting/ # System params, model configs
│ └── domain/ # Base entities, repositories
└── security-common/ # JWT utilities, auth helpers
└── src/main/java/com/deepdata/security/
```
## Libraries
### 1. domain-common
#### BusinessException
Unified business exception handling mechanism:
```java
// Throw business exception
throw BusinessException.of(ErrorCode.DATASET_NOT_FOUND)
.withDetail("dataset_id", datasetId);
// Exception with context
throw BusinessException.of(ErrorCode.VALIDATION_FAILED)
.withDetail("field", "email")
.withDetail("reason", "Invalid format");
```
#### ErrorCode
Error code enumeration interface:
```java
public interface ErrorCode {
String getCode();
String getMessage();
HttpStatus getHttpStatus();
}
// Example
public enum CommonErrorCode implements ErrorCode {
SUCCESS("0000", "Success", HttpStatus.OK),
DATABASE_NOT_FOUND("4001", "Database not found", HttpStatus.NOT_FOUND);
}
```
#### BaseEntity
Base class for all entities, including audit fields:
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseEntity<T> implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private String id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableField(fill = FieldFill.INSERT)
private String createdBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
}
```
### 2. security-common
#### JWT Utilities
JWT Token generation and validation:
```java
// Generate Token
String token = JwtUtil.generateToken(userId, secret, expiration);
// Validate Token
Claims claims = JwtUtil.validateToken(token, secret);
String userId = claims.getSubject();
```
## Usage
### Using Shared Libraries in Services
#### Maven Dependencies
```xml
<dependency>
<groupId>com.deepdata</groupId>
<artifactId>domain-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.deepdata</groupId>
<artifactId>security-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
#### Using BusinessException
```java
@RestController
@RequiredArgsConstructor
public class DatasetController {
public ResponseEntity<DatasetResponse> getDataset(String id) {
Dataset dataset = datasetService.findById(id);
if (dataset == null) {
throw BusinessException.of(ErrorCode.DATASET_NOT_FOUND);
}
return ResponseEntity.ok(DatasetResponse.from(dataset));
}
}
```
## Quick Start
### Build Shared Libraries
```bash
cd backend
mvn clean install
```
### Use in Services
Shared libraries are automatically inherited by all backend services.
## Documentation
- [AGENTS.md](./AGENTS.md)
## Related Links
- [Backend README](../README.md)
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.deepdata</groupId>
<artifactId>services</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../services/pom.xml</relativePath>
</parent>
<artifactId>domain-common</artifactId>
<name>Domain Common</name>
<description>DDD领域通用组件</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,27 @@
package com.deepdata.common.domain;
import com.deepdata.common.domain.model.base.BaseEntity;
/**
* DDD聚合根基类
*/
public abstract class AggregateRoot<ID> extends BaseEntity<ID> {
protected AggregateRoot() {
super();
}
protected AggregateRoot(ID id) {
super(id);
}
/**
* 获取聚合版本号(用于乐观锁)
*/
public abstract Long getVersion();
/**
* 设置聚合版本号
*/
public abstract void setVersion(Long version);
}
@@ -0,0 +1,16 @@
package com.deepdata.common.domain;
/**
* DDD值对象基类
*/
public abstract class ValueObject {
@Override
public abstract boolean equals(Object obj);
@Override
public abstract int hashCode();
@Override
public abstract String toString();
}
@@ -0,0 +1,15 @@
package com.deepdata.common.domain.enums;
/**
* 边类型:DATA_COLLECTION/DATA_CLEANING/DATA_LABELING/DATA_SYNTHESIS/DATA_RATIO
*
* @since 2026/1/23
*/
public enum EdgeType {
DATA_COLLECTION,
DATA_CLEANING,
DATA_LABELING,
DATA_SYNTHESIS,
DATA_RATIO,
KNOWLEDGE_BASE,
}
@@ -0,0 +1,13 @@
package com.deepdata.common.domain.enums;
/**
* 节点类型:DATASOURCE/DATASET/KNOWLEDGE_BASE/MODEL等
*
* @since 2026/1/23
*/
public enum NodeType {
DATASOURCE,
DATASET,
KNOWLEDGE_BASE,
MODEL
}
@@ -0,0 +1,49 @@
package com.deepdata.common.domain.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 文件切片上传请求实体(与数据库表 t_chunk_upload_request 对齐)
*/
@Getter
@Setter
@Builder
public class ChunkUploadPreRequest {
private String id; // UUID
private Integer totalFileNum; // 总文件数
private Integer uploadedFileNum; // 已上传文件数
private String uploadPath; // 文件路径
private LocalDateTime timeout; // 上传请求超时时间
private String serviceId; // 上传请求所属服务:DATA-MANAGEMENT(数据管理)
private String checkInfo; // 业务信息
/**
* 增加已上传文件数
*/
public void incrementUploadedFileNum() {
if (this.uploadedFileNum == null) {
this.uploadedFileNum = 1;
return;
}
this.uploadedFileNum++;
}
/**
* 检查是否已完成上传
*/
public boolean isUploadComplete() {
return this.uploadedFileNum != null && this.uploadedFileNum.equals(this.totalFileNum);
}
/**
* 检查是否已超时
*/
public boolean isRequestTimeout() {
return this.timeout != null && LocalDateTime.now().isAfter(this.timeout);
}
}
@@ -0,0 +1,36 @@
package com.deepdata.common.domain.model;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件切片上传请求实体(与数据库表 t_chunk_upload_request 对齐)
*/
@Getter
@Setter
public class ChunkUploadRequest {
/** 预上传返回的id,用来确认同一个任务 */
private String reqId;
/** 文件编号,用于标识批量上传中的第几个文件 */
private int fileNo;
/** 文件名称 */
private String fileName;
/** 文件总分块数量 */
private int totalChunkNum;
/** 当前分块编号,从1开始 */
private int chunkNo;
/** 上传的文件分块内容 */
private MultipartFile file;
/** 切片大小 */
private Long fileSize;
/** 文件分块的校验和(十六进制字符串),用于验证文件完整性 */
private String checkSumHex;
}
@@ -0,0 +1,24 @@
package com.deepdata.common.domain.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.io.File;
@Getter
@Setter
@Builder
public class FileUploadResult {
/** 切片是否已经全部上传 */
boolean isAllFilesUploaded;
/** 业务上传信息 */
String checkInfo;
/** 保存的文件 */
File savedFile;
/** 真实文件名 */
String fileName;
}
@@ -0,0 +1,65 @@
package com.deepdata.common.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.deepdata.common.domain.enums.EdgeType;
import lombok.Getter;
import lombok.Setter;
/**
* 数据血缘:边表
* 边表示处理流程(归集任务、数据清洗、数据标注、数据合成、数据配比等)
*
* @since 2026/1/23
*/
@Getter
@Setter
@TableName("t_lineage_edge")
public class LineageEdge {
/**
* 边ID
*/
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**
* 图ID
*/
private String graphId;
/**
* 处理流程ID
*/
private String processId;
/**
* 边类型:DATA_COLLECTION/DATA_CLEANING/DATA_LABELING/DATA_SYNTHESIS/DATA_RATIO等
*/
private EdgeType edgeType;
/**
* 边名称
*/
private String name;
/**
* 边描述
*/
private String description;
/**
* 边扩展信息(JSON)
*/
private String edgeMetadata;
/**
* 源节点ID
*/
private String fromNodeId;
/**
* 目标节点ID
*/
private String toNodeId;
}
@@ -0,0 +1,50 @@
package com.deepdata.common.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.deepdata.common.domain.enums.NodeType;
import lombok.Getter;
import lombok.Setter;
/**
* 数据血缘:节点表
* 节点表示实体对象(归集来源、数据集、知识库、模型等)
*
* @since 2026/1/23
*/
@Getter
@Setter
@TableName("t_lineage_node")
public class LineageNode {
/**
* 节点ID
*/
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**
* 图ID
*/
private String graphId;
/**
* 节点类型:DATASOURCE/DATASET/KNOWLEDGE_BASE/MODEL等
*/
private NodeType nodeType;
/**
* 节点名称
*/
private String name;
/**
* 节点描述
*/
private String description;
/**
* 节点扩展信息(JSON)
*/
private String nodeMetadata;
}
@@ -0,0 +1,61 @@
package com.deepdata.common.domain.model.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 实体基类
*
* @param <ID> 实体ID类型
*/
@Getter
@Setter
@NoArgsConstructor
public abstract class BaseEntity<ID> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实体ID
*/
@TableId(type = IdType.ASSIGN_ID)
protected ID id;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
protected LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
protected LocalDateTime updatedAt;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
protected String createdBy;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
protected String updatedBy;
public BaseEntity(ID id) {
super();
this.id = id;
}
}
@@ -0,0 +1,93 @@
package com.deepdata.common.domain.service;
import com.deepdata.common.domain.model.ChunkUploadPreRequest;
import com.deepdata.common.domain.model.ChunkUploadRequest;
import com.deepdata.common.domain.model.FileUploadResult;
import com.deepdata.common.domain.utils.ChunksSaver;
import com.deepdata.common.infrastructure.exception.BusinessException;
import com.deepdata.common.infrastructure.exception.CommonErrorCode;
import com.deepdata.common.infrastructure.mapper.ChunkUploadRequestMapper;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* 文件服务
*/
@Component
public class FileService {
private static final int DEFAULT_TIMEOUT = 120;
private final ChunkUploadRequestMapper chunkUploadRequestMapper;
public FileService(ChunkUploadRequestMapper chunkUploadRequestMapper) {
this.chunkUploadRequestMapper = chunkUploadRequestMapper;
}
/**
* 预上传
*/
@Transactional
public String preUpload(ChunkUploadPreRequest chunkUploadPreRequest) {
chunkUploadPreRequest.setId(UUID.randomUUID().toString());
chunkUploadPreRequest.setTimeout(LocalDateTime.now().plusSeconds(DEFAULT_TIMEOUT));
chunkUploadRequestMapper.insert(chunkUploadPreRequest);
return chunkUploadPreRequest.getId();
}
/**
* 切片上传
*/
@Transactional
public FileUploadResult chunkUpload(ChunkUploadRequest uploadFileRequest) {
uploadFileRequest.setFileSize(uploadFileRequest.getFile().getSize());
ChunkUploadPreRequest preRequest = chunkUploadRequestMapper.findById(uploadFileRequest.getReqId());
if (preRequest == null || preRequest.isUploadComplete() || preRequest.isRequestTimeout()) {
throw BusinessException.of(CommonErrorCode.PRE_UPLOAD_REQUEST_NOT_EXIST);
}
File savedFile;
if (uploadFileRequest.getTotalChunkNum() > 1) {
savedFile = uploadChunk(uploadFileRequest, preRequest);
} else {
savedFile = uploadFile(uploadFileRequest, preRequest);
}
if (chunkUploadRequestMapper.update(preRequest) == 0) {
throw BusinessException.of(CommonErrorCode.PRE_UPLOAD_REQUEST_NOT_EXIST);
}
boolean isFinish = Objects.equals(preRequest.getUploadedFileNum(), preRequest.getTotalFileNum());
if (isFinish) {
// 删除存分片的临时路径
ChunksSaver.deleteFolder(new File(preRequest.getUploadPath(),
String.format(ChunksSaver.TEMP_DIR_NAME_FORMAT, preRequest.getId())).getPath());
chunkUploadRequestMapper.deleteById(preRequest.getId());
}
return FileUploadResult.builder()
.isAllFilesUploaded(isFinish)
.checkInfo(preRequest.getCheckInfo())
.savedFile(savedFile)
.fileName(uploadFileRequest.getFileName())
.build();
}
private File uploadFile(ChunkUploadRequest fileUploadRequest, ChunkUploadPreRequest preRequest) {
File savedFile = ChunksSaver.saveFile(fileUploadRequest, preRequest);
preRequest.setTimeout(LocalDateTime.now().plusSeconds(DEFAULT_TIMEOUT));
preRequest.incrementUploadedFileNum();
return savedFile;
}
private File uploadChunk(ChunkUploadRequest fileUploadRequest, ChunkUploadPreRequest preRequest) {
Optional<File> savedFile = ChunksSaver.save(fileUploadRequest, preRequest);
if (savedFile.isPresent()) {
preRequest.incrementUploadedFileNum();
return savedFile.get();
}
preRequest.setTimeout(LocalDateTime.now().plusSeconds(DEFAULT_TIMEOUT));
return null;
}
}
@@ -0,0 +1,168 @@
package com.deepdata.common.domain.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.deepdata.common.domain.model.LineageEdge;
import com.deepdata.common.domain.model.LineageNode;
import com.deepdata.common.infrastructure.mapper.LineageEdgeMapper;
import com.deepdata.common.infrastructure.mapper.LineageNodeMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* 血缘服务层
*
* @since 2026/1/23
*/
@Component
@RequiredArgsConstructor
public class LineageService {
private final LineageEdgeMapper lineageEdgeMapper;
private final LineageNodeMapper lineageNodeMapper;
/**
* 生成血缘图
*
* @param fromNode 源节点
* @param edge 边
* @param toNode 目的节点
*/
public void generateGraph(LineageNode fromNode, LineageEdge edge, LineageNode toNode) {
// 如果 fromNode 为空,直接返回不做任何处理
if (fromNode == null) {
return;
}
// 如果 fromNode 有值,检查 fromNode 是否存在
LineageNode existingFrom = lineageNodeMapper.selectById(fromNode.getId());
String fromGraphId;
if (existingFrom == null) {
// fromNode 不存在,创建 fromNode,记录 fromGraphId
fromGraphId = UUID.randomUUID().toString();
fromNode.setGraphId(fromGraphId);
lineageNodeMapper.insert(fromNode);
// 处理 edge 和 toNode
} else {
// fromNode 存在,记录 fromGraphId
fromGraphId = existingFrom.getGraphId();
}
// 处理 edge 和 toNode
generateGraph(edge, toNode, fromGraphId);
}
private void generateGraph(LineageEdge edge, LineageNode toNode, String graphId) {
// 无 edge,直接返回
if (edge == null) {
return;
}
// 有 edge,处理 edge,检查是否有 toNode
handleLineageEdge(graphId, edge);
if (toNode == null) {
// 无 toNode,返回
return;
}
// 有 toNode,检查 toNode 是否存在
LineageNode existingTo = lineageNodeMapper.selectById(toNode.getId());
if (existingTo == null) {
// toNode 不存在,创建 toNode 后结束
toNode.setGraphId(graphId);
lineageNodeMapper.insert(toNode);
} else {
// toNode 存在,将 fromNode 所在的图 并入 toNode 所在的图后结束
mergeGraph(graphId, existingTo.getGraphId());
}
}
private void handleLineageEdge(String graphId, LineageEdge edge) {
LambdaQueryWrapper<LineageEdge> wrapper = new LambdaQueryWrapper<LineageEdge>()
.eq(LineageEdge::getGraphId, graphId)
.eq(LineageEdge::getFromNodeId, edge.getFromNodeId())
.eq(LineageEdge::getToNodeId, edge.getToNodeId());
List<LineageEdge> lineageEdges = lineageEdgeMapper.selectList(wrapper);
if (CollectionUtils.isEmpty(lineageEdges)) {
edge.setId(UUID.randomUUID().toString());
edge.setGraphId(graphId);
lineageEdgeMapper.insert(edge);
} else {
edge.setId(lineageEdges.getFirst().getId());
lineageEdgeMapper.updateById(edge);
}
}
private void mergeGraph(String fromGraphId, String toGraphId) {
if (fromGraphId == null || toGraphId == null || fromGraphId.equals(toGraphId)) {
return;
}
List<LineageNode> fromNodes = getNodesByGraphId(fromGraphId);
List<LineageNode> toNodes = getNodesByGraphId(toGraphId);
// choose smaller graph as source, larger as target
String sourceGraphId = fromNodes.size() <= toNodes.size() ? fromGraphId : toGraphId;
String targetGraphId = sourceGraphId.equals(fromGraphId) ? toGraphId : fromGraphId;
List<LineageNode> sourceNodes = sourceGraphId.equals(fromGraphId) ? fromNodes : toNodes;
// update nodes' graphId
for (LineageNode node : sourceNodes) {
if (node == null) continue;
node.setGraphId(targetGraphId);
lineageNodeMapper.updateById(node);
}
// update edges' graphId (edges belonging to the source graph)
List<LineageEdge> edges = getEdgesByGraphId(sourceGraphId);
if (edges == null) {
edges = Collections.emptyList();
}
for (LineageEdge edge : edges) {
if (edge == null) continue;
edge.setGraphId(targetGraphId);
lineageEdgeMapper.updateById(edge);
}
}
/**
* 从图ID获取图的节点列表
*
* @param graphId 图ID
* @return 图的节点列表
*/
public List<LineageNode> getNodesByGraphId(String graphId) {
LambdaQueryWrapper<LineageNode> wrapper = new LambdaQueryWrapper<LineageNode>()
.eq(LineageNode::getGraphId, graphId);
return lineageNodeMapper.selectList(wrapper);
}
/**
* 从图ID获取图的边列表
*
* @param graphId 图ID
* @return 图的边列表
*/
public List<LineageEdge> getEdgesByGraphId(String graphId) {
LambdaQueryWrapper<LineageEdge> wrapper = new LambdaQueryWrapper<LineageEdge>()
.eq(LineageEdge::getGraphId, graphId);
return lineageEdgeMapper.selectList(wrapper);
}
/**
* 从节点ID获取节点
*
* @param nodeId 节点ID
* @return 对应节点
*/
public LineageNode getNodeById(String nodeId) {
return lineageNodeMapper.selectById(nodeId);
}
}
@@ -0,0 +1,51 @@
package com.deepdata.common.domain.utils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* 解析工具类
*/
public class AnalyzerUtils {
/** zip压缩包文件后缀类型 */
public static final String TYPE_ZIP = "zip";
/** tar压缩包文件后缀类型 */
public static final String TYPE_TAR_GZ = "tar.gz";
private static final List<String> SPECIAL_EXTENSIONS = Collections.singletonList(TYPE_TAR_GZ);
/**
* 从文件路径获取文件后缀类型
*
* @param filePath 文件类型
* @return 文件后缀类型
*/
public static String getExtension(final String filePath) {
String filename = CommonUtils.trimFilePath(filePath);
for (String ext : SPECIAL_EXTENSIONS) {
if (StringUtils.endsWithIgnoreCase(filename, "." + ext)) {
return ext;
}
}
int firstDotIndex = filename.lastIndexOf(".");
if (firstDotIndex == -1) {
return "";
}
return filename.substring(firstDotIndex + 1).toLowerCase(Locale.ROOT);
}
/**
* 判断是否为压缩包
*
* @param filePath 文件路径
* @return 返回信息
*/
public static boolean isPackage(String filePath) {
String extension = getExtension(filePath);
return extension.toLowerCase(Locale.ROOT).equals(TYPE_ZIP) || extension.toLowerCase(Locale.ROOT).equals(TYPE_TAR_GZ);
}
}
@@ -0,0 +1,209 @@
package com.deepdata.common.domain.utils;
import com.deepdata.common.domain.model.FileUploadResult;
import com.deepdata.common.infrastructure.exception.BusinessException;
import com.deepdata.common.infrastructure.exception.SystemErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FileUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
/**
* Responsible for validating and unpacking archive files.
*
* @since 2023-11-17
*/
@Slf4j
public class ArchiveAnalyzer {
private static final int DEFAULT_BUFFER_SIZE = 4096;
/**
* Process list.
*
* @param fileDto The uploaded file DTO
* @return the list
*/
public static List<FileUploadResult> process(FileUploadResult fileDto) {
log.info("Start unpacking [{}]", fileDto.getFileName());
File file = fileDto.getSavedFile();
Path archivePath;
try {
archivePath = Paths.get(file.getCanonicalPath());
} catch (IOException e) {
log.error("Failed to get the archive file path.");
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR, "Failed to get the archive file path.");
}
List<FileUploadResult> unpacked;
String extension = AnalyzerUtils.getExtension(fileDto.getFileName());
if (AnalyzerUtils.TYPE_ZIP.equalsIgnoreCase(extension)) {
log.info("ZIP unpacking [{}]", fileDto.getFileName());
unpacked = processZip(archivePath);
log.info("ZIP unpacking FINISHED [{}]", fileDto.getFileName());
} else if (AnalyzerUtils.TYPE_TAR_GZ.equalsIgnoreCase(extension)) {
unpacked = processTarGz(archivePath);
} else {
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR, "Unrecognized archive format.");
}
if (!archivePath.toFile().delete()) {
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR, "Unable to delete the archive file " + archivePath.toAbsolutePath());
}
log.info("Finished unpacking [{}]", fileDto.getFileName());
return unpacked;
}
private static List<FileUploadResult> processZip(Path archivePath) {
try (ArchiveInputStream<ZipArchiveEntry> inputStream = new ZipArchiveInputStream(
new BufferedInputStream(Files.newInputStream(archivePath)))) {
return unpackArchive(inputStream, archivePath);
} catch (IOException e) {
log.error("Failed to unpack zip archive:", e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR, "Failed to unpack zip archive.");
}
}
private static List<FileUploadResult> processTarGz(Path archivePath) {
try (ArchiveInputStream<TarArchiveEntry> inputStream = new TarArchiveInputStream(
new GzipCompressorInputStream(new BufferedInputStream(Files.newInputStream(archivePath))),
StandardCharsets.UTF_8.toString())) {
return unpackArchive(inputStream, archivePath);
} catch (IOException e) {
log.error("Failed to unpack tar.gz archive:", e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR, "Failed to unpack tar.gz archive.");
}
}
private static List<FileUploadResult> unpackArchive(ArchiveInputStream<?> archiveInputStream, Path archivePath) throws IOException {
List<FileUploadResult> unpacked = new ArrayList<>();
long unpackedSize = 0L;
try {
ArchiveEntry archiveEntry;
int entryCount = 0;
while ((archiveEntry = archiveInputStream.getNextEntry()) != null) {
if (isSymlink(archiveEntry)) {
// 解压时跳过symlink文件
continue;
}
entryCount++;
if (checkUnpackSizeAndFileSize(entryCount, unpacked) || checkVersionSize(unpackedSize, archiveEntry.getSize())) {
break;
}
if (!archiveEntry.isDirectory()) {
unpackedSize = addFileAndCountFileSize(archiveInputStream, archiveEntry, unpacked,
unpackedSize, archivePath);
}
}
} catch (IOException e) {
unpacked.forEach(v -> deleteFile(v.getSavedFile()));
throw e;
}
return unpacked;
}
private static boolean checkVersionSize(long unpackedSize, long currFileSize) {
return false;
}
private static long addFileAndCountFileSize(ArchiveInputStream<?> archiveInputStream, ArchiveEntry archiveEntry,
List<FileUploadResult> unpacked, long unpackedSize, Path archivePath) throws IOException {
Optional<FileUploadResult> uploadFileDto = extractEntity(archiveInputStream, archiveEntry, archivePath);
long newSize = unpackedSize;
if (uploadFileDto.isPresent()) {
FileUploadResult dto = uploadFileDto.get();
unpacked.add(dto);
newSize += dto.getSavedFile().length();
}
return newSize;
}
private static boolean checkUnpackSizeAndFileSize(int entryCount, List<FileUploadResult> unpacked) {
return false;
}
private static Optional<FileUploadResult> extractEntity(ArchiveInputStream<?> archiveInputStream, ArchiveEntry archiveEntry, Path archivePath)
throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
// 防止 Zip Slip 攻击:验证归档条目名称
String entryName = archiveEntry.getName();
if (entryName.contains("..")) {
log.warn("Path traversal attempt detected in archive entry: {}", entryName);
return Optional.empty();
}
Path parentDir = archivePath.getParent();
Path path = parentDir.resolve(entryName).normalize();
// 确保解析后的路径仍然位于父目录内
if (!path.startsWith(parentDir)) {
log.warn("Zip Slip attempt detected: entry {} resolves outside parent directory", entryName);
return Optional.empty();
}
File file = path.toFile();
long fileSize = 0L;
FileUtils.createParentDirectories(file);
long supportFileSize = 1024*1024*1024; // 上传大小暂定为1个G
try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(file.toPath()))) {
int byteRead;
while ((byteRead = archiveInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, byteRead);
fileSize += byteRead;
if (fileSize > supportFileSize) {
break;
}
}
} catch (IOException e) {
log.error("error happened while write entry to file system");
file.delete();
throw e;
}
if (fileSize > supportFileSize) {
// 文件大小超过限制,删除
log.info("file {} size exceeds limit", archiveEntry.getName());
file.delete();
return Optional.empty();
}
return Optional.of(FileUploadResult.builder().savedFile(file).fileName(CommonUtils.trimFilePath(archiveEntry.getName())).build());
}
private static void deleteFile(File file) {
Path fileToDeletePath = Paths.get(file.getPath());
if (Files.exists(fileToDeletePath)) {
try {
Files.delete(fileToDeletePath);
} catch (IOException e1) {
log.error("Failed to delete file.", e1);
}
}
}
private static boolean isSymlink(ArchiveEntry archiveEntry) {
if (archiveEntry instanceof TarArchiveEntry) {
return ((TarArchiveEntry) archiveEntry).isSymbolicLink();
}
return false;
}
}
@@ -0,0 +1,170 @@
package com.deepdata.common.domain.utils;
import com.deepdata.common.domain.model.ChunkUploadPreRequest;
import com.deepdata.common.domain.model.ChunkUploadRequest;
import com.deepdata.common.infrastructure.exception.BusinessException;
import com.deepdata.common.infrastructure.exception.SystemErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
@Slf4j
public class ChunksSaver {
/**
* 分片保存的临时目录
*/
public static final String TEMP_DIR_NAME_FORMAT = "req_%s_chunks";
/**
* 保存分片
*
* @param fileUploadRequest 上传分片的请求
* @param preUploadReq 上传文件的请求
* @return 保存后完整的文件
*/
public static Optional<File> save(ChunkUploadRequest fileUploadRequest, ChunkUploadPreRequest preUploadReq) {
Path uploadPath = Paths.get(preUploadReq.getUploadPath(),
String.format(TEMP_DIR_NAME_FORMAT, preUploadReq.getId()));
LocalDateTime startTime = LocalDateTime.now();
// 临时文件名为文件序号
File targetFile = new File(uploadPath.toString(), String.valueOf(fileUploadRequest.getFileNo()));
// 追加分片到目标文件末尾
appendToTargetFile(targetFile, getFileInputStream(fileUploadRequest.getFile()));
// 判断是否分片已经全部上传,全部上传后将重组文件移动到指定路径,否则返回空
if (fileUploadRequest.getTotalChunkNum() != fileUploadRequest.getChunkNo()) {
log.debug("save chunk {} cost {}", fileUploadRequest.getChunkNo(),
ChronoUnit.MILLIS.between(startTime, LocalDateTime.now()));
return Optional.empty();
}
File finalFile = new File(preUploadReq.getUploadPath(), fileUploadRequest.getFileName());
// 防止路径穿越:规范化后校验仍在 uploadPath 下
Path uploadBasePath = Paths.get(preUploadReq.getUploadPath()).normalize().toAbsolutePath();
Path finalFilePath = finalFile.toPath().normalize().toAbsolutePath();
if (!finalFilePath.startsWith(uploadBasePath)) {
log.error("Path traversal attempt detected in chunk save: fileName={}, finalPath={}",
fileUploadRequest.getFileName(), finalFilePath);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
// 确保父目录存在(处理嵌套文件夹上传的情况)
File parentDir = finalFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
try {
boolean created = parentDir.mkdirs();
if (!created && !parentDir.exists()) {
// mkdirs 返回 false 且目录仍不存在,才是真正的失败
log.error("failed to create parent directory for file:{}, req Id:{}", finalFile.getPath(), fileUploadRequest.getReqId());
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
} catch (Exception e) {
log.error("failed to create parent directory for file:{}, req Id:{}, error:{}", finalFile.getPath(), fileUploadRequest.getReqId(), e.getMessage(), e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
}
if (!targetFile.renameTo(finalFile)) {
log.error("failed to mv file:{}, req Id:{}", targetFile.getName(), fileUploadRequest.getReqId());
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
log.debug("save chunk {} cost {}", fileUploadRequest.getChunkNo(),
ChronoUnit.MILLIS.between(startTime, LocalDateTime.now()));
return Optional.of(finalFile);
}
private static InputStream getFileInputStream(MultipartFile file) {
try {
return file.getInputStream();
} catch (IOException e) {
log.error("get uploaded file input stream failed", e);
throw new IllegalArgumentException();
}
}
/**
* 保存文件
*
* @param fileUploadRequest 上传分片的请求
* @param preUploadReq 上传文件的请求
*/
public static File saveFile(ChunkUploadRequest fileUploadRequest, ChunkUploadPreRequest preUploadReq) {
// 保存文件
File targetFile = new File(preUploadReq.getUploadPath(), fileUploadRequest.getFileName());
// 防止路径穿越:规范化后校验仍在 uploadPath 下
Path uploadBasePath = Paths.get(preUploadReq.getUploadPath()).normalize().toAbsolutePath();
Path targetFilePath = targetFile.toPath().normalize().toAbsolutePath();
if (!targetFilePath.startsWith(uploadBasePath)) {
log.error("Path traversal attempt detected in saveFile: fileName={}, targetPath={}",
fileUploadRequest.getFileName(), targetFilePath);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
// 确保父目录存在(处理嵌套文件夹上传的情况)
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
try {
boolean created = parentDir.mkdirs();
if (!created && !parentDir.exists()) {
// mkdirs 返回 false 且目录仍不存在,才是真正的失败
log.error("failed to create parent directory for file:{}", targetFile.getPath());
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
} catch (Exception e) {
log.error("failed to create parent directory for file:{}, error:{}", targetFile.getPath(), e.getMessage(), e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
}
try {
log.info("file path {}, file size {}", targetFile.toPath(), targetFile.getTotalSpace());
FileUtils.copyInputStreamToFile(getFileInputStream(fileUploadRequest.getFile()), targetFile);
} catch (IOException e) {
throw new IllegalArgumentException();
}
return targetFile;
}
/**
* 追加分片到文件末尾
*
* @param targetFile 目标文件
* @param inputStream file stream
*/
public static void appendToTargetFile(File targetFile, InputStream inputStream) {
try {
byte[] buffer = new byte[1024 * 1024];
int byteRead;
while ((byteRead = inputStream.read(buffer)) != -1) {
FileUtils.writeByteArrayToFile(targetFile, buffer, 0, byteRead, true);
}
} catch (IOException e) {
throw new IllegalArgumentException();
}
}
/**
* 删除指定路径下的所有文件
*
* @param uploadPath 文件路径
*/
public static void deleteFolder(String uploadPath) {
File folder = new File(uploadPath);
if (!folder.exists()) {
log.info("folder {} does not exist", uploadPath);
return;
}
try {
FileUtils.deleteDirectory(folder);
} catch (IOException e) {
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
}
}
@@ -0,0 +1,41 @@
package com.deepdata.common.domain.utils;
import java.io.File;
import java.util.UUID;
/**
* 通用工具类
*/
public class CommonUtils {
/**
* 从文件路径中获取文件名(带后缀)
*
* @param filePath 文件路径
* @return 文件名(带后缀)
*/
public static String trimFilePath(String filePath) {
int lastSlashIndex = filePath.lastIndexOf(File.separator);
String filename = filePath;
if (lastSlashIndex != -1) {
filename = filePath.substring(lastSlashIndex + 1);
}
return filename;
}
/**
* 判断字符串是否是uuid
*
* @param str 要判断的字符串
* @return 判断结果
*/
public static boolean isUUID(String str) {
if (str == null) return false;
try {
UUID.fromString(str);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
@@ -0,0 +1,15 @@
package com.deepdata.common.infrastructure.common;
import java.lang.annotation.*;
/**
* 忽略响应包装注解
* <p>
* 在使用全局响应包装时,如果某个接口或类不需要进行响应包装,可以使用此注解进行标记
* </p>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreResponseWrap {
}
@@ -0,0 +1,63 @@
package com.deepdata.common.infrastructure.common;
import com.deepdata.common.infrastructure.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serial;
import java.io.Serializable;
/**
* 通用返回体
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private String code;
/**
* 消息
*/
private String message;
/**
* 数据
*/
private T data;
/**
* 构造成功时的返回体
*
* @param data 返回数据
* @param <T> 返回数据类型
* @return 返回体内容
*/
public static <T> Response<T> ok(T data) {
return new Response<>("0", "success", data);
}
/**
* 构造错误时的返回体
*
* @param errorCode 错误码
* @param data 返回数据
* @param <T> 返回数据类型
* @return 返回体内容
*/
public static <T> Response<T> error(ErrorCode errorCode, T data) {
return new Response<>(errorCode.getCode(), errorCode.getMessage(), data);
}
public static <T> Response<T> error(ErrorCode errorCode) {
return new Response<>(errorCode.getCode(), errorCode.getMessage(), null);
}
}
@@ -0,0 +1,107 @@
package com.deepdata.common.infrastructure.config;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.deepdata.common.domain.model.base.BaseEntity;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.schema.Column;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
/**
* 数据隔离处理
*
* @since 2026/1/19
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class DataScopeHandle implements DataPermissionHandler, Interceptor {
private static final ThreadLocal<String> userInfoHolder = new ThreadLocal<>();
private static final StringValue SYSTEM_USER = new StringValue("system");
private static final String FILTER_COLUMN_NAME = "created_by";
private static final String C_DOTS = ".";
public static void setUserInfo(String user) {
userInfoHolder.set(user);
}
public static void removeUserInfo() {
userInfoHolder.remove();
}
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
if (StringUtils.isBlank(userInfoHolder.get())) {
return where;
}
try {
String className = mappedStatementId.substring(0, mappedStatementId.lastIndexOf(C_DOTS));
Class<?> clazz = Class.forName(className);
IgnoreDataScopeAnnotation annotation = clazz.getAnnotation(IgnoreDataScopeAnnotation.class);
if (annotation != null) {
return where;
}
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(C_DOTS) + 1);
for (Method method : clazz.getMethods()) {
if (method.getName().equals(methodName)) {
annotation = method.getAnnotation(IgnoreDataScopeAnnotation.class);
if (annotation != null) {
return where;
}
break;
}
}
ParenthesedExpressionList<StringValue> valueList =
new ParenthesedExpressionList<>(Arrays.asList(new StringValue(userInfoHolder.get()), SYSTEM_USER));
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(new Column(FILTER_COLUMN_NAME));
inExpression.setRightExpression(valueList);
return ObjectUtils.isNotEmpty(where) ? new AndExpression(where, inExpression) : inExpression;
} catch (Exception e) {
log.warn(e.getMessage());
}
return where;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
if (parameter instanceof BaseEntity baseEntity) {
// 根据SQL命令类型设置审计字段
log.info("current user {}, ms {}", userInfoHolder.get(), ms);
if (Objects.requireNonNull(ms.getSqlCommandType()) != SqlCommandType.UPDATE) {
baseEntity.setCreatedAt(LocalDateTime.now());
baseEntity.setCreatedBy(userInfoHolder.get());
}
baseEntity.setUpdatedAt(LocalDateTime.now());
baseEntity.setUpdatedBy(userInfoHolder.get());
}
return invocation.proceed();
}
}
@@ -0,0 +1,60 @@
package com.deepdata.common.infrastructure.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* 持久化实体元数据对象处理器
*
* @author dallas
* @since 2025-10-17
*/
@Slf4j
@Configuration
public class EntityMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("Starting insert fill...");
// 创建时间填充
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
// 更新时间填充
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
// 创建人填充(需要从安全上下文获取当前用户)
String currentUser = getCurrentUser();
this.strictInsertFill(metaObject, "createdBy", String.class, currentUser);
// 更新人填充
this.strictInsertFill(metaObject, "updatedBy", String.class, currentUser);
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("Starting update fill...");
// 更新时间填充
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
// 更新人填充
this.strictUpdateFill(metaObject, "updatedBy", String.class, getCurrentUser());
}
/**
* 获取当前用户(需要根据你的安全框架实现)
*/
private String getCurrentUser() {
// todo 这里需要根据你的安全框架实现,例如Spring Security、Shiro等
// 示例:返回默认用户或从SecurityContext获取
try {
// 如果是Spring Security
// return SecurityContextHolder.getContext().getAuthentication().getName();
// 临时返回默认值,请根据实际情况修改
return "system";
} catch (Exception e) {
log.error("Error getting current user", e);
return "unknown";
}
}
}
@@ -0,0 +1,54 @@
package com.deepdata.common.infrastructure.config;
import com.deepdata.common.infrastructure.common.Response;
import com.deepdata.common.infrastructure.exception.BusinessException;
import com.deepdata.common.infrastructure.exception.SystemErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
*
*
* @author dallas
* @since 2025-10-17
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Response<?>> handleBusinessException(BusinessException e) {
log.warn("BusinessException: code={}, message={}", e.getCode(), e.getMessage(), e);
return ResponseEntity.internalServerError().body(Response.error(e.getErrorCodeEnum()));
}
/**
* 处理参数校验和数据绑定异常
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public ResponseEntity<Response<?>> handleMethodArgumentNotValidException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("Parameter validation failed: {}", message);
return ResponseEntity.badRequest().body(Response.error(SystemErrorCode.INVALID_PARAMETER, message));
}
/**
* 处理系统兜底异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Response<?>> handleException(Exception e) {
log.error("SystemException: ", e);
return ResponseEntity.internalServerError().body(Response.error(SystemErrorCode.SYSTEM_BUSY));
}
}
@@ -0,0 +1,77 @@
package com.deepdata.common.infrastructure.config;
import com.deepdata.common.infrastructure.common.IgnoreResponseWrap;
import com.deepdata.common.infrastructure.common.Response;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 全局响应处理器
*
* @author dallas
* @since 2025-10-17
*/
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice(basePackages = "com.deepdata")
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper;
/**
* 判断哪些返回值需要被包装。
* 返回true表示执行beforeBodyWrite方法。
*/
@Override
public boolean supports(MethodParameter returnType, @Nullable Class converterType) {
// 1. 如果返回类型已经是Response,直接跳过包装
if (returnType.getParameterType().isAssignableFrom(Response.class)) {
return false;
}
// 2. 检查方法或所在类上是否有@IgnoreResponseWrap的注解,如果有,返回false
if (returnType.hasMethodAnnotation(IgnoreResponseWrap.class) ||
returnType.getContainingClass().isAnnotationPresent(IgnoreResponseWrap.class)) {
return false;
}
// 3. 默认情况下,对其他返回类型进行包装
return true;
}
/**
* 对响应体进行实际包装处理。
*/
@Override
public Object beforeBodyWrite(Object body,
@Nullable MethodParameter returnType,
@Nullable MediaType selectedContentType,
@Nullable Class selectedConverterType,
@Nullable ServerHttpRequest request,
@Nullable ServerHttpResponse response) {
// 如果返回体本身就是Response类型(通常来自异常处理),直接返回
if (body instanceof Response) {
return body;
}
// 如果返回值是String类型,需要特殊处理
if (body instanceof String) {
// 手动将Response对象序列化为JSON字符串返回
try {
return objectMapper.writeValueAsString(Response.ok(body));
} catch (JsonProcessingException e) {
// 记录日志或抛出运行时异常
log.error("Error serializing response", e);
throw new RuntimeException("Error converting response to JSON", e);
}
}
// 对于正常的返回结果,统一包装成成功的Response
return Response.ok(body);
}
}
@@ -0,0 +1,15 @@
package com.deepdata.common.infrastructure.config;
import java.lang.annotation.*;
/**
* 忽略数据隔离注解
* 该注解添加到对应的mapper上,添加后该mapper的方法查询时就不需要进行用户过滤了
*
* @since 2026/1/20
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreDataScopeAnnotation {
}
@@ -0,0 +1,49 @@
package com.deepdata.common.infrastructure.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Mybatis Plus 配置类
*
* @author dallas
* @since 2025-10-20
*/
@Configuration
public class MybatisPlusConfig {
/**
* 配置 JacksonTypeHandler 以支持 Java 8 日期时间类型
*/
@Bean
@Primary
public JacksonTypeHandler jacksonTypeHandler() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
JacksonTypeHandler handler = new JacksonTypeHandler(Object.class);
JacksonTypeHandler.setObjectMapper(objectMapper);
return handler;
}
/**
* 配置 Mybatis Plus 分页插件
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(DataScopeHandle dataScopeHandle) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataPermissionInterceptor(dataScopeHandle));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return interceptor;
}
}
@@ -0,0 +1,40 @@
package com.deepdata.common.infrastructure.config;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.postgresql.util.PGobject;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 指定处理的 Java 类型和 JDBC 类型
@MappedTypes({Object.class})
@MappedJdbcTypes(JdbcType.OTHER)
public class PgJsonTypeHandler extends JacksonTypeHandler {
public PgJsonTypeHandler(Class<?> type) {
super(type);
}
public PgJsonTypeHandler(Class<?> type, Field field) {
super(type, field);
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
// 1. 先借助父类转成 JSON 字符串
String json = this.toJson(parameter);
// 2. 核心:封装成 PGobject,并指定类型为 'json' 或 'jsonb'
PGobject jsonObject = new PGobject();
// 如果你的数据库字段是 jsonb,这里一定要写 "jsonb";如果是 json,写 "json"
jsonObject.setType("json");
jsonObject.setValue(json);
// 3. 传给驱动
ps.setObject(i, jsonObject);
}
}
@@ -0,0 +1,35 @@
package com.deepdata.common.infrastructure.exception;
import java.util.Collection;
/**
* 断言工具类,用于在业务逻辑中快速检查条件是否满足,不满足时抛出业务异常
*
* @author dallas
* @since 2025-10-17
*/
public class BusinessAssert {
public static void isTrue(boolean condition, ErrorCode errorCode) {
if (!condition) {
throw BusinessException.of(errorCode);
}
}
public static void notNull(Object obj, ErrorCode errorCode) {
if (obj == null) {
throw BusinessException.of(errorCode);
}
}
public static void notEmpty(Collection<?> collection, ErrorCode errorCode) {
if (collection == null || collection.isEmpty()) {
throw BusinessException.of(errorCode);
}
}
public static void isTrue(boolean condition, ErrorCode errorCode, String customMessage) {
if (!condition) {
throw BusinessException.of(errorCode, customMessage);
}
}
}
@@ -0,0 +1,73 @@
package com.deepdata.common.infrastructure.exception;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 业务异常基类
*
* @author dallas
* @since 2025-10-15
*/
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> details;
// 核心构造方法
private BusinessException(ErrorCode errorCode, String customMessage,
Map<String, Object> details, Throwable cause) {
super(customMessage != null ? customMessage : errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.details = details != null ? new HashMap<>(details) : new HashMap<>();
}
// 静态工厂方法
public static BusinessException of(ErrorCode errorCode) {
return new BusinessException(errorCode, null, null, null);
}
public static BusinessException of(ErrorCode errorCode, String customMessage) {
return new BusinessException(errorCode, customMessage, null, null);
}
public static BusinessException of(ErrorCode errorCode, Map<String, Object> details) {
return new BusinessException(errorCode, null, details, null);
}
public static BusinessException of(ErrorCode errorCode, String customMessage,
Map<String, Object> details) {
return new BusinessException(errorCode, customMessage, details, null);
}
// 快速创建方法 - 支持链式调用添加详情
public static BusinessException create(ErrorCode errorCode) {
return new BusinessException(errorCode, null, null, null);
}
public BusinessException withDetail(String key, Object value) {
this.details.put(key, value);
return this;
}
public BusinessException withCustomMessage(String customMessage) {
return new BusinessException(this.errorCode, customMessage, this.details, null);
}
// Getter方法
public String getCode() {
return errorCode.getCode();
}
public ErrorCode getErrorCodeEnum() {
return errorCode;
}
public Map<String, Object> getDetails() {
return Collections.unmodifiableMap(details);
}
public String getOriginalMessage() {
return errorCode.getMessage();
}
}
@@ -0,0 +1,20 @@
package com.deepdata.common.infrastructure.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* CommonErrorCode
*
* @since 2025/12/5
*/
@Getter
@AllArgsConstructor
public enum CommonErrorCode implements ErrorCode{
PARAM_ERROR("common.0001", "参数错误"),
PRE_UPLOAD_REQUEST_NOT_EXIST("common.0101", "预上传请求不存在"),
SIGNUP_ERROR("common.0400", "用户名或者邮箱已经存在"),
UNAUTHORIZED("common.0401", "认证失败");
private final String code;
private final String message;
}
@@ -0,0 +1,23 @@
package com.deepdata.common.infrastructure.exception;
/**
* 错误码接口
*
* @author dallas
* @since 2025-10-17
*/
public interface ErrorCode {
/**
* 获取错误码
*
* @return 错误码
*/
String getCode();
/**
* 获取错误信息
*
* @return 错误信息
*/
String getMessage();
}
@@ -0,0 +1,15 @@
package com.deepdata.common.infrastructure.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class ErrorCodeImpl implements ErrorCode {
private final String code;
private final String message;
public static ErrorCodeImpl of(String code, String message) {
return new ErrorCodeImpl(code, message);
}
}
@@ -0,0 +1,28 @@
package com.deepdata.common.infrastructure.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 知识库错误码
*
* @author dallas
* @since 2025-10-24
*/
@Getter
@AllArgsConstructor
public enum KnowledgeBaseErrorCode implements ErrorCode {
/**
* 知识库不存在
*/
KNOWLEDGE_BASE_NOT_FOUND("knowledge.0001", "知识库不存在"),
/**
* 文件不存在
*/
RAG_FILE_NOT_FOUND("knowledge.0002", "文件不存在");
private final String code;
private final String message;
}
@@ -0,0 +1,43 @@
package com.deepdata.common.infrastructure.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 系统错误码枚举
*
* @author dallas
* @since 2025-10-17
*/
@Getter
@AllArgsConstructor
public enum SystemErrorCode implements ErrorCode {
/**
* 未知错误
*/
UNKNOWN_ERROR("sys.0001", "未知错误"),
/**
* 系统繁忙,请稍后重试
*/
SYSTEM_BUSY("sys.0002", "系统繁忙,请稍后重试"),
/**
* 参数错误
*/
INVALID_PARAMETER("sys.0003", "参数错误"),
/**
* 资源未找到
*/
RESOURCE_NOT_FOUND("sys.0004", "资源未找到"),
/**
* 权限不足
*/
INSUFFICIENT_PERMISSIONS("sys.0005", "权限不足"),
/**
* 文件系统错误
*/
FILE_SYSTEM_ERROR("sys.0006", "文件系统错误");
private final String code;
private final String message;
}
@@ -0,0 +1,51 @@
package com.deepdata.common.infrastructure.mapper;
import com.deepdata.common.domain.model.ChunkUploadPreRequest;
import com.deepdata.common.infrastructure.config.IgnoreDataScopeAnnotation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文件切片上传请求Mapper
*/
@Mapper
@IgnoreDataScopeAnnotation
public interface ChunkUploadRequestMapper {
/**
* 根据ID查询
*/
ChunkUploadPreRequest findById(@Param("id") String id);
/**
* 根据服务ID查询
*/
List<ChunkUploadPreRequest> findByServiceId(@Param("serviceId") String serviceId);
/**
* 查询所有
*/
List<ChunkUploadPreRequest> findAll();
/**
* 插入
*/
int insert(ChunkUploadPreRequest request);
/**
* 更新
*/
int update(ChunkUploadPreRequest request);
/**
* 根据ID删除
*/
int deleteById(@Param("id") String id);
/**
* 根据服务ID删除
*/
int deleteByServiceId(@Param("serviceId") String serviceId);
}
@@ -0,0 +1,16 @@
package com.deepdata.common.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.deepdata.common.domain.model.LineageEdge;
import com.deepdata.common.infrastructure.config.IgnoreDataScopeAnnotation;
import org.apache.ibatis.annotations.Mapper;
/**
* 边映射器接口
*
* @since 2026/1/23
*/
@Mapper
@IgnoreDataScopeAnnotation
public interface LineageEdgeMapper extends BaseMapper<LineageEdge> {
}
@@ -0,0 +1,16 @@
package com.deepdata.common.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.deepdata.common.domain.model.LineageNode;
import com.deepdata.common.infrastructure.config.IgnoreDataScopeAnnotation;
import org.apache.ibatis.annotations.Mapper;
/**
* 节点映射器接口
*
* @since 2026/1/23
*/
@Mapper
@IgnoreDataScopeAnnotation
public interface LineageNodeMapper extends BaseMapper<LineageNode> {
}
@@ -0,0 +1,37 @@
package com.deepdata.common.interfaces;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class PagedResponse<T> {
// 当前页码(从 1 开始)
private long page;
// 每页数量
private long size;
private long totalElements;
private long totalPages;
private List<T> content;
private PagedResponse(long page, long size, long totalElements, long totalPages, List<T> content) {
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.content = content;
}
public static <T> PagedResponse<T> of(long page, long size, long totalElements, long totalPages, List<T> content) {
return new PagedResponse<>(page, size, totalElements, totalPages, content);
}
public static <T> PagedResponse<T> of(IPage<T> page) {
return new PagedResponse<>(page.getCurrent(), page.getSize(), page.getTotal(), page.getPages(), page.getRecords());
}
}
@@ -0,0 +1,41 @@
package com.deepdata.common.interfaces;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springaicommunity.mcp.annotation.McpToolParam;
@Getter
@NoArgsConstructor
public class PagingQuery {
/**
* 页码,从0开始
*/
@McpToolParam(description = "页码,从0开始")
private Integer page = 0;
/**
* 每页大小
*/
@McpToolParam(description = "每页大小")
private Integer size = 20;
public void setPage(Integer page) {
if (page == null || page < 0) {
this.page = 0;
} else {
this.page = page;
}
}
public void setSize(Integer size) {
if (size == null || size <= 0) {
this.size = 20;
} else {
this.size = size;
}
}
public PagingQuery(Integer page, Integer size) {
setPage(page);
setSize(size);
}
}
@@ -0,0 +1,85 @@
package com.deepdata.common.setting.application;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.deepdata.common.infrastructure.exception.BusinessAssert;
import com.deepdata.common.interfaces.PagedResponse;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import com.deepdata.common.setting.domain.repository.ModelConfigRepository;
import com.deepdata.common.setting.infrastructure.client.ModelClient;
import com.deepdata.common.setting.infrastructure.exception.ModelsErrorCode;
import com.deepdata.common.setting.interfaces.rest.dto.CreateModelRequest;
import com.deepdata.common.setting.interfaces.rest.dto.QueryModelRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 模型配置应用服务类
*
* @author dallas
* @since 2025-10-27
*/
@Service
@RequiredArgsConstructor
public class ModelConfigApplicationService {
private final ModelConfigRepository modelConfigRepository;
public List<ModelConfig> getProviders() {
List<ModelConfig> providers = new ArrayList<>();
providers.add(ModelConfig.builder().provider("ModelEngine").baseUrl("http://localhost:9981").build());
providers.add(ModelConfig.builder().provider("Ollama").baseUrl("http://localhost:11434").build());
providers.add(ModelConfig.builder().provider("OpenAI").baseUrl("https://api.openai.com/v1").build());
providers.add(ModelConfig.builder().provider("DeepSeek").baseUrl("https://api.deepseek.com/v1").build());
providers.add(ModelConfig.builder().provider("火山方舟").baseUrl("https://ark.cn-beijing.volces.com/api/v3").build());
providers.add(ModelConfig.builder().provider("阿里云百炼").baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1").build());
providers.add(ModelConfig.builder().provider("硅基流动").baseUrl("https://api.siliconflow.cn/v1").build());
providers.add(ModelConfig.builder().provider("智谱AI").baseUrl("https://open.bigmodel.cn/api/paas/v4").build());
return providers;
}
public PagedResponse<ModelConfig> getModels(QueryModelRequest queryModelRequest) {
// 从数据库查询模型配置
IPage<ModelConfig> page = modelConfigRepository.page(queryModelRequest);
return PagedResponse.of(page);
}
public ModelConfig getModelDetail(String modelId) {
return modelConfigRepository.getById(modelId);
}
public ModelConfig createModel(CreateModelRequest modelConfig) {
ModelConfig newConfig = ModelConfig.builder()
.provider(modelConfig.getProvider())
.modelName(modelConfig.getModelName())
.type(modelConfig.getType())
.baseUrl(modelConfig.getBaseUrl())
.apiKey(modelConfig.getApiKey())
.isEnabled(true)
.isDefault(modelConfig.getIsDefault())
.build();
ModelClient.checkHealth(newConfig);
modelConfigRepository.saveAndSetDefault(newConfig);
return newConfig;
}
public ModelConfig updateModel(String modelId, @Valid CreateModelRequest updateModelRequest) {
ModelConfig modelConfig = modelConfigRepository.getById(modelId);
BusinessAssert.notNull(modelConfig, ModelsErrorCode.MODEL_CONFIG_NOT_FOUND);
modelConfig.setProvider(updateModelRequest.getProvider());
modelConfig.setModelName(updateModelRequest.getModelName());
modelConfig.setType(updateModelRequest.getType());
modelConfig.setBaseUrl(updateModelRequest.getBaseUrl());
modelConfig.setApiKey(updateModelRequest.getApiKey());
modelConfig.setIsEnabled(true);
ModelClient.checkHealth(modelConfig);
modelConfigRepository.updateAndSetDefault(modelConfig, updateModelRequest.getIsDefault());
return modelConfig;
}
public void deleteModel(String modelId) {
modelConfigRepository.removeById(modelId);
}
}
@@ -0,0 +1,94 @@
package com.deepdata.common.setting.application;
import com.deepdata.common.infrastructure.exception.BusinessAssert;
import com.deepdata.common.infrastructure.exception.SystemErrorCode;
import com.deepdata.common.setting.domain.entity.SysParam;
import com.deepdata.common.setting.domain.repository.SysParamRepository;
import com.deepdata.common.setting.infrastructure.client.RedisClient;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 系统参数应用服务
*
* @author dallas
* @since 2025-11-04
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysParamApplicationService {
private final SysParamRepository sysParamRepository;
private final RedisClient redisClient;
private final AtomicBoolean redisEnable = new AtomicBoolean(true);
/**
* 列表查询系统参数
*
* @return 系统参数列表
*/
public List<SysParam> list() {
List<SysParam> sysParams = sysParamRepository.list();
sysParams.sort(Comparator.comparing(SysParam::getParamType));
return sysParams;
}
/**
* 根据参数id修改系统参数值
*
* @param paramId 参数id
* @param paramValue 参数值
*/
public void updateParamValueById(String paramId, String paramValue) {
SysParam sysParam = sysParamRepository.getById(paramId);
BusinessAssert.notNull(sysParam, SystemErrorCode.RESOURCE_NOT_FOUND);
sysParam.setParamValue(paramValue);
sysParamRepository.updateById(sysParam);
redisClient.setParam(sysParam.getId(), paramValue);
}
public void deleteParamById(String paramKey) {
SysParam sysParam = sysParamRepository.getById(paramKey);
BusinessAssert.notNull(sysParam, SystemErrorCode.RESOURCE_NOT_FOUND);
sysParamRepository.removeById(paramKey);
redisClient.delParam(sysParam.getId());
}
public String getParamByKey(String paramId) {
String value = null;
if (redisEnable.get()) {
try {
value = redisClient.getParamWithThrow(paramId);
} catch (Exception e) {
redisEnable.set(false);
log.warn(e.getMessage());
}
}
if (value == null) {
SysParam sysParam = sysParamRepository.getById(paramId);
if (sysParam != null) {
value = sysParam.getParamValue();
}
}
return value;
}
/**
* 初始化系统参数到Redis
*/
@PostConstruct
public void init() {
try {
List<SysParam> sysParams = sysParamRepository.list();
sysParams.forEach(sysParam -> redisClient.setParam(sysParam.getId(), sysParam.getParamValue()));
} catch (Exception e) {
log.error("Init sys params to redis error", e);
}
}
}
@@ -0,0 +1,55 @@
package com.deepdata.common.setting.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.deepdata.common.domain.model.base.BaseEntity;
import lombok.*;
/**
* 模型配置实体类
*
* @author dallas
* @since 2025-10-27
*/
@Getter
@Setter
@TableName("t_models")
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ModelConfig extends BaseEntity<String> {
/**
* 模型名称(如 qwen2)
*/
private String modelName;
/**
* 模型提供商(如 Ollama、OpenAI、DeepSeek)
*/
private String provider;
/**
* API 基础地址
*/
private String baseUrl;
/**
* API 密钥(无密钥则为空)
*/
private String apiKey;
/**
* 模型类型(如 chat、embedding)
*/
private ModelType type;
/**
* 是否启用:1-启用,0-禁用
*/
private Boolean isEnabled;
/**
* 是否默认:1-默认,0-非默认
*/
private Boolean isDefault;
/**
* 是否删除:1-已删除,0-未删除
*/
private Boolean isDeleted;
}
@@ -0,0 +1,18 @@
package com.deepdata.common.setting.domain.entity;
/**
* 模型类型枚举类
*
* @author dallas
* @since 2025-10-27
*/
public enum ModelType {
/**
* 语言模型
*/
CHAT,
/**
* 嵌入模型
*/
EMBEDDING
}
@@ -0,0 +1,52 @@
package com.deepdata.common.setting.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.deepdata.common.domain.model.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* 系统参数设置实体类
*
* @author dallas
* @since 2025-10-27
*/
@Setter
@Getter
@TableName("t_sys_param")
public class SysParam extends BaseEntity<String> {
/**
* 设置项值
*/
private String paramValue;
/**
* 设置项类型(如 string、integer、boolean)
*/
private String paramType;
/**
* 选项列表(JSON格式,仅对enum类型有效)
*/
private String optionList;
/**
* 设置项描述
*/
private String description;
/**
* 是否内置:1-是,0-否
*/
private Boolean isBuiltIn;
/**
* 是否可修改:1-可修改,0-不可修改
*/
private Boolean canModify;
/**
* 是否启用:1-启用,0-禁用
*/
private Boolean isEnabled;
}
@@ -0,0 +1,29 @@
package com.deepdata.common.setting.domain.repository;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import com.deepdata.common.setting.domain.entity.ModelType;
import com.deepdata.common.setting.interfaces.rest.dto.QueryModelRequest;
/**
* 模型配置仓库接口
*
* @author dallas
* @since 2025-10-27
*/
public interface ModelConfigRepository extends IRepository<ModelConfig> {
/**
* 分页查询模型配置
*
* @param queryModelRequest 分页查询参数
* @return 模型配置列表
*/
IPage<ModelConfig> page(QueryModelRequest queryModelRequest);
void saveAndSetDefault(ModelConfig modelConfig);
void removeDefault(ModelType type);
void updateAndSetDefault(ModelConfig modelConfig, Boolean isDefault);
}
@@ -0,0 +1,13 @@
package com.deepdata.common.setting.domain.repository;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.deepdata.common.setting.domain.entity.SysParam;
/**
* 系统参数仓库接口
*
* @author dallas
* @since 2025-11-04
*/
public interface SysParamRepository extends IRepository<SysParam> {
}
@@ -0,0 +1,64 @@
package com.deepdata.common.setting.infrastructure.client;
import com.deepdata.common.infrastructure.exception.BusinessException;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import com.deepdata.common.setting.infrastructure.exception.ModelsErrorCode;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import lombok.extern.slf4j.Slf4j;
/**
* 模型客户端接口
*
* @author dallas
* @since 2025-10-27
*/
@Slf4j
public class ModelClient {
public static <T> T invokeModel(ModelConfig modelConfig, Class<T> modelInterface) {
return switch (modelConfig.getType()) {
case CHAT -> modelInterface.cast(invokeChatModel(modelConfig));
case EMBEDDING -> modelInterface.cast(invokeEmbeddingModel(modelConfig));
};
}
public static EmbeddingModel invokeEmbeddingModel(ModelConfig modelConfig) {
return OpenAiEmbeddingModel.builder()
.baseUrl(modelConfig.getBaseUrl())
.apiKey(modelConfig.getApiKey())
.modelName(modelConfig.getModelName())
.build();
}
public static ChatModel invokeChatModel(ModelConfig modelConfig) {
return OpenAiChatModel.builder()
.baseUrl(modelConfig.getBaseUrl())
.apiKey(modelConfig.getApiKey())
.modelName(modelConfig.getModelName())
.build();
}
public static void checkHealth(ModelConfig modelConfig) {
try {
switch (modelConfig.getType()) {
case CHAT -> checkChatModelHealth(modelConfig);
case EMBEDDING -> checkEmbeddingModelHealth(modelConfig);
}
} catch (Exception e) {
log.error("Model health check failed for modelConfig: {}", modelConfig, e);
throw BusinessException.of(ModelsErrorCode.MODEL_HEALTH_CHECK_FAILED);
}
}
private static void checkEmbeddingModelHealth(ModelConfig modelConfig) {
EmbeddingModel embeddingModel = invokeEmbeddingModel(modelConfig);
embeddingModel.embed("text");
}
private static void checkChatModelHealth(ModelConfig modelConfig) {
ChatModel chatModel = invokeChatModel(modelConfig);
chatModel.chat("hello");
}
}
@@ -0,0 +1,30 @@
package com.deepdata.common.setting.infrastructure.client;
import com.deepdata.common.setting.infrastructure.utils.FunctionUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@AllArgsConstructor
public class RedisClient {
private final StringRedisTemplate redisTemplate;
public void setParam(String key, String value) {
FunctionUtil.doWithoutThrow((k, v) -> redisTemplate.opsForValue().set(k, v), key, value);
}
public String getParam(String key) {
return FunctionUtil.getWithoutThrow((k) -> redisTemplate.opsForValue().get(k), key);
}
public void delParam(String key) {
FunctionUtil.doWithoutThrow(redisTemplate::delete, key);
}
public String getParamWithThrow(String key) {
return redisTemplate.opsForValue().get(key);
}
}
@@ -0,0 +1,32 @@
package com.deepdata.common.setting.infrastructure.exception;
import com.deepdata.common.infrastructure.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 模型配置错误码枚举类
*
* @author dallas
* @since 2025-10-27
*/
@Getter
@AllArgsConstructor
public enum ModelsErrorCode implements ErrorCode {
/**
* 模型配置不存在
*/
MODEL_CONFIG_NOT_FOUND("model.0001", "模型配置不存在"),
/**
* 模型配置已存在
*/
MODEL_CONFIG_ALREADY_EXISTS("model.0002", "模型配置已存在"),
/**
* 模型健康检查失败
*/
MODEL_HEALTH_CHECK_FAILED("model.0003", "模型健康检查失败");
private final String code;
private final String message;
}
@@ -0,0 +1,70 @@
package com.deepdata.common.setting.infrastructure.persistence.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.repository.CrudRepository;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import com.deepdata.common.setting.domain.entity.ModelType;
import com.deepdata.common.setting.domain.repository.ModelConfigRepository;
import com.deepdata.common.setting.infrastructure.persistence.mapper.ModelConfigMapper;
import com.deepdata.common.setting.interfaces.rest.dto.QueryModelRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* 模型配置仓库实现类
*
* @author dallas
* @since 2025-10-27
*/
@Repository
@RequiredArgsConstructor
public class ModelConfigRepositoryImpl extends CrudRepository<ModelConfigMapper, ModelConfig> implements ModelConfigRepository {
private final ModelConfigMapper modelConfigMapper;
@Override
public IPage<ModelConfig> page(QueryModelRequest queryModelRequest) {
IPage<ModelConfig> page = new Page<>(queryModelRequest.getPage(), queryModelRequest.getSize());
return this.page(page, new LambdaQueryWrapper<ModelConfig>()
.eq(StringUtils.hasText(queryModelRequest.getProvider()), ModelConfig::getProvider, queryModelRequest.getProvider())
.eq(Objects.nonNull(queryModelRequest.getType()), ModelConfig::getType, queryModelRequest.getType())
.eq(Objects.nonNull(queryModelRequest.getIsEnabled()), ModelConfig::getIsEnabled, queryModelRequest.getIsEnabled())
.eq(Objects.nonNull(queryModelRequest.getIsDefault()), ModelConfig::getIsDefault, queryModelRequest.getIsDefault()));
}
@Transactional
public void saveAndSetDefault(ModelConfig modelConfig) {
LambdaQueryWrapper<ModelConfig> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ModelConfig::getType, modelConfig.getType()).eq(ModelConfig::getIsDefault, true);
if (this.getOne(queryWrapper) == null) {
modelConfig.setIsDefault(true);
} else {
removeDefault(modelConfig.getType());
}
super.save(modelConfig);
}
public void removeDefault(ModelType type) {
LambdaUpdateWrapper<ModelConfig> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(ModelConfig::getIsDefault, false)
.eq(ModelConfig::getType, type)
.eq(ModelConfig::getIsDefault, true);
this.update(updateWrapper);
}
@Transactional
public void updateAndSetDefault(ModelConfig modelConfig, Boolean isDefault) {
if (!modelConfig.getIsDefault() && isDefault) {
removeDefault(modelConfig.getType());
}
modelConfig.setIsDefault(isDefault);
this.updateById(modelConfig);
}
}
@@ -0,0 +1,17 @@
package com.deepdata.common.setting.infrastructure.persistence.impl;
import com.baomidou.mybatisplus.extension.repository.CrudRepository;
import com.deepdata.common.setting.domain.entity.SysParam;
import com.deepdata.common.setting.domain.repository.SysParamRepository;
import com.deepdata.common.setting.infrastructure.persistence.mapper.SysParamMapper;
import org.springframework.stereotype.Repository;
/**
* 系统参数仓储实现类
*
* @author dallas
* @since 2025-11-04
*/
@Repository
public class SysParamRepositoryImpl extends CrudRepository<SysParamMapper, SysParam> implements SysParamRepository {
}
@@ -0,0 +1,15 @@
package com.deepdata.common.setting.infrastructure.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* 模型配置映射器接口
*
* @author dallas
* @since 2025-10-27
*/
@Mapper
public interface ModelConfigMapper extends BaseMapper<ModelConfig> {
}
@@ -0,0 +1,15 @@
package com.deepdata.common.setting.infrastructure.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.deepdata.common.setting.domain.entity.SysParam;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统参数映射器
*
* @author dallas
* @since 2025-11-04
*/
@Mapper
public interface SysParamMapper extends BaseMapper<SysParam> {
}
@@ -0,0 +1,35 @@
package com.deepdata.common.setting.infrastructure.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@Slf4j
public class FunctionUtil {
public static <T, R> R getWithoutThrow(Function<T, R> action, T key) {
try {
return action.apply(key);
} catch (Exception e) {
log.warn(e.getMessage());
return null;
}
}
public static <T> void doWithoutThrow(Consumer<T> action, T key) {
try {
action.accept(key);
} catch (Exception e) {
log.warn(e.getMessage());
}
}
public static <T, R> void doWithoutThrow(BiConsumer<T, R> action, T t, R r) {
try {
action.accept(t, r);
} catch (Exception e) {
log.warn(e.getMessage());
}
}
}
@@ -0,0 +1,90 @@
package com.deepdata.common.setting.interfaces.rest;
import com.deepdata.common.interfaces.PagedResponse;
import com.deepdata.common.setting.application.ModelConfigApplicationService;
import com.deepdata.common.setting.domain.entity.ModelConfig;
import com.deepdata.common.setting.interfaces.rest.dto.CreateModelRequest;
import com.deepdata.common.setting.interfaces.rest.dto.QueryModelRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 模型配置控制器类
*
* @author dallas
* @since 2025-10-27
*/
@RestController
@RequestMapping("/models")
@RequiredArgsConstructor
public class ModelConfigController {
private final ModelConfigApplicationService modelConfigApplicationService;
/**
* 获取厂商列表
*
* @return 厂商列表
*/
@GetMapping("/providers")
public List<ModelConfig> getProviders() {
return modelConfigApplicationService.getProviders();
}
/**
* 获取模型列表
*
* @return 模型列表
*/
@GetMapping("/list")
public PagedResponse<ModelConfig> getModels(QueryModelRequest queryModelRequest) {
return modelConfigApplicationService.getModels(queryModelRequest);
}
/**
* 获取模型详情
*
* @param modelId 模型 ID
* @return 模型详情
*/
@GetMapping("/{modelId}")
public ModelConfig getModelDetail(@PathVariable("modelId") String modelId) {
return modelConfigApplicationService.getModelDetail(modelId);
}
/**
* 创建模型配置
*
* @param createModelRequest 创建模型配置请求
* @return 创建的模型配置
*/
@PostMapping("/create")
public ModelConfig createModel(@RequestBody @Valid CreateModelRequest createModelRequest) {
return modelConfigApplicationService.createModel(createModelRequest);
}
/**
* 更新模型配置
*
* @param modelId 模型 ID
* @param updateModelRequest 更新模型配置请求
* @return 更新后的模型配置
*/
@PutMapping("/{modelId}")
public ModelConfig updateModel(@PathVariable String modelId, @RequestBody @Valid CreateModelRequest updateModelRequest) {
return modelConfigApplicationService.updateModel(modelId, updateModelRequest);
}
/**
* 删除模型配置
*
* @param modelId 模型 ID
*/
@DeleteMapping("/{modelId}")
public void deleteModel(@PathVariable String modelId) {
modelConfigApplicationService.deleteModel(modelId);
}
}
@@ -0,0 +1,54 @@
package com.deepdata.common.setting.interfaces.rest;
import com.deepdata.common.setting.application.SysParamApplicationService;
import com.deepdata.common.setting.interfaces.rest.dto.ParamRequest;
import com.deepdata.common.setting.domain.entity.SysParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 系统参数控制器
*
* @author dallas
* @since 2025-11-04
*/
@RestController
@RequestMapping("/sys-param")
@RequiredArgsConstructor
public class SysParamController {
private final SysParamApplicationService sysParamApplicationService;
/**
* 获取系统参数列表
*
* @return 系统参数列表
*/
@GetMapping("/list")
public List<SysParam> list() {
return sysParamApplicationService.list();
}
/**
* 根据参数id修改系统参数值
*
* @param paramId 参数id
* @param paramRequest 参数值请求体
*/
@PutMapping("/{paramId}")
public void updateParamValueById(@PathVariable("paramId") String paramId, @RequestBody ParamRequest paramRequest) {
sysParamApplicationService.updateParamValueById(paramId, paramRequest.paramValue());
}
/**
* 根据参数id删除系统参数
*
* @param paramId 参数id
*/
@DeleteMapping("/{paramId}")
public void deleteParamById(@PathVariable("paramId") String paramId) {
sysParamApplicationService.deleteParamById(paramId);
}
}
@@ -0,0 +1,51 @@
package com.deepdata.common.setting.interfaces.rest.dto;
import com.deepdata.common.setting.domain.entity.ModelType;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* 创建模型配置请求类
*
* @author dallas
* @since 2025-10-27
*/
@Setter
@Getter
public class CreateModelRequest {
/**
* 模型名称(如 qwen2)
*/
@NotEmpty(message = "模型名称不能为空")
private String modelName;
/**
* 模型提供商(如 Ollama、OpenAI、DeepSeek)
*/
@NotEmpty(message = "模型提供商不能为空")
private String provider;
/**
* API 基础地址
*/
@NotEmpty(message = "API 基础地址不能为空")
private String baseUrl;
/**
* API 密钥(无密钥则为空)
*/
private String apiKey;
/**
* 模型类型(如 chat、embedding)
*/
@NotNull(message = "模型类型不能为空")
private ModelType type;
/**
* 是否启用:1-启用,0-禁用
*/
private Boolean isEnabled;
/**
* 是否默认:1-默认,0-非默认
*/
private Boolean isDefault;
}
@@ -0,0 +1,4 @@
package com.deepdata.common.setting.interfaces.rest.dto;
public record ParamRequest(String paramValue, boolean isEnabled) {
}
@@ -0,0 +1,29 @@
package com.deepdata.common.setting.interfaces.rest.dto;
import com.deepdata.common.interfaces.PagingQuery;
import com.deepdata.common.setting.domain.entity.ModelType;
import lombok.Getter;
import lombok.Setter;
/**
* 模型查询请求 DTO
*
* @author dallas
* @since 2025-10-27
*/
@Getter
@Setter
public class QueryModelRequest extends PagingQuery {
/**
* 模型提供商(如 Ollama、OpenAI、DeepSeek)
*/
private String provider;
/**
* 模型类型(如 chat、embedding)
*/
private ModelType type;
private Boolean isEnabled;
private Boolean isDefault;
}
@@ -0,0 +1,65 @@
<?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.deepdata.common.infrastructure.mapper.ChunkUploadRequestMapper">
<resultMap id="ChunkUploadRequestResultMap" type="com.deepdata.common.domain.model.ChunkUploadPreRequest">
<id column="id" property="id"/>
<result column="total_file_num" property="totalFileNum"/>
<result column="uploaded_file_num" property="uploadedFileNum"/>
<result column="upload_path" property="uploadPath"/>
<result column="timeout" property="timeout"/>
<result column="service_id" property="serviceId"/>
<result column="check_info" property="checkInfo"/>
</resultMap>
<sql id="Base_Column_List">
id, total_file_num, uploaded_file_num, upload_path, timeout, service_id, check_info
</sql>
<select id="findById" parameterType="string" resultMap="ChunkUploadRequestResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_chunk_upload_request
WHERE id = #{id}
</select>
<select id="findByServiceId" parameterType="string" resultMap="ChunkUploadRequestResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_chunk_upload_request
WHERE service_id = #{serviceId}
ORDER BY timeout DESC
</select>
<select id="findAll" resultMap="ChunkUploadRequestResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_chunk_upload_request
ORDER BY timeout DESC
</select>
<insert id="insert" parameterType="com.deepdata.common.domain.model.ChunkUploadPreRequest">
INSERT INTO t_chunk_upload_request (
id, total_file_num, uploaded_file_num, upload_path, timeout, service_id, check_info
) VALUES (
#{id}, #{totalFileNum}, #{uploadedFileNum}, #{uploadPath}, #{timeout}, #{serviceId}, #{checkInfo}
)
</insert>
<update id="update" parameterType="com.deepdata.common.domain.model.ChunkUploadPreRequest">
UPDATE t_chunk_upload_request
SET total_file_num = #{totalFileNum},
uploaded_file_num = #{uploadedFileNum},
upload_path = #{uploadPath},
timeout = #{timeout},
service_id = #{serviceId},
check_info = #{checkInfo}
WHERE id = #{id}
</update>
<delete id="deleteById" parameterType="string">
DELETE FROM t_chunk_upload_request WHERE id = #{id}
</delete>
<delete id="deleteByServiceId" parameterType="string">
DELETE FROM t_chunk_upload_request WHERE service_id = #{serviceId}
</delete>
</mapper>
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.deepdata</groupId>
<artifactId>services</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../services/pom.xml</relativePath>
</parent>
<artifactId>security-common</artifactId>
<name>Security Common</name>
<description>安全通用组件</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,106 @@
package com.deepdata.common.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
@Component
public class JwtUtils {
@Value("${deepdata.jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400}") // 24小时
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成JWT令牌
*/
public String generateToken(String username, Map<String, Object> claims) {
Map<String, Object> tokenClaims = new HashMap<>();
if (claims != null) {
tokenClaims.putAll(claims);
}
tokenClaims.put("sub", username);
return Jwts.builder()
.setClaims(tokenClaims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
/**
* 从令牌中获取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 从令牌中获取声明
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 验证令牌是否过期
*/
public Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 验证令牌
*/
public Boolean validateToken(String token, String username) {
try {
String tokenUsername = getUsernameFromToken(token);
return (username.equals(tokenUsername) && !isTokenExpired(token));
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
/**
* 刷新令牌
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000));
return Jwts.builder()
.setClaims(claims)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
}