Jelajahi Sumber

完成新 File 的功能

YunaiV 3 tahun lalu
induk
melakukan
87670d18fd
26 mengubah file dengan 277 tambahan dan 205 penghapusan
  1. 1 3
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/AbstractFileClient.java
  2. 11 4
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClient.java
  3. 20 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileContentFrameworkDAO.java
  4. 12 12
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
  5. 0 22
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/FileRespVO.java
  6. 2 2
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java
  7. 31 0
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java
  8. 1 1
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConvert.java
  9. 5 13
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java
  10. 41 0
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java
  11. 9 0
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java
  12. 2 17
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java
  13. 0 12
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/FileConfiguration.java
  14. 0 22
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/FileProperties.java
  15. 0 16
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/package-info.java
  16. 1 1
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java
  17. 16 0
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java
  18. 5 0
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java
  19. 6 5
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java
  20. 33 19
      yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
  21. 51 36
      yudao-module-infra/yudao-module-infra-impl/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java
  22. 5 2
      yudao-module-infra/yudao-module-infra-impl/src/test/resources/sql/create_tables.sql
  23. 0 2
      yudao-server/src/main/resources/application-dev.yaml
  24. 0 2
      yudao-server/src/main/resources/application-local.yaml
  25. 1 1
      yudao-server/src/main/resources/application.yaml
  26. 24 13
      yudao-ui-admin/src/views/infra/file/index.vue

+ 1 - 3
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/AbstractFileClient.java

@@ -1,8 +1,6 @@
 package cn.iocoder.yudao.framework.file.core.client;
 
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.file.core.client.FileClient;
-import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
 import lombok.extern.slf4j.Slf4j;
 
 /**
@@ -65,7 +63,7 @@ public abstract class AbstractFileClient<Config extends FileClientConfig> implem
      * @return URL 访问地址
      */
     protected String formatFileUrl(String domain, String path) {
-        return StrUtil.format("{}/system-api/{}/get/{}", domain, getId(), path);
+        return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path);
     }
 
 }

+ 11 - 4
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileClient.java

@@ -18,24 +18,31 @@ public class DBFileClient extends AbstractFileClient<DBFileClientConfig> {
 
     @Override
     protected void doInit() {
-        dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
     }
 
     @Override
     public String upload(byte[] content, String path) {
-        dao.insert(getId(), path, content);
+        getDao().insert(getId(), path, content);
         // 拼接返回路径
         return super.formatFileUrl(config.getDomain(), path);
     }
 
     @Override
     public void delete(String path) {
-        dao.delete(getId(), path);
+        getDao().delete(getId(), path);
     }
 
     @Override
     public byte[] getContent(String path) {
-        return dao.selectContent(getId(), path);
+        return getDao().selectContent(getId(), path);
+    }
+
+    private DBFileContentFrameworkDAO getDao() {
+        // 延迟获取,因为 SpringUtil 初始化太慢
+        if (dao == null) {
+            dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
+        }
+        return dao;
     }
 
 }

+ 20 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/db/DBFileContentFrameworkDAO.java

@@ -7,10 +7,30 @@ package cn.iocoder.yudao.framework.file.core.client.db;
  */
 public interface DBFileContentFrameworkDAO {
 
+    /**
+     * 插入文件内容
+     *
+     * @param configId 配置编号
+     * @param path 路径
+     * @param content 内容
+     */
     void insert(Long configId, String path, byte[] content);
 
+    /**
+     * 删除文件内容
+     *
+     * @param configId 配置编号
+     * @param path 路径
+     */
     void delete(Long configId, String path);
 
+    /**
+     * 获得文件内容
+     *
+     * @param configId 配置编号
+     * @param path 路径
+     * @return 内容
+     */
     byte[] selectContent(Long configId, String path);
 
 }

+ 12 - 12
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java

@@ -4,8 +4,8 @@ import cn.hutool.core.io.IoUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
 import cn.iocoder.yudao.module.infra.convert.file.FileConvert;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.service.file.FileService;
@@ -50,9 +50,9 @@ public class FileController {
 
     @DeleteMapping("/delete")
     @ApiOperation("删除文件")
-    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = String.class)
+    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
     @PreAuthorize("@ss.hasPermission('infra:file:delete')")
