主要对 hutool 的 ExcelUtil._readBySax(_inputStream, 0, rowHandler_)_; API 进行了增强,自定义 RowHandler 实现如下的增强功能:

  1. 解析一行数据,给出一个对象
  2. 自己对某个业务字段判定时,可以获取到该字段在文件中的位置信息,比如第 n 行,n 列
  3. 每个字段转换异常可以获取到详细信息

依赖如下

  1. implementation 'cn.hutool:hutool-all:5.5.4'
  2. implementation 'org.apache.poi:poi:4.1.2'
  3. implementation 'org.apache.poi:poi-ooxml:4.1.2'
  4. // 数据对象使用了 lombok 注解,可以自行解决不使用
  5. compileOnly 'org.projectlombok:lombok'
  6. annotationProcessor 'org.projectlombok:lombok'

工具代码

核心处理器 DataRowHandler

  1. package cn.mrcode.parse.data;
  2. import java.lang.reflect.Field;
  3. import java.util.HashMap;
  4. import java.util.LinkedHashMap;
  5. import java.util.List;
  6. import java.util.Map;
  7. import java.util.function.Function;
  8. import java.util.function.Supplier;
  9. import java.util.stream.Collectors;
  10. import cn.hutool.core.annotation.Alias;
  11. import cn.hutool.core.convert.Convert;
  12. import cn.hutool.core.util.StrUtil;
  13. import cn.hutool.poi.excel.sax.handler.RowHandler;
  14. import lombok.Data;
  15. /**
  16. * 通用详细位置的数据行解析
  17. * <pre>
  18. * 接受行内容的数据对象, 请继承 DataRowNumber 对象,可以给你提供行号
  19. * 另外,对象字段使用 cn.hutool.core.annotation.Alias 注解指定对应表头信息,如下所示:
  20. * @Alias("客户名称")
  21. * private String customerName;
  22. * 使用方式:ExcelUtil.readBySax(inputStream, 0, rowHandler); 将本对象传递给 rowHandler
  23. * </pre>
  24. *
  25. * @param <T> 接受行内容的数据对象
  26. * @author mrcode
  27. * @date 2021/9/16 20:03
  28. */
  29. public class DataRowHandler<T extends DataRowNumber> implements RowHandler {
  30. private DataRowFunction<T, Boolean> successFun;
  31. private Function<DataRowFailMsg, Boolean[]> failFun;
  32. private Supplier<T> factoryNew;
  33. private List<String> headers;
  34. Map<String, FieldItem> fieldItemMap; // 字段类型映射
  35. /**
  36. * @param successFun 当一行数据解析成功时的回调函数,你处理之后,如果需要继续处理,请返回 true,否则返回 false
  37. * @param failFun 当某行数据的某个字段解析失败时的回调函数,返回两个值:1:决定当前对象剩余字段是否还继续解析,2:当前整个文件解析是否还继续, true
  38. * 继续,false 停止; 或则直接返回 null,会继续解析剩余数据
  39. * @param factoryNew 当需要一个新的行对象时,请返回一个初始化对象
  40. */
  41. public DataRowHandler(DataRowFunction<T, Boolean> successFun,
  42. Function<DataRowFailMsg, Boolean[]> failFun,
  43. Supplier<T> factoryNew) {
  44. this.successFun = successFun;
  45. this.failFun = failFun;
  46. this.factoryNew = factoryNew;
  47. this.buildFieldItemMap();
  48. }
  49. /**
  50. * 获取该行对应的某个字段信息
  51. *
  52. * @param headerName 表头字段的别名
  53. * @param item 给你的行结果信息,主要为了获取里面的行号
  54. * @return
  55. */
  56. public DataRowFieldInfo getFieldInfo(String headerName, T item) {
  57. final DataRowFieldInfo fieldInfo = new DataRowFieldInfo();
  58. fieldInfo.setRowNum(item.getRowNum());
  59. fieldInfo.setField(headerName);
  60. // 根据表头字段顺序定位该字段在 excel 中的位置
  61. for (int i = 0; i < headers.size(); i++) {
  62. if (headerName.equals(headers.get(i))) {
  63. fieldInfo.setPosition(ExcelColumnUtil.excelColIndexToStr(i + 1) + item.getRowNum());
  64. break;
  65. }
  66. }
  67. return fieldInfo;
  68. }
  69. @Override
  70. public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
  71. // 第 0 行为:表头字段
  72. if (rowIndex == 0) {
  73. // 将表头收集起来,后续以此顺序 判定某个字段所在的位置
  74. headers = rowList.stream().map(Object::toString).collect(Collectors.toList());
  75. return;
  76. }
  77. // 将该行内容与 表头一一对应上
  78. final int headerSize = headers.size();
  79. final int rowSize = rowList.size();
  80. final Map<String, Object> kvMap = new LinkedHashMap<>(headerSize);
  81. for (int i = 0; i < headerSize; i++) {
  82. if (i < rowSize) {
  83. kvMap.put(headers.get(i), rowList.get(i));
  84. }
  85. }
  86. // 解析该行内容成对象
  87. T row = parse(rowIndex, kvMap);
  88. if (!successFun.apply(row, this)) {
  89. throw new DataRowParseStopException();
  90. }
  91. }
  92. private T parse(long rowIndex, Map<String, Object> kvMap) {
  93. final long excelRowNum = rowIndex + 1;
  94. final T row = factoryNew.get();
  95. row.setRowNum(excelRowNum);
  96. // 按表头顺序获取内容
  97. for (int i = 0; i < headers.size(); i++) {
  98. final String header = headers.get(i);
  99. final Object valueObject = kvMap.get(header);
  100. if (valueObject == null || valueObject.toString() == "") {
  101. continue;
  102. }
  103. final FieldItem fieldItem = fieldItemMap.get(header);
  104. // 当一行中出现了某个字段是无法识别时,该行解析失败
  105. if (fieldItem == null) {
  106. final DataRowFailMsg dataRowFailMsg = new DataRowFailMsg();
  107. dataRowFailMsg.setRowNum(excelRowNum);
  108. dataRowFailMsg.setField(header);
  109. dataRowFailMsg.setPosition(ExcelColumnUtil.excelColIndexToStr(i + 1) + excelRowNum);
  110. dataRowFailMsg.setMsg("无法识别的字段");
  111. // 如果不继续,则直接抛出停止异常
  112. final Boolean[] isContinues = failFun.apply(dataRowFailMsg);
  113. if (isContinues != null) {
  114. // 剩余文件不继续解析
  115. if (!isContinues[0]) {
  116. throw new DataRowParseStopException();
  117. }
  118. // 剩余字段不继续解析
  119. if (!isContinues[1]) {
  120. break;
  121. }
  122. // 剩余字段继续解析
  123. continue;
  124. }
  125. }
  126. // 利用反射给对应的字段赋值
  127. final Class<?> type = fieldItem.getType();
  128. final Field field = fieldItem.getField();
  129. try {
  130. final Object value = Convert.convert(type, valueObject);
  131. field.set(row, value);
  132. } catch (Exception e) {
  133. String msg = StrUtil.format("值转换异常,目标值类型={} ,原始值={}",
  134. type.getName(),
  135. valueObject
  136. );
  137. final DataRowFailMsg dataRowFailMsg = new DataRowFailMsg();
  138. dataRowFailMsg.setRowNum(excelRowNum);
  139. dataRowFailMsg.setField(header);
  140. dataRowFailMsg.setPosition(ExcelColumnUtil.excelColIndexToStr(i + 1) + excelRowNum);
  141. dataRowFailMsg.setMsg(msg);
  142. // 如果不继续,则直接抛出停止异常
  143. final Boolean[] isContinues = failFun.apply(dataRowFailMsg);
  144. if (isContinues != null) {
  145. // 剩余文件不继续解析
  146. if (!isContinues[0]) {
  147. throw new DataRowParseStopException();
  148. }
  149. // 剩余字段不继续解析
  150. if (!isContinues[1]) {
  151. row.setError(true);
  152. break;
  153. }
  154. // 剩余字段继续解析
  155. row.setError(true);
  156. continue;
  157. }
  158. }
  159. }
  160. return row;
  161. }
  162. /**
  163. * 构建字段别名
  164. * <pre>
  165. * 表头使用中文字段,想要处理过程中中文字段与对象字段对应上,
  166. * 该方法就是将:中文字段 与 对象的字段 关联上,方便后续的 set 操作
  167. * </pre>
  168. */
  169. private void buildFieldItemMap() {
  170. final Class clzz = factoryNew.get().getClass();
  171. final Field[] declaredFields = clzz.getDeclaredFields();
  172. Map<String, FieldItem> fieldItemMap = new HashMap<>();
  173. for (Field declaredField : declaredFields) {
  174. final Class<?> type = declaredField.getType();
  175. final Alias aliasAnno = declaredField.getAnnotation(Alias.class);
  176. if (aliasAnno == null) {
  177. continue;
  178. }
  179. final String value = aliasAnno.value();
  180. declaredField.setAccessible(true);
  181. fieldItemMap.put(value, new FieldItem(declaredField, type));
  182. }
  183. this.fieldItemMap = fieldItemMap;
  184. }
  185. @Data
  186. private static class FieldItem {
  187. // 字段实例
  188. private Field field;
  189. // 参数类型
  190. private Class<?> type;
  191. public FieldItem(Field field, Class<?> type) {
  192. this.field = field;
  193. this.type = type;
  194. }
  195. }
  196. }

其他辅助类,响应的字段位置、行号、异常等类

  1. package cn.mrcode.parse.data;
  2. import lombok.Data;
  3. import lombok.ToString;
  4. /**
  5. * 数据所在行
  6. * @author mrcode
  7. * @date 2021/9/16 20:03
  8. */
  9. @Data
  10. @ToString
  11. public class DataRowNumber {
  12. // 行号
  13. private Long rowNum;
  14. // 这一行数据中是否有解析错误的信息,当某个字段解析失败还继续解析剩余字段时,这里就会标识为 true,标识该条数据不完整
  15. private boolean isError;
  16. }
  1. package cn.mrcode.parse.data;
  2. import lombok.Data;
  3. import lombok.EqualsAndHashCode;
  4. /**
  5. * 某个字段的位置信息
  6. * @author mrcode
  7. * @date 2021/9/16 20:03
  8. */
  9. @Data
  10. @EqualsAndHashCode(callSuper = true)
  11. public class DataRowFieldInfo extends DataRowNumber {
  12. // 位置
  13. private String position;
  14. // 字段
  15. private String field;
  16. @Override
  17. public String toString() {
  18. return "DataRowFieldInfo{" +
  19. "rowNum=" + getRowNum() +
  20. ", position='" + position + '\'' +
  21. ", field='" + field + '\'' +
  22. '}';
  23. }
  24. /**
  25. * 转换为错误信息,可用于二次加工
  26. *
  27. * @return
  28. */
  29. public DataRowFailMsg toFailMsg() {
  30. final DataRowFailMsg failMsg = new DataRowFailMsg();
  31. failMsg.setRowNum(this.getRowNum());
  32. failMsg.setField(this.field);
  33. failMsg.setPosition(this.position);
  34. return failMsg;
  35. }
  36. }
  1. package cn.mrcode.parse.data;
  2. import lombok.Data;
  3. import lombok.EqualsAndHashCode;
  4. /**
  5. * 解析失败时的位置定位信息详情
  6. * @author mrcode
  7. * @date 2021/9/16 20:03
  8. */
  9. @Data
  10. @EqualsAndHashCode(callSuper = true)
  11. public class DataRowFailMsg extends DataRowFieldInfo {
  12. // 错误信息
  13. private String msg;
  14. @Override
  15. public String toString() {
  16. return "DataRowFailMsg{" +
  17. "rowNum=" + getRowNum() +
  18. ", position='" + getPosition() + '\'' +
  19. ", field='" + getField() + '\'' +
  20. ", msg='" + msg + '\'' +
  21. '}';
  22. }
  23. }
  1. package cn.mrcode.parse.data;
  2. /**
  3. * 解析停止时的异常; 用于手动停止解析的中断逻辑流程
  4. * @author mrcode
  5. * @date 2021/9/16 20:03
  6. */
  7. public class DataRowParseStopException extends RuntimeException {
  8. public DataRowParseStopException() {
  9. }
  10. }
  1. package cn.mrcode.parse.data;
  2. /**
  3. * 数据解析成功,回调函数
  4. * @author mrcode
  5. * @date 2021/9/16 20:03
  6. */
  7. @FunctionalInterface
  8. public interface DataRowFunction<T, R> {
  9. /**
  10. * @param t
  11. * @param handler 行解析对象本身
  12. * @return
  13. */
  14. R apply(T t, DataRowHandler handler);
  15. }

