diff --git a/ruoyi-site/ruoyi-h5/pom.xml b/ruoyi-site/ruoyi-h5/pom.xml
index 204bc0ced..1e4e90aa8 100644
--- a/ruoyi-site/ruoyi-h5/pom.xml
+++ b/ruoyi-site/ruoyi-h5/pom.xml
@@ -84,6 +84,17 @@
dubbo-nacos-spring-boot-starter
+
+ me.liuxp
+ minio-plus-all-springboot-starter
+ 1.0.5
+
+
+ me.liuxp
+ s3-api-minio
+ 1.0.5
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/WebMvcConfig.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/WebMvcConfig.java
index e6dcc72d2..50a943410 100644
--- a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/WebMvcConfig.java
+++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/config/WebMvcConfig.java
@@ -1,7 +1,9 @@
package com.luban.api.config;
+import com.luban.api.s3.config.S3LoginUserInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -19,4 +21,14 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
+
+ /**
+ * 前置拦截器
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ // 登录用户获取用户id
+ registry.addInterceptor(new S3LoginUserInterceptor()).addPathPatterns("/storage/**");
+ }
+
}
diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/config/S3LoginUserInterceptor.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/config/S3LoginUserInterceptor.java
new file mode 100644
index 000000000..595642d4c
--- /dev/null
+++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/config/S3LoginUserInterceptor.java
@@ -0,0 +1,59 @@
+package com.luban.api.s3.config;
+
+
+import cn.hutool.core.text.CharSequenceUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.liuxp.minioplus.extension.context.UserHolder;
+import org.springframework.lang.Nullable;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ * 登录用户拦截器
+ *
+ * @author contact@liuxp.me
+ * @since 2024/06/11
+ */
+@Slf4j
+public class S3LoginUserInterceptor implements HandlerInterceptor {
+
+ /**
+ * 处理登录用户信息
+ *
+ * @param request 请求
+ * @param response 返回
+ * @param handler 要执行的处理程序
+ * @return 是否继续执行下一个拦截器
+ */
+ @Override
+ public boolean preHandle(HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull Object handler) {
+ String userId = request.getHeader("Authorization");
+ if (CharSequenceUtil.isBlank(userId)) {
+ UserHolder.set("");
+ } else {
+ //TODO 用户id还未生成
+ UserHolder.set(userId);
+ }
+
+ return true;
+ }
+
+ /**
+ * 清理资源
+ *
+ * @param request 请求
+ * @param response 返回
+ * @param handler 要执行的处理程序
+ * @param exception 异常信息
+ */
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception exception) {
+ log.debug("Project ThreadLocal 清理之前:{}", UserHolder.get());
+ UserHolder.clean();
+ log.debug("Project ThreadLocal 清理之之后:{}", UserHolder.get());
+ }
+}
diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/dao/FileMetadataInfoMapper.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/dao/FileMetadataInfoMapper.java
new file mode 100644
index 000000000..0af9a293f
--- /dev/null
+++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/dao/FileMetadataInfoMapper.java
@@ -0,0 +1,9 @@
+package com.luban.api.s3.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import com.luban.api.s3.entity.FileMetadataInfoEntity;
+
+@Mapper
+public interface FileMetadataInfoMapper extends BaseMapper {
+}
diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/entity/FileMetadataInfoEntity.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/entity/FileMetadataInfoEntity.java
new file mode 100644
index 000000000..4393a6470
--- /dev/null
+++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/entity/FileMetadataInfoEntity.java
@@ -0,0 +1,102 @@
+package com.luban.api.s3.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.Date;
+
+/**
+ * 文件元数据信息表Entity
+ *
+ * @author contact@liuxp.me
+ * @since 2024-05-22
+ **/
+@Getter
+@Setter
+@ToString
+@TableName(value = "file_metadata_info")
+public class FileMetadataInfoEntity {
+
+ @TableId(value = "id", type = IdType.AUTO)
+ @Schema(description = "主键")
+ private Long id;
+
+ @Schema(description = "文件KEY")
+ @TableField(value = "file_key")
+ private String fileKey;
+
+ @Schema(description = "文件md5")
+ @TableField(value = "file_md5")
+ private String fileMd5;
+
+ @Schema(description = "文件名")
+ @TableField(value = "file_name")
+ private String fileName;
+
+ @Schema(description = "MIME类型")
+ @TableField(value = "file_mime_type")
+ private String fileMimeType;
+
+ @Schema(description = "文件后缀")
+ @TableField(value = "file_suffix")
+ private String fileSuffix;
+
+ @Schema(description = "文件长度")
+ @TableField(value = "file_size")
+ private Long fileSize;
+
+ @Schema(description = "预览图 0:无 1:有")
+ @TableField(value = "is_preview")
+ private Boolean isPreview;
+
+ @Schema(description = "是否私有 0:否 1:是")
+ @TableField(value = "is_private")
+ private Boolean isPrivate;
+
+ @Schema(description = "存储桶")
+ @TableField(value = "bucket")
+ private String storageBucket;
+
+ @Schema(description = "存储桶路径")
+ @TableField(value = "bucket_path")
+ private String storagePath;
+
+ @Schema(description = "上传任务id,用于合并切片")
+ @TableField(value = "upload_id")
+ private String uploadTaskId;
+
+ @Schema(description = "状态 0:未完成 1:已完成")
+ @TableField(value = "is_finished")
+ private Boolean isFinished;
+
+ @Schema(description = "是否分块 0:否 1:是")
+ @TableField(value = "is_part")
+ private Boolean isPart;
+
+ @Schema(description = "分块数量")
+ @TableField(value = "part_number")
+ private Integer partNumber;
+
+ @Schema(description = "创建人")
+ @TableField("create_user")
+ private String createUser;
+
+ @Schema(description = "创建时间")
+ @TableField("create_time")
+ private Date createTime;
+
+ @Schema(description = "修改人")
+ @TableField("update_user")
+ private String updateUser;
+
+ @Schema(description = "修改时间")
+ @TableField("update_time")
+ private Date updateTime;
+
+}
diff --git a/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/service/MetadataRepositoryImpl.java b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/service/MetadataRepositoryImpl.java
new file mode 100644
index 000000000..0051e6097
--- /dev/null
+++ b/ruoyi-site/ruoyi-h5/src/main/java/com/luban/api/s3/service/MetadataRepositoryImpl.java
@@ -0,0 +1,121 @@
+package com.luban.api.s3.service;
+
+
+import cn.hutool.core.text.CharSequenceUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.liuxp.minioplus.api.model.dto.FileMetadataInfoDTO;
+import org.liuxp.minioplus.api.model.dto.FileMetadataInfoSaveDTO;
+import org.liuxp.minioplus.api.model.dto.FileMetadataInfoUpdateDTO;
+import org.liuxp.minioplus.api.model.vo.FileMetadataInfoVo;
+import org.liuxp.minioplus.core.repository.MetadataRepository;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import com.luban.api.s3.dao.FileMetadataInfoMapper;
+import com.luban.api.s3.entity.FileMetadataInfoEntity;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 文件元数据接口实现类
+ *
+ * @author contact@liuxp.me
+ * @since 2024/05/22
+ */
+@Slf4j
+@Service
+public class MetadataRepositoryImpl extends ServiceImpl implements MetadataRepository {
+
+ @Override
+ public List list(FileMetadataInfoDTO searchDTO) {
+
+ // 组装查询参数
+ QueryWrapper queryWrapper = buildParams(searchDTO);
+
+ List fileMetadataInfoEntityList = super.list(queryWrapper);
+
+ List fileMetadataInfoVoList = new ArrayList<>();
+
+ for (FileMetadataInfoEntity fileMetadataInfoEntity : fileMetadataInfoEntityList) {
+ FileMetadataInfoVo fileMetadataInfoVo = new FileMetadataInfoVo();
+ BeanUtils.copyProperties(fileMetadataInfoEntity, fileMetadataInfoVo);
+ fileMetadataInfoVoList.add(fileMetadataInfoVo);
+ }
+
+ return fileMetadataInfoVoList;
+ }
+
+ @Override
+ public FileMetadataInfoVo one(FileMetadataInfoDTO searchDTO) {
+
+ // 组装查询参数
+ QueryWrapper queryWrapper = buildParams(searchDTO);
+ queryWrapper.last("limit 1");
+
+ FileMetadataInfoEntity fileMetadataInfoEntity = super.getOne(queryWrapper);
+
+ FileMetadataInfoVo fileMetadataInfoVo = null;
+
+ if (null != fileMetadataInfoEntity) {
+ fileMetadataInfoVo = new FileMetadataInfoVo();
+ BeanUtils.copyProperties(fileMetadataInfoEntity, fileMetadataInfoVo);
+ }
+
+ return fileMetadataInfoVo;
+ }
+
+ @Override
+ public FileMetadataInfoVo save(FileMetadataInfoSaveDTO saveDTO) {
+
+ FileMetadataInfoEntity fileMetadataInfoEntity = new FileMetadataInfoEntity();
+ BeanUtils.copyProperties(saveDTO, fileMetadataInfoEntity);
+ fileMetadataInfoEntity.setCreateTime(new Date());
+ fileMetadataInfoEntity.setUpdateTime(new Date());
+
+ boolean result = super.save(fileMetadataInfoEntity);
+
+ FileMetadataInfoVo fileMetadataInfoVo = new FileMetadataInfoVo();
+ if (result) {
+ BeanUtils.copyProperties(fileMetadataInfoEntity, fileMetadataInfoVo);
+ }
+
+ return fileMetadataInfoVo;
+ }
+
+ @Override
+ public FileMetadataInfoVo update(FileMetadataInfoUpdateDTO updateDTO) {
+
+ FileMetadataInfoEntity fileMetadataInfoEntity = new FileMetadataInfoEntity();
+ BeanUtils.copyProperties(updateDTO, fileMetadataInfoEntity);
+ fileMetadataInfoEntity.setUpdateTime(new Date());
+ boolean result = super.updateById(fileMetadataInfoEntity);
+
+ FileMetadataInfoVo fileMetadataInfoVo = new FileMetadataInfoVo();
+ if (result) {
+ BeanUtils.copyProperties(fileMetadataInfoEntity, fileMetadataInfoVo);
+ }
+
+ return fileMetadataInfoVo;
+ }
+
+ @Override
+ public Boolean remove(Long id) {
+ return super.removeById(id);
+ }
+
+ private QueryWrapper buildParams(FileMetadataInfoDTO searchDTO) {
+ // 组装查询参数
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq(CharSequenceUtil.isNotBlank(searchDTO.getFileKey()), "file_key", searchDTO.getFileKey());
+ queryWrapper.eq(CharSequenceUtil.isNotBlank(searchDTO.getFileMd5()), "file_md5", searchDTO.getFileMd5());
+ queryWrapper.eq(CharSequenceUtil.isNotBlank(searchDTO.getBucket()), "bucket", searchDTO.getBucket());
+ queryWrapper.eq(null != searchDTO.getIsPrivate(), "is_private", searchDTO.getIsPrivate());
+ queryWrapper.eq(CharSequenceUtil.isNotBlank(searchDTO.getCreateUser()), "create_user", searchDTO.getCreateUser());
+
+ return queryWrapper;
+ }
+
+}
diff --git a/ruoyi-site/ruoyi-h5/src/main/resources/application.yml b/ruoyi-site/ruoyi-h5/src/main/resources/application.yml
index da568eed3..3e1e3a85e 100644
--- a/ruoyi-site/ruoyi-h5/src/main/resources/application.yml
+++ b/ruoyi-site/ruoyi-h5/src/main/resources/application.yml
@@ -73,3 +73,10 @@ mybatis-plus:
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
# 如需改为自增 需要将数据库表全部设置为自增
idType: ASSIGN_ID
+minioplus:
+ # rustfs 部署地址
+ backend: http://localhost:9000
+ # 授权key
+ key: rustfsadmin
+ # 密钥
+ secret: rustfsadmin
diff --git a/script/ruoyi_docker/docker-compose.yml b/script/ruoyi_docker/docker-compose.yml
index 1d976fa91..95ba9afab 100644
--- a/script/ruoyi_docker/docker-compose.yml
+++ b/script/ruoyi_docker/docker-compose.yml
@@ -7,3 +7,18 @@ services:
- /d/Program Files/docker/path/mall-swarm/mongo/db:/data/db #数据文件挂载
ports:
- 27017:27017
+ rustfs:
+ image: registry.cn-shanghai.aliyuncs.com/study-03/rustfs:latest
+ container_name: rustfs
+ ports:
+ - "19000:9000" # 管理控制台
+ - "9090:9090" # API服务端口
+ volumes:
+ - rustfs_data_volume:/data
+ environment:
+ - RUSTFS_ROOT_USER=admin
+ - RUSTFS_ROOT_PASSWORD=admin123
+ restart: unless-stopped
+
+volumes:
+ rustfs_data_volume: