前言
之前一直把用户上传的图片和文件保存在本地服务器的文件系统中,长而久之会产生以下弊端:
- 当文件数量过多之后严重消耗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存储HTML、CSS、JS、图片、合同、视频、日志文件、备份数据和容器镜像等非结构化的静态数据;
四、搭建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>
参考