excel 位置信息工具类

  1. package cn.mrcode.parse.data;
  2. /**
  3. * Excel 列的下标 和 字母互转
  4. *
  5. * @author Stephen.Huang
  6. * @version 2015-7-8
  7. */
  8. public class ExcelColumnUtil {
  9. public static int excelColStrToNum(String colStr) {
  10. return excelColStrToNum(colStr, colStr.length());
  11. }
  12. /**
  13. * 列字母转数字
  14. * <pre>
  15. *
  16. * 注意:Excel column index 从 1 开始
  17. *
  18. * </pre>
  19. *
  20. * @param colStr
  21. * @param length
  22. * @return
  23. */
  24. public static int excelColStrToNum(String colStr, int length) {
  25. int num = 0;
  26. int result = 0;
  27. for (int i = 0; i < length; i++) {
  28. char ch = colStr.charAt(length - i - 1);
  29. num = (int) (ch - 'A' + 1);
  30. num *= Math.pow(26, i);
  31. result += num;
  32. }
  33. return result;
  34. }
  35. /**
  36. * 将列转成字母
  37. *
  38. * @param columnIndex 注意:Excel column index 从 1 开始
  39. * @return
  40. */
  41. public static String excelColIndexToStr(int columnIndex) {
  42. if (columnIndex <= 0) {
  43. return null;
  44. }
  45. String columnStr = "";
  46. columnIndex--;
  47. do {
  48. if (columnStr.length() > 0) {
  49. columnIndex--;
  50. }
  51. columnStr = ((char) (columnIndex % 26 + (int) 'A')) + columnStr;
  52. columnIndex = (int) ((columnIndex - columnIndex % 26) / 26);
  53. } while (columnIndex > 0);
  54. return columnStr;
  55. }
  56. public static void main(String[] args) {
  57. String colstr = "AA";
  58. int colIndex = excelColStrToNum(colstr, colstr.length());
  59. System.out.println("'" + colstr + "' column index of " + colIndex);
  60. colIndex = 26;
  61. colstr = excelColIndexToStr(colIndex);
  62. System.out.println(colIndex + " column in excel of " + colstr);
  63. colstr = "AAAA";
  64. colIndex = excelColStrToNum(colstr, colstr.length());
  65. System.out.println("'" + colstr + "' column index of " + colIndex);
  66. colIndex = 466948;
  67. colstr = excelColIndexToStr(colIndex);
  68. System.out.println(colIndex + " column in excel of " + colstr);
  69. }
  70. }

