前言

之前一直把用户上传的图片和文件保存在本地服务器的文件系统中,长而久之会产生以下弊端:

  • 当文件数量过多之后严重消耗Linux文件系统的inode;
  • 当数据量过大之后不易分布式扩展;
  • 数据备份困难,不方便前端展;
  • 文件的目录层级越来越深导致文件查找的速度逐渐变慢;

于是想搭建1个私有的阿里云-OSS服务,即对象存储服务;

由专门的对象存储服务来统一管理文件,文件上传之后OSS返回1个唯一的URL地址,后端返回前端,用户可以直接访问;

二、存储分类

根据不同的分类方法,存储也会被分成不同的类型,但最终到的用途是都是一致的存放数据;

1.本地存储、外置存储

根据存储设备所在的位置不同划分为

本地存储:电脑内置的存储设备例如硬盘、内存;

外置存储:U盘、移动硬盘

2.DAS、SAN、NAS

根据连接存储设备的介质不同划分为

DAS(Direct Attached Stroage):直连使存储,使用连接介质直接连接到主机、服务器等设备上,连接无需借助网络;

SAN(Stroage Area Network):  存储区域网络,使用专有的网络(非以太网)连接存储设备;

NAS(Network Attached Stroage):网络附加存储,任何1个终端(主机、服务器)都可以通过网络连接到存储设备上;

3.块存储、文件存储、对象存储

根据存储设备文件系统所在位置的不同划分为

块存储:    一切以磁盘形式存在的存储设备都可以称为块存储,块存储使用终端的文件系统,存储服务不利于共享;

文件存储:  块存储拥有了文件系统之后称为文件存储,文件存储使用服务端的文件系统,存储服务利于共享,文件系统使用传统的目录结构管理文件;

对象存储:对象存储的文件系统使用二层结构(Bucket+对象ID)来管理文件,适合存储非结构化数据,速度快、存储服务利于共享、可扩展性强;

三、Minio概述

以上我们得知对象存储是1种全新的存储架构,综合了块存储、和文件存储的优点;

对象存储颠覆了传统的文件系统以目录结构管理文件的方式,对象存储的文件系统使用2层结构(Bucket+对象ID)来管理1个唯一的文件对象,适合存储非结构化数据;

对象存储服务读写速度快、易于共享、可扩展性强;

Minio是一款可以实现分布式对象存储服务的软件,我一般会使用minio存储图片、合同、短视频等非结构化数据;

四、搭建Minio服务

 Minion是使用Golang开发下载完了二进制可执行文件,直接在当操作系统运行服务;

[root@localhost /]# minio server --console-address ":8000" ./data/
WARNING: Detected Linux kernel version older than 4.0.0 release, there are some known potential performance problems with this kernel version. MinIO recommends a minimum of 4.x.x linux kernel version for best performance
Automatically configured API requests per node based on available memory on the system: 151
Finished loading IAM sub-system (took 0.0s of 0.0s to load data).
Status:         1 Online, 0 Offline.
API: http://192.168.56.18:9000  http://192.168.122.1:9000  http://127.0.0.1:9000
RootUser: minioadmin
RootPass: minioadmin

Console: http://192.168.56.18:8000 http://192.168.122.1:8000 http://127.0.0.1:8000
RootUser: minioadmin
RootPass: minioadmin

Command-line: https://docs.min.io/docs/minio-client-quickstart-guide
   $ mc alias set myminio http://192.168.56.18:9000 minioadmin minioadmin

Documentation: https://docs.min.io

1.设置权限

访问Minio的Console接口修改对象服务的读写权限;

2.SpringBoot调用minionAPI

1.pom依赖

    <!--minio -->
            <dependency>
                <groupId>io.minio</groupId>
                <artifactId>minio</artifactId>
                <version>${miniio.version}</version>
            </dependency>

2.配置文件

