初始化项目

This commit is contained in:
2026-04-21 16:12:04 +08:00
parent 4541af2c63
commit f9d96473da
443 changed files with 36365 additions and 19 deletions

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>org.xujun</groupId>
<artifactId>xtools-app-sys</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>xtools-app-sys-file</artifactId>
<!-- 依赖 -->
<dependencies>
<!-- xtools begin -->
<!-- xtools-extend begin -->
<!-- bcprov 加密库 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-extend</artifactId>
</dependency>
<!-- xtools-extend end -->
<!-- xtools-boot-storage-base -->
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-boot-storage-base</artifactId>
</dependency>
<!-- xtools-boot-web-base -->
<!-- xtools end -->
<!-- 项目模块 begin -->
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-app-sys-api</artifactId>
</dependency>
<!-- 项目模块 end -->
<!-- jakarta.annotation-api -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,46 @@
package xtools.app.sys.file.enums;
import xtools.boot.api.enums.BaseEnum;
/**
* <p>Title : FileBizBaseEnum</p>
* <p>Description : FileBizBaseEnum</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/3/24 09:18
*/
public interface FileBizBaseEnum extends BaseEnum {
/**
* 存储桶
*
* @return 存储桶
*/
String bucket();
/**
* 文件权限
*
* @return 文件权限
*/
FilePermissionType permission();
/**
* 文件过期时间(小时)
*
* @return 文件过期时间
*/
int expireTime();
/**
* 文件扩展名
*
* @return 文件扩展名
*/
String[] extArr();
}

View File

@@ -0,0 +1,138 @@
package xtools.app.sys.file.enums;
/**
* <p>Title : FileBizEnum</p>
* <p>Description : FileBizEnum</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/3/17 21:09
*/
public enum FileBizEnum implements FileBizBaseEnum {
// 系统通知
SYS_NOTICE(-10, "系统通知", "sys-notice-editor-img", FilePermissionType.PUBLIC, 12, "png", "jpg", "jpeg", "gif", "bmp", "webp");
/**
* 编码
*/
private final int code;
/**
* 说明
*/
private final String desc;
/**
* 存储桶
*/
private final String bucket;
/**
* 文件权限
*/
private final FilePermissionType permission;
/**
* 文件过期时间(小时)
*/
private final int expireTime;
/**
* 扩展名
*/
private final String[] extArr;
/**
* 构造函数
*
* @param code 编码
* @param desc 说明
* @param bucket 存储桶
* @param permission 文件权限
* @param expireTime 文件过期时间(小时)
* @param extArr 扩展名
*/
FileBizEnum(int code, String desc, String bucket, FilePermissionType permission, int expireTime, String... extArr) {
this.code = code;
this.desc = desc;
this.bucket = bucket;
this.permission = permission;
this.expireTime = expireTime;
this.extArr = extArr;
}
/**
* 获取所有枚举
*
* @return 所有枚举
*/
@Override
public FileBizBaseEnum[] all() {
return values();
}
/**
* 获取枚举编码
*
* @return 枚举编码
*/
@Override
public int code() {
return code;
}
/**
* 获取枚举说明
*
* @return 枚举说明
*/
@Override
public String desc() {
return desc;
}
/**
* 获取存储桶
*
* @return 存储桶
*/
@Override
public String bucket() {
return bucket;
}
/**
* 文件权限
*
* @return 文件权限
*/
@Override
public FilePermissionType permission() {
return permission;
}
/**
* 获取文件过期时间
*
* @return 文件过期时间
*/
@Override
public int expireTime() {
return expireTime;
}
/**
* 获取扩展名
*
* @return 扩展名
*/
@Override
public String[] extArr() {
return extArr;
}
}

View File