-    public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
+    public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) {
         fileService.deleteFile(id);
         return success(true);
     }
@@ -60,19 +60,19 @@ public class FileController {
     @GetMapping("/{configId}/get/{path}")
     @ApiOperation("下载文件")
     @ApiImplicitParams({
-            @ApiImplicitParam(name = "configId", value = "配置编号",  required = true, dataTypeClass = String.class),
+            @ApiImplicitParam(name = "configId", value = "配置编号",  required = true, dataTypeClass = Long.class),
             @ApiImplicitParam(name = "path", value = "文件路径", required = true, dataTypeClass = String.class)
     })
-    public void getFile(HttpServletResponse response,
-                        @PathVariable("configId") String configId,
-                        @PathVariable("path") String path) throws IOException {
-        FileDO file = fileService.getFile(path);
-        if (file == null) {
-            log.warn("[getFile][path({}) 文件不存在]", path);
+    public void getFileContent(HttpServletResponse response,
+                               @PathVariable("configId") Long configId,
+                               @PathVariable("path") String path) throws IOException {
+        byte[] content = fileService.getFileContent(configId, path);
+        if (content == null) {
+            log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
             response.setStatus(HttpStatus.NOT_FOUND.value());
             return;
         }
-        ServletUtils.writeAttachment(response, path, file.getContent());
+        ServletUtils.writeAttachment(response, path, content);
     }
 
     @GetMapping("/page")

+ 0 - 22
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/FileRespVO.java

@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.Date;
-
-@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
-@Data
-public class FileRespVO {
-
-    @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
-    private String id;
-
-    @ApiModelProperty(value = "文件类型", required = true, example = "jpg")
-    private String type;
-
-    @ApiModelProperty(value = "创建时间", required = true)
-    private Date createTime;
-
-}

+ 2 - 2
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/FilePageReqVO.java → yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
+package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import io.swagger.annotations.ApiModel;
@@ -19,7 +19,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 public class FilePageReqVO extends PageParam {
 
     @ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
-    private String id;
+    private String path;
 
     @ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
     private String type;

+ 31 - 0
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
+@Data
+public class FileRespVO {
+
+    @ApiModelProperty(value = "文件编号", required = true, example = "1024")
+    private Long id;
+
+    @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
+    private String path;
+
+    @ApiModelProperty(value = "文件 URL", required = true, example = "https://www.iocoder.cn/yudao.jpg")
+    private String url;
+
+    @ApiModelProperty(value = "文件类型", example = "jpg")
+    private String type;
+
+    @ApiModelProperty(value = "文件大小", example = "2048", required = true)
+    private Integer size;
+
+    @ApiModelProperty(value = "创建时间", required = true)
+    private Date createTime;
+
+}

+ 1 - 1
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConvert.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.infra.convert.file;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;

+ 5 - 13
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.module.infra.dal.dataobject.file;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-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 lombok.*;
 
@@ -27,8 +25,7 @@ public class FileDO extends BaseDO {
     /**
      * 编号,数据库自增
      */
-    @TableId(type = IdType.INPUT)
-    private String id;
+    private Long id;
     /**
      * 配置编号
      *
@@ -39,6 +36,10 @@ public class FileDO extends BaseDO {
      * 路径,即文件名
      */
     private String path;
+    /**
+     * 访问地址
+     */
+    private String url;
     /**
      * 文件类型
      *
@@ -46,18 +47,9 @@ public class FileDO extends BaseDO {
      */
     @TableField(value = "`type`")
     private String type;
-    /**
-     * 访问地址
-     */
-    private String url;
     /**
      * 文件大小
      */
     private Integer size;
-    /**
-     * 文件内容
-     */
-    @Deprecated
-    private byte[] content;
 
 }

+ 41 - 0
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.file;
+
+import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+
+@Repository
+public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
+
+    @Resource
+    private FileContentMapper fileContentMapper;
+
+    @Override
+    public void insert(Long configId, String path, byte[] content) {
+        FileContentDO entity = new FileContentDO().setConfigId(configId)
+                .setPath(path).setContent(content);
+        fileContentMapper.insert(entity);
+    }
+
+    @Override
+    public void delete(Long configId, String path) {
+        fileContentMapper.delete(buildQuery(configId, path));
+    }
+
+    @Override
+    public byte[] selectContent(Long configId, String path) {
+        FileContentDO fileContentDO = fileContentMapper.selectOne(
+                buildQuery(configId, path).select(FileContentDO::getContent));
+        return fileContentDO != null ? fileContentDO.getContent() : null;
+    }
+
+    private LambdaQueryWrapper<FileContentDO> buildQuery(Long configId, String path) {
+        return new LambdaQueryWrapper<FileContentDO>()
+                .eq(FileContentDO::getConfigId, configId)
+                .eq(FileContentDO::getPath, path);
+    }
+
+}

+ 9 - 0
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.file;
+
+import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FileContentMapper extends BaseMapper<FileContentDO> {
+}

+ 2 - 17
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.dal.mysql.file;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -17,25 +17,10 @@ public interface FileMapper extends BaseMapperX<FileDO> {
 
     default PageResult<FileDO> selectPage(FilePageReqVO reqVO) {
         return selectPage(reqVO, new QueryWrapperX<FileDO>()
-                .likeIfPresent("id", reqVO.getId())
+                .likeIfPresent("path", reqVO.getPath())
                 .likeIfPresent("type", reqVO.getType())
                 .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
                 .orderByDesc("create_time"));
     }
 
-    default Long selectCountById(String id) {
-        return selectCount(FileDO::getId, id);
-    }
-
-    /**
-     * 基于 Path 获取文件
-     * 实际上,是基于 ID 查询
-     *
-     * @param path 路径
-     * @return 文件
-     */
-    default FileDO selectByPath(String path) {
-        return selectById(path);
-    }
-
 }

+ 0 - 12
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/FileConfiguration.java

@@ -1,12 +0,0 @@
-package cn.iocoder.yudao.module.infra.framework.file.config;
-
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * 文件 配置类
- */
-@Configuration
-@EnableConfigurationProperties(FileProperties.class)
-public class FileConfiguration {
-}

+ 0 - 22
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/FileProperties.java

@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.module.infra.framework.file.config;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.validation.annotation.Validated;
-
-import javax.validation.constraints.NotNull;
-
-@ConfigurationProperties(prefix = "yudao.file")
-@Validated
-@Data
-public class FileProperties {
-
-    /**
-     * 对应 FileController 的 getFile 方法
-     */
-    @NotNull(message = "基础文件路径不能为空")
-    private String basePath;
-
-    // TODO 七牛、等等
-
-}

+ 0 - 16
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/file/package-info.java

@@ -1,16 +0,0 @@
-/**
- * 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务
- *
- * 在不采用云服务的情况下,我们有几种技术选型:
- * 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等
- * 方案 2. 使用服务器的文件系统存储
- * 方案 3. 使用数据库进行存储
- *
- * 如果考虑额外在搭建服务,推荐方案 1。
- * 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做
- * 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件
- *
- * 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。
- * 随着文件的量级大了之后,还是推荐采用云服务。
- */
-package cn.iocoder.yudao.module.infra.framework.file;

+ 1 - 1
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java

@@ -36,7 +36,7 @@ public class SecurityConfiguration {
                 registry.antMatchers(adminSeverContextPath).anonymous()
                         .antMatchers(adminSeverContextPath + "/**").anonymous();
                 // 文件的获取接口,可匿名访问
-                registry.antMatchers(buildAdminApi("/infra/file/get/**"), buildAppApi("/infra/file/get/**")).anonymous();
+                registry.antMatchers(buildAdminApi("/infra/file/*/get/**"), buildAppApi("/infra/file/get/**")).permitAll();
             }
 
         };

+ 16 - 0
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.infra.service.file;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.file.core.client.FileClient;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
@@ -83,4 +84,19 @@ public interface FileConfigService {
      */
     String testFileConfig(Long id);
 
+    /**
+     * 获得指定编号的文件客户端
+     *
+     * @param id 配置编号
+     * @return 文件客户端
+     */
+    FileClient getFileClient(Long id);
+
+    /**
+     * 获得 Master 文件客户端
+     *
+     * @return 文件客户端
+     */
+    FileClient getMasterFileClient();
+
 }

+ 5 - 0
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java

@@ -233,4 +233,9 @@ public class FileConfigServiceImpl implements FileConfigService {
         return fileClientFactory.getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg");
     }
 
+    @Override
+    public FileClient getFileClient(Long id) {
+        return fileClientFactory.getFileClient(id);
+    }
+
 }

+ 6 - 5
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.infra.service.file;
 
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 
@@ -33,14 +33,15 @@ public interface FileService {
      *
      * @param id 编号
      */
-    void deleteFile(String id);
+    void deleteFile(Long id);
 
     /**
-     * 获得文件
+     * 获得文件内容
      *
+     * @param configId 配置编号
      * @param path 文件路径
-     * @return 文件
+     * @return 文件内容
      */
-    FileDO getFile(String path);
+    byte[] getFileContent(Long configId, String path);
 
 }

+ 33 - 19
yudao-module-infra/yudao-module-infra-impl/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java

@@ -1,18 +1,19 @@
 package cn.iocoder.yudao.module.infra.service.file;
 
 import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
+import cn.iocoder.yudao.framework.file.core.client.FileClient;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
-import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.io.ByteArrayInputStream;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
 
 /**
  * 文件 Service 实现类
@@ -23,10 +24,10 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
 public class FileServiceImpl implements FileService {
 
     @Resource
-    private FileMapper fileMapper;
+    private FileConfigService fileConfigService;
 
     @Resource
-    private FileProperties fileProperties;
+    private FileMapper fileMapper;
 
     @Override
     public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
@@ -35,36 +36,49 @@ public class FileServiceImpl implements FileService {
 
     @Override
     public String createFile(String path, byte[] content) {
-        if (fileMapper.selectCountById(path) > 0) {
-            throw exception(FILE_PATH_EXISTS);
-        }
+        // 上传到文件存储器
+        FileClient client = fileConfigService.getMasterFileClient();
+        Assert.notNull(client, "客户端(master) 不能为空");
+        String url = client.upload(content, path);
+
         // 保存到数据库
         FileDO file = new FileDO();
-        file.setId(path);
+        file.setConfigId(client.getId());
+        file.setPath(path);
+        file.setUrl(url);
         file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
-        file.setContent(content);
+        file.setSize(content.length);
         fileMapper.insert(file);
-        // 拼接路径返回
-        return fileProperties.getBasePath() + path;
+        return url;
     }
 
     @Override
-    public void deleteFile(String id) {
+    public void deleteFile(Long id) {
         // 校验存在
-        this.validateFileExists(id);
-        // 更新
+        FileDO file = this.validateFileExists(id);
+
+        // 从文件存储器中删除
+        FileClient client = fileConfigService.getFileClient(file.getConfigId());
+        Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
+        client.delete(file.getPath());
+
+        // 删除记录
         fileMapper.deleteById(id);
     }
 
-    private void validateFileExists(String id) {
-        if (fileMapper.selectById(id) == null) {
+    private FileDO validateFileExists(Long id) {
+        FileDO fileDO = fileMapper.selectById(id);
+        if (fileDO == null) {
             throw exception(FILE_NOT_EXISTS);
         }
+        return fileDO;
     }
 
     @Override
-    public FileDO getFile(String path) {
-        return fileMapper.selectByPath(path);
+    public byte[] getFileContent(Long configId, String path) {
+        FileClient client = fileConfigService.getFileClient(configId);
+        Assert.notNull(client, "客户端({}) 不能为空", configId);
+        return client.getContent(path);
     }
 
 }

+ 51 - 36
yudao-module-infra/yudao-module-infra-impl/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java

@@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.infra.service.file;
 import cn.hutool.core.io.resource.ResourceUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.file.core.client.FileClient;
 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
-import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
 import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
@@ -17,47 +17,46 @@ import javax.annotation.Resource;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.*;
 
-@Import({FileServiceImpl.class, FileProperties.class})
+@Import({FileServiceImpl.class})
 public class FileServiceTest extends BaseDbUnitTest {
 
     @Resource
     private FileService fileService;
 
-    @MockBean
-    private FileProperties fileProperties;
-
     @Resource
     private FileMapper fileMapper;
 
+    @MockBean
+    private FileConfigService fileConfigService;
+
     @Test
     public void testGetFilePage() {
         // mock 数据
         FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到
-            o.setId("yunai");
+            o.setPath("yunai");
             o.setType("jpg");
             o.setCreateTime(buildTime(2021, 1, 15));
         });
         fileMapper.insert(dbFile);
-        // 测试 id 不匹配
-        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setId("tudou")));
+        // 测试 path 不匹配
+        fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou")));
         // 测试 type 不匹配
         fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
-            o.setId("yunai02");
             o.setType("png");
         }));
         // 测试 createTime 不匹配
         fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
-            o.setId("yunai03");
             o.setCreateTime(buildTime(2020, 1, 15));
         }));
         // 准备参数
         FilePageReqVO reqVO = new FilePageReqVO();
-        reqVO.setId("yunai");
+        reqVO.setPath("yunai");
         reqVO.setType("jp");
         reqVO.setBeginCreateTime(buildTime(2021, 1, 10));
         reqVO.setEndCreateTime(buildTime(2021, 1, 20));
@@ -67,7 +66,7 @@ public class FileServiceTest extends BaseDbUnitTest {
         // 断言
         assertEquals(1, pageResult.getTotal());
         assertEquals(1, pageResult.getList().size());
-        AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0), "content");
+        AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
     }
 
     @Test
@@ -75,52 +74,68 @@ public class FileServiceTest extends BaseDbUnitTest {
         // 准备参数
         String path = randomString();
         byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+        // mock Master 文件客户端
+        FileClient client = mock(FileClient.class);
+        when(fileConfigService.getMasterFileClient()).thenReturn(client);
+        String url = randomString();
+        when(client.upload(same(content), same(path))).thenReturn(url);
+        when(client.getId()).thenReturn(10L);
 
         // 调用
-        String url = fileService.createFile(path, content);
+        String result = fileService.createFile(path, content);
         // 断言
-        assertEquals(fileProperties.getBasePath() + path, url);
+        assertEquals(result, url);
         // 校验数据
-        FileDO file = fileMapper.selectById(path);
-        assertEquals(path, file.getId());
+        FileDO file = fileMapper.selectOne(FileDO::getPath, path);
+        assertEquals(10L, file.getConfigId());
+        assertEquals(path, file.getPath());
+        assertEquals(url, file.getUrl());
         assertEquals("jpg", file.getType());
-        assertArrayEquals(content, file.getContent());
-    }
-
-    @Test
-    public void testCreateFile_exists() {
-        // mock 数据
-        FileDO dbFile = randomPojo(FileDO.class);
-        fileMapper.insert(dbFile);
-        // 准备参数
-        String path = dbFile.getId(); // 模拟已存在
-        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
-
-        // 调用,并断言异常
-        assertServiceException(() -> fileService.createFile(path, content), FILE_PATH_EXISTS);
+        assertEquals(content.length, file.getSize());
     }
 
     @Test
     public void testDeleteFile_success() {
         // mock 数据
-        FileDO dbFile = randomPojo(FileDO.class);
+        FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg"));
         fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据
+        // mock Master 文件客户端
+        FileClient client = mock(FileClient.class);
+        when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
         // 准备参数
-        String id = dbFile.getId();
+        Long id = dbFile.getId();
 
         // 调用
         fileService.deleteFile(id);
         // 校验数据不存在了
         assertNull(fileMapper.selectById(id));
+        // 校验调用
+        verify(client).delete(eq("tudou.jpg"));
     }
 
     @Test
     public void testDeleteFile_notExists() {
         // 准备参数
-        String id = randomString();
+        Long id = randomLongId();
 
         // 调用, 并断言异常
         assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS);
     }
 
+    @Test
+    public void testGetFileContent() {
+        // 准备参数
+        Long configId = 10L;
+        String path = "tudou.jpg";
+        // mock 方法
+        FileClient client = mock(FileClient.class);
+        when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
+        byte[] content = new byte[]{};
+        when(client.getContent(eq("tudou.jpg"))).thenReturn(content);
+
+        // 调用
+        byte[] result = fileService.getFileContent(configId, path);
+        // 断言
+        assertSame(result, content);
+    }
 }

+ 5 - 2
yudao-module-infra/yudao-module-infra-impl/src/test/resources/sql/create_tables.sql

@@ -32,9 +32,12 @@ CREATE TABLE IF NOT EXISTS "infra_file_config" (
 ) COMMENT '文件配置表';
 
 CREATE TABLE IF NOT EXISTS "infra_file" (
-    "id" varchar(188) NOT NULL,
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "config_id" bigint NOT NULL,
+    "path" varchar(512),
+    "url" varchar(1024),
     "type" varchar(63) DEFAULT NULL,
-    "content" blob NOT NULL,
+    "size" bigint NOT NULL,
     "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "updater" varchar(64) DEFAULT '',

+ 0 - 2
yudao-server/src/main/resources/application-dev.yaml

@@ -172,8 +172,6 @@ yudao:
     session-timeout: 30m
     mock-enable: true
     mock-secret: test
-  file:
-    base-path: http://api-dashboard.yudao.iocoder.cn${yudao.web.admin-api.prefix}/infra/file/get/
   xss:
     enable: false
     exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系

+ 0 - 2
yudao-server/src/main/resources/application-local.yaml

@@ -184,8 +184,6 @@ yudao:
     session-timeout: 1d
     mock-enable: true
     mock-secret: test
-  file:
-    base-path: http://127.0.0.1:${server.port}${yudao.web.admin-api.prefix}/infra/file/get/
   xss:
     enable: false
     exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系

+ 1 - 1
yudao-server/src/main/resources/application.yaml

@@ -92,7 +92,7 @@ yudao:
     ignore-urls:
       - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
       - /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关
-      - /admin-api/infra/file/get/* # 获取图片,和租户无关
+      - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
       - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
     ignore-tables:
       - system_tenant

+ 24 - 13
yudao-ui-admin/src/views/infra/file/index.vue

@@ -3,8 +3,8 @@
 
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="文件路径" prop="id">
-        <el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
+      <el-form-item label="文件路径" prop="path">
+        <el-input v-model="queryParams.path" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="文件类型" prop="type">
         <el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
@@ -31,21 +31,23 @@
 
     <!-- 列表 -->
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="文件路径" align="center" prop="id" width="300" />
+      <el-table-column label="文件名" align="center" prop="path" />
+      <el-table-column label="URL" align="center" prop="url" />
+      <el-table-column label="文件大小" align="center" prop="size" width="120" :formatter="sizeFormat" />
       <el-table-column label="文件类型" align="center" prop="type" width="80" />
-      <el-table-column label="文件内容" align="center" prop="content">
-        <template slot-scope="scope">
-          <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"
-               width="200px" :src="getFileUrl + scope.row.id">
-          <i v-else>非图片,无法预览</i>
-        </template>
-      </el-table-column>
-      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+<!--      <el-table-column label="文件内容" align="center" prop="content">-->
+<!--        <template slot-scope="scope">-->
+<!--          <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"-->
+<!--               width="200px" :src="getFileUrl + scope.row.id">-->
+<!--          <i v-else>非图片,无法预览</i>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="上传时间" align="center" prop="createTime" width="180">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.createTime) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
         <template slot-scope="scope">
           <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
                      v-hasPermi="['infra:file:delete']">删除</el-button>
@@ -102,7 +104,7 @@ export default {
       queryParams: {
         pageNo: 1,
         pageSize: 10,
-        id: null,
+        path: null,
         type: null,
       },
       // 用户导入参数
@@ -193,6 +195,15 @@ export default {
         this.$modal.msgSuccess("删除成功");
       }).catch(() => {});
     },
+    // 用户昵称展示
+    sizeFormat(row, column) {
+      const unitArr = ["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"];
+      const srcSize = parseFloat(row.size);
+      const index = Math.floor(Math.log(srcSize) / Math.log(1024));
+      let size =srcSize/Math.pow(1024,index);
+      size = size.toFixed(2);//保留的小数位数
+      return size + ' ' + unitArr[index];
+    },
   }
 };
 </script>