deepdata: core product code (backend, frontend, runtime, deepflow module)
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
+27
@@ -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);
|
||||
}
|
||||
+16
@@ -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();
|
||||
}
|
||||
+15
@@ -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,
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
@@ -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;
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
+65
@@ -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;
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
+61
@@ -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;
|
||||
}
|
||||
}
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
+168
@@ -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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+209
@@ -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;
|
||||
}
|
||||
}
|
||||
+170
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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 {
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
}
|
||||
+107
@@ -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();
|
||||
}
|
||||
}
|
||||
+60
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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));
|
||||
}
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
@@ -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 {
|
||||
}
|
||||
+49
@@ -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;
|
||||
}
|
||||
}
|
||||
+40
@@ -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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -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();
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
+23
@@ -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();
|
||||
}
|
||||
+15
@@ -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);
|
||||
}
|
||||
}
|
||||
+28
@@ -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;
|
||||
}
|
||||
+43
@@ -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;
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
+16
@@ -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> {
|
||||
}
|
||||
+16
@@ -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> {
|
||||
}
|
||||
+37
@@ -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());
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
+85
@@ -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);
|
||||
}
|
||||
}
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -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;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.deepdata.common.setting.domain.entity;
|
||||
|
||||
/**
|
||||
* 模型类型枚举类
|
||||
*
|
||||
* @author dallas
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
public enum ModelType {
|
||||
/**
|
||||
* 语言模型
|
||||
*/
|
||||
CHAT,
|
||||
/**
|
||||
* 嵌入模型
|
||||
*/
|
||||
EMBEDDING
|
||||
}
|
||||
+52
@@ -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;
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+13
@@ -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> {
|
||||
}
|
||||
+64
@@ -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");
|
||||
}
|
||||
}
|
||||
+30
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
+70
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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 {
|
||||
}
|
||||
+15
@@ -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> {
|
||||
}
|
||||
+15
@@ -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> {
|
||||
}
|
||||
+35
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -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);
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -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;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.deepdata.common.setting.interfaces.rest.dto;
|
||||
|
||||
public record ParamRequest(String paramValue, boolean isEnabled) {
|
||||
}
|
||||
+29
@@ -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>
|
||||
@@ -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>
|
||||
+106
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user