完成新 File 的功能

This commit is contained in:
YunaiV 2022-03-16 23:31:26 +08:00
parent cdcecd0d4a
commit 87670d18fd
26 changed files with 277 additions and 205 deletions

View File

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

View File

@ -18,24 +18,31 @@ public class DBFileClient extends AbstractFileClient<DBFileClientConfig> {
@Override @Override
protected void doInit() { protected void doInit() {
dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
} }
@Override @Override
public String upload(byte[] content, String path) { public String upload(byte[] content, String path) {
dao.insert(getId(), path, content); getDao().insert(getId(), path, content);
// 拼接返回路径 // 拼接返回路径
return super.formatFileUrl(config.getDomain(), path); return super.formatFileUrl(config.getDomain(), path);
} }
@Override @Override
public void delete(String path) { public void delete(String path) {
dao.delete(getId(), path); getDao().delete(getId(), path);
} }
@Override @Override
public byte[] getContent(String path) { 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;
} }
} }

View File

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

View File

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

View File

@ -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;
}

View File

@ -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 cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.annotations.ApiModel; 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 { public class FilePageReqVO extends PageParam {
@ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配") @ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
private String id; private String path;
@ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配") @ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
private String type; private String type;

View File

@ -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;
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.infra.convert.file; package cn.iocoder.yudao.module.infra.convert.file;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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 cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;

View File

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

View File

@ -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);
}
}

View File

@ -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> {
}

View File

@ -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.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 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 cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -17,25 +17,10 @@ public interface FileMapper extends BaseMapperX<FileDO> {
default PageResult<FileDO> selectPage(FilePageReqVO reqVO) { default PageResult<FileDO> selectPage(FilePageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<FileDO>() return selectPage(reqVO, new QueryWrapperX<FileDO>()
.likeIfPresent("id", reqVO.getId()) .likeIfPresent("path", reqVO.getPath())
.likeIfPresent("type", reqVO.getType()) .likeIfPresent("type", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("create_time")); .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);
}
} }

View File

@ -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 {
}

View File

@ -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 七牛等等
}

View File

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

View File

@ -36,7 +36,7 @@ public class SecurityConfiguration {
registry.antMatchers(adminSeverContextPath).anonymous() registry.antMatchers(adminSeverContextPath).anonymous()
.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();
} }
}; };

View File

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

View File

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

View File

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

View File

@ -1,18 +1,19 @@
package cn.iocoder.yudao.module.infra.service.file; package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; 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 org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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 实现类 * 文件 Service 实现类
@ -23,10 +24,10 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
public class FileServiceImpl implements FileService { public class FileServiceImpl implements FileService {
@Resource @Resource
private FileMapper fileMapper; private FileConfigService fileConfigService;
@Resource @Resource
private FileProperties fileProperties; private FileMapper fileMapper;
@Override @Override
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) { public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
@ -35,36 +36,49 @@ public class FileServiceImpl implements FileService {
@Override @Override
public String createFile(String path, byte[] content) { 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(); 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.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
file.setContent(content); file.setSize(content.length);
fileMapper.insert(file); fileMapper.insert(file);
// 拼接路径返回 return url;
return fileProperties.getBasePath() + path;
} }
@Override @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); fileMapper.deleteById(id);
} }
private void validateFileExists(String id) { private FileDO validateFileExists(Long id) {
if (fileMapper.selectById(id) == null) { FileDO fileDO = fileMapper.selectById(id);
if (fileDO == null) {
throw exception(FILE_NOT_EXISTS); throw exception(FILE_NOT_EXISTS);
} }
return fileDO;
} }
@Override @Override
public FileDO getFile(String path) { public byte[] getFileContent(Long configId, String path) {
return fileMapper.selectByPath(path); FileClient client = fileConfigService.getFileClient(configId);
Assert.notNull(client, "客户端({}) 不能为空", configId);
return client.getContent(path);
} }
} }

View File

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

View File

@ -32,9 +32,12 @@ CREATE TABLE IF NOT EXISTS "infra_file_config" (
) COMMENT '文件配置表'; ) COMMENT '文件配置表';
CREATE TABLE IF NOT EXISTS "infra_file" ( 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, "type" varchar(63) DEFAULT NULL,
"content" blob NOT NULL, "size" bigint NOT NULL,
"creator" varchar(64) DEFAULT '', "creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '', "updater" varchar(64) DEFAULT '',

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="文件路径" prop="id"> <el-form-item label="文件路径" prop="path">
<el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/> <el-input v-model="queryParams.path" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
</el-form-item> </el-form-item>
<el-form-item label="文件类型" prop="type"> <el-form-item label="文件类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small"> <el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
@ -31,21 +31,23 @@
<!-- 列表 --> <!-- 列表 -->
<el-table v-loading="loading" :data="list"> <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="type" width="80" />
<el-table-column label="文件内容" align="center" prop="content"> <!-- <el-table-column label="文件内容" align="center" prop="content">-->
<template slot-scope="scope"> <!-- <template slot-scope="scope">-->
<img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'" <!-- <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"-->
width="200px" :src="getFileUrl + scope.row.id"> <!-- width="200px" :src="getFileUrl + scope.row.id">-->
<i v-else>非图片无法预览</i> <!-- <i v-else>非图片无法预览</i>-->
</template> <!-- </template>-->
</el-table-column> <!-- </el-table-column>-->
<el-table-column label="创建时间" align="center" prop="createTime" width="180"> <el-table-column label="上传时间" align="center" prop="createTime" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span> <span>{{ parseTime(scope.row.createTime) }}</span>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['infra:file:delete']">删除</el-button> v-hasPermi="['infra:file:delete']">删除</el-button>
@ -102,7 +104,7 @@ export default {
queryParams: { queryParams: {
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
id: null, path: null,
type: null, type: null,
}, },
// //
@ -193,6 +195,15 @@ export default {
this.$modal.msgSuccess("删除成功"); this.$modal.msgSuccess("删除成功");
}).catch(() => {}); }).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> </script>