@@ -0,0 +1,90 @@
package xtools.app.sys.file.enums;
import xtools.boot.api.enums.BaseEnum;
/**
* <p>Title : FilePermissionType</p>
* <p>Description : FilePermissionType</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/2/15 10:29
*/
public enum FilePermissionType implements BaseEnum {
// 公开
PUBLIC(0, "公开"),
// 私有
PRIVATE(1, "私有"),
;
/**
* 编码
*/
private final int code;
/**
* 说明
*/
private final String desc;
/**
* 初始化方法
*
* @param code Code
* @param desc 说明
*/
FilePermissionType(int code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 判断枚举值类型
*
* @param code 枚举值
* @return 枚举值类型
*/
public static FilePermissionType valueOfCode(int code) {
for (FilePermissionType type : values()) {
if (type.code == code) {
return type;
}
}
return null;
}
/**
* 获取所有枚举
*
* @return 所有枚举
*/
@Override
public BaseEnum[] all() {
return values();
}
/**
* 获取枚举编码
*
* @return 枚举编码
*/
@Override
public int code() {
return code;
}
/**
* 获取枚举说明
*
* @return 枚举说明
*/
@Override
public String desc() {
return desc;
}
}

View File

@@ -0,0 +1,28 @@
package xtools.app.sys.file.service;
import xtools.app.sys.model.dto.resp.SysFileResp;
/**
* <p>Title : SysFileDownloadCallback</p>
* <p>Description : SysFileDownloadCallback</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : Windows11</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/4/10 21:59
*/
public interface SysFileDownloadCallback {
/**
* 下载前回调
*
* @param fileInfo 文件信息
* @return 是否继续下载
*/
default boolean before(SysFileResp fileInfo) {
return true;
}
}

View File

@@ -0,0 +1,142 @@
package xtools.app.sys.file.service;
import org.jspecify.annotations.NonNull;
import xtools.app.sys.file.enums.FileBizBaseEnum;
import xtools.app.sys.model.dto.resp.SysFileResp;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
* <p>Title : SysFileOptService</p>
* <p>Description : SysFileOptService</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/3/14 14:06
*/
public interface SysFileOptService {
/**
* 文件是否存在
*
* @param id 文件ID
* @return 是否存在
*/
boolean exists(long id);
/**
* 文件是否存在
*
* @param bucket 存储桶
* @param fileName 文件名
* @return 是否存在
*/
boolean exists(String bucket, String fileName);
/**
* 文件上传
*
* @param fileName 文件名
* @param inputStream 文件输入流
* @param contentLength 文件长度
* @param userId 用户ID
* @param userName 用户名
* @param bizType 业务类型
* @param bizId 业务ID
* @return 文件ID
*/
Long upload(
@NonNull String fileName,
@NonNull InputStream inputStream,
long contentLength,
@NonNull Long userId,
@NonNull String userName,
@NonNull FileBizBaseEnum bizType,
Long bizId
);
/**
* 文件下载
*
* @param bucket 存储桶
* @param fileName 文件名
* @param outputStream 输出流
* @return 文件信息
*/
SysFileResp download(String bucket, String fileName, @NonNull OutputStream outputStream);
/**
* 文件下载
*
* @param bucket 存储桶
* @param fileName 文件名
* @param outputStream 输出流
* @param callback 回调
* @return 文件信息
*/
SysFileResp download(String bucket, String fileName, @NonNull OutputStream outputStream, SysFileDownloadCallback callback);
/**
* 文件下载
*
* @param id 文件ID
* @param outputStream 输出流
* @return 文件信息
*/
SysFileResp download(
long id,
@NonNull OutputStream outputStream
);
/**
* 文件下载
*
* @param id 文件ID
* @param outputStream 输出流
* @param callback 回调
* @return 文件信息
*/
SysFileResp download(
long id,
@NonNull OutputStream outputStream,
SysFileDownloadCallback callback
);
/**
* 文件删除
*
* @param id 文件ID
* @return 删除结果
*/
boolean delete(long id);
/**
* 文件删除
*
* @param idList 文件ID列表
* @return 删除结果
*/
boolean delete(List<Long> idList);
/**
* 文件成功
*
* @param id 文件ID
* @return 成功结果
*/
boolean success(long id);
/**
* 文件成功
*
* @param idList 文件ID列表
* @return 成功结果
*/
boolean success(List<Long> idList);
}

View File

@@ -0,0 +1,353 @@
package xtools.app.sys.file.service.impl;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.stereotype.Service;
import xtools.app.sys.api.SysFileApi;
import xtools.app.sys.file.enums.FileBizBaseEnum;
import xtools.app.sys.file.service.SysFileDownloadCallback;
import xtools.app.sys.file.service.SysFileOptService;
import xtools.app.sys.model.dto.req.SysFileAddReq;
import xtools.app.sys.model.dto.req.SysFileChangeReq;
import xtools.app.sys.model.dto.resp.SysFileResp;
import xtools.base.config.BaseParams;
import xtools.boot.api.enums.FileDataType;
import xtools.boot.api.exection.BizError;
import xtools.boot.api.model.dto.Result;
import xtools.boot.storage.base.config.StorageConfig;
import xtools.boot.storage.base.service.StorageService;
import xtools.core.CollectionUtils;
import xtools.core.FileUtils;
import xtools.core.HexUtils;
import xtools.core.StringUtils;
import xtools.core.UuidUtils;
import xtools.core.encrypt.Md5Utils;
import xtools.core.extend.CheckUtils;
import xtools.core.time.CalendarUtils;
import xtools.extend.encrypt.Sm3Utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
/**
* <p>Title : SysFileOptServiceImpl</p>
* <p>Description : SysFileOptServiceImpl</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/3/14 14:16
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysFileOptServiceImpl implements SysFileOptService, BaseParams {
/**
* 文件最大长度
*/
private final static long MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024L;
private final SysFileApi sysFileApi;
@Resource
private StorageConfig storageConfig;
@Resource
private StorageService storageService;
/**
* 文件是否存在
*
* @param id 文件ID
* @return 是否存在
*/
@Override
public boolean exists(long id) {
SysFileResp data = getFileData(id);
return storageService.exists(data.getBucket(), data.getFilePath());
}
/**
* 文件是否存在
*
* @param bucket 存储桶
* @param fileName 文件名
* @return 是否存在
*/
@Override
public boolean exists(String bucket, String fileName) {
SysFileResp data = getFileData(bucket, fileName);
return storageService.exists(data.getBucket(), data.getFilePath());
}
/**
* 文件上传
*
* @param fileName 文件名
* @param inputStream 文件输入流
* @param contentLength 文件长度
* @param userId 用户ID
* @param userName 用户名
* @param bizType 业务类型
* @param bizId 业务ID
* @return 文件ID
*/
@Override
public Long upload(
@NonNull String fileName,
@NonNull InputStream inputStream,
long contentLength,
@NonNull Long userId,
@NonNull String userName,
@NonNull FileBizBaseEnum bizType,
Long bizId
) {
// 上传事件
Instant uploadTime = Instant.now();
// 计算文件码
String md5 = null;
String sm3 = null;
if (contentLength <= MAX_FILE_SIZE) {
// 小于5MB的文件才计算文件码
try {
byte[] bytes = inputStream.readAllBytes();
inputStream = new ByteArrayInputStream(bytes);
contentLength = bytes.length;
if (contentLength <= 0) {
throw new BizError("文件长度错误");
}
byte[] md5Bytes = Md5Utils.encrypt(bytes);
if (Objects.nonNull(md5Bytes)) {
md5 = HexUtils.formatHex(md5Bytes);
}
byte[] sm3Bytes = Sm3Utils.encrypt(bytes);
if (Objects.nonNull(sm3Bytes)) {
sm3 = HexUtils.formatHex(sm3Bytes);
}
} catch (IOException e) {
log.error("计算文件码失败", e);
throw new BizError("计算文件码失败");
}
}
// 获取文件后缀
String suffix = FileUtils.getSuffix(fileName);
if (StringUtils.isBlank(suffix)) {
throw new BizError("文件格式错误");
}
// 文件存储器的文件名
String filePath = UuidUtils.get() + CP_LINE + fileName;
// 获取存储桶
String bucket = bizType.bucket();
// 计算过期时间
int expireTime = bizType.expireTime();
if (expireTime <= CP_NUM0) {
expireTime = CP_NUM1;
}
Instant expireDate = CalendarUtils.calc(Calendar.HOUR_OF_DAY, expireTime);
// 保存文件
storageService.save(bucket, filePath, inputStream, contentLength);
SysFileAddReq req = new SysFileAddReq();
req.setUserId(userId);
req.setUserName(userName);
req.setBizId(Objects.nonNull(bizId) ? bizId : bizType.code());
req.setBizType(bizType.desc());
req.setPermission(bizType.permission().code());
req.setBucket(bucket);
req.setStorageType(storageConfig.getType());
req.setUploadTime(uploadTime);
req.setFilePath(filePath);
req.setFileName(fileName);
req.setFileSize(contentLength);
req.setFileType(suffix);
req.setMd5(md5);
req.setSm3(sm3);
req.setDataType(FileDataType.TEMP.code());
req.setExpireTime(expireDate);
Result<Long> result = sysFileApi.add(req);
Long id = result.data();
if (CheckUtils.id(id)) {
return id;
}
// 保存记录失败删除文件
storageService.del(bucket, fileName);
throw new BizError("文件上传失败");
}
/**
* 文件下载
*
* @param bucket 存储桶
* @param fileName 文件名
* @param outputStream 输出流
* @return 文件信息
*/
@Override
public SysFileResp download(String bucket, String fileName, @NonNull OutputStream outputStream) {
return download(bucket, fileName, outputStream, null);
}
/**
* 文件下载
*
* @param bucket 存储桶
* @param fileName 文件名
* @param outputStream 输出流
* @param callback 回调
* @return 文件信息
*/
@Override
public SysFileResp download(String bucket, String fileName, @NonNull OutputStream outputStream, SysFileDownloadCallback callback) {
return download(getFileData(bucket, fileName), outputStream, callback);
}
/**
* 文件下载
*
* @param id 文件ID
* @param outputStream 输出流
* @return 文件信息
*/
@Override
public SysFileResp download(long id, @NonNull OutputStream outputStream) {
return download(id, outputStream, null);
}
/**
* 文件下载
*
* @param id 文件ID
* @param outputStream 输出流
* @param callback 回调
* @return 文件信息
*/
@Override
public SysFileResp download(long id, @NonNull OutputStream outputStream, SysFileDownloadCallback callback) {
return download(getFileData(id), outputStream, callback);
}
/**
* 文件下载
*
* @param data 文件信息
* @param outputStream 输出流
* @param callback 回调
* @return 文件信息
*/
private SysFileResp download(SysFileResp data, @NonNull OutputStream outputStream, SysFileDownloadCallback callback) {
if (Objects.isNull(data)) {
throw new BizError("文件不存在");
}
if (Objects.equals(FileDataType.DELETE.code(), data.getDataType())) {
throw new BizError("文件已删除");
}
if (Objects.equals(FileDataType.TEMP.code(), data.getDataType()) && Instant.now().isAfter(data.getExpireTime())) {
delete(data.getId());
throw new BizError("文件已过期");
}
String type = storageConfig.getType();
if (!Objects.equals(type, data.getStorageType())) {
throw new BizError("文件不存在");
}
if (Objects.nonNull(callback)) {
if (!callback.before(data)) {
return data;
}
}
storageService.get(data.getBucket(), data.getFilePath(), outputStream);
return data;
}
/**
* 文件删除
*
* @param id 文件ID
* @return 删除结果
*/
@Override
public boolean delete(long id) {
return delete(List.of(id));
}
/**
* 文件删除
*
* @param idList 文件ID列表
* @return 删除结果
*/
@Override
public boolean delete(List<Long> idList) {
if (CollectionUtils.isEmpty(idList)) {
return true;
}
sysFileApi.changeDataTypeByIds(new SysFileChangeReq(idList, FileDataType.DELETE));
return true;
}
/**
* 文件成功
*
* @param id 文件ID
* @return 成功结果
*/
@Override
public boolean success(long id) {
return success(List.of(id));
}
/**
* 文件成功
*
* @param idList 文件ID列表
* @return 成功结果
*/
@Override
public boolean success(List<Long> idList) {
if (CollectionUtils.isEmpty(idList)) {
return true;
}
sysFileApi.changeDataTypeByIds(new SysFileChangeReq(idList, FileDataType.OK));
return true;
}
/**
* 获取文件数据
*
* @param id 文件ID
* @return 文件数据
*/
private SysFileResp getFileData(long id) {
Result<SysFileResp> result = sysFileApi.getById(id);
SysFileResp data = result.data();
if (Objects.isNull(data)) {
throw new BizError("文件不存在");
}
return data;
}
/**
* 获取文件数据
*
* @param bucket 桶名称
* @param fileName 文件名
* @return 文件数据
*/
private SysFileResp getFileData(String bucket, String fileName) {
Result<SysFileResp> result = sysFileApi.getByName(bucket, fileName);
SysFileResp data = result.data();
if (Objects.isNull(data)) {
throw new BizError("文件不存在");
}
return data;
}
}

View File

@@ -0,0 +1,82 @@
package xtools.app.sys.file.utils;
import lombok.extern.slf4j.Slf4j;
import xtools.boot.api.exection.BizError;
import xtools.core.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Title : FileIdUtils</p>
* <p>Description : FileIdUtils</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/3/17 19:49
*/
@Slf4j
public class FileIdUtils {
/**
* 获取基础文件ID的正则表达式
*/
private final static Pattern BASE_PATTERN = Pattern.compile("/sys/common/get/(\\d+)/file");
/**
* 获取文件ID
*
* @param content 文件内容
* @return 文件ID
*/
public static List<Long> get(String content) {
return get(BASE_PATTERN, content);
}
/**
* 获取文件ID
*
* @param content 文件内容
* @param regex 正则表达式
* @return 文件ID
*/
public static List<Long> get(String content, String regex) {
if (StringUtils.isBlank(regex)) {
throw new BizError("regex 不能为空");
}
return get(Pattern.compile(regex), content);
}
/**
* 获取文件ID
*
* @param pattern 正则表达式
* @param content 文件内容
* @return 文件ID
*/
private static List<Long> get(Pattern pattern, String content) {
List<Long> idList = new ArrayList<>();
if (StringUtils.isBlank(content)) {
return idList;
}
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
String number = matcher.group(1);
if (StringUtils.isBlank(number)) {
continue;
}
try {
idList.add(Long.parseLong(number.trim()));
} catch (NumberFormatException e) {
log.info("{}无法转为Long类型", number);
}
}
return idList;
}
}