# Miniio配置
minio:
  endpoint: 192.168.56.18
  port: 9000
  accessKey: minioadmin
  secretKey: minioadmin
  secure: false
  bucketName: "huike-crm"
  configDir: "/data/excel"

3.配置类

package com.huike.common.config;

import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
/**
 * @className: MinioConfig
 * @author Hope
 * @date 2022/7/28 13:43
 * @description: MinioConfig
 */

@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {

    private final static String HTTP = "http://";

    //endPoint是一个URL,域名,IPv4或者IPv6地址
    private String endpoint;

    //TCP/IP端口号
    private int port;

    //accessKey类似于用户ID,用于唯一标识你的账户
    private String accessKey;

    //secretKey是你账户的密码
    private String secretKey;

    //如果是true,则用的是https而不是http,默认值是true
    private Boolean secure;

    //默认存储桶
    private String bucketName;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

3.上传和下载文件

 @Autowired
    MinioConfig minioConfig;
    @Override
    public AjaxResult upload(MultipartFile file) {
        InputStream inputStream = null;
        //创建Minio的连接对象
        MinioClient minioClient = getClient();
        String bucketName = minioConfig.getBucketName();
        try {
            inputStream = file.getInputStream();
            //基于官网的内容,判断文件存储的桶是否存在 如果桶不存在就创建桶
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!found) {
                // 创建新桶
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            } else {
                System.out.println("桶" + bucketName + "已经存在");
            }
            String filename = file.getOriginalFilename();
            String objectName = "/data/excel/" + new SimpleDateFormat("yyyy/MM/dd").format(new Date())
                    + "/" + System.currentTimeMillis() + filename.substring(filename.lastIndexOf("."));
            //上传文件到minio
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .stream(inputStream, -1, 10485760)
                            .contentType("application/pdf")
                            .build());
            System.out.println("上传成功");
            Map<String, Object> msgMap = new HashMap<>();
            msgMap.put("msg", "操作成功");
            msgMap.put("fileName", objectName);
            msgMap.put("url", "http://" + minioConfig.getEndpoint() + ":" + minioConfig.getPort() + objectName);
            msgMap.put("code", 200);
            AjaxResult ajax = AjaxResult.success(msgMap);

            /**
             * 封装需要的数据进行返回
             */
            return ajax;
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.error("上传失败");
        } finally {
            //防止内存泄漏
            if (inputStream != null) {
                try {
                    inputStream.close(); // 关闭流
                } catch (IOException e) {
                    log.debug("inputStream close IOException:" + e.getMessage());
                }
            }
        }
    }
五、EasyExcel

Java解析Excel 

1.Java使用EasyExcel

1.1.pom依赖

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhanggen</groupId>
    <artifactId>easy-excel-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.0</version>
            <!--<exclusions>-->
                <!--<exclusion>-->
                    <!--<artifactId>poi-ooxml-schemas</artifactId>-->
                    <!--<groupId>org.apache.poi</groupId>-->
                <!--</exclusion>-->
            <!--</exclusions>-->
        </dependency>
        <!-- lombok 管理 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.2.domain

package com.zhanggen.domain;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

@Data
@EqualsAndHashCode
public class ExcelData {
    //@ExcelProperty("字符串标题") 声明当前字段对应的列
    @ExcelProperty("字符串标题")
    private String title;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double number;
}

---------------------------

package com.zhanggen.domain;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.util.Date;

@Data
public class User {
    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("创建时间")
    private Date createTime;

    @ExcelIgnore//忽略这个字段
    private String ignore;
}

1.3.listener监听器

package com.zhanggen.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.zhanggen.domain.ExcelData;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

//准备1个监听器
@Slf4j
// 有个很重要的点ExcelDataReadListener不能被spring管理,就不能从Spring容器中获取dao对象
// 如果想用Spring中的dao对象,需要在当前Listener提供一个构造函数,然后将dao对象接进来
public class ExcelDataReadListener implements ReadListener<ExcelData> {
    //控制临时缓存集合的大小(收集到100条之后,往数据库中保存)
    private static final int BATCH_COUNT = 100;

