Spring boot实操演练5 ——导入Excel文件到数据库

目录

1、业务场景

(1)、分享原因

(2)、需求分析

(3)、数据库设计

2、开发环境和工具

3、代码思路

4、参考代码

导出模板vo(KitchenStoreExcelVo)

商铺信息表(HwKitchenStoreInfo)

Mapper 接口(HwKitchenStoreInfoMapper)

Mapping.xml(HwKitchenStoreInfoMapper.xml)

controller层(KitchenStoreManageController)

service层(IKitchenStoreManageService)

serviceImpl层(KitchenStoreManageServiceImpl)

5、总结


1、业务场景

(1)、分享原因

之前笔者分享过导出功能的一种写法,既然有导出功能,那相对的导入功能也是存在的一种需求,本期笔者就分享一下导入功能的一种思路;

(2)、需求分析

导入功能可分为两个子需求,子需求1是下载导入模板,子需求2是导入模板数据;

(3)、数据库设计

本次代码演示使用的业务背景是商铺基本信息管理,预插入数据对应的数据库设计如下图所示:

餐厨商铺信息表

Spring boot实操演练5 ——导入Excel文件到数据库_第1张图片

2、开发环境和工具

因为该业务功能是在我实习公司已有项目下的拓展,所以环境依赖较多,我将重点阐述我所负责的后端代码逻辑,开发环境和工具只会简单带过;如果在借鉴代码过程中缺失包或者依赖,请根据IDE提示或者自行百度调试(就当锻炼自己对开发工具的熟悉程度啦);

(1)、IDE:IntelliJ IDEA 2022.1

(2)、测试:Postman

(3)、JDK:jdk-11.0.15

(4)、Spring Boot Version:2.2.5.RELEASE

(5)、modelVersion:4.0.0

(6)、pomVersion:0.0.1-SNAPSHOT

3、代码思路

Spring boot实操演练5 ——导入Excel文件到数据库_第2张图片

4、参考代码

本次导入功能以最简单的(商铺)管理为代码背景来进行展示和说明:

导出模板vo(KitchenStoreExcelVo)


代码说明:

(1)、@Excel里面需要声明三个东西:

name:列名,对应数据库需要的字段;必填还是选填取决于数据库对应的字段能否为空,可以添加适当备注引导使用者正确填写;注意!并不是所有的数据库字段都需要用户导入,这里选择字段时参照的标准是对应新增功能需要用户填写的信息,所以本质是一个变相的新增功能,只是可以达到批量新增的效果而已;

orderNum:第几列,从0(这里的0等价于第1列)开始;

width:列宽,长度定义取决于你的列名(name)的长度,自行调试感受就行;

(2)、其他就是Vo的基本写法了,定义好属性后idea可以一键生成get和set方法,只需要右键空白处然后点击弹出菜单的Generate选择Getter and Setter,最后全选对应的属性即可;

package xxx.vo;

import cn.afterturn.easypoi.excel.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

