自定义导入导出和文件下载
项目地址:https://github.com/ysmc-slg/excel-poi
Excel导入导出
在需要导出的实体类上加上 @Excel 注解,注解有多个属性,下面做一下具体说明:
// 导出到Excel中的名字
public String name() default "";
// 日期格式 如: yyyy/MM/dd
public String dateFormat() default "";
// 读取内容表达式 (如: 0=男,1=女,2=未知),下载时会将数字转成字符,上传将字符转成数字
public String readConverterExp() default "";
/**
* 文字后缀,如% 90 变成90%
*/
public String suffix() default "";
/**
* 当值为空时,字段的默认值
*/
public String defaultValue() default "";
/**
* 鼠标放上去的提示信息
*/
public String prompt() default "";
/**
* 设置只能选择不能输入的列内容.下拉框,格式为:"xxx,xxx"
*/
public String[] combo() default {};
/**
* 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
*/
public boolean isExport() default true;
/**
* 另一个类中的属性名称,支持多级获取,以小数点隔开
*/
public String targetAttr() default "";
/**
* 是否自动统计数据,在最后追加一行统计数据总和
*/
public boolean isStatistics() default false;
/**
* 导出字体颜色
*/
public IndexedColors color() default IndexedColors.BLACK;
/**
* 导出字段对齐方式
*/
public HorizontalAlignment align() default HorizontalAlignment.CENTER;
/**
* 分隔符,读取字符串组内容
*/
public String separator() default ",";
/**
* BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
*/
public int scale() default -1;
/**
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
*/
public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
/**
* 导出时在excel中每个列的高度 单位为字符
*/
public double height() default 14;
/**
* 导出时在excel中每个列的宽 单位为字符
*/
public double width() default 16;
/**
* 字段类型(0:导出导入;1:仅导出;2:仅导入)
*/
Type type() default Type.ALL;
public enum Type {
ALL(0), EXPORT(1), IMPORT(2);
private final int value;
Type(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
/**
* 导出单元格类型(0数字 1字符串 2图片)
*/
public ColumnType cellType() default ColumnType.STRING;
public enum ColumnType {
NUMERIC(0), STRING(1), IMAGE(2);
private final int value;
ColumnType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
具体用法如下:
首先在实体类中加入注解:
public class TienchinChannel{
private Long channelId;
@Excel(name = "渠道名称")
private String channelName;
@Excel(name = "渠道状态",prompt="请选择",combo="正常,禁用",readConverterExp="0=禁用,1=正常")
private String status;
@Excel(name = "渠道类型",prompt="请选择",combo="线上渠道,线下渠道",readConverterExp="1=线上渠道,2=线下渠道")
private Integer type;
@Excel(name = "创建时间",dateFormat = "yyyy/MM/dd",type=Type.EXPORT)
@JsonFormat(pattern = "yyyy-MM-dd HH:ss:mm")
private Date createTime;
// ... 省略setter/getter 方法
}
导出方法:
@PostMapping("/export")
public void export(HttpServletResponse response, TienchinChannel user) {
// 查询要导出的数据
List<TienchinChannel> list = channelService.selectTienchinChannelList(user);
ExcelUtil<TienchinChannel> util = new ExcelUtil<TienchinChannel>(TienchinChannel.class);
util.exportExcel(response, list, "渠道数据");
}

导入方法:
导入首先要导出模板
/**
导出模板
*/
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<TienchinChannel> util = new ExcelUtil<TienchinChannel>(TienchinChannel.class);
util.importTemplateExcel(response, "渠道数据");
}
导入方法:
其中 updateSupport 参数是已存在的数据要不要修改,这里的业务逻辑需要自己去写
@PostMapping("/importData")
public String importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<TienchinChannel> util = new ExcelUtil<TienchinChannel>(TienchinChannel.class);
List<TienchinChannel> userList = util.importExcel(file.getInputStream());
// String operName = getUsername();
// String message = userService.importUser(userList, updateSupport, operName);
return "df";
}
文件上传下载
首先要说的是此工具是将文件上传到本地。
配置文件保存的路径
demo:
# 文件路径 示例( Windows配置D:/demo/uploadPath,Linux配置 /home/demo/uploadPath)
profile: D:/demo/uploadPath
然后通过通用的请求处理进行上传下载:
/**
* 通用请求处理
*
* @author tienchin
*/
@RestController
@RequestMapping("/common")
public class CommonController {
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
private static final String FILE_DELIMETER = ",";
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public String uploadFile(MultipartFile file) throws Exception {
try {
// 上传文件路径
String filePath = ExcelConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = getUrl() + fileName;
// AjaxResult ajax = AjaxResult.success();
// ajax.put("url", url);
// ajax.put("fileName", fileName);
// ajax.put("newFileName", FileUtils.getName(fileName));
// ajax.put("originalFilename", file.getOriginalFilename());
return url;
} catch (Exception e) {
return e.getMessage();
}
}
/**
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
public String uploadFiles(List<MultipartFile> files) throws Exception {
try {
// 上传文件路径
String filePath = ExcelConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files) {
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
// AjaxResult ajax = AjaxResult.success();
// ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
// ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
// ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
// ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
return "成功";
} catch (Exception e) {
return e.getMessage();
}
}
/**
* 本地资源通用下载
*/
@GetMapping("/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception {
try {
if (!FileUtils.checkAllowDownload(resource)) {
throw new Exception(String.format("资源(%s)非法,不允许下载。",resource));
}
// 本地资源路径
String localPath = ExcelConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + StringUtils.substringAfter(resource, "/profile");
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
} catch (Exception e) {
log.error("下载文件失败", e);
}
}
/**
* 通用下载请求
*
* @param fileName 文件名称
* @param delete 是否删除
*/
@GetMapping("/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) {
try {
if (!FileUtils.checkAllowDownload(fileName)) {
throw new Exception(String.format("文件名称(%s)非法,不允许下载。",fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
String filePath = ExcelConfig.getDownloadPath() + fileName;
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete) {
FileUtils.deleteFile(filePath);
}
} catch (Exception e) {
log.error("下载文件失败", e);
}
}
/**
* 获取完整的请求路径,包括:域名,端口,上下文访问路径
*
* @return 服务地址
*/
public String getUrl() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return getDomain(request);
}
public static String getDomain(HttpServletRequest request) {
StringBuffer url = request.getRequestURL();
String contextPath = request.getServletContext().getContextPath();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
}
}
需要注意的是,上传文件返回的路径前缀是 /profile,如保存文件路径是 D:\demo\uploadPath\upload\2023\01\11\xxx.jpg而方法 upload 返回路径是 \profile\upload\2023\01\11\xxx.jpg,最后在拼接完整的请求路径http://localhost:8080/profile/upload/2023/01/12/xxx.jpg 也就是保存到数据库中的。加上前缀的原因是,要对文件进行匹配映射,如下:
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 路径匹配映射 */
registry.addResourceHandler("/profile" + "/**")
.addResourceLocations("file:" + ExcelConfig.getProfile() + "/");
}
}
拦截 /profile 路径,映射到本地目录,最后的 / 必须加,所有系统都这样写。
前端
// 通用下载方法
download(url, params, filename) {
axios.post(url, params, {
// 对参数进行处理,将参数转成乱码
transformRequest: [(params) => { return this.tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob'
}).then((res) => {
if(res.status === 200){
const content = res.data;
console.log(content)
const blob = new Blob([content]);
if('download' in document.createElement('a')){
//非IE下载
const a = document.createElement('a');
a.download = filename;
a.style.display = 'none';
a.href = window.URL.createObjectURL(blob);
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}else{
//IE10+下载
if(typeof window.navigator.msSaveBlob !== 'undefined'){
window.navigator.msSaveBlob(blob, _this.selected);
}else{
let URL = window.URL || window.webkitURL;
let downloadUrl = URL.createObjectURL(blob);
window.location = downloadUrl;
}
}
}
// downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
// ElMessage.error('下载文件出现错误,请联系管理员!')
// downloadLoadingInstance.close();
})
},
tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result
}
注意:
Content-Type 为 application/x-www-form-urlencoded 则表示传递参数的格式为 &key=value&key2=value2,一旦使用了 application/x-www-form-urlencoded 就必须使用 encodeURIComponent() 对参数进行编码,这就是tansParams函数做的事。
为什么要使用encodeuricomponent?
encodeuricomponent
可把字符串作为 URI 组件进行编码。
该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ’ ( ) 。
其他字符(比如 :;/?&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
encodeuricomponent什么时候使用:
用于url作为参数传递的场景中使用url当作参数传递的时候,当参数出现空格这样的特殊字段,后台只可以读取到空格前的内容,后面内容丢失,造成数据读取失败,但是如果用encodeURIComponent(),则这些特殊字符进行转义,这样后台就可以成功读取了,所以encodeURIComponent()用于url作为参数传递的场景中使用。