    //创建缓存集合
    private List<ExcelData> cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

//    假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
//    private DemoDAO demoDAO;
//    public ExcelDataReadListener(ExcelData demoDAO) {
//        this.demoDAO = demoDAO;
//    }


    //1.每解析1条Excel记录都会调用的回调函数,把记录保存中临时集合缓存中,一旦临时集合缓存容量达到了上限,就将缓存的数据保存到数据库,然后情况缓存继续收集Excel中的记录
    @Override
    public void invoke(ExcelData row, AnalysisContext analysisContext) {
        System.out.println(row);
        cacheDataList.add(row);
        //如果达到了临时集合缓存容量的上限,就将缓存的数据保存到数据库
        if (cacheDataList.size() >= BATCH_COUNT) {
            System.out.println(row);
            saveData();
            //存储数据库完成之后,清理集合
            ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }


    //2.所有数据解析完成了之后调用一次,进行一次存库操作;收尾工作!
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        System.out.println("所有数据解析完成!");
    }

    //模拟向数据库中保存数据
    private void saveData() {
        System.out.println(cacheDataList.size() + "条数据,开始存储到数据库!");
        //demoDao.save(cacheDataList)
        System.out.println("存储数据库完成");
    }


}

------------------------

2.4.单元测试

package com.zhanggen.test;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.listener.PageReadListener;
import com.zhanggen.domain.ExcelData;
import com.zhanggen.listener.ExcelDataReadListener;
import org.junit.Test;
import com.alibaba.excel.read.metadata.ReadSheet;

public class ReadExcelTest {

    //方式一:
    @Test
    public void test1() {
        String fileName = "D:/upload/excel读取测试.xlsx";
        EasyExcel.read(fileName, ExcelData.class, new PageReadListener<ExcelData>(dataList -> {
            for (ExcelData row : dataList) {
                System.out.println(row);
            }
        })).sheet().doRead();
    }


    //方式二:
    @Test
    public void test2() {
        String fileName = "D:/upload/excel读取测试.xlsx";
        // 一个文件一个reader
        try (ExcelReader excelReader = EasyExcel.read(fileName, ExcelData.class, new ExcelDataReadListener()).build()) {
            // 构建一个sheet 这里可以指定名字或者no
            ReadSheet readSheet = EasyExcel.readSheet(0).build();
            // 读取一个sheet
            excelReader.read(readSheet);
        }
    }
}

------------------------

package com.zhanggen.test;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.zhanggen.domain.ExcelData;
import com.zhanggen.domain.User;
import org.junit.Test;

import java.util.Date;
import java.util.List;

public class WriteExcelTest {

    private static List<User> list = ListUtils.newArrayList();

    //准备输出的数据
    static {
        for (int i = 0; i < 10; i++) {
            User data = new User();
            data.setName("字符串" + i);
            data.setAge(10 + i);
            data.setCreateTime(new Date());
            list.add(data);
        }
    }

    @Test
    public void test1() {
        String fileName = "D:/upload/excel输出测试.xlsx";
        EasyExcel.write(fileName, User.class).sheet("模板").doWrite(list);
    }

    @Test
    public void simpleWrite2() {
        String fileName = "D:/upload/excel输出测试.xlsx";
        // 这里 需要指定写用哪个class去写
        ExcelWriter excelWriter = null;
        try {
            excelWriter = EasyExcel.write(fileName, ExcelData.class).build();
            //向1个Excel的Sheet中填充数据
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            excelWriter.write(list, writeSheet);
        } finally {
            // 千万别忘记finish 会帮忙关闭流
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }
}

2.springBoot使用EasyExcel

1.pom依赖

