Commit b26eb6266fa1b5195c4510d4b12f55afdc394c37

Authored by puhui999
1 parent 1ad5602a

CRM:新增客户导入模版所属行业、客户等级、客户来源、客户地区单元格下拉选择

yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.io.resource.ResourceUtil;
4 4 import cn.hutool.core.lang.Assert;
5 5 import cn.hutool.core.text.csv.CsvRow;
6 6 import cn.hutool.core.text.csv.CsvUtil;
  7 +import cn.hutool.core.util.StrUtil;
7 8 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
8 9 import cn.iocoder.yudao.framework.ip.core.Area;
9 10 import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
... ... @@ -75,6 +76,32 @@ public class AreaUtils {
75 76 }
76 77  
77 78 /**
  79 + * 获取所有节点的全路径名称如:河南省/石家庄市/新华区
  80 + *
  81 + * @param areas 地区树
  82 + * @return 所有节点的全路径名称
  83 + */
  84 + public static List<String> getAllAreaNodePaths(List<Area> areas) {
  85 + return convertList(areas, AreaUtils::buildTreePath);
  86 + }
  87 +
  88 + // TODO @puhui999: 展开树再构建全路径
  89 + private static String buildTreePath(Area node) {
  90 + if (node.getParent() == null || Area.ID_CHINA.equals(node.getParent().getId())) { // 忽略中国
  91 + // 已经是根节点,直接返回节点名称
  92 + return node.getName();
  93 + } else {
  94 + // 递归拼接上级节点的名称
  95 + Area parent = getArea(node.getParent().getId());
  96 + if (parent != null) {
  97 + String parentPath = buildTreePath(parent);
  98 + return parentPath + "/" + node.getName();
  99 + }
  100 + }
  101 + return StrUtil.EMPTY;
  102 + }
  103 +
  104 + /**
78 105 * 格式化区域
79 106 *
80 107 * @param id 区域编号
... ... @@ -88,13 +115,13 @@ public class AreaUtils {
88 115 * 格式化区域
89 116 *
90 117 * 例如说:
91   - * 1. id = “静安区”时:上海 上海市 静安区
92   - * 2. id = “上海市”时:上海 上海市
93   - * 3. id = “上海”时:上海
94   - * 4. id = “美国”时:美国
  118 + * 1. id = “静安区”时:上海 上海市 静安区
  119 + * 2. id = “上海市”时:上海 上海市
  120 + * 3. id = “上海”时:上海
  121 + * 4. id = “美国”时:美国
95 122 * 当区域在中国时,默认不显示中国
96 123 *
97   - * @param id 区域编号
  124 + * @param id 区域编号
98 125 * @param separator 分隔符
99 126 * @return 格式化后的区域
100 127 */
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.handler;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  5 +import com.alibaba.excel.write.handler.SheetWriteHandler;
  6 +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
  7 +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
  8 +import org.apache.poi.hssf.usermodel.HSSFDataValidation;
  9 +import org.apache.poi.ss.usermodel.*;
  10 +import org.apache.poi.ss.util.CellRangeAddressList;
  11 +
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 基于固定 sheet 实现下拉框
  16 + *
  17 + * @author HUIHUI
  18 + */
  19 +public class SelectSheetWriteHandler implements SheetWriteHandler {
  20 +
  21 + private List<KeyValue<Integer, List<String>>> selectMap;
  22 +
  23 + private final char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
  24 + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
  25 +
  26 + public SelectSheetWriteHandler(List<KeyValue<Integer, List<String>>> selectMap) {
  27 + this.selectMap = selectMap;
  28 + }
  29 +
  30 + @Override
  31 + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
  32 + if (selectMap == null || CollUtil.isEmpty(selectMap)) {
  33 + return;
  34 + }
  35 + // 需要设置下拉框的sheet页
  36 + Sheet curSheet = writeSheetHolder.getSheet();
  37 + DataValidationHelper helper = curSheet.getDataValidationHelper();
  38 + String dictSheetName = "字典sheet";
  39 + Workbook workbook = writeWorkbookHolder.getWorkbook();
  40 + // 数据字典的sheet页
  41 + Sheet dictSheet = workbook.createSheet(dictSheetName);
  42 + for (KeyValue<Integer, List<String>> keyValue : selectMap) {
  43 + // 设置下拉单元格的首行、末行、首列、末列
  44 + CellRangeAddressList rangeAddressList = new CellRangeAddressList(1, 65533, keyValue.getKey(), keyValue.getKey());
  45 + int rowLen = keyValue.getValue().size();
  46 + // 设置字典sheet页的值 每一列一个字典项
  47 + for (int i = 0; i < rowLen; i++) {
  48 + Row row = dictSheet.getRow(i);
  49 + if (row == null) {
  50 + row = dictSheet.createRow(i);
  51 + }
  52 + row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
  53 + }
  54 + String excelColumn = getExcelColumn(keyValue.getKey());
  55 + // 下拉框数据来源 eg:字典sheet!$B1:$B2
  56 + String refers = dictSheetName + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen;
  57 + // 创建可被其他单元格引用的名称
  58 + Name name = workbook.createName();
  59 + // 设置名称的名字
  60 + name.setNameName("dict" + keyValue.getKey());
  61 + // 设置公式
  62 + name.setRefersToFormula(refers);
  63 + // 设置引用约束
  64 + DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey());
  65 + // 设置约束
  66 + DataValidation validation = helper.createValidation(constraint, rangeAddressList);
  67 + if (validation instanceof HSSFDataValidation) {
  68 + validation.setSuppressDropDownArrow(false);
  69 + } else {
  70 + validation.setSuppressDropDownArrow(true);
  71 + validation.setShowErrorBox(true);
  72 + }
  73 + // 阻止输入非下拉框的值
  74 + validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
  75 + validation.createErrorBox("提示", "此值不存在于下拉选择中!");
  76 + // 添加下拉框约束
  77 + writeSheetHolder.getSheet().addValidationData(validation);
  78 + }
  79 + }
  80 +
  81 + /**
  82 + * 将数字列转化成为字母列
  83 + *
  84 + * @param num 数字
  85 + * @return 字母
  86 + */
  87 + private String getExcelColumn(int num) {
  88 + String column = "";
  89 + int len = alphabet.length - 1;
  90 + int first = num / len;
  91 + int second = num % len;
  92 + if (num <= len) {
  93 + column = alphabet[num] + "";
  94 + } else {
  95 + column = alphabet[first - 1] + "";
  96 + if (second == 0) {
  97 + column = column + alphabet[len] + "";
  98 + } else {
  99 + column = column + alphabet[second - 1] + "";
  100 + }
  101 + }
  102 + return column;
  103 + }
  104 +
  105 +}
