完成新 File 的功能

plp
YunaiV 4 years ago
parent cdcecd0d4a
commit 87670d18fd

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

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

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

@ -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")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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 '',

@ -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仅仅是为了演示去掉配置也没关系

@ -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仅仅是为了演示去掉配置也没关系

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

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

Loading…
Cancel
Save