业务代码与测试

步骤 1:首先准备一份 excel 文件,如下图所示:第一行是表头,第二行开始是数据
image.png
步骤 2:准备对应的数据对象

  1. package cn.mrcode.parse.data.test;
  2. import cn.mrcode.parse.data.DataRowNumber;
  3. import cn.hutool.core.annotation.Alias;
  4. import lombok.Data;
  5. import lombok.EqualsAndHashCode;
  6. import lombok.ToString;
  7. @Data
  8. @ToString
  9. @EqualsAndHashCode(callSuper = true)
  10. public class CustomerRow extends DataRowNumber {
  11. @Alias(CustomerRowFieldAlias.CUSTOMER_NAME)
  12. private String customerName;
  13. @Alias(CustomerRowFieldAlias.COMPANY)
  14. private String company;
  15. @Alias(CustomerRowFieldAlias.PRODUCT_TYPE)
  16. private String productType;
  17. @Alias(CustomerRowFieldAlias.BRAND)
  18. private String brand;
  19. @Alias(CustomerRowFieldAlias.WEBSITE)
  20. private String website;
  21. @Alias(CustomerRowFieldAlias.AREA)
  22. private String area;
  23. @Alias(CustomerRowFieldAlias.CHANNEL_CODE)
  24. private String channelCoding;
  25. @Alias(CustomerRowFieldAlias.SOURCE)
  26. private String source;
  27. @Alias(CustomerRowFieldAlias.CONTACT_INFO)
  28. private String contactInfo;
  29. @Alias(CustomerRowFieldAlias.DEMAND)
  30. private String demand;
  31. @Alias(CustomerRowFieldAlias.REMARK)
  32. private Integer remark;
  33. }

文件表头对应的别名常量类

  1. public interface CustomerRowFieldAlias {
  2. String CUSTOMER_NAME="客户名称(必填)";
  3. String COMPANY="客户公司";
  4. String PRODUCT_TYPE="产品品类";
  5. String BRAND="客户品牌";
  6. String WEBSITE="客户网站";
  7. String AREA="地区";
  8. String CHANNEL_CODE="来源渠道(必填)";
  9. String SOURCE="来源";
  10. String CONTACT_INFO="联系方式";
  11. String DEMAND="客户需求";
  12. String REMARK="备注";
  13. }