0 106 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
1 1 package cn.iocoder.yudao.framework.excel.core.util;
2 2  
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
3 5 import com.alibaba.excel.EasyExcel;
4 6 import com.alibaba.excel.converters.longconverter.LongStringConverter;
5 7 import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
  8 +import jakarta.servlet.http.HttpServletResponse;
6 9 import org.springframework.web.multipart.MultipartFile;
7 10  
8   -import jakarta.servlet.http.HttpServletResponse;
9 11 import java.io.IOException;
10 12 import java.net.URLEncoder;
11 13 import java.nio.charset.StandardCharsets;
... ... @@ -21,12 +23,12 @@ public class ExcelUtils {
21 23 /**
22 24 * 将列表以 Excel 响应给前端
23 25 *
24   - * @param response 响应
25   - * @param filename 文件名
  26 + * @param response 响应
  27 + * @param filename 文件名
26 28 * @param sheetName Excel sheet 名
27   - * @param head Excel head 头
28   - * @param data 数据列表哦
29   - * @param <T> 泛型,保证 head 和 data 类型的一致性
  29 + * @param head Excel head 头
  30 + * @param data 数据列表哦
  31 + * @param <T> 泛型,保证 head 和 data 类型的一致性
30 32 * @throws IOException 写入失败的情况
31 33 */
32 34 public static <T> void write(HttpServletResponse response, String filename, String sheetName,
... ... @@ -38,12 +40,37 @@ public class ExcelUtils {
38 40 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
39 41 .sheet(sheetName).doWrite(data);
40 42 // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
41   - response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
  43 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
  44 + response.setContentType("application/vnd.ms-excel;charset=UTF-8");
  45 + }
  46 +
  47 + /**
  48 + * 将列表以 Excel 响应给前端
  49 + *
  50 + * @param response 响应
  51 + * @param filename 文件名
  52 + * @param sheetName Excel sheet 名
  53 + * @param head Excel head 头
  54 + * @param data 数据列表哦
  55 + * @param selectMap 下拉选择数据 Map
  56 + * @throws IOException 写入失败的情况
  57 + */
  58 + public static <T> void write(HttpServletResponse response, String filename, String sheetName,
  59 + Class<T> head, List<T> data, List<KeyValue<Integer, List<String>>> selectMap) throws IOException {
  60 + // 输出 Excel
  61 + EasyExcel.write(response.getOutputStream(), head)
  62 + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
  63 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
  64 + .registerWriteHandler(new SelectSheetWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框
  65 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
  66 + .sheet(sheetName).doWrite(data);
  67 + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
  68 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
42 69 response.setContentType("application/vnd.ms-excel;charset=UTF-8");
43 70 }
44 71  
45 72 public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
46   - return EasyExcel.read(file.getInputStream(), head, null)
  73 + return EasyExcel.read(file.getInputStream(), head, null)
47 74 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
48 75 .doReadAllSync();
49 76 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.map.MapUtil;
  5 +import cn.iocoder.yudao.framework.common.core.KeyValue;
5 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 8 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
... ... @@ -10,6 +11,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
10 11 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
11 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
12 13 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  14 +import cn.iocoder.yudao.framework.ip.core.Area;
13 15 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
14 16 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
15 17 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
... ... @@ -19,6 +21,7 @@ import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService
19 21 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
20 22 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
21 23 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  24 +import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
22 25 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
23 26 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
24 27 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -34,9 +37,7 @@ import org.springframework.web.bind.annotation.*;
34 37  
35 38 import java.io.IOException;
36 39 import java.time.LocalDateTime;
37   -import java.util.Arrays;
38   -import java.util.List;
39   -import java.util.Map;
  40 +import java.util.*;
40 41 import java.util.stream.Stream;
41 42  
42 43 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
... ... @@ -44,6 +45,7 @@ import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
44 45 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
45 46 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
46 47 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  48 +import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
47 49 import static java.util.Collections.singletonList;
48 50  
49 51 @Tag(name = "管理后台 - CRM 客户")
... ... @@ -61,6 +63,8 @@ public class CrmCustomerController {
61 63 private DeptApi deptApi;
62 64 @Resource
63 65 private AdminUserApi adminUserApi;
  66 + @Resource
  67 + private DictDataApi dictDataApi;
64 68  
65 69 @PostMapping("/create")
66 70 @Operation(summary = "创建客户")
... ... @@ -258,7 +262,7 @@ public class CrmCustomerController {
258 262 .areaId(null).detailAddress("").remark("").build()
259 263 );
260 264 // 输出
261   - ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
  265 + ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list, builderSelectMap());
262 266 }
263 267  
264 268 @PostMapping("/import")
... ... @@ -314,4 +318,23 @@ public class CrmCustomerController {
314 318 return success(true);
315 319 }
316 320  
  321 + private List<KeyValue<Integer, List<String>>> builderSelectMap() {
  322 + List<KeyValue<Integer, List<String>>> selectMap = new ArrayList<>();
  323 + // 获取地区下拉数据
  324 + Area area = AreaUtils.getArea(Area.ID_CHINA);
  325 + selectMap.add(new KeyValue<>(6, AreaUtils.getAllAreaNodePaths(area.getChildren())));
  326 + // 获取客户所属行业
  327 + List<String> customerIndustries = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_INDUSTRY);
  328 + selectMap.add(new KeyValue<>(8, customerIndustries));
  329 + // 获取客户等级
  330 + List<String> customerLevels = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_LEVEL);
  331 + selectMap.add(new KeyValue<>(9, customerLevels));
  332 + // 获取客户来源
  333 + List<String> customerSources = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_SOURCE);
  334 + selectMap.add(new KeyValue<>(10, customerSources));
  335 + // 升序不然创建下拉会报错
  336 + selectMap.sort(Comparator.comparing(item -> item.getValue().size()));
  337 + return selectMap;
  338 + }
  339 +
