• 应用开发平台业务支撑功能——集成阿里EasyExcel组件实现Excel导入导出功能


    背景

    数据导入导出是应用系统常见需求,而excel是主要的辅助工具,特别是数据导入。数据导出场景excel用的也相对最多,其他导出格式,如word、pdf,通常用于线下打印或电子传递,用报表工具处理更多一些。

    java领域操作excel组件库主要是EasyPoi,功能比较齐备,但是用起来比较繁琐,需要额外做一些定制化开发工作,并且据说存在若干BUG以及性能问题。

    阿里在EasyPoi基础上做了二次封装和优化,推出了EasyExcel开源项目。
    官网:https://easyexcel.opensource.alibaba.com/docs/current/

    以下资料来源于easyexcel官网

    Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

    easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

    16M内存23秒读取75M(46W行25列)的Excel(3.2.1+版本)

    当然还有极速模式能更快,但是内存占用会在100M多一点

    关键设计问题思考

    核心功能设计

    1.首先必须有模板,直接读写excel,表格样式很难看
    2.导入功能需要进行数据验证
    3.需要数据转换,导入时将文本转换成编码、标识,导出时将编码转换为文本

    上传文件和数据导入是否要分为两个动作?

    从功能角度考虑,这的确是两个动作。但从业务角度考虑,主要目的是导入数据,上传文件只是辅助工作或者其中1个步骤,数据导入失败往往是因为数据存在问题,需要修改后重新上传,如拆分为两个动作,则需要临时存储以及及时清理。从用户体验角度,点击导入按钮,选择excel文件,开始上传,并导入数据,更直观方便。

    数据转换如何处理

    理论上,excel导入数据与用户通过表单录入数据是类似的,但实际上还是有差异,表单录入有控件支撑,例如数据字典,传给后台的直接就是编码,而不是名称。而在excel中,用户输入的通常只能是名称,系统需要将名称转换为编码后存库。
    为方便数据转换,有一些变通的作法,例如在数据导入的excel模板上预处理,提前设置好下拉列表,将编码和名称的拼接作为下拉项(如:用户状态表示为“正常 | NORMAL”),处理时直接截取即可,这种方式的优点是不需要做额外的查询和转换,适用于数据字典和少数据量的平铺型基础数据,缺点是需要进行预处理以及与系统同步。例如,系统运维人员对数据字典或基础数据进行了调整,就需要同步调整excel模板,繁琐易出错,也容易发生因忘记调整导致的不一致问题。
    并且,该方案对于以下两种情况,难以实现:
    1.对于树形结构,如导入用户时设置组织机构,下拉选择项一方面较多,难以选择,另一方面也无法直观体现出树形层次来。
    2.大数据量,如物料,几百几千甚至上万,不适合将数据库中的数据在excel中再建立副本用于选择。

    综合考虑以上因素,最终不采用excel模板预处理的方案,而是在导入数据后,存库前,由平台进行数据转换工作。

    数据验证如何处理

    首先要说的是,excel导入数据,同样需要进行严格的数据验证,这是一个容易忽视的点。见过不少系统,通过系统表单录入进行重重验证,而在数据导入环节则放松,导致有问题的数据进入了系统,引发了后续一系列问题。最常见的就是某个字段不能为空,导入时不做验证,后续使用数据的环节,出现了空指针。还有一种情况,是导入的数据,出现了非预期的值,跟后端的数据字典或基础数据对应不上,后果也是显而易见的。

    再说说实现方案。对于表单验证,用Hibernate validator组件实现的,具体是在vo对象上加注解的模式,这种模式简单灵活,具备通用性。excel导入复用该技术方案。EasyExcel功能组件,可以将读取到的excel行记录,映射为一个java的对象,然后自己来调用Hibernate validator组件触发验证就行了。

    接下来面临的问题是,原来为表单录入实现的vo对象,是否也可以复用?粗略一想,应该复用,深入考虑,有问题。表单录入有UI控件处理和转换,Excel导入只能是原生的文本。例如,对于组织机构来说,机构类型这个字段,表单录入映射的是type字段,前端经过UI控件处理,返回的直接的就是编码COMPANY(公司)或DEPARTMENT(部门)等,后端vo验证的是type属性不是为空。而Excel输入的是类型一栏只能是公司/部门这样的中文描述,映射到了typeName字段。即相当于对于表单录入,需要验证type不能为空;对于Excel导入,需要验证typeName不能为空,这是有差异的。

    对于这种差异性,实际Hibernate validator也考虑到了,有对应的解决方案,即进行验证分组。写法上比较奇怪,在类内部定义两个空接口,接口名作为组名,然后在验证注解的groups属性中指定需要采用哪个分组或哪些个分组,在早期的平台研发版本,采用过这种方案,示例代码如下:

    package tech.popsoft.platform.core.modules.system.vo;
    
    
    import com.alibaba.excel.annotation.ExcelIgnore;
    import com.alibaba.excel.annotation.ExcelProperty;
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    import tech.popsoft.platform.common.base.BaseVO;
    
    import javax.validation.constraints.NotBlank;
    
    /**
     * 组织机构 视图对象
     * @author wqliu
     */
    @Data
    @EqualsAndHashCode(callSuper = true)
    @ApiModel(value = "Organization对象", description = "组织机构")
    public class OrganizationVO extends BaseVO {
    
        private static final long serialVersionUID = 1L;
    
        /**
         * 表单录入
         */
        public interface FormInput{
    
        }
    
        /**
         * excel导入
         */
        public interface ExcelImport{
    
        }
    
    
        @ApiModelProperty(value = "父标识")
        private String parentId;
    
        @ApiModelProperty(value = "名称")
        @NotBlank(message = "名称不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.FormInput.class, tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("名称")
        private String name;
    
        @ApiModelProperty(value = "编码")
        @ExcelProperty("编码")
        private String code;
    
        @ApiModelProperty(value = "类型")
        @NotBlank(message = "请选择类型",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.FormInput.class})
        @ExcelIgnore
        private String type;
    
        @ApiModelProperty(value = "备注")
        @ExcelProperty("备注")
        private String remark;
    
        @ApiModelProperty(value = "状态")
        @ExcelIgnore
        private String status;
    
        @ApiModelProperty(value = "排序号")
        @ExcelProperty("排序号")
        private String orderNo;
    
    
        @ApiModelProperty(value = "类型名称")
        @NotBlank(message = "类型不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("类型")
        private String typeName;
    
        @ApiModelProperty(value = "状态名称")
        @NotBlank(message = "状态不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("状态")
        private String statusName;
    
        @ApiModelProperty(value = "上级名称")
        @NotBlank(message = "上级不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("上级名称")
        private String parentName;
    
        @ApiModelProperty(value = "忽略上级")
        @ExcelIgnore
        private Boolean ignoreParent;
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    像下面这样定义分组名。

        /**
         * 表单录入
         */
        public interface FormInput{
    
        }
    
        /**
         * excel导入
         */
        public interface ExcelImport{
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在属性注解的groups属性中设置该字段要分到哪个/哪些组中。

       
        @NotBlank(message = "名称不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.FormInput.class, tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("名称")
        private String name;
    
    
     	@ApiModelProperty(value = "类型")
        @NotBlank(message = "请选择类型",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.FormInput.class})
        @ExcelIgnore
        private String type;
    
        @ApiModelProperty(value = "类型名称")
        @NotBlank(message = "类型不能为空",groups = {tech.popsoft.platform.core.modules.system.vo.UserVO.ExcelImport.class})
        @ExcelProperty("类型")
        private String typeName;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在需要进行数据验证的地方,如controller中,需要指定验证分组。

        /**
         * 新增
         */
        @ApiOperation(value = "新增")
        @PostMapping("/")
        @SystemLog(value = "组织机构-新增")
        @PreAuthorize("hasPermission(null,'system:organization:add')")
        public ResponseEntity<Result> add(@Validated(OrganizationVO.FormInput.class) @RequestBody OrganizationVO vo) {
            Organization entity = convert2Entity(vo);
            organizationService.add(entity);
            OrganizationVO newVO = convert2VO(entity);
            return ResultUtil.success(newVO);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在excel导入时,将分组也一并指定

        @PostMapping("/importExcel")
        @Override
        @PreAuthorize("hasPermission(null,'system:organization:import')")
        @SystemLog(value = "导入excel", logRequestParam = false)
        public ResponseEntity<Result> importExcel(MultipartFile file) {
            SimpleReadDataListener readListener = new SimpleReadDataListener<OrganizationVO, Organization, OrganizationVO.ExcelImport>(organizationService) {
                @Override
                public Organization convertData(OrganizationVO vo) {
                    return convert2EntityForExcel(vo);
                }
    
            };
            super.setReadListener(readListener);
            return super.importExcel(file);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    现在来看,建验证分组,复用vo对象的方式其实并不好,违反单一职责和开闭原则,特别是在系统只有少量功能才有Excel导入需求的情况下,再叠加低代码开发的代码生成因素,更适合分离。

    即保持原VO不变,作为系统前后端交互的数据对象。
    如该功能需要Excel导入,则新建一个VO,如OrganizationForImportVO,在其中定义导入相关的专有列,相对表单输入和Excel导入公用一个VO的情况,更优雅。

    导入相关方法放在哪实现?

    与Excel导入有关的新方法需要放到controller层,如下载模板、上传文件、导入数据、数据转换等,虽然可以在相应的实体类中追加,但存在的问题跟上面说的复用VO对象类似。更优雅的方案,是将这部分功能拆出来单独实现。即新建一个OrganizationExcelImportExtensionController,暴露的接口、权限控制编码与OrganizationController保持一致,这样做对前端而言是透明的,无感的,对于后端则是进行了功能分离。

    而且,这部分功能同样有共性部分,将这部分共性抽离出来,实现一个父类ExcelImportExtension,来实现复用,同样是基于模板方法设计模式思想的实践。

    系统实现

    先来说说整体工作,然后分别介绍下导入和导出。

    引入依赖

    第一步,当然是引入依赖,这里选的是最新版本,2023年5月6日更新的,可以说是新鲜出炉了~
    在platform-common模块的pom文件中添加如下依赖。

    
    <dependency>
      <groupId>com.alibabagroupId>
      <artifactId>easyexcelartifactId>
      <version>3.3.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建基类

    创建包extension,以及ExcelImportExtension类,用于公共处理

    package tech.abc.platform.common.extension;
    
    import com.alibaba.excel.EasyExcel;
    import org.apache.commons.io.IOUtils;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.http.ResponseEntity;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.multipart.MultipartFile;
    import tech.abc.platform.common.base.BaseController;
    import tech.abc.platform.common.component.easyexcel.ExcelExceptionEnum;
    import tech.abc.platform.common.component.easyexcel.ReadDataListener;
    import tech.abc.platform.common.exception.CustomException;
    import tech.abc.platform.common.exception.FileException;
    import tech.abc.platform.common.utils.ResultUtil;
    import tech.abc.platform.common.vo.Result;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.lang.reflect.ParameterizedType;
    import java.net.URLEncoder;
    
    
    /**
     * excel导入功能扩展
     *
     * @author wqliu
     * @date 2023-05-17
     */
    public class ExcelImportExtension<V, E> extends BaseController {
    
        /**
         * 数据监听器
         */
        private ReadDataListener readListener;
    
        /**
         * 导入模板
         */
        private String importTemplate;
    
    
        /**
         * 设置数据监听器
         *
         * @param readListener
         */
        public void setReadListener(ReadDataListener readListener) {
            this.readListener = readListener;
        }
    
    
        /**
         * 设置导入模板
         */
        protected void setImportTemplate(String importTemplate) {
            this.importTemplate = importTemplate;
        }
    
        /**
         * 下载导入模板
         */
        public void downloadImportTemplate(HttpServletResponse response) {
    
            ClassPathResource classPathResource = new ClassPathResource(importTemplate);
            try (InputStream inputStream = classPathResource.getInputStream();
                 OutputStream outputStream = response.getOutputStream()) {
    
                // 设置响应信息
                response.setContentType("application/vnd.ms-excel");
                response.setCharacterEncoding("utf-8");
                // 这里URLEncoder.encode可以防止中文乱码
                String fileName = URLEncoder.encode("导入模板.xlsx", "UTF-8");
                response.setHeader("Content-disposition", "attachment;filename=" + fileName);
                IOUtils.copy(inputStream, outputStream);
    
            } catch (Exception exception) {
                throw new CustomException(FileException.DOWNLOAD_FAILURE, exception.getMessage());
            }
    
        }
    
        /**
         * excel上传
         */
        @Transactional(rollbackFor = Exception.class)
        public ResponseEntity<Result> importExcel(MultipartFile file) {
    
            if (file == null) {
                throw new CustomException(FileException.UPLOAD_IS_NULL);
            }
            ResponseEntity<Result> result = null;
    
            try {
                EasyExcel.read(file.getInputStream(), this.getClazz(), this.readListener).sheet().doRead();
                return ResultUtil.success();
            } catch (Exception exception) {
                long currentRowNo = this.readListener.getCurrentRowNo();
                Throwable throwable = exception;
                while (throwable.getCause() != null) {
                    throwable = throwable.getCause();
    
                }
                throw new CustomException(FileException.EXCEL_IMPORT_FAILURE, currentRowNo, throwable.getMessage());
    
            }
        }
    
    
        /**
         * 数据转换
         */
        protected E convert2EntityForExcel(V vo) {
            throw new CustomException(ExcelExceptionEnum.EXPORT_METHOD_UNIMPLEMENTED);
        }
    
    
        /**
         * 通过父类获取运行时泛型类型
         *
         * @return
         */
        private Class<V> getClazz() {
            // 获得当前类型的带有泛型类型的父类
            Class subclass;
            ParameterizedType pd = (ParameterizedType) this.getClass().getGenericSuperclass();
            Class<V> clazz = (Class) pd.getActualTypeArguments()[0];
            return clazz;
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    创建数据监听器

    EasyExcel在导入时采用监听器机制,来读取和解析数据,核心就是监听器,我们自己实现了ReadListener接口,并且该监听器需要多实体复用,使用了泛型。
    同时需要注意的是,该监听器是有状态的,不能被spring管理,要每次读取excel都要new,然后里面使用到的服务类,通过构造方法的方法传进去。

    package tech.abc.platform.common.component.easyexcel;
    
    
    import com.alibaba.excel.context.AnalysisContext;
    import com.alibaba.excel.read.listener.ReadListener;
    import com.alibaba.fastjson.JSON;
    import lombok.extern.slf4j.Slf4j;
    import tech.abc.platform.common.base.BaseService;
    import tech.abc.platform.common.exception.CommonException;
    import tech.abc.platform.common.exception.CustomException;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    import java.util.Set;
    
    
    /**
     * 读取数据监听器
     *
     * @author wqliu
     * @date 2023-05-17
     */
    @Slf4j
    public class ReadDataListener<V, E> implements ReadListener<V> {
    
    
        /**
         * 当前处理行号
         */
        private long currentRowNo = 0;
        /**
         * 服务
         */
        private BaseService service;
    
        /**
         * 获取当前行号
         *
         * @return long 行号
         */
        public long getCurrentRowNo() {
            return currentRowNo;
        }
    
        /**
         * 构造方法
         * 每次创建Listener的时候需要把spring管理的类传进来
         *
         * @param service
         */
        public ReadDataListener(BaseService service) {
            this.service = service;
        }
    
        /**
         * 解析数据
         *
         * @param data    单行记录
         * @param context
         */
        @Override
        public void invoke(V data, AnalysisContext context) {
            currentRowNo++;
            log.info("解析到一条数据:{}", JSON.toJSONString(data));
            // 处理数据
            E entity = handleData(data);
            // 保存数据
            service.add(entity);
    
        }
    
        /**
         * 所有数据解析完成了 都会来调用
         *
         * @param context
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            log.info("所有数据解析完成!");
        }
    
        /**
         * 数据预处理,可转化实体类中的字典数据,也可以设置默认字段
         *
         * @param data
         */
        public E handleData(V vo) {
    
            // 数据校验
            validateData(vo);
            // 设置默认值
            setDefaultValue(vo);
            // 数据转换
            return convertData(vo);
        }
    
        private void validateData(V vo) {
            // 进行数据验证
            ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
            Validator validator = vf.getValidator();
            Set<ConstraintViolation<V>> set = validator.validate(vo);
            for (ConstraintViolation<V> constraintViolation : set) {
                throw new CustomException(CommonException.DATA_VALIDATE_FAILURE, constraintViolation.getMessage());
            }
    
        }
    
    
        /**
         * 设置默认值
         *
         * @param vo 视图对象
         */
        protected void setDefaultValue(V vo) {
            // 如无需设置,则该方法可为空
    
        }
    
    
        /**
         * 转换数据
         *
         * @param vo 视图对象
         * @return {@link E}
         */
        protected E convertData(V vo) {
            throw new CustomException(ExcelExceptionEnum.EXPORT_METHOD_UNIMPLEMENTED);
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    单条处理还是批量处理
    EasyExcel提供的范例是解析出数据后,放到一个集合中,到了指定数据量,如300条,批量存库,这样从技术角度而言性能更高,但是用户通过excel整理的数据通常不规范,需要系统给出友好出错提示,具体是哪一行的那个属性有问题,此外,系统框架自身在创建业务实体时有处理逻辑(新增前验证是否为空、是否重复、是否存在,新增后触发相关对象的处理等),而且,用户通过excel导入的通常是小批量的主数据,几十条以内,至多几百条,成千上万罕见,因此调整为每次只处理1条数据。

    创建导入控制器类

    包括了下载模板、导入Excel数据的处理,公共部分的代码还是抽取到了父类ExcelImportExtension,这里主要用于覆写,指定模板的路径,核心还是在于数据的读取和处理。

    对于组织机构而言,有可能存在重名,如生产一部和生产二部两个部门下各存在一个名称为“1号车间”的部门,这时候通过上级名称就没法确定这条数据到底要导入到哪个部门下,因此引入了上级编码。编码是唯一的,系统处理时优先找编码,编码为空的时候再通过名称来处理,如还是找到多个同名部门,则放到未分配里,转人工处理。

    package tech.abc.platform.system.controller.extension;
    
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.BeanUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    import tech.abc.platform.common.annotation.SystemLog;
    import tech.abc.platform.common.component.easyexcel.ReadDataListener;
    import tech.abc.platform.common.exception.CustomException;
    import tech.abc.platform.common.extension.ExcelImportExtension;
    import tech.abc.platform.common.vo.Result;
    import tech.abc.platform.system.constant.SystemConstant;
    import tech.abc.platform.system.entity.Organization;
    import tech.abc.platform.system.exception.OrganizationExceptionEnum;
    import tech.abc.platform.system.service.OrganizationService;
    import tech.abc.platform.system.vo.OrganizationForImportVO;
    
    import javax.servlet.http.HttpServletResponse;
    import java.util.List;
    
    
    /**
     * 实现Excel导入功能的组织机构控制器
     *
     * @author wqliu
     * @date 2023-05-17
     */
    @RestController
    @RequestMapping("/system/organization")
    @Slf4j
    public class OrganizationExcelImportExtensionController extends ExcelImportExtension<OrganizationForImportVO, Organization> {
    
        @Autowired
        private OrganizationService organizationService;
    
    
        @GetMapping("/downloadImportTemplate")
        @Override
        @PreAuthorize("hasPermission(null,'system:organization:downloadImportTemplate')")
        @SystemLog(value = "下载excel模板", logResponseData = false)
        public void downloadImportTemplate(HttpServletResponse response) {
            super.setImportTemplate("/template/system/organization/import.xlsx");
            super.downloadImportTemplate(response);
        }
    
    
        @PostMapping("/importExcel")
        @Override
        @PreAuthorize("hasPermission(null,'system:organization:import')")
        @SystemLog(value = "导入excel", logRequestParam = false)
        @Transactional
        public ResponseEntity<Result> importExcel(MultipartFile file) {
            ReadDataListener readListener = new ReadDataListener<OrganizationForImportVO, Organization>(organizationService) {
                @Override
                public Organization convertData(OrganizationForImportVO vo) {
                    return convert2EntityForExcel(vo);
                }
    
            };
            super.setReadListener(readListener);
            return super.importExcel(file);
    
        }
    
    
        @Override
        protected Organization convert2EntityForExcel(OrganizationForImportVO vo) {
            Organization entity = organizationService.init();
            BeanUtils.copyProperties(vo, entity);
            entity.setType(dictionaryUtil.getCodeByName("OrganizationType", vo.getTypeName()));
    
            // 处理上级
            if (StringUtils.isNotBlank(vo.getParentCode())) {
                // 优先判断上级编码是否存在,若存在,则根据编码找上级
                List<Organization> organizationList = organizationService.lambdaQuery()
                        .eq(Organization::getCode, vo.getParentCode()).list();
                if (organizationList.size() == 1) {
                    // 找到数据,设置父级标识
                    entity.setOrganization(organizationList.get(0).getId());
                } else {
                    // 未找到,抛出异常
                    throw new CustomException(OrganizationExceptionEnum.CODE_NOT_FOUND);
                }
            } else if (StringUtils.isNotBlank(vo.getParentName())) {
    
                // 将上级名称转换为标识
                List<Organization> organizationList = organizationService.lambdaQuery()
                        .eq(Organization::getName, vo.getParentName()).list();
                if (organizationList.size() == 1) {
                    // 根据名称找到唯一的部门,设置部门标识
                    entity.setOrganization(organizationList.get(0).getId());
                } else if (organizationList.size() == 0) {
                    throw new CustomException(OrganizationExceptionEnum.NAME_NOT_FOUND);
                } else {
                    // 找到多个同名部门,不抛异常,统一设置到预定义的未分配部门,转人工处理
                    entity.setOrganization(SystemConstant.UNSIGNED_ORGANIZATION_ID);
                }
    
            } else {
                // 上级名称和编码都为空,数据不合法
                throw new CustomException(OrganizationExceptionEnum.PARENT_NAME_AND_CODE_CANOT_NULL);
            }
    
            return entity;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116

    创建数据导入视图对象类

    EasyExcel提供了两种模式来映射数据,一是列下标,二是列名。列下标会因为列的增、删、移动而变化,不可靠,采用列名更合适,需要在vo对象的属性上加上注解@ExcelProperty(“名称”)。

    采用专用的vo,只需要把excel中出现的有限的几个列建立对应的属性的就行了,如果采用与表单输入公用VO的方案,还得用@ExcelIgnore注解把不需要的字段给忽略掉,要不然导入环节会出错。

    这里有个问题需要注意,不能加@Accessors(chain = true)。这个注解将使属性的set方法返回值是对象本身,而EasyExcel组件解析处理映射使用了BeanMap从Map拷贝到Bean, 需要Map 的Key与Bean的变量名一致, 并有对应的 set方法, 且set方法为 void, 才能拷贝成功。

    package tech.abc.platform.system.vo;
    
    
    import com.alibaba.excel.annotation.ExcelProperty;
    import lombok.Data;
    
    import javax.validation.constraints.NotBlank;
    
    /**
     * 组织机构导入 视图对象类
     *
     * @author wqliu
     * @date 2023-05-18
     */
    @Data
    public class OrganizationForImportVO {
    
    
        /**
         * 名称
         */
        @NotBlank(message = "【名称】不能为空")
        @ExcelProperty("名称")
        private String name;
    
        /**
         * 编码
         */
        @ExcelProperty("编码")
        private String code;
    
    
        /**
         * 类型
         */
        @NotBlank(message = "【类型】不能为空")
        @ExcelProperty("类型")
        private String typeName;
    
        /**
         * 上级名称
         */
        @ExcelProperty("上级名称")
        private String parentName;
    
        /**
         * 上级编码
         */
        @ExcelProperty("上级编码")
        private String parentCode;
    
    
        /**
         * 排序
         */
        @ExcelProperty("排序")
        private String orderNo;
    
        /**
         * 备注
         */
        @ExcelProperty("备注")
        private String remark;
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    配置导入模板

    预先配置导入模板有诸多好处,除了美观外,主要是简单易用,避免出错。此外,还可以在预置模板里做一些简单配置,如单元格的格式(如文本、金额、日期),以及简单数据源的下拉选择(如是否),放一些示例数据等。
    image.png
    模板我们放在了对应模块的resources目录下/template/system/organization/import.xlsx,注意这里的模块编码实际不能少,否则容易产生不同模块下实体名相同的问题,造成打包时模板覆盖问题。

    pom打包,记得附加**/*.xlsx,把excel模板作为资源打包进去。

     <build>
            <finalName>abc-platform-systemfinalName>
            <plugins>
                
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <version>3.8.1version>
                    <configuration>
                        
                        <source>1.8source>
                        <target>1.8target>
                        <encoding>UTF-8encoding>
                    configuration>
                plugin>
                
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-surefire-pluginartifactId>
                    <version>2.22.2version>
                    <configuration>
                        
                        <skipTests>trueskipTests>
                    configuration>
                plugin>
    
            plugins>
            <resources>
                
                <resource>
                    <directory>src/main/javadirectory>
                    <includes>
                        <include>**/*.xmlinclude>
                    includes>
                resource>
                
                <resource>
                    <directory>src/main/resourcesdirectory>
                    <includes>
                        
                        <include>*.ymlinclude>
                        
                        <include>**/*.xlsxinclude>
                        
                        <include>*.xmlinclude>
                    includes>
                resource>
            resources>
        build>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    配置权限项

    这块没啥好说的,使用平台系统管理模块下的权限项配置功能,按照规范和约定增加配置项即可。
    image.png

    前端列表视图增加按钮

    <el-button
        v-permission="pageCode + 'downloadImportTemplate'"
        type="primary"
        icon="Download"
        @click="downloadImportTemplate"
        >下载模板el-button
      >
      <el-upload
        ref="uploader"
        :limit="1"
        :http-request="importData"
        action="/"
        :show-file-list="false"
        :before-upload="onBeforeUpload"
        class="uploader"
      >
        <el-button v-permission="pageCode + 'import'" type="primary" icon="List"
          >批量导入
        el-button>
      el-upload>
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里有个坑点,el-upload控件默认会另起一行
    image.png
    加了display: inline;也不够,会成下面这个样子
    image.png
    跟按钮水平对齐,最终加的样式如下:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    最后实现效果如下,完美对齐。
    image.png

    前端调用

    vue页面调用,对应按钮事件,如下

    // 下载导入模板
      downloadImportTemplate() {
        this.api.downloadImportTemplate()
      },
      // 导入
      importData(file) {
        const formData = new FormData()
        formData.append('file', file.file)
        this.api.import(formData).finally(() => {
          this.clearFile()
        })
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    system模块统一封装的api,接收vue调用,然后调用axios服务实例,如下

      // 下载导入模板
      downloadImportTemplate() {
        return request.download({ url: this.serveUrl + 'downloadImportTemplate' })
      },
      // 导入
      import(formData) {
        return request.upload({ url: this.serveUrl + 'importExcel', data: formData })
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    下载比较简单,没什么好说的。
    这地方有个需要特别注意的点,:http-request="importData"绑定的事件,传入的参数把file又封装了一层,如果写成formData.append(‘file’, file),后端用SpringMVC收到的file参数一直为null,需要使用的是参数file的属性file,即lformData.append(‘file’, file.file)。

    实现时遇到的问题,使用element plus的uploader控件,上传文件只能执行一次,再次上传(数据验证失败,修改后重新导入或多次导入),因为缓存问题会无操作响应(不是浏览器假死,而是不触发操作),需要在上传成功和失败的情况下,均执行清理文件操作。

      clearFile() {
          // 上传成功之后清除历史记录,否则再次上传浏览器无响应
          this.$refs.uploader.clearFiles()
        }
    
    • 1
    • 2
    • 3
    • 4

    扩展Axios,实现上传与下载

    路径为src\config\axios\index.ts,之前实现了get、put、post和delete,虽然下载用的是get,上传用的是post,但因为文件处理比较特殊,有额外的处理,因此单独封装。

    下载,必须指定responseType属性为’blob’。
    上传,必须指定headersType为’multipart/form-data’

     download: (option: any) => {
        request({ method: 'get', responseType: 'blob', ...option })
          .then((res) => {
            const { data, headers } = res
            const fileName = headers['content-disposition'].replace(/\w+;filename=(.*)/, '$1')
            // 此处当返回json文件时需要先对data进行JSON.stringify处理,其他类型文件不用做处理
            const blob = new Blob([data], { type: headers['content-type'] })
            const dom = document.createElement('a')
            const url = window.URL.createObjectURL(blob)
            dom.href = url
            dom.download = decodeURI(fileName)
            dom.style.display = 'none'
            document.body.appendChild(dom)
            dom.click()
            dom.parentNode.removeChild(dom)
            window.URL.revokeObjectURL(url)
          })
          .catch((err) => {
            reject(err)
          })
      },
      upload: (option: any) => {
        return new Promise((resolve, reject) => {
          option.headersType = 'multipart/form-data'
          request({ method: 'post', ...option })
            .then((res) => {
              // 明确设置为true时,显示提示
              if (option.showInfo === true) {
                ElMessage.info(res.data.message)
              }
              resolve(res.data)
            })
            .catch((err) => {
              reject(err)
            })
        })
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    数据导出

    虽然EasyExcel提供了直接的导出Excel的api,但是关键问题有两个,一是Excel的格式,在程序中设置相当地繁琐,并且不够美观,如涉及到复合表头、合并单元格,那么不是一般的复杂,另一方面,需要在视图模型类上加诸多的注解,来控制是否显示以及必要的格式转换,这两点的初始化工作,以及调整的工作量都偏大,灵活性也太差。

    从设计角度考虑,数据和展现应该是分离的,不应该耦合在一块,因此使用EasyExcel的填充api,即先人工编辑好excel模板,设置好样式,以及数据的占位,具体的数据,由应用程序后端动态生成来填充,各司其职,提供灵活性和扩展性。

    如何处理数据导出
    不同的场景下,导出的数据量是不同的,如系统的主数据组织机构、人员等,数据量有限,可以一次性读取到内存,一次性写入到Excel中,但是不可避免存在导出大量业务单据的情况,例如几万甚至几十万的数据,这时候,应该分批读取和分批写入,避免大量占用应用服务器内存,也减少全量垃圾回收次数,使应用运行更稳定。

    有两种方案,一是平台封装两个方法,分别是一次性处理和分批处理,由业务功能开发时根据估算数据量,来自行决定调用哪一个;二是平台只提供一个方法,内部根据数据量大小及配置来决定是一次性处理还是分批处理。

    经考虑后,采取以方案2,这样使用方无需过多关注细节,只是去调用导出即可,而且某些业务单据可能上百万,但是用户选择了时间段等过滤条件后,数据量可能只有几百几千条,同样更适合一次性处理。

    实现阶段进行方案优化,在控制器层,父类获取获取数据量比较困难(技术上能拿到,但需要每个子类去实现获取总量的方法),因此采用变通的处理方式,即统一使用分批处理的模式,每次处理数据量设置一个较大值,比如10000,这样小数据量的导出,处理一次就结束了,而大数据量,仍会分批多次处理。

    需要导出excel的功能的业务实体对应的控制器,只需要继承控制器父类,并覆写一个获取分页数据的方法即可。
    并且可以自行控制单次处理数据量(如列特别多,逻辑处理复杂等)

    数据导出的具体设计与实现思路与导入高度类似,这里只放出关键代码,具体参见开源代码库。

    excel导出功能公共基类

    package tech.abc.platform.common.extension;
    
    import com.alibaba.excel.EasyExcel;
    import com.alibaba.excel.ExcelWriter;
    import com.alibaba.excel.write.metadata.WriteSheet;
    import com.baomidou.mybatisplus.core.metadata.IPage;
    import org.springframework.core.io.ClassPathResource;
    import tech.abc.platform.common.base.BaseController;
    import tech.abc.platform.common.component.easyexcel.ExcelExceptionEnum;
    import tech.abc.platform.common.exception.CustomException;
    import tech.abc.platform.common.exception.FileException;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.lang.reflect.ParameterizedType;
    import java.net.URLEncoder;
    
    
    /**
     * excel导出功能扩展
     *
     * @author wqliu
     * @date 2023-05-19
     */
    public class ExcelExportExtension<V, E> extends BaseController {
    
    
        /**
         * 导出数据分页大小
         * TODO:可配置化
         */
        private static final long EXPORT_DATA_PAGE_SIZE = 200;
    
        /**
         * 导出模板
         */
        private String exportTemplate;
    
        /**
         * 设置导出模板
         */
        public void setExportTemplate(String exportTemplate) {
            this.exportTemplate = exportTemplate;
        }
    
    
        /**
         * 导出到excel
         *
         * @param queryParam
         * @param response
         * @throws Exception
         */
        public void exportExcel(V queryParam, HttpServletResponse response) {
    
            ClassPathResource classPathResource = new ClassPathResource(this.exportTemplate);
            try (InputStream inputStream = classPathResource.getInputStream();
                 OutputStream outputStream = response.getOutputStream()) {
    
                // 设置响应信息
                response.setContentType("application/vnd.ms-excel");
                response.setCharacterEncoding("utf-8");
                // 这里URLEncoder.encode可以防止中文乱码
                String fileName = URLEncoder.encode("导出数据.xlsx", "UTF-8");
                response.setHeader("Content-disposition", "attachment;filename=" + fileName);
    
    
                // excel导出处理
                ExcelWriter excelWriter = EasyExcel.write(outputStream, this.getClazz())
                        .withTemplate(inputStream).build();
                WriteSheet writeSheet = EasyExcel.writerSheet().build();
    
                // 此处并没有获取数据总量后自行计算分页,而是利用了已有的分页查询功能
                long pageSize = EXPORT_DATA_PAGE_SIZE;
    
                // 开始第一次查询,并获取分页总数
                IPage<V> pagedResult = getExportData(queryParam, pageSize, 1);
                excelWriter.fill(pagedResult.getRecords(), writeSheet);
                // 读取后续数据
                for (int i = 2; i <= pagedResult.getPages(); i++) {
                    // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                    pagedResult = getExportData(queryParam, pageSize, i);
                    excelWriter.fill(pagedResult.getRecords(), writeSheet);
                }
                // 关闭流
                excelWriter.finish();
    
            } catch (Exception exception) {
                throw new CustomException(FileException.EXCEL_EXPORT_FAILURE, exception.getMessage());
            }
        }
    
    
        /**
         * 获取导出数据
         *
         * @return
         * @throws Exception
         */
        public IPage<V> getExportData(V queryParam, long pageSize, long pageNum) {
            throw new CustomException(ExcelExceptionEnum.EXPORT_METHOD_UNIMPLEMENTED);
        }
    
    
        /**
         * 通过父类获取运行时泛型类型
         *
         * @return
         */
        private Class<V> getClazz() {
            // 获得当前类型的带有泛型类型的父类
            Class subclass;
            ParameterizedType pd = (ParameterizedType) this.getClass().getGenericSuperclass();
            Class<V> clazz = (Class) pd.getActualTypeArguments()[0];
            return clazz;
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121

    具体的导出控制器类

    package tech.abc.platform.system.controller.extension;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.metadata.IPage;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import tech.abc.platform.common.annotation.SystemLog;
    import tech.abc.platform.common.extension.ExcelExportExtension;
    import tech.abc.platform.common.query.QueryGenerator;
    import tech.abc.platform.system.controller.OrganizationController;
    import tech.abc.platform.system.entity.Organization;
    import tech.abc.platform.system.service.OrganizationService;
    import tech.abc.platform.system.vo.OrganizationVO;
    
    import javax.servlet.http.HttpServletResponse;
    import java.util.List;
    
    
    /**
     * 实现Excel导出功能的组织机构控制器
     *
     * @author wqliu
     * @date 2023-05-19
     */
    @RestController
    @RequestMapping("/system/organization")
    @Slf4j
    public class OrganizationExcelExportExtensionController extends ExcelExportExtension<OrganizationVO, Organization> {
    
        @Autowired
        private OrganizationService organizationService;
    
        @Autowired
        protected OrganizationController organizationController;
    
    
        @GetMapping("/exportExcel")
        @Override
        @PreAuthorize("hasPermission(null,'system:organization:export')")
        @SystemLog(value = "导出excel", logResponseData = false)
        public void exportExcel(OrganizationVO vo, HttpServletResponse response) {
    
    
            // 设置模板名称
            super.setExportTemplate("/template/system/organization/export.xlsx");
    
            // 当勾选查询所有复选框时,查询所有数据
            if (vo.getIgnoreParent() != null && vo.getIgnoreParent()) {
                vo.setOrganization(null);
            }
            // 导出到excel
            super.exportExcel(vo, response);
    
        }
    
        @Override
        public IPage<OrganizationVO> getExportData(OrganizationVO queryParam, long pageSize, long pageNum) {
    
            // 构造分页对象
            IPage<Organization> page = new Page<Organization>(pageNum, pageSize);
            // 构造查询条件
            QueryWrapper<Organization> queryWrapper = QueryGenerator.generateQueryWrapper(Organization.class, queryParam);
    
            // 排序
            queryWrapper.lambda().orderByAsc(Organization::getOrganization).orderByAsc(Organization::getOrderNo);
    
    
            organizationService.page(page, queryWrapper);
    
    
            // 转换vo
            IPage<OrganizationVO> pageVO = mapperFacade.map(page, IPage.class);
            List<OrganizationVO> organizationVOList = organizationController.convert2VO(page.getRecords());
            pageVO.setRecords(organizationVOList);
            return pageVO;
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    数据导出效果
    image.png

    开发平台资料

    平台名称:一二三开发平台
    简介: 企业级通用开发平台
    设计资料:csdn专栏
    开源地址:Gitee
    开源协议:MIT

    欢迎收藏、点赞、评论,你的支持是我前行的动力。

  • 相关阅读:
    昔年邢台稻田不下万顷 国稻种芯·中国水稻节:河北谷子收获
    【面试刷题】——Linux基础命令
    NSS [SWPUCTF 2022 新生赛]numgame
    C++ | 无root权限在centOS7.9上安装新版本gcc编译器
    TeX的一些用法
    vcruntime140d.dl丢失怎么办?
    MySQL表加字段可为null导致ArrayIndexOutOfBoundsException报错问题记录
    Java代码优化的30个小技巧
    C#的AOP(最经典实现)
    已解决java.nio.charset.CoderMalfunctionError: 编码器故障错误的正确解决方法,亲测有效!!!
  • 原文地址:https://blog.csdn.net/seawaving/article/details/131299250