 <!-- easy Excel -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>easyexcel</artifactId>
                <version>${esay_excel.version}</version>
            </dependency>

2.监听器

package com.huike.clues.utils.easyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.huike.clues.domain.dto.ImportResultDTO;
import com.huike.clues.domain.vo.TbClueExcelVo;
import com.huike.clues.service.ITbClueService;

/**
 * EasyExcel监听器,用于解析数据并执行操作
 */
public class ExcelListener extends AnalysisEventListener<TbClueExcelVo> {

    /**
     * 利用构造方法获取对应的service
     */
    public ITbClueService clueService;

    private ImportResultDTO resultDTO;

    /**
     * 提供带参构造方法,在这里需要通过构造方法的方式获取对应的service层
     * 谁调用这个监听器谁提供需要的service
     * @param clueService
     */
    public ExcelListener(ITbClueService clueService) {
        this.clueService = clueService;
        this.resultDTO = new ImportResultDTO();
    }

    /**
     * 每解析一行数据都要执行一次
     * 每条都执行一次插入操作
     * @param row
     * @param context
     */
    @Override
    public void invoke(TbClueExcelVo row, AnalysisContext context) {
        ImportResultDTO addTbClue = clueService.importCluesData(row);
        resultDTO.addAll(addTbClue);
    }

    /**
     * 当所有数据都解析完成后会执行
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    }

    /**
     * 返回结果集对象
     * @return
     */
    public ImportResultDTO getResult(){
        return resultDTO;
    }

}

3.Controller

有个很重要的点ExcelDataReadListener不能被Spring容器管理,就不能从Spring容器中获取dao对象;

如果想用Spring中的dao对象,需要在当前Listener提供一个构造函数,然后将dao对象接进来;

import com.alibaba.excel.EasyExcel;
import com.huike.clues.utils.easyExcel.ExcelListener;

@Log(title = "上传线索", businessType = BusinessType.IMPORT) @PostMapping("/importData") public AjaxResult importData(MultipartFile file) throws Exception { ExcelListener excelListener = new ExcelListener(tbClueService); EasyExcel.read(file.getInputStream(), TbClueExcelVo.class, excelListener).sheet().doRead(); return AjaxResult.success(excelListener.getResult()); }

4.Service

 @Override
    public ImportResultDTO importCluesData(TbClueExcelVo row) {
        //创建数据库记录对象
        TbClue clue_entry = new TbClue();
        //把前端的数据 封装到数据库记录对象属性
        BeanUtils.copyProperties(row, clue_entry);
        //设置当前线索的创建人
        clue_entry.setCreateBy(SecurityUtils.getUsername());
        //设置当前线索的创建时间
        clue_entry.setCreateTime(DateUtils.getNowDate());
        //获取当前活动编号
        String activityCode = row.getActivityCode();
        //判断活动编号对应的活动是否存在?  isNoneBlank如果不为空
        if (StringUtils.isNoneBlank(activityCode)) {
            TbActivity activity = activityService.selectTbActivityByCode(activityCode);
            if (activity == null) {
                return ImportResultDTO.error();
            } else {
                clue_entry.setActivityId(activity.getId());
            }
        }
        //校验当前线索的手机号是否为空?如果为空证明是错误数据,不进行添加 返回error
        if (StringUtils.isBlank(clue_entry.getPhone())) {
            return ImportResultDTO.error();
        }
        //判断当前线索的渠道来源是否正确?
        if (StringUtils.isNoneBlank(clue_entry.getChannel())) {
            String channel = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.CHANNEL.getDictType(), clue_entry.getChannel());
            clue_entry.setChannel(channel);
        }

