21-1-线索批量导入(实现)
21-1-线索批量导入(实现)
线索导入需求:
线索导入需求:
。因为线下做活动或线上做推广的时候(比如你留一个手机号给给你一瓶水),活动的运营人员会收取很多的手机号,个人信息,这些手机号不可能让活动人员到我们的crm系统中一个一个进行录入,他最多整理成一份excel,然后在crm端应该有一个功能,这个功需要能批量的导入excel中的线索到我们的数据库中

在学员任务资料-任务22-技术应用
- 线索模板-clues.xlsx (线索数据的模板)
- 线索数据-2100clues.xlsx (线索模板)
导入完成后需要在页面上显示成功了多少条,失败了多少条

那么多的数据,需要利用EasyExcel解析出每一列的数据,整理好数据后存储到数据库中 🚩
接口文档:
- 接口名:/clue/importData
- 请求方式:Post
- 传入参数:file 上传的文件
- 返回值:
{
"msg":"操作成功",
"code":200,
"data":{
"successNum":27,
"failureNum":2173
}
}
EasyExcel技术
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 github地址:https://github.com/alibaba/easyexcel

准备环境
版本支持
- 2+ 版本支持 Java7和Java6
- 3+ 版本至少 Java8
通过官网我们可以看到EasyExcel,主要是对Excel进行读写填充这样的操作

对应的导入的jar包同样也有提供
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.7</version>
</dependency>
1.准备接口
读Excel
具体代码参考官网上的demo
https://www.yuque.com/easyexcel/doc/read
总结:
- 根据excel里的内容,创建对应的实体类
- 创建对应的监听类,并且实现ReadListener接口并且需要在监听器内,invoke方法和doAfterAllAnalysed方法,其中invoke方法当EasyExcel解析了一条数据后就会执行,doAfterAllAnalysed方法,在所有数据都解析完成后会执行
我们基于我们的业务来尝试编写代码
- 基于Excel里的信息来建立实体类