/**
 * 

* 商铺信息导入excel模板下载用vo *

* * @author Mike-GY * @since 2022-08-29 */ public class KitchenStoreExcelVo { /** * 商铺名称 */ @Excel(name = "商铺名称(必填,不能重复)", orderNum = "0", width = 30) private String storeName; /** * 商铺负责人名称 */ @Excel(name = "负责人(选填)", orderNum = "1", width = 20) private String leader; /** * 商铺负责人电话 */ @Excel(name = "联系电话(选填)", orderNum = "2", width = 20) private String leaderPhone; /** * 商铺类型名称 */ @Excel(name = "商铺类型(必填,请按照新增商铺功能下商铺类型可选选项来填写)", orderNum = "3", width = 60) private String typeName; /** * 所属政府区域名称 */ @Excel(name = "所属组织(必填,请按照新增商铺功能下所属组织可选选项来填写)", orderNum = "4", width = 60) private String depName; /** * 使用的垃圾桶数量 */ @Excel(name = "容器数量(个)(选填)", orderNum = "5", width = 25) private Integer garbageCanNum; /** * 商铺地址 */ @Excel(name = "地址(选填)", orderNum = "6", width = 20) private String storeAddress; /** * 营业状态名称 */ @Excel(name = "营业状态(选填)", orderNum = "7", width = 20) private String storeStatusName; /** * 签约时间 */ @Excel(name = "签约时间(选填,时间格式:yyyy-MM-dd)", orderNum = "8", width = 40) @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GTM+8") private Date signTime; public String getStoreName() { return storeName; } public void setStoreName(String storeName) { this.storeName = storeName; } public String getLeader() { return leader; } public void setLeader(String leader) { this.leader = leader; } public String getLeaderPhone() { return leaderPhone; } public void setLeaderPhone(String leaderPhone) { this.leaderPhone = leaderPhone; } public String getTypeName() { return typeName; } public void setTypeName(String typeName) { this.typeName = typeName; } public String getDepName() { return depName; } public void setDepName(String depName) { this.depName = depName; } public Integer getGarbageCanNum() { return garbageCanNum; } public void setGarbageCanNum(Integer garbageCanNum) { this.garbageCanNum = garbageCanNum; } public String getStoreAddress() { return storeAddress; } public void setStoreAddress(String storeAddress) { this.storeAddress = storeAddress; } public String getStoreStatusName() { return storeStatusName; } public void setStoreStatusName(String storeStatusName) { this.storeStatusName = storeStatusName; } public Date getSignTime() { return signTime; } public void setSignTime(Date signTime) { this.signTime = signTime; } }

商铺信息表(HwKitchenStoreInfo


该model对应的是之前提到过的预插入数据对应的数据库的model,我是使用代码生成器快捷生成的,只能借鉴个结构,最上方的注入估计读者们抄不了,因为依赖存放的包位置绝对不一样,请自行查找补充;

package xxx.model;

import com.baomidou.mybatisplus.enums.IdType;
import java.util.Date;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.io.Serializable;

/**
 * 

* 餐厨商铺信息表 *

* * @author Mike-GY * @since 2022-08-26 */ @TableName("hw_kitchen_store_info") public class HwKitchenStoreInfo implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 商铺名称 */ @TableField("store_name") private String storeName; /** * 商铺负责人名称 */ private String leader; /** * 商铺负责人电话 */ @TableField("leader_phone") private String leaderPhone; /** * 商铺类型编码(来源于字典表) */ @TableField("type_code") private String typeCode; /** * 商铺类型名称 */ @TableField("type_name") private String typeName; /** * 所属车组或部门id */ @TableField("dep_id") private Integer depId; /** * 所属车组或部门名称 */ @TableField("dep_name") private String depName; /** * 使用的垃圾桶数量 */ @TableField("garbage_can_num") private Integer garbageCanNum; /** * 经度 */ private Double longitude; /** * 纬度 */ private Double latitude; /** * 商铺地址 */ @TableField("store_address") private String storeAddress; /** * 营业状态编码(来源于字典表) */ @TableField("store_status_code") private String storeStatusCode; /** * 营业状态名称 */ @TableField("store_status_name") private String storeStatusName; /** * 签约时间 */ @TableField("sign_time") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GTM+8") private Date signTime; /** * 删除标识:0:未删除,1:已删除; */ private Integer deleted; /** * 创建时间 */ @TableField("create_time") private Date createTime; /** * 更新时间 */ @TableField("update_time") private Date updateTime; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getStoreName() { return storeName; } public void setStoreName(String storeName) { this.storeName = storeName; } public String getLeader() { return leader; } public void setLeader(String leader) { this.leader = leader; } public String getLeaderPhone() { return leaderPhone; } public void setLeaderPhone(String leaderPhone) { this.leaderPhone = leaderPhone; } public String getTypeCode() { return typeCode; } public void setTypeCode(String typeCode) { this.typeCode = typeCode; } public String getTypeName() { return typeName; } public void setTypeName(String typeName) { this.typeName = typeName; } public Integer getDepId() { return depId; } public void setDepId(Integer depId) { this.depId = depId; } public String getDepName() { return depName; } public void setDepName(String depName) { this.depName = depName; } public Integer getGarbageCanNum() { return garbageCanNum; } public void setGarbageCanNum(Integer garbageCanNum) { this.garbageCanNum = garbageCanNum; } public Double getLongitude() { return longitude; } public void setLongitude(Double longitude) { this.longitude = longitude; } public Double getLatitude() { return latitude; } public void setLatitude(Double latitude) { this.latitude = latitude; } public String getStoreAddress() { return storeAddress; } public void setStoreAddress(String storeAddress) { this.storeAddress = storeAddress; } public String getStoreStatusCode() { return storeStatusCode; } public void setStoreStatusCode(String storeStatusCode) { this.storeStatusCode = storeStatusCode; } public String getStoreStatusName() { return storeStatusName; } public void setStoreStatusName(String storeStatusName) { this.storeStatusName = storeStatusName; } public Date getSignTime() { return signTime; } public void setSignTime(Date signTime) { this.signTime = signTime; } public Integer getDeleted() { return deleted; } public void setDeleted(Integer deleted) { this.deleted = deleted; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } @Override public String toString() { return "HwKitchenStoreInfo{" + "id=" + id + ", storeName=" + storeName + ", leader=" + leader + ", leaderPhone=" + leaderPhone + ", typeCode=" + typeCode + ", typeName=" + typeName + ", depId=" + depId + ", depName=" + depName + ", garbageCanNum=" + garbageCanNum + ", longitude=" + longitude + ", latitude=" + latitude + ", storeAddress=" + storeAddress + ", storeStatusCode=" + storeStatusCode + ", storeStatusName=" + storeStatusName + ", signTime=" + signTime + ", deleted=" + deleted + ", createTime=" + createTime + ", updateTime=" + updateTime + "}"; } }

Mapper 接口(HwKitchenStoreInfoMapper)


package xxx.mapper;

import xxx.entity.model.HwKitchenStoreInfo;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import org.springframework.stereotype.Component;

/**
 * 

* 餐厨商铺信息表 Mapper 接口 *

* * @author Mike-GY * @since 2022-08-26 */ @Component public interface HwKitchenStoreInfoMapper extends BaseMapper { }

Mapping.xml(HwKitchenStoreInfoMapper.xml)






    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

    
    
        id, store_name, leader, leader_phone, type_code, type_name, dep_id, dep_name, garbage_can_num, longitude, latitude, store_address, store_status_code, store_status_name, sign_time, deleted, create_time, update_time
    

controller层(KitchenStoreManageController)


package xxx.kitchen.controller;

import xxx.exception.BusinessException;
import xxx.utils.message.Messages;
import xxx.utils.util.CommonResponse;
import xxx.utils.util.MessagesUtil;
import xxx.mc.utils.FileWithExcelUtil;
import xxx.kitchen.service.IKitchenStoreManageService;
import xxx.kitchen.vo.KitchenStoreExcelVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;


import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


/**
 * @author Mike-GY
 */
@RestController
@RequestMapping("/kitchen/KitchenStoreManage")

public class KitchenStoreManageController {

    //    依赖注入
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final IKitchenStoreManageService iKitchenStoreManageService;

    public KitchenStoreManageController(IKitchenStoreManageService iKitchenStoreManageService) {
        this.iKitchenStoreManageService = iKitchenStoreManageService;
    }
    
    /**
     * 导入商铺信息——下载导入模板
     * @param response http响应
     * @author Mike-GY
     * @since 2022/8/29
     */
    @PostMapping("/exportStoreInfoExcel")
    public void exportStoreInfoExcel(HttpServletResponse response) {
        try {
            //    将excelVo里的属性复制成一个list
            List excelVoList = new ArrayList<>();
//    将list通过FileWithExcelUtil.exportExcel导出成自定义的excel文件            FileWithExcelUtil.exportExcel(excelVoList,null,null,KitchenStoreExcelVo.class, "商铺信息导入模板.xls",response);
            //    返回提示信息
            logger.info("商铺信息导入模板下载成功");
        } catch (Exception e) {
            logger.error("商铺信息导入模板下载失败,错误原因为:" + e.getMessage());
        }
    }

    /**
     * 导入商铺信息——导入模板数据
     * @param file 导入模板文件
     * @return res
     * @author Mike-GY
     * @since 2022/8/29
     */
    @PostMapping("/importStoreInfoExcel")
    public CommonResponse importStoreInfoExcel(@RequestBody MultipartFile file) throws Exception {
        CommonResponse res = CommonResponse.getInstance();
        //    对文件进行判空处理
        if (file == null ) {
            throw new BusinessException(Messages.CODE_500202, MessagesUtil.getMessage(Messages.CODE_500202));
        }
        //    如果文件不为空则调用导入服务
        Map result = iKitchenStoreManageService.importStoreInfoExcel(file);
        //    返回导入服务调用结果
        res.setResultData(result);
        return res;
    }

}

service层(IKitchenStoreManageService)


package xxx.kitchen.service;


import org.springframework.web.multipart.MultipartFile;


import java.util.Map;

/**
 * @author Mike-GY
 */
public interface IKitchenStoreManageService {



    /**
     * 导入商铺信息——导入模板数据
     * @param file 导入模板文件
     * @return res
     * @throws Exception 抛出异常
     */
    Map importStoreInfoExcel(MultipartFile file) throws Exception;
}

serviceImpl层(KitchenStoreManageServiceImpl)


package xxx.kitchen.service.impl;

import xxx.utils.util.ExcelUtil;
import xxx.entity.CodeMaster;
import xxx.entity.DepartmentInfo;
import xxx.entity.mapper.HwKitchenStoreInfoMapper;
import xxx.entity.model.HwKitchenStoreInfo;
import xxx.mapper.CodeMasterMapper;
import xxx.mapper.DepartmentInfoMapper;
import xxx.kitchen.service.IKitchenStoreManageService;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;


import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author Mike-GY
 */
@Service
public class KitchenStoreManageServiceImpl implements IKitchenStoreManageService {

    //    注入需要的mapper
    private final HwKitchenStoreInfoMapper hwKitchenStoreInfoMapper;
    private final CodeMasterMapper codeMasterMapper ;
    private final DepartmentInfoMapper departmentInfoMapper;

    public KitchenStoreManageServiceImpl(
            HwKitchenStoreInfoMapper hwKitchenStoreInfoMapper,
            CodeMasterMapper codeMasterMapper,
            DepartmentInfoMapper departmentInfoMapper
    ) {
        this.hwKitchenStoreInfoMapper = hwKitchenStoreInfoMapper;
        this.codeMasterMapper = codeMasterMapper;
        this.departmentInfoMapper = departmentInfoMapper;
    }
    
    /**
     * 导入商铺信息——导入模板数据
     */
    @Override
    public Map importStoreInfoExcel(MultipartFile file) throws Exception {
        //    定义一个HashMap来存放导入结果
        Map result = new HashMap(8);
        //    定义一个list来存放导入失败的数据
        List> failedImportList = new ArrayList<>();
        //    定义一个int类型的变量来表示导入成功的数据数量
        int successImport = 0;
        //    读取导入excel文件当中的数据
        List> readExcel = ExcelUtil.readExcel(file, 1, 2);
        //    对读取结果进行判空
        if (CollectionUtils.isNotEmpty(readExcel)) {
            //    不为空则使用for循环开始逐一导入
            for (Map map : readExcel) {
                StringBuilder sb = new StringBuilder();
                HwKitchenStoreInfo info = new HwKitchenStoreInfo();
                //    部分通用的属性可在直接赋值,无需做其他处理
                info.setDeleted(0);
                info.setCreateTime(new Date());
                info.setUpdateTime(new Date());
                //    商铺名称
                String storeName = map.get(0);
                //    注意读取map的位置,要与excel文件对应
                if (StringUtils.isNotEmpty(storeName)) {
                    //    验证商铺名称是否重复
                    HwKitchenStoreInfo nameInfo = findStoreByName(storeName);
                    if (nameInfo != null) {
                        sb.append("该商铺名称已经存在");
                    } else {
                        info.setStoreName(storeName);
                    }
                } else  {
                    sb.append("商铺名称不能为空");
                }
                //  负责人
                String leader = map.get(1);
                if (StringUtils.isNotEmpty(leader)) {
                    info.setLeader(leader);
                }
                //  联系电话
                String leaderPhone = map.get(2);
                if (StringUtils.isNotEmpty(leaderPhone)) {
                    info.setLeaderPhone(leaderPhone);
                }
                //    商铺类型
               	/** 回看之前数据库设计可知某些字段需要对应的编码,这就需要我们根据导入的名称去其他表检索对应的编码并将其赋值给商铺信息,如果查询结果不满足需求则返回对应提示信息;这样做的原因是导入人/用户只需要填写简单的基本信息即可正常使用导入功能,使该功能更易于被人接受;你也不想去查一大堆你看不懂的商业编码表吧?用户不一定能够看懂而且非常容易输错 */
                String typeName = map.get(3);
                if (StringUtils.isNotEmpty(typeName)) {
                    CodeMaster typeInfo = findTypeCodeByTypeName(typeName);
                    if (typeInfo != null) {
                        info.setTypeCode(typeInfo.getCode());
                        info.setTypeName(typeName);
                    } else {
                        sb.append("商铺类型找不到对应编码,请修改商铺类型后重新导入");
                    }
                } else {
                    sb.append("商铺类型不能为空");
                }
                //  所属组织
                String depName = map.get(4);
                if (StringUtils.isNotEmpty(depName)) {
                    DepartmentInfo depInfo = findDepIdByDepName(depName);
                    if (depInfo != null) {
                        info.setDepId(depInfo.getId());
                        info.setDepName(depName);
                    } else {
                        sb.append(("找不到所属组织,请修改组织名称后重新导入"));
                    }
                } else {
                    sb.append("所属组织不能为空");
                }
                //  容器个数
                String canNum = map.get(5);
                if (StringUtils.isNotEmpty(canNum)) {
                Integer num = Integer.parseInt(canNum);
                info.setGarbageCanNum(num);
                }
                //  地址
                String address = map.get(6);
                if (StringUtils.isNotEmpty(address)) {
                    info.setStoreAddress(address);
                }
                //  营业状态
                String status = map.get(7);
                if (StringUtils.isNotEmpty(status)) {
                    CodeMaster statusInfo = findStatusCodeByStatusName(status);
                    if (statusInfo != null) {
                        info.setStoreStatusCode(statusInfo.getCode());
                        info.setStoreStatusName(status);
                    }
                }
                //  签约时间;
                String time = map.get(8);
                if (StringUtils.isNotEmpty(time)) {
                    String date = time.split(" ")[0];
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                    Date signTime = simpleDateFormat.parse(date);
                    info.setSignTime(signTime);
                }
                //  创建一个errMap来接收导入失败的数据
                Map errMap = new HashMap<>(8);
                errMap.put("storeName", storeName);
                errMap.put("leader", leader);
                errMap.put("leaderPhone", leaderPhone);
                errMap.put("typeName", typeName);
                errMap.put("depName", depName);
                errMap.put("canNum", canNum);
                errMap.put("address", address);
                errMap.put("status", status);
                errMap.put("time", time);
                //  将不符合导入规则的数据放入errMap,符合的则插入到数据库中
                if (sb.length() > 0) {
                    errMap.put("message", sb.toString());
                    failedImportList.add(errMap);
                }else {
                    try {
                        hwKitchenStoreInfoMapper.insert(info);
                        successImport ++;
                    } catch (Exception e) {
                        sb.append("导入数据库失败");
                        errMap.put("message", sb.toString());
                        failedImportList.add(errMap);
                    }
                }
            }
        }
        //  调用服务完成后返回导入成功和导入失败的数量,并将导入失败的数据用一个list返回给controller
        result.put("successImport", successImport);
        result.put("failedImport", failedImportList.size());
        result.put("failedImportList", failedImportList);
        return result;
    }

    /**
     * 根据商铺名称查询商铺信息(验证名称是否重复)
     */
    private HwKitchenStoreInfo findStoreByName(String storeName) {
        HwKitchenStoreInfo info = new HwKitchenStoreInfo();
        EntityWrapper storeInfoEntityWrapper =new EntityWrapper<>();
        storeInfoEntityWrapper.eq("store_name",storeName);
        storeInfoEntityWrapper.eq("deleted",0);
        List infos = hwKitchenStoreInfoMapper.selectList(storeInfoEntityWrapper);
        if (CollectionUtils.isNotEmpty(infos)) {
            info = infos.get(0);
        }
        return info;
    }

    /**
     * 根据商铺类型查询商铺类型编码(来源于字典表)(补全excel未提供但是数据库需要的信息)
     */
    private CodeMaster findTypeCodeByTypeName(String typeName) {
        CodeMaster info = new CodeMaster();
        EntityWrapper typeCodeWrapper = new EntityWrapper<>();
        typeCodeWrapper.eq("name",typeName);
        typeCodeWrapper.eq("enable",1);
        List infos = codeMasterMapper.selectList(typeCodeWrapper);
        if (CollectionUtils.isNotEmpty(infos)) {
            info = infos.get(0);
        }
        return info;
    }

    /**
     * 根据组织名称查询组织id(补全excel未提供但是数据库需要的信息)
     */
    private DepartmentInfo findDepIdByDepName(String depName) {
        DepartmentInfo info = new DepartmentInfo();
        EntityWrapper depNameWrapper = new EntityWrapper<>();
        depNameWrapper.eq("name",depName);
        depNameWrapper.eq("dep_status",0);
        List infos = departmentInfoMapper.selectList(depNameWrapper);
        if (CollectionUtils.isNotEmpty(infos)) {
            info = infos.get(0);
        }
        return info;
    }

    /**
     * 根据商铺营业状态查询营业状态编码(来源于字典表)(补全excel未提供但是数据库需要的信息)
     */
    private CodeMaster findStatusCodeByStatusName(String statusName){
         CodeMaster info = new CodeMaster();
        EntityWrapper statusCodeWrapper = new EntityWrapper<>();
        statusCodeWrapper.eq("name",statusName);
        statusCodeWrapper.eq("enable",1);
        List infos = codeMasterMapper.selectList(statusCodeWrapper);
        if (CollectionUtils.isNotEmpty(infos)) {
            info = infos.get(0);
        }
        return info;
    }
}

5、总结


1、需要的包和依赖需要自行引入;

2、个人理解:让用户填写的信息越简单越好,数据库的设计(字段覆盖面)越完整越好,那导入的时候如何补全两者之间的差异呢?如果是软件上的新增商铺功能,可以让前端绑定传参,比如新增商铺时用户选择了某个商铺所属部门,前端会将部门id一并传给你,你设置一个参数接收它就行;

3、而如果你想用文件,比如说excel来导入的话,则需要后端自行根据传入部门名称去部门这张表做查询并赋值到对应的商铺信息表,可以学我在servicelmpl里另写几个私有函数完成这个数据处理的过程;

你可能感兴趣的:(Spring,boot实操演练,java,后端,spring,boot)