317 340 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.api.dict;
3 3 import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
4 4  
5 5 import java.util.Collection;
  6 +import java.util.List;
6 7  
7 8 /**
8 9 * 字典数据 API 接口
... ... @@ -39,4 +40,12 @@ public interface DictDataApi {
39 40 */
40 41 DictDataRespDTO parseDictData(String type, String label);
41 42  
  43 + /**
  44 + * 获得字典数据标签列表
  45 + *
  46 + * @param dictType 字典类型
  47 + * @return 字典数据标签列表
  48 + */
  49 + List<String> getDictDataLabelList(String dictType);
  50 +
42 51 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java
1 1 package cn.iocoder.yudao.module.system.api.dict;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 5 import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
5 6 import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
6 7 import cn.iocoder.yudao.module.system.service.dict.DictDataService;
  8 +import jakarta.annotation.Resource;
7 9 import org.springframework.stereotype.Service;
8 10  
9   -import jakarta.annotation.Resource;
10 11 import java.util.Collection;
  12 +import java.util.Collections;
  13 +import java.util.List;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
11 16  
12 17 /**
13 18 * 字典数据 API 实现类
... ... @@ -37,4 +42,13 @@ public class DictDataApiImpl implements DictDataApi {
37 42 return BeanUtils.toBean(dictData, DictDataRespDTO.class);
38 43 }
39 44  
  45 + @Override
  46 + public List<String> getDictDataLabelList(String dictType) {
  47 + List<DictDataDO> dictDataList = dictDataService.getDictDataListByDictType(dictType);
  48 + if (CollUtil.isEmpty(dictDataList)) {
  49 + return Collections.emptyList();
  50 + }
  51 + return convertList(dictDataList, DictDataDO::getLabel);
  52 + }
  53 +
40 54 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java
... ... @@ -99,4 +99,12 @@ public interface DictDataService {
99 99 */
100 100 DictDataDO parseDictData(String dictType, String label);
101 101  
  102 + /**
  103 + * 获得字典数据列表
  104 + *
  105 + * @param dictType 字典类型
  106 + * @return 字典数据列表
  107 + */
  108 + List<DictDataDO> getDictDataListByDictType(String dictType);
  109 +
102 110 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java
... ... @@ -11,10 +11,10 @@ import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
11 11 import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO;
12 12 import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper;
13 13 import com.google.common.annotations.VisibleForTesting;
  14 +import jakarta.annotation.Resource;
14 15 import lombok.extern.slf4j.Slf4j;
15 16 import org.springframework.stereotype.Service;
16 17  
17   -import jakarta.annotation.Resource;
18 18 import java.util.Collection;
19 19 import java.util.Comparator;
20 20 import java.util.List;
... ... @@ -169,4 +169,9 @@ public class DictDataServiceImpl implements DictDataService {
169 169 return dictDataMapper.selectByDictTypeAndLabel(dictType, label);
170 170 }
171 171  
  172 + @Override
  173 + public List<DictDataDO> getDictDataListByDictType(String dictType) {
  174 + return dictDataMapper.selectList(DictDataDO::getDictType, dictType);
  175 + }
  176 +
172 177 }
... ...