对应的字段有 :
1)手机号(11位手机号、不可有空格) 第0列
2)渠道来源 第1列
3)活动编号(来源于活动列表8位字母或数字) 第2列
4)客户姓名 第3列
5)意向学科 第4列
6)意向级别 第5列
7)性别 第6列
1️⃣基于这部分字段信息建立对应的实体类:
package com.huike.clues.domain.vo;
@Data
public class TbClueExcelVo{
/** 客户手机号 手机号(11位手机号,不可有空格) */
private String phone;
/** 渠道 */
private String channel;
/** 活动编号 (来源于活动列表8位字母或数字)*/
private String activityCode;
/** "客户姓名 **/
private String name;
/** 意向学科 */
private String subject;
/** 意向级别 */
private String level;
/** 性别 */
private String sex;
}
定义了对应的属性参数,但是属性参数需要和excel里的列对应上
官网提供了多种方式与Excel中的列对应上
/**
* 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
*/
@ExcelProperty(index = 2)
/**
* 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty("字符串标题")
public class ImportDavaVO{
@ExcelProperty(index = 0)
private String telephone;
@ExcelProperty(index = 1)
private String channel;
}
2️⃣ 使用官网提供的方式构建实体类:
TbClueExcelVo
package com.huike.clues.domain.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data
public class TbClueExcelVo{
/** 客户手机号 手机号(11位手机号,不可有空格) */
@ExcelProperty(value = "手机号(11位手机号,不可有空格)",index = 0)
private String phone;
/** 渠道 */
@ExcelProperty(value = "渠道来源",index = 1)
private String channel;
/** 活动编号 (来源于活动列表8位字母或数字)*/
@ExcelProperty(value = "活动编号(来源于活动列表8位字母或数字)",index = 2)
private String activityCode;
/** "客户姓名 **/
@ExcelProperty(value = "客户姓名",index = 3)
private String name;
/** 意向学科 */
@ExcelProperty(value = "意向学科",index = 4)
private String subject;
/** 意向级别 */
@ExcelProperty(value = "意向级别",index = 5)
private String level;
/** 性别 */
@ExcelProperty(value = "性别",index = 6)
private String sex;
}
3️⃣ 仿照官网的demo构建一个监听类ExcelListener,并且重写里面的两个方法
package com.huike.clues.utils.easyExcel;
public class ExcelListener extends AnalysisEventListener<TbClueExcelVo> {
/**
* 每解析一行数据都要执行一次
* @param data
* @param context
*/
@Override
public void invoke(TbClueExcelVo data, AnalysisContext context) {
xxx
}
/**
* 当所有数据都解析完成后会执行
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
xxx
}
}
我们的需求是什么很简单,将所有的数据都要存入数据库中
那么如何实现呢?
- 我们应该在这个里调用service或mapper来执行Insert操作
- 将成功条数或失败条数返回给前端
TbController
@Autowired
private ITbClueService tbClueService;
/**
*上传线索
*/
@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
public class ExcelListener extends AnalysisEventListener<TbClueExcelVo> {
public ITbClueService clueService;
public ExcelListener(ITbClueService clueService) {
this.clueService = clueService;
}
/**
* 每解析一行数据都要执行一次
* @param data
* @param context
*/
@Override
public void invoke(TbClueExcelVo data, AnalysisContext context) {
//对应的service里需要实现线索的上传
clueService.importCluesData(data);
}
/**
* 当所有数据都解析完成后会执行
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
现在又有问题了,通过阅读接口文档,我们需要返回 导入成功多少条,导入失败多少条
需要构建一个实体类用来接收本次成功多少条失败多少条
/**
* 线索导入结果集对象
*/
@Data
public class ImportResultDTO {
//成功数量
private Integer successNum;
//失败数量
private Integer failureNum;
}
注意
这里又出问题了,我们的事件是单条插入,每次只能知道这条数据有没有成功,如果说是最后所有数据一次性导入还好说最终的时候判断一下就好了,这里我们每次执行的时候都只能知道这条数据有没有成功,那怎么给前端返回最终成功多少条,失败多少条呢?这里就可以借用享元设计模式的思想
如果我们在监听器里创建一个ImportResultDTO,而后续所有的添加操作都返回本次操作的ImportResultDTO,那么我们只要最终返回监听器里的ImportResultDTO是不是就讲最终的方案实现了呢?
对应的我们修改代码
在监听器里构建两个成员属性,一个是service另一个是全局的ImportResultDTO
package com.huike.clues.utils.easyExcel;
public class ExcelListener extends AnalysisEventListener<TbClueExcelVo> {
public ITbClueService clueService;
private ImportResultDTO resultDTO;
public ExcelListener(ITbClueService clueService) {
this.clueService = clueService;
this.resultDTO = new ImportResultDTO();
}
/**
* 每解析一行数据都要执行一次
* @param data
* @param context
*/
@Override
public void invoke(TbClueExcelVo data, AnalysisContext context) {
ImportResultDTO addTbClue = clueService.importCluesData(data);
resultDTO.addAll(addTbClue);
}
/**
* 当所有数据都解析完成后会执行
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
/**
* 返回结果集对象
* @return
*/
public ImportResultDTO getResult(){
return resultDTO;
}
}
在ImportResultDTO里添加一个方法用来整合每次添加成功的数据
package com.huike.clues.domain.dto;
/**
* 线索导入结果集对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImportResultDTO {
//成功数量
private Integer successNum;
//失败数量
private Integer failureNum;
public static ImportResultDTO error(){
return new ImportResultDTO(0,1);
}
public static ImportResultDTO success(){
return new ImportResultDTO(1,0);
}
public ImportResultDTO addAll(ImportResultDTO data){
this.failureNum+=data.getFailureNum();
this.successNum+=data.getSuccessNum();
return this;
}
}
修改controller层将监听器对应的全局ImportResultDTO返回即可
/**
*上传线索
*/
@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());
}
2.插入信息
/**
* 线索数据添加入库
*
* @param data
* @return
*/
@Override
public ImportResultDTO importCluesData(TbClueExcelVo data) {
//===============校验线索数据,封装属性,插入数据库,根据规则进行分配======================
/**
* 1 判断活动编号对应的活动是否存在
* 1.1 如果活动编号不存在 即错误数据,不进行添加操作,返回错误 ImportResultDTO.error()
* 1.2 如果活动编号存在 设置活动id
*/
//TODO 补全上述逻辑代码
// 新建线索
TbClue clue = new TbClue();
BeanUtils.copyProperties(data, clue);
clue.setCreateBy(SecurityUtils.getUsername());
clue.setCreateTime(DateUtils.getNowDate());
// 获得活动编号
String activityCode = data.getActivityCode();
//1 判断活动编号对应的活动是否存在
if (StringUtils.isNoneBlank(activityCode)) {
//利用空间换时间将缓存中的活动编号查询出来
Set<String> codeSets = redisCache.getCacheSet(Constants.ACT_CODE_KEY);
//1.1 如果活动编号不在系统中则不进行录入
if(!codeSets.contains(activityCode)){
log.info("活动:{},此{} 不在活动中!!!",codeSets,activityCode);
return ImportResultDTO.error();
}
//1.2 如果活动编号存在 设置活动id
TbActivity tbActivity = activityService.selectTbActivityByCode(activityCode);
clue.setActivityId(tbActivity.getId());
}
/**
* 校验手机号和渠道是否为空
* 如果为空证明是错误数据,不进行添加 返回error
* return ImportResultDTO.error();
*/
//TODO 补全上述逻辑代码
if (StringUtils.isBlank(clue.getPhone())) {
return ImportResultDTO.error();
}
if (StringUtils.isBlank(clue.getChannel())) {
return ImportResultDTO.error();
}
// 验证线索中是否存在这个用户--意味着 是否存在相同的线索
TbClue dbcule = tbClueMapper.selectTbClueByPhone(clue.getPhone());
if (dbcule == null) {
/**
* 字典值的替换
* 因为excel里传入的是中文名,需要替换成对应的字典值
* 需要渠道来源/学科/意向等级/性别/跟进状态
*/
//TODO 补全上述逻辑代码
// 特殊字段处理
String channel = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.CHANNEL.getDictType(),
clue.getChannel());
clue.setChannel(channel);
if (StringUtils.isNoneBlank(clue.getSubject())) {
String subject = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.SUBJECT.getDictType(),
clue.getSubject());
clue.setSubject(subject);
}
if (StringUtils.isNoneBlank(clue.getLevel())) {
String level = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.LEVEL.getDictType(),
clue.getLevel());
clue.setLevel(level);
}
if (StringUtils.isNoneBlank(clue.getSex())) {
String sex = sysDictDataMapper.selectDictValue(TbClue.ImportDictType.SEX.getDictType(),
clue.getSex());
clue.setSex(sex);
}
clue.setStatus(TbClue.StatusType.UNFOLLOWED.getValue());
/**
* 将线索数据入库
* 参考添加线索接口调用的mapper
* 仅仅只插入到线索表中
*/
//TODO 补全上述逻辑代码
tbClueMapper.insertTbClue(clue);
/**
* 根据规则动态分配线索给具体的销售人员
* 利用策略模式来进行实现
* rule.loadRule(clue);
*/
//TODO 补全上述逻辑代码
}else{
// 线索中有存在相同的数据,直接错误+1
return ImportResultDTO.error();
}
/**
*分配完成 返回成功 ,成功+1
*/
return ImportResultDTO.success();
}
注意这里在进行自动分配的时候使用rule.loadRule(clue)方法
传入的是封装好的并且替换了字典表里内容的线索对象
3. 线索自动分配
// 默认分配超级管理员 //如果线索添加成功,利用策略将线索分配给具体的人
策略模式的使用
注意这里的rule是通过@Autowired的方式进行注入的
@Service
public class TbClueServiceImpl implements ITbClueService {
@Autowired
private Rule rule;
注入成功后,对于Rule接口的实现类有两个:RuleStrategy和AdminStrategy
这两个类是具体选择那个类作为最终Rule的实现类,是通过@ConditionalOnProperty注解来实现的
由管理员来进行分配的实现类
/**
* admin 处理策略
*
* 由admin来处理所有的线索导入和转商机的数据
*
* 全部导入到admin 统一由admin来处理所有的线索
* exchange 转商机的时候统一转换到admin,再由admin来统一分片商机
*/
@ConditionalOnProperty(name = "rule.clue.import", havingValue = "admin")
@Service("ClueAdminStrategy")
public class AdminClueStrategy implements Rule {
通过规则来进行分配的实现类
/**
* 使用规则的方式进行线索的自动分配
* 规则:
* 1.将想要学java的分配给zhangsan
* 2.将想要学前端的分配给zhangsan1
* 3.不满足规则的不进行分配,由管理员或主管来进行分配
*/
@ConditionalOnProperty(name = "rule.clue.import", havingValue = "rule")
@Service("ClueRuleStrategy")
public class RuleStrategy implements Rule {
空间换时间
当我们选择使用规则的方式进行自动分配的时候
注意这部分代码是已经提供好的我们重点需要关注我们提前缓存了哪些数据分别是什么
private static SysUser zhangsan = new SysUser();
private static SysUser zhangsan1 = new SysUser();
//内存中JAVA学科的内容--提前预加载在内存中
private static SysDictData subjectJAVA = new SysDictData();
//内存中前端学科的内容--提前预加载在内存中
private static SysDictData subjectHtml = new SysDictData();
@PostConstruct
public void init() {
//空间换时间的方式将数据库中的学科读取到内存中
//预加载学科数据到内存中
List<SysDictData> course_subject = dictDataMapper.selectDictDataByType("course_subject");
for (SysDictData index: course_subject) {
//找到java和前端两个学科对应的数值
if(index.getDictLabel().equals("Java")){
subjectJAVA = index;
}
if(index.getDictLabel().equals("前端")){
subjectHtml = index;
}
}
//预加载lisi和lisi1的数据到内存中
zhangsan = userMapper.selectUserByName("zhangsan");
zhangsan1 = userMapper.selectUserByName("zhangsan1");
}
我们在成员位置将JAVA学科和前端学科的数据提前加载到成员位置
根据编写规则
根据规则来进行分配
/**=========================利用空间换时间将部分的数据提前存放到内存中================================**/
@Override
public Boolean loadRule(TbClue clue) {
//注意处理空指针的问题
if(subjectJAVA.getDictValue().equals(clue.getSubject())){
//如果意向学科是java--分配给lisi
return distribute(clue,zhangsan);
}else if(subjectHtml.getDictValue().equals(clue.getSubject())){
//如果意向学科是前端--分配给lisi1
return distribute(clue,zhangsan1);
}else{
//不进行分配,直接添加-----即待分配状态
return true;
}
}
}
将规则分配给具体的销售人员
分配线索给具体的用户,即添加线索分配记录
/**
* 分配线索给具体用户的方法
* @param clue
* @param user
* @return
*/
private Boolean distribute(TbClue business,SysUser user){
TbAssignRecord tbAssignRecord =new TbAssignRecord();
tbAssignRecord.setAssignId(business.getId());
tbAssignRecord.setUserId(user.getUserId());
tbAssignRecord.setUserName(user.getUserName());
tbAssignRecord.setDeptId(user.getDeptId());
tbAssignRecord.setCreateBy(SecurityUtils.getUsername());
tbAssignRecord.setCreateTime(DateUtils.getNowDate());
tbAssignRecord.setType(TbAssignRecord.RecordType.CLUES.getValue());
business.setNextTime(null);
return assignRecordMapper.insertAssignRecord(tbAssignRecord)>0;
}