        //判断当前线索的意向学科是否正确?
        if (StringUtils.isNoneBlank(clue_entry.getSubject())) {
            String subject = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.SUBJECT.getDictType(), clue_entry.getSubject());
            clue_entry.setSubject(subject);
        }
        //判断当前线索的意向级别是否正确?
        if (StringUtils.isNoneBlank(clue_entry.getLevel())) {
            String level = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.LEVEL.getDictType(), clue_entry.getLevel());
            clue_entry.setLevel(level);//意向级别
        }
        //判断当前线索的联系人的性别是否正确?
        if (StringUtils.isNoneBlank(clue_entry.getSex())) {
            String sex = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.SEX.getDictType(),
                    clue_entry.getSex());//性别
            clue_entry.setSex(sex);
        }
        //数据校验最后1步,设置当前线索的状态为待跟进
        clue_entry.setStatus(TbClue.StatusType.UNFOLLOWED.getValue());
        //将当前线索写入库
        tbClueMapper.insertTbClue(clue_entry);
        //根据规则动态分配线索给具体的销售人员,利用策略模式来进行实现
        rule.loadRule(clue_entry);
        return ImportResultDTO.success();
    }
六、Spring项目常见的实体类对象

Java网站后台通常划分为3层即Controller、Service、Dao层,当用户请求到达服务端是,后台就开始进行层层调用,完成客户请求的响应;

为了更好的区别以上不同层使用到的实例对象,我们可以对实体类对象进行以下规则的命名,方便我们区分该对象当前的作用;

1.POJO

全称为:Plain Ordinary Java Object,即简单普通的java对象。

一般用在数据层映射到数据库表的类,类的属性与表字段一一对应。

2.PO

全称为:Persistant Object,即持久化对象。

可以理解为数据库中的一条数据即一个BO对象,也可以理解为POJO经过持久化后的对象。

3.DTO

全称为:Data Transfer Object,即数据传输对象。

一般用于向数据层外围提供仅需的数据,如查询一个表有50个字段,界面或服务只需要用到其中的某些字段,DTO就包装出去的对象。可用于隐藏数据层字段定义,也可以提高系统性能,减少不必要字段的传输损耗。

5.BO

全称为:Business Object,即业务对象。

一般用在业务层,当业务比较复杂,用到比较多的业务对象时,可用BO类组合封装所有的对象一并传递。

6.VO

全称为:Value Object,有的也称为View Object,即值对象或页面对象。一般用于web层向view层封装并提供需要展现的数据。

7.代码示例

以下是1个代码示例,将诠释xxVO对象和XXBO对象的使用场景;

其中商机表和商机表和商机跟进记录表为1对多的关系;

新增商机1条跟进记录,需要更新对应商机记录的的跟进状态;

1.Web层

businessTrackVo 实例对象仅在Web层使用

/**
     * 新增商机跟进记录
     */
    @PreAuthorize("@ss.hasPermi('business:record:add')")
    @Log(title = "商机跟进记录", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody BusinessTrackVo businessTrackVo){
        //businessTrackVo 实例对象仅在Controller表示层使用
        
        //1. 创建商机跟进记录对象,并赋值
        TbBusinessTrackRecord tbBusinessTrackRecord = new TbBusinessTrackRecord();
        //属性拷贝
        BeanUtils.copyProperties(businessTrackVo,tbBusinessTrackRecord);
        //设置商机跟进记录的创建时间
        tbBusinessTrackRecord.setCreateTime(DateUtils.getNowDate());
        //设置商机跟进记录的创建人
        tbBusinessTrackRecord.setCreateBy(SecurityUtils.getUsername());

        //2.创建商机对象并赋值
        TbBusiness tbBusiness = new TbBusiness();
        //属性拷贝
        BeanUtils.copyProperties(businessTrackVo,tbBusiness);
        //设置商机的状态
        tbBusiness.setStatus(TbBusiness.StatusType.FOLLOWING.getValue());
        //设置商机的id
        tbBusiness.setId(businessTrackVo.getBusinessId());

        //3.调用Service层,传入商机根据记录和商机2个对象便于,更新商机状态、新增商机跟进记录的事务操作;
        tbBusinessTrackRecordService.add(tbBusinessTrackRecord,tbBusiness);
        return AjaxResult.success();
    }