步骤 3:编写解析代码测试

  1. package cn.mrcode.parse.data.test;
  2. import cn.mrcode.parse.data.test.CustomerRow;
  3. import cn.mrcode.parse.data.test.CustomerRowFieldAlias;
  4. import org.junit.jupiter.api.Test;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. import java.nio.file.Files;
  8. import java.nio.file.Path;
  9. import java.nio.file.Paths;
  10. import cn.hutool.core.util.StrUtil;
  11. import cn.hutool.poi.excel.ExcelUtil;
  12. /**
  13. * @author mrcode
  14. * @date 2021/9/16 20:03
  15. */
  16. class DataRowHandlerTest {
  17. @Test
  18. void handle() {
  19. Path path = Paths.get("C:\\clue-customer.xlsx");
  20. try (final InputStream inputStream = Files.newInputStream(path)) {
  21. final DataRowHandler<CustomerRow> rowHandler = new DataRowHandler<>(
  22. // 一行数据解析成功
  23. (item, handler) -> {
  24. System.out.println("数据解析成功,其中是否有某个字段解析失败?" + item.isError());
  25. System.out.println("数据解析成功:" + item);
  26. // 这里可以做业务的校验等,比如该字段是必须的,
  27. final String customerName = item.getCustomerName();
  28. if (StrUtil.isBlank(customerName)) {
  29. // 校验失败之后,获取该字段在文件中的位置信息
  30. DataRowFieldInfo fieldInfo = handler.getFieldInfo(CustomerRowFieldAlias.CUSTOMER_NAME, item);
  31. System.out.println("业务校验未通过,字段信息:" + fieldInfo);
  32. // 遇到一个错误之后,就不再继续解析文件了
  33. return false;
  34. }
  35. return true;
  36. },
  37. // 一行的某个字段解析失败时调用
  38. item -> {
  39. System.out.println("数据解析失败:" + item);
  40. // 如果该字段解析失败,还需要继续往下解析,发挥 true,否则返回 false
  41. // 第一个值:剩余文件是否还继续解析, 如果文件都不继续了,则直接返回,剩余字段信息也不会解析
  42. // 第二个值:剩余字段还是否继续解析, 如果是 true,那么这一行数据将可能会出现多个字段解析异常的消息,最后会回调解析成功的函数
  43. // 继而可以在回调成功的函数中,使用 item.isError() 判定该条数据是否完整
  44. return new Boolean[]{true, false};
  45. },
  46. // 一行数据的承载对象
  47. CustomerRow::new
  48. );
  49. // 开始解析,只解析第一个 sheet 的内容
  50. ExcelUtil.readBySax(inputStream, 0, rowHandler);
  51. } catch (DataRowParseStopException e) {
  52. System.out.println("手动中断解析");
  53. } catch (IOException e) {
  54. e.printStackTrace();
  55. System.out.println("未知异常,需要看源码确定是哪里的问题");
  56. }
  57. }
  58. }

自定义业务校验失败时

  1. 数据解析成功:CustomerRow(customerName=null, company=北京科技有限公司, productType=成衣,假发, brand=null, website=null, area=null, channelCoding=请填写渠道编码,如:abc123, source=来源渠道补充信息, contactInfo=联系名称和联系方式用英文冒号分隔,多个用英文;分隔,如:手机:123456;QQ:123456, demand=null, remark=null)
  2. 业务校验未通过,字段信息:DataRowFieldInfo{rowNum=2, position='A2', field='客户名称(必填)'}
  3. 手动中断解析

某行字段解析失败,并且剩余文件不继续解析、剩余字段不继续解析时

  1. 数据解析失败:DataRowFailMsg{rowNum=2, position='K2', field='备注', msg='值转换异常,目标值类型=java.lang.Integer ,原始值=15xxx,解析错误'}
  2. 手动中断解析

某行字段解析失败,并且剩余文件继续解析、剩余字段不继续解析时:

  1. 数据解析失败:DataRowFailMsg{rowNum=2, position='K2', field='备注', msg='值转换异常,目标值类型=java.lang.Integer ,原始值=算法ss'}
  2. 数据解析成功,其中是否有某个字段解析失败?true
  3. 数据解析成功:CustomerRow(customerName=张三, company=北京科技有限公司, productType=成衣,假发, brand=null, website=null, area=null, channelCoding=请填写渠道编码,如:abc123, source=来源渠道补充信息, contactInfo=联系名称和联系方式用英文冒号分隔,多个用英文;分隔,如:手机:123456;QQ:123456, demand=null, remark=null)

