一、背景
公司运营的同事有个任务,提供一个数据文件给我,然后从数据库中找出对应的加密串再导出来给他。这个活不算是很难,但时不时就会有需求。
同事给我的文件有时是给excel表格,每一行有4列,逗号隔开,合并成一列数据,这类文件需要把所有数据复制到文本编辑器进行处理,把逗号替换成空格,再使用列块编辑模式复制2、3、4列替换原来的excel数据。有时是给.DAT的文件,这类文件需要手动修改后缀为csv,修改后就跟普通的excel表格一样打开,去掉第一列。最后添加一行表头,再对第一列进行筛选去重。
去重后准备导入到数据库的临时表,在此之前需要手动清空临时表的历史数据。导入后再执行一段sql语句,然后把查询结果导出为excel文件给到同事。
这样的工作重复重复再重复,确实挺无趣的,何不鼓捣一个工具给同事自己去处理?
二、步骤
2.1 项目搭建
项目结构如下图:
创建项目,使用springboot 2.5.14
、poi 4.1.2
、mybatis
,前端使用 thymeleaf
+ layui-v2.6.8
。
具体看maven
配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.xxx</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
</exclusion>
<exclusion>
<groupId>org.zaxxer</groupId>
<artifactId>SparseBitSet</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.3</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.xxx.AdminApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
为了节省jar包体积,尽可能把不需要的依赖给排除。
2.2 后端处理逻辑
Controller
内容
import com.xxx.domain.Result;
import com.xxx.domain.CellItem;
import com.xxx.domain.HelmetConfig;
import com.xxx.service.HelmetService;
import com.xxx.utils.file.DatUtil;
import com.xxx.utils.poi.ExcelUtil;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* 通用请求处理
*
* @author admin
*/
@Controller
public class CommonController {
public static final String[] EXCEL_EXTENSION = {"xls", "xlsx", "XLS", "XLSX"};
public static final String DAT_EXTENSION = "DAT";
@Resource
private HelmetService helmetService;
@GetMapping(value = {"/", "/index"})
public String index(Model model) {
return "index";
}
/**
* 通用下载请求
*/
@GetMapping("/download")
public void fileDownload(HttpServletResponse response) {
List<HelmetConfig> list = helmetService.queryAll();
ExcelUtil<HelmetConfig> util = new ExcelUtil<>(HelmetConfig.class);
util.exportExcel(response, list, "Sheet1");
}
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
@ResponseBody
public Result uploadFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
return Result.error("文件不能为空");
}
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
List<CellItem> list;
if (Arrays.asList(EXCEL_EXTENSION).contains(extension)) {
list = ExcelUtil.getData(file);
} else if (DAT_EXTENSION.equalsIgnoreCase(extension)) {
list = DatUtil.readDat(file);
} else {
return Result.error("文件格式不正确");
}
if (list.isEmpty()) {
return Result.error("操作失败,请重试");
}
helmetService.batchAdd(list);
return Result.success("操作成功,请点击【下载文件】");
}
}
数据库根据最后的查询sql创建一个视图(View),通过mybatis对这个试图进行查询,然后把结构进行导出即可。
ExcelUtil.getData()
内容
public static List<CellItem> getData(MultipartFile file) {
InputStream inputStream = null;
List<CellItem> rowList = new ArrayList<>();
try {
inputStream = file.getInputStream();
XSSFWorkbook wb = new XSSFWorkbook(inputStream);
int ignoreRows = 0;
int sheetNum = wb.getNumberOfSheets();
//for循环:取前N个表,下标从0开始
for (int i = 0; i < sheetNum; i++) {
XSSFSheet sheetI = wb.getSheetAt(i);
//列数
int cellSize = sheetI.getRow(0).getLastCellNum();
//第N+1行开始,可以通过传参,从第N+1行开始取
for (int rowIndex = ignoreRows; rowIndex <= sheetI.getLastRowNum(); rowIndex++) {
XSSFRow row = sheetI.getRow(rowIndex);
if (row == null) {
continue;
}
if (cellSize == 1) {
XSSFCell cell = row.getCell(0);
String cellValue = cell.getStringCellValue();
if (cellValue.contains(",")) {
CellItem item = new CellItem();
String[] cells = cellValue.split(",");
String deviceId = cells[1];
Boolean exists = checkExists(rowList, deviceId);
if (exists) {
continue;
}
item.setDeviceId(deviceId.trim());
item.setProductId(cells[2]);
item.setMac(cells[3]);
rowList.add(item);
}
} else if (cellSize == 4){
//在每行中的每一列,从下标1开始,忽略第一列,一直取到所有
CellItem item = new CellItem();
String deviceId = row.getCell(1).getStringCellValue();
Boolean exists = checkExists(rowList, deviceId);
if (exists) {
continue;
}
item.setDeviceId(deviceId.trim());
item.setProductId(row.getCell(2).getStringCellValue());
item.setMac(row.getCell(3).getStringCellValue());
rowList.add(item);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
log.error("文件流关闭失败:{}", e.getMessage());
}
}
}
return rowList;
}
private static Boolean checkExists(List<CellItem> rowList, String key) {
for (int i = 0; i < rowList.size(); i++) {
CellItem item = rowList.get(i);
if (item.getDeviceId().equals(key.trim())) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
DatUtil.readDat()
public static List<CellItem> readDat(MultipartFile file) {
List<CellItem> list = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
String[] split = line.split(",");
String deviceId = split[1];
Boolean exists = checkExists(list, deviceId);
if (exists) {
continue;
}
CellItem item = new CellItem();
item.setDeviceId(deviceId.trim());
item.setMac(split[2]);
item.setProductId(split[3]);
list.add(item);
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
private static Boolean checkExists(List<CellItem> rowList, String key) {
for (int i = 0; i < rowList.size(); i++) {
CellItem item = rowList.get(i);
if (item.getDeviceId().equals(key.trim())) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
导出的代码这里省略了。
2.3 配置
application.yml
# 开发环境配置
server:
# 服务器的HTTP端口
port: 8080
servlet:
# 应用的访问路径
context-path: /
# Spring配置
spring:
profiles:
active: druid
#thymeleaf 页面的缓存开关
thymeleaf:
enabled: true
cache: true
mode: HTML5
encoding: utf-8
suffix: .html
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 50MB
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.xxx.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath:mapper/*.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# 日志配置
logging:
level:
com.xxx: info
org.springframework: warn
数据库配置application-druid.yml
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.16.1.2:3306/test?useUnicode=true&useSSL=false&allowLoadLocalInfile=false&autoReconnect=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
username: root
password: root
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
2.3 前端处理逻辑
把layui
的相关文件放到resources/static
目录,再新建一个index.html
文件放入resources/templates
目录,这两个目录是thymeleaf默认的,如果要修改可以在application.yml
进行配置。静态文件如下:
为了压缩jar包的体积,把所有不必要的文件都精简掉了。
以下是index.html
内容
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试</title>
<script th:src="@{/layui.js}"></script>
<link rel="stylesheet" th:href="@{/css/layui.css}" media="all">
</head>
<body>
<div class="layui-card">
<div class="layui-card-header">操作面板</div>
<div class="layui-card-body">
<div class="layui-tab" lay-filter="window">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="uploadTab">文件上传</li>
<li lay-id="downloadTab">文件下載</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<form id="upload_form" class="layui-form" enctype="multipart/form-data">
<div class="layui-form-item">
<label class="layui-form-label">文件</label>
<div class="layui-input-block">
<button type="button" class="layui-btn" id="upload">
<i class="layui-icon"></i>选择文件
</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button id="btnSubmit" class="layui-btn" onclick="return false;">立即提交</button>
</div>
</div>
</form>
</div>
<div class="layui-tab-item">
<div class="layui-form-item">
<label class="layui-form-label">文件</label>
<div class="layui-input-block">
<button type="button" class="layui-btn" id="downloadBtn">
<i class="layui-icon"></i>下载文件
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<script>
layui.use(['upload', 'layer', 'element'], function () {
let $ = layui.jquery
, layer = layui.layer
, element = layui.element
, upload = layui.upload;
//执行实例
upload.render({
elem: '#upload' //绑定元素
, url: '/upload' //上传接口
, accept: 'file' //允许上传的文件类型,不写默认是图片
, acceptMime: ".xlsx,.xls,.DAT,.dat" //不写默认验证图片格式,一定要省略【exts】参数
, auto: false //选择文件后不自动上传
, bindAction: '#btnSubmit' //指向一个按钮触发上传
, before: function (obj) {
layer.load(); //上传loading
}
,done: function (res) {
console.log(res)
layer.closeAll('loading'); //关闭loading
layer.alert(res.msg);
if (res.code === 200) {
element.tabChange('window', 'downloadTab');
}
}
, error: function (res) {
console.error(res)
layer.msg(res.msg);
layer.closeAll('loading'); //关闭loading
}
});
$("#downloadBtn").on('click', function () {
location.href = "/download";
})
});
</script>
编辑好测试没问题直接打包放到服务器上执行就可以啦。
三、实现效果
3.1 文件导入
导入成功后会自动切换到【文件下载】的tab页