2.Service业务层

从业务层开始,对更新商机记录和新增商机跟进记录这2个业务进行分流;

便于2个子业务(更新商机状态和新增商机跟进记录)成为1组整体的事务操作;

2.1.业务层接口

    //新增商机根据记录
    void add(TbBusinessTrackRecord businessTrackRecord, TbBusiness tbBusiness);

2.2.业务层实现类

    //新增商机根据记录
    @Override
    @Transactional
    public void add(TbBusinessTrackRecord tbBusinessTrackRecord, TbBusiness tbBusiness) {
        //业务层更新商机状态
        tbBusinessMapper.updateTbBusiness(tbBusiness);
        //业务层新增商机跟进记录
        tbBusinessTrackRecordMapper.addRecord(tbBusinessTrackRecord);

    }

2.3.Mapper持久层

更新商机的Mapper层

   public int updateTbBusiness(TbBusiness tbBusiness);

更新商机的MyBatis配置

 <update id="updateTbBusiness" parameterType="TbBusiness">
        update tb_business
        <trim prefix="SET" suffixOverrides=",">
            <if test="name != null">name = #{name},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="channel != null">channel = #{channel},</if>
            <if test="activityId != null">activity_id = #{activityId},</if>
            <if test="provinces != null">provinces = #{provinces},</if>
            <if test="city != null">city = #{city},</if>
            <if test="sex != null and sex != ''">sex = #{sex},</if>
            <if test="age != null">age = #{age},</if>
            <if test="weixin != null">weixin = #{weixin},</if>
            <if test="qq != null">qq = #{qq},</if>
            <if test="level != null">level = #{level},</if>
            <if test="subject != null">subject = #{subject},</if>
            <if test="courseId != null">course_id = #{courseId},</if>
            <if test="createBy != null">create_by = #{createBy},</if>
            <if test="createTime != null">create_time = #{createTime},</if>
            <if test="occupation != null">occupation = #{occupation},</if>
            <if test="education != null">education = #{education},</if>
            <if test="job != null">job = #{job},</if>
            <if test="salary != null">salary = #{salary},</if>
            <if test="major != null">major = #{major},</if>
            <if test="expectedSalary != null">expected_salary = #{expectedSalary},</if>
            <if test="reasons != null">reasons = #{reasons},</if>
            <if test="plan != null">plan = #{plan},</if>
            <if test="planTime != null">plan_time = #{planTime},</if>
            <if test="otherIntention != null">other_intention = #{otherIntention},</if>
            <if test="nextTime != null">next_time = #{nextTime},</if>
            <if test="status != null">status = #{status},</if>
        </trim>
        where id = #{id}
    </update>

新增商机记录的Mapper层

 //新增商机根据记录
    void addRecord(TbBusinessTrackRecord tbBusinessTrackRecord);

新增商机记录的MyBatis配置

 <!--新增商机根据记录-->
    <insert id="addRecord" useGeneratedKeys="true" keyProperty="id">
        insert into tb_business_track_record
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="businessId != null and businessId != ''">business_id,</if>
            <if test="createBy != null and createBy != ''">create_by,</if>
            <if test="keyItems != null">key_items,</if>
            <if test="record != null">record,</if>
            <if test="createTime != null">create_time,</if>
            <if test="trackStatus != null">track_status,</if>
            <if test="nextTime != null">next_time,</if>
            <if test="type != null">type,</if>
            <if test="falseReason != null">false_reason,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="businessId != null and businessId != ''">#{businessId},</if>
            <if test="createBy != null and createBy != ''">#{createBy},</if>
            <if test="keyItems != null">#{keyItems},</if>
            <if test="record != null">#{record},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="trackStatus != null">#{trackStatus},</if>
            <if test="nextTime != null">#{nextTime},</if>
            <if test="type != null">#{type},</if>
            <if test="falseReason != null">#{falseReason},</if>
        </trim>
    </insert>

参考