真实业务场景使用

需求:对一个文件进行解析入库,解析成功的行、经过业务校验通过后,入库,未经过校验的或则解析失败的,响应详细信息给前端

  1. @Override
  2. @AccessLog(value = "线索导入", isPrintRes = false)
  3. public ImportCustomerResult importCustomer(Path path, UserInfo userInfo) {
  4. // 解析的总数
  5. AtomicReference<Integer> totalCount = new AtomicReference<>(0);
  6. // 失败的数量
  7. AtomicReference<Integer> failCount = new AtomicReference<>(0);
  8. // 成功的数量
  9. AtomicReference<Integer> successCount = new AtomicReference<>(0);
  10. // 用于存储失败行的错误信息,每行错误信息只存储一条
  11. List<DataRowFailMsg> errDetails = new ArrayList<>();
  12. try (final InputStream inputStream = Files.newInputStream(path)) {
  13. final DataRowHandler<CustomerRow> rowHandler = new DataRowHandler<>(
  14. // 一行数据解析成功
  15. (item, handler) -> {
  16. //由于忽略某一行解析失败, 除了致命的文件解析异常,都会进入到这里,在这里进行总行数的统计
  17. totalCount.getAndSet(totalCount.get() + 1);
  18. // 该行数据有异常,剩余文件数据继续解析
  19. if (item.isError()) {
  20. // 失败数量+1
  21. failCount.getAndSet(failCount.get() + 1);
  22. return true;
  23. }
  24. final DataRowFailMsg failMsg = this.checkData(item, handler);
  25. if (failMsg != null) {
  26. errDetails.add(failMsg);
  27. failCount.getAndSet(failCount.get() + 1);
  28. return true;
  29. }
  30. successCount.getAndSet(successCount.get() + 1);
  31. // 插入数据库操作
  32. return true;
  33. },
  34. // 一行的某个字段解析失败时调用
  35. item -> {
  36. errDetails.add(item); // 将该行数据的第一个错误信息添加到详情中
  37. // 某个字段解析失败,文件继续解析,剩余字段不继续解析
  38. return new Boolean[]{true, false};
  39. },
  40. // 一行数据的承载对象
  41. CustomerRow::new
  42. );
  43. // 开始解析,只解析第一个 sheet 的内容
  44. ExcelUtil.readBySax(inputStream, 0, rowHandler);
  45. } catch (DataRowParseStopException e) {
  46. // 响应导入结果
  47. final ImportCustomerResult result = new ImportCustomerResult();
  48. result.setTotalCount(totalCount.get());
  49. result.setFailCount(failCount.get());
  50. result.setSuccessCount(successCount.get());
  51. result.setErrDetails(errDetails);
  52. return result;
  53. } catch (IOException e) {
  54. log.error("文件解析异常", e);
  55. throwErr("导入失败,未知错误");
  56. }
  57. // 响应导入结果
  58. final ImportCustomerResult result = new ImportCustomerResult();
  59. result.setTotalCount(totalCount.get());
  60. result.setFailCount(failCount.get());
  61. result.setSuccessCount(successCount.get());
  62. result.setErrDetails(errDetails);
  63. return result;
  64. }
  65. /**
  66. * 业务数据校验
  67. *
  68. * @param item
  69. * @param handler
  70. * @return
  71. */
  72. private DataRowFailMsg checkData(CustomerRow item, DataRowHandler handler) {
  73. final String customerName = item.getCustomerName();
  74. if (StrUtil.isBlank(customerName)) {
  75. DataRowFieldInfo fieldInfo = handler.getFieldInfo(CustomerRowFieldAlias.CUSTOMER_NAME, item);
  76. DataRowFailMsg failMsg = fieldInfo.toFailMsg();
  77. failMsg.setMsg("该数据必填");
  78. return failMsg;
  79. }
  80. final String channelCoding = item.getChannelCoding();
  81. if (StrUtil.isBlank(channelCoding)) {
  82. DataRowFieldInfo fieldInfo = handler.getFieldInfo(CustomerRowFieldAlias.CHANNEL_CODE, item);
  83. DataRowFailMsg failMsg = fieldInfo.toFailMsg();
  84. failMsg.setMsg("该数据必填");
  85. return failMsg;
  86. }
  87. // 客户名称不能重复校验
  88. // 渠道编码真实性校验
  89. return null;
  90. }

返回给前端渲染报告图
image.png