init datamate

This commit is contained in:
Dallas98
2025-10-21 23:00:48 +08:00
commit 1c97afed7d
692 changed files with 135442 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
<?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.datamate</groupId>
<artifactId>data-mate-platform</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../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>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
package com.datamate.common.domain;
import com.datamate.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);
}

View File

@@ -0,0 +1,16 @@
package com.datamate.common.domain;
/**
* DDD值对象基类
*/
public abstract class ValueObject {
@Override
public abstract boolean equals(Object obj);
@Override
public abstract int hashCode();
@Override
public abstract String toString();
}

View File

@@ -0,0 +1,49 @@
package com.datamate.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);
}
}

View File

@@ -0,0 +1,36 @@
package com.datamate.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;
}

View File

@@ -0,0 +1,24 @@
package com.datamate.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;
}

View File

@@ -0,0 +1,7 @@
package com.datamate.common.domain.model;
/**
* 上传检查信息基类
*/
public abstract class UploadCheckInfo {
}

View File

@@ -0,0 +1,61 @@
package com.datamate.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;
}
}

View File

@@ -0,0 +1,91 @@
package com.datamate.common.domain.service;
import com.datamate.common.domain.model.ChunkUploadPreRequest;
import com.datamate.common.domain.model.ChunkUploadRequest;
import com.datamate.common.domain.model.FileUploadResult;
import com.datamate.common.domain.utils.ChunksSaver;
import com.datamate.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 new IllegalArgumentException("预上传请求不存在");
}
File savedFile;
if (uploadFileRequest.getTotalChunkNum() > 1) {
savedFile = uploadChunk(uploadFileRequest, preRequest);
} else {
savedFile = uploadFile(uploadFileRequest, preRequest);
}
if (chunkUploadRequestMapper.update(preRequest) == 0) {
throw new IllegalArgumentException("预上传请求不存在");
}
boolean isFinish = Objects.equals(preRequest.getUploadedFileNum(), preRequest.getTotalFileNum());
if (isFinish) {
// 删除存分片的临时路径
ChunksSaver.deleteFiles(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;
}
}

View File

@@ -0,0 +1,40 @@
package com.datamate.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);
}
}

View File

@@ -0,0 +1,134 @@
package com.datamate.common.domain.utils;
import com.datamate.common.domain.model.ChunkUploadPreRequest;
import com.datamate.common.domain.model.ChunkUploadRequest;
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());
if (!targetFile.renameTo(finalFile)) {
log.error("failed to mv file:{}, req Id:{}", targetFile.getName(), fileUploadRequest.getReqId());
throw new IllegalArgumentException("failed to move file to target dir");
}
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());
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 deleteFiles(String uploadPath) {
File dic = new File(uploadPath);
if (!dic.exists()) {
return;
}
File[] files = dic.listFiles();
if (files == null || files.length == 0) {
dic.delete();
return;
}
try {
for (File file : files) {
if (file.isDirectory()) {
deleteFiles(file.getPath());
} else {
file.delete();
}
}
if (dic.exists()) {
dic.delete();
}
} catch (SecurityException e) {
log.warn("Fail to delete file", e);
}
}
}

View File

@@ -0,0 +1,24 @@
package com.datamate.common.domain.utils;
import java.io.File;
/**
* 通用工具类
*/
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;
}
}

View File

@@ -0,0 +1,15 @@
package com.datamate.common.infrastructure.common;
import java.lang.annotation.*;
/**
* 忽略响应包装注解
* <p>
* 在使用全局响应包装时,如果某个接口或类不需要进行响应包装,可以使用此注解进行标记
* </p>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreResponseWrap {
}

View File

@@ -0,0 +1,63 @@
package com.datamate.common.infrastructure.common;
import com.datamate.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);
}
}

View File

@@ -0,0 +1,60 @@
package com.datamate.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";
}
}
}

View File

@@ -0,0 +1,54 @@
package com.datamate.common.infrastructure.config;
import com.datamate.common.infrastructure.common.Response;
import com.datamate.common.infrastructure.exception.BusinessException;
import com.datamate.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));
}
}

View File

@@ -0,0 +1,77 @@
package com.datamate.common.infrastructure.config;
import com.datamate.common.infrastructure.common.IgnoreResponseWrap;
import com.datamate.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.datamate")
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);
}
}

View File

@@ -0,0 +1,47 @@
package com.datamate.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.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() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,35 @@
package com.datamate.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);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.datamate.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();
}
}

View File

@@ -0,0 +1,23 @@
package com.datamate.common.infrastructure.exception;
/**
* 错误码接口
*
* @author dallas
* @since 2025-10-17
*/
public interface ErrorCode {
/**
* 获取错误码
*
* @return 错误码
*/
String getCode();
/**
* 获取错误信息
*
* @return 错误信息
*/
String getMessage();
}

View File

@@ -0,0 +1,15 @@
package com.datamate.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);
}
}

View File

@@ -0,0 +1,43 @@
package com.datamate.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;
}

View File

@@ -0,0 +1,49 @@
package com.datamate.common.infrastructure.mapper;
import com.datamate.common.domain.model.ChunkUploadPreRequest;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文件切片上传请求Mapper
*/
@Mapper
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);
}

View File

@@ -0,0 +1,44 @@
package com.datamate.common.interfaces;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PagedResponse <T> {
private long page;
private long size;
private long totalElements;
private long totalPages;
private List<T> content;
public PagedResponse(List<T> content) {
this.page = 0;
this.size = content.size();
this.totalElements = content.size();
this.totalPages = 1;
this.content = content;
}
public PagedResponse(List<T> content, long page, long totalElements, long totalPages) {
this.page = page;
this.size = content.size();
this.totalElements = totalElements;
this.totalPages = totalPages;
this.content = content;
}
public static <T> PagedResponse<T> of(List<T> content) {
return new PagedResponse<>(content);
}
public static <T> PagedResponse<T> of(List<T> content, long page, long totalElements, long totalPages) {
return new PagedResponse<>(content, page, totalElements, totalPages);
}
}

View File

@@ -0,0 +1,22 @@
package com.datamate.common.interfaces;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PagingQuery {
/**
* 页码,从0开始
*/
private Integer page = 0;
/**
* 每页大小
*/
private Integer size = 20;
}

View File

@@ -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.datamate.common.infrastructure.mapper.ChunkUploadRequestMapper">
<resultMap id="ChunkUploadRequestResultMap" type="com.datamate.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.datamate.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.datamate.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>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.datamate</groupId>
<artifactId>data-mate-platform</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../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>

View File

@@ -0,0 +1,106 @@
package com.datamate.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("${jwt.secret:datamate-secret-key-for-jwt-token-generation}")
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();
}
}