Commit e001bbbdbbd05659eb0df41f59e2231aeb9875bd

Authored by YunaiV
1 parent 7b35b162

feat: 新增 Excel 多值字典转换器

- 新增 MultiDictConvert,支持半角逗号存储的多值字典字段
- 支持 String、Collection、Set、具体集合类和数组字段
- 未知字典 label/value 时整体转换失败,避免返回部分结果导致数据缺失
- 补充单值、多值、空值、顺序保持和失败场景单元测试
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MultiDictConvert.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.convert;
  2 +
  3 +import cn.hutool.core.convert.Convert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
  6 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  7 +import cn.idev.excel.converters.Converter;
  8 +import cn.idev.excel.enums.CellDataTypeEnum;
  9 +import cn.idev.excel.metadata.GlobalConfiguration;
  10 +import cn.idev.excel.metadata.data.ReadCellData;
  11 +import cn.idev.excel.metadata.data.WriteCellData;
  12 +import cn.idev.excel.metadata.property.ExcelContentProperty;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +import java.lang.reflect.Array;
  16 +import java.lang.reflect.Field;
  17 +import java.lang.reflect.Modifier;
  18 +import java.lang.reflect.ParameterizedType;
  19 +import java.lang.reflect.Type;
  20 +import java.util.ArrayList;
  21 +import java.util.Collection;
  22 +import java.util.LinkedHashSet;
  23 +import java.util.List;
  24 +import java.util.Set;
  25 +
  26 +/**
  27 + * Excel 多值数据字典转换器
  28 + *
  29 + * 数据库存储值使用半角逗号分隔,例如 {@code 1,2}
  30 + * Excel 展示使用顿号分隔,例如 {@code 男、女}
  31 + * 使用时,需要在字段上同时配置
  32 + * {@code @ExcelProperty(converter = MultiDictConvert.class)} 和 {@link DictFormat}
  33 + *
  34 + * @author NaCl
  35 + */
  36 +@Slf4j
  37 +public class MultiDictConvert implements Converter<Object> {
  38 +
  39 + private static final String EXCEL_SEPARATOR = "、";
  40 + private static final String DB_SEPARATOR = ",";
  41 + private static final String EXCEL_SEPARATOR_REGEX = "[、,,]";
  42 +
  43 + @Override
  44 + public Class<?> supportJavaTypeKey() {
  45 + throw new UnsupportedOperationException("暂不支持,也不需要");
  46 + }
  47 +
  48 + @Override
  49 + public CellDataTypeEnum supportExcelTypeKey() {
  50 + throw new UnsupportedOperationException("暂不支持,也不需要");
  51 + }
  52 +
  53 + @Override
  54 + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
  55 + GlobalConfiguration globalConfiguration) {
  56 + // 空时,返回空值
  57 + Field field = contentProperty.getField();
  58 + String labels = readCellData.getStringValue();
  59 + if (StrUtil.isBlank(labels)) {
  60 + return convertToFieldValue(field, new ArrayList<>());
  61 + }
  62 +
  63 + // 使用字典解析
  64 + String type = getType(contentProperty);
  65 + String[] labelArray = labels.split(EXCEL_SEPARATOR_REGEX);
  66 + List<String> values = new ArrayList<>(labelArray.length);
  67 + for (String item : labelArray) {
  68 + String label = item.trim();
  69 + if (StrUtil.isBlank(label)) {
  70 + continue;
  71 + }
  72 + String value = DictFrameworkUtils.parseDictDataValue(type, label);
  73 + if (value == null) {
  74 + log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
  75 + return null;
  76 + }
  77 + values.add(value);
  78 + }
  79 + // 将 String 的 value 转换成对应的属性
  80 + return convertToFieldValue(field, values);
  81 + }
  82 +
  83 + @Override
  84 + public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
  85 + GlobalConfiguration globalConfiguration) {
  86 + // 空时,返回空
  87 + if (object == null) {
  88 + return new WriteCellData<>("");
  89 + }
  90 +
  91 + // 使用字典格式化
  92 + String type = getType(contentProperty);
  93 + List<String> values = convertToStringList(object);
  94 + List<String> labels = new ArrayList<>(values.size());
  95 + for (String value : values) {
  96 + String label = DictFrameworkUtils.parseDictDataLabel(type, value);
  97 + if (label == null) {
  98 + log.error("[convertToExcelData][type({}) 转换不了 value({})]", type, value);
  99 + return new WriteCellData<>("");
  100 + }
  101 + labels.add(label);
  102 + }
  103 + // 生成 Excel 小表格
  104 + return new WriteCellData<>(String.join(EXCEL_SEPARATOR, labels));
  105 + }
  106 +
  107 + private static Object convertToFieldValue(Field field, List<String> values) {
  108 + Class<?> fieldClazz = field.getType();
  109 + if (String.class == fieldClazz) {
  110 + return String.join(DB_SEPARATOR, values);
  111 + }
  112 + if (fieldClazz.isArray()) {
  113 + return convertToArray(fieldClazz.getComponentType(), values);
  114 + }
  115 + if (Collection.class.isAssignableFrom(fieldClazz)) {
  116 + return convertToCollection(field, values);
  117 + }
  118 + return Convert.convert(fieldClazz, String.join(DB_SEPARATOR, values));
  119 + }
  120 +
  121 + private static Object convertToArray(Class<?> componentType, List<String> values) {
  122 + Object array = Array.newInstance(componentType, values.size());
  123 + for (int i = 0; i < values.size(); i++) {
  124 + Array.set(array, i, Convert.convert(componentType, values.get(i)));
  125 + }
  126 + return array;
  127 + }
  128 +
  129 + private static Collection<?> convertToCollection(Field field, List<String> values) {
  130 + Class<?> elementClazz = getCollectionElementClazz(field);
  131 + Collection<Object> result = createCollection(field.getType());
  132 + for (String value : values) {
  133 + result.add(Convert.convert(elementClazz, value));
  134 + }
  135 + return result;
  136 + }
  137 +
  138 + @SuppressWarnings("unchecked")
  139 + private static Collection<Object> createCollection(Class<?> fieldClazz) {
  140 + if (!fieldClazz.isInterface() && !Modifier.isAbstract(fieldClazz.getModifiers())) {
  141 + try {
  142 + return (Collection<Object>) fieldClazz.getDeclaredConstructor().newInstance();
  143 + } catch (Exception ignored) {
  144 + // 使用默认集合实现
  145 + }
  146 + }
  147 + return Set.class.isAssignableFrom(fieldClazz) ? new LinkedHashSet<>() : new ArrayList<>();
  148 + }
  149 +
  150 + private static Class<?> getCollectionElementClazz(Field field) {
  151 + Type genericType = field.getGenericType();
  152 + if (!(genericType instanceof ParameterizedType)) {
  153 + return String.class;
  154 + }
  155 + Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
  156 + if (actualType instanceof Class<?>) {
  157 + return (Class<?>) actualType;
  158 + }
  159 + if (actualType instanceof ParameterizedType
  160 + && ((ParameterizedType) actualType).getRawType() instanceof Class<?>) {
  161 + return (Class<?>) ((ParameterizedType) actualType).getRawType();
  162 + }
  163 + return String.class;
  164 + }
  165 +
  166 + private static List<String> convertToStringList(Object object) {
  167 + List<String> values = new ArrayList<>();
  168 + if (object instanceof String) {
  169 + String[] valueArray = ((String) object).split(DB_SEPARATOR);
  170 + for (String value : valueArray) {
  171 + addStringValue(values, value);
  172 + }
  173 + return values;
  174 + }
  175 + if (object instanceof Collection<?>) {
  176 + for (Object item : (Collection<?>) object) {
  177 + addStringValue(values, item);
  178 + }
  179 + return values;
  180 + }
  181 + if (object.getClass().isArray()) {
  182 + int length = Array.getLength(object);
  183 + for (int i = 0; i < length; i++) {
  184 + addStringValue(values, Array.get(object, i));
  185 + }
  186 + return values;
  187 + }
  188 + addStringValue(values, object);
  189 + return values;
  190 + }
  191 +
  192 + private static void addStringValue(List<String> values, Object value) {
  193 + if (value == null) {
  194 + return;
  195 + }
  196 + String str = String.valueOf(value).trim();
  197 + if (StrUtil.isBlank(str)) {
  198 + return;
  199 + }
  200 + values.add(str);
  201 + }
  202 +
  203 + private static String getType(ExcelContentProperty contentProperty) {
  204 + return contentProperty.getField().getAnnotation(DictFormat.class).value();
  205 + }
  206 +
  207 +}
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/convert/MultiDictConvertTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.excel.core.convert;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.idev.excel.metadata.data.ReadCellData;
  5 +import cn.idev.excel.metadata.data.WriteCellData;
  6 +import cn.idev.excel.metadata.property.ExcelContentProperty;
  7 +import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
  8 +import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
  9 +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
  10 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  11 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.mockito.Mock;
  15 +
  16 +import java.lang.reflect.Field;
  17 +import java.util.Arrays;
  18 +import java.util.HashSet;
  19 +import java.util.LinkedHashSet;
  20 +import java.util.List;
  21 +import java.util.Set;
  22 +
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  24 +import static org.junit.jupiter.api.Assertions.assertArrayEquals;
  25 +import static org.junit.jupiter.api.Assertions.assertEquals;
  26 +import static org.junit.jupiter.api.Assertions.assertNull;
  27 +import static org.junit.jupiter.api.Assertions.assertTrue;
  28 +import static org.mockito.ArgumentMatchers.eq;
  29 +import static org.mockito.Mockito.mock;
  30 +import static org.mockito.Mockito.when;
  31 +
  32 +/**
  33 + * {@link MultiDictConvert} 的单元测试
  34 + *
  35 + * @author NaCl
  36 + */
  37 +public class MultiDictConvertTest extends BaseMockitoUnitTest {
  38 +
  39 + private static final String DICT_TYPE = "test_dict";
  40 +
  41 + @Mock
  42 + private DictDataCommonApi dictDataApi;
  43 +
  44 + private MultiDictConvert convert;
  45 +
  46 + @BeforeEach
  47 + public void setUp() {
  48 + convert = new MultiDictConvert();
  49 + DictFrameworkUtils.init(dictDataApi);
  50 + DictFrameworkUtils.clearCache();
  51 + }
  52 +
  53 + @Test
  54 + public void testConvertToExcelData_string() throws NoSuchFieldException {
  55 + // mock 数据
  56 + mockDictDatas();
  57 +
  58 + // 调用
  59 + WriteCellData<String> result = convert.convertToExcelData("1,2", mockContentProperty("stringValues"), null);
  60 + // 断言
  61 + assertEquals("男、女", result.getStringValue());
  62 + }
  63 +
  64 + @Test
  65 + public void testConvertToExcelData_single() throws NoSuchFieldException {
  66 + // mock 数据
  67 + mockDictDatas();
  68 +
  69 + // 调用
  70 + WriteCellData<String> result = convert.convertToExcelData("1", mockContentProperty("stringValues"), null);
  71 + // 断言
  72 + assertEquals("男", result.getStringValue());
  73 + }
  74 +
  75 + @Test
  76 + public void testConvertToExcelData_order() throws NoSuchFieldException {
  77 + // mock 数据
  78 + mockDictDatas();
  79 +
  80 + // 调用
  81 + WriteCellData<String> result = convert.convertToExcelData("2,1", mockContentProperty("stringValues"), null);
  82 + // 断言
  83 + assertEquals("女、男", result.getStringValue());
  84 + }
  85 +
  86 + @Test
  87 + public void testConvertToExcelData_null() {
  88 + // 调用
  89 + WriteCellData<String> result = convert.convertToExcelData(null, null, null);
  90 + // 断言
  91 + assertEquals("", result.getStringValue());
  92 + }
  93 +
  94 + @Test
  95 + public void testConvertToExcelData_collection() throws NoSuchFieldException {
  96 + // mock 数据
  97 + mockDictDatas();
  98 +
  99 + // 调用
  100 + WriteCellData<String> result = convert.convertToExcelData(ListUtil.of(1, 2),
  101 + mockContentProperty("listValues"), null);
  102 + // 断言
  103 + assertEquals("男、女", result.getStringValue());
  104 + }
  105 +
  106 + @Test
  107 + public void testConvertToExcelData_array() throws NoSuchFieldException {
  108 + // mock 数据
  109 + mockDictDatas();
  110 +
  111 + // 调用
  112 + WriteCellData<String> result = convert.convertToExcelData(new int[]{1, 2},
  113 + mockContentProperty("stringValues"), null);
  114 + // 断言
  115 + assertEquals("男、女", result.getStringValue());
  116 + }
  117 +
  118 + @Test
  119 + public void testConvertToExcelData_unknownValue() throws NoSuchFieldException {
  120 + // mock 数据
  121 + mockDictDatas();
  122 +
  123 + // 调用
  124 + WriteCellData<String> result = convert.convertToExcelData("1,3", mockContentProperty("stringValues"), null);
  125 + // 断言
  126 + assertEquals("", result.getStringValue());
  127 + }
  128 +
  129 + @Test
  130 + public void testConvertToJavaData_string() throws NoSuchFieldException {
  131 + // mock 数据
  132 + mockDictDatas();
  133 +
  134 + // 调用
  135 + Object result = convert.convertToJavaData(mockReadCellData("男、女"),
  136 + mockContentProperty("stringValues"), null);
  137 + // 断言
  138 + assertEquals("1,2", result);
  139 + }
  140 +
  141 + @Test
  142 + public void testConvertToJavaData_single() throws NoSuchFieldException {
  143 + // mock 数据
  144 + mockDictDatas();
  145 +
  146 + // 调用
  147 + Object result = convert.convertToJavaData(mockReadCellData("男"),
  148 + mockContentProperty("stringValues"), null);
  149 + // 断言
  150 + assertEquals("1", result);
  151 + }
  152 +
  153 + @Test
  154 + public void testConvertToJavaData_blank() throws NoSuchFieldException {
  155 + // 调用
  156 + Object result = convert.convertToJavaData(mockReadCellData(""),
  157 + mockContentProperty("stringValues"), null);
  158 + // 断言
  159 + assertEquals("", result);
  160 + }
  161 +
  162 + @Test
  163 + public void testConvertToJavaData_blankList() throws NoSuchFieldException {
  164 + // 调用
  165 + Object result = convert.convertToJavaData(mockReadCellData(""),
  166 + mockContentProperty("listValues"), null);
  167 + // 断言
  168 + assertTrue(result instanceof List);
  169 + assertTrue(((List<?>) result).isEmpty());
  170 + }
  171 +
  172 + @Test
  173 + public void testConvertToJavaData_blankSet() throws NoSuchFieldException {
  174 + // 调用
  175 + Object result = convert.convertToJavaData(mockReadCellData(""),
  176 + mockContentProperty("setValues"), null);
  177 + // 断言
  178 + assertTrue(result instanceof LinkedHashSet);
  179 + assertTrue(((Set<?>) result).isEmpty());
  180 + }
  181 +
  182 + @Test
  183 + public void testConvertToJavaData_blankArray() throws NoSuchFieldException {
  184 + // 调用
  185 + Object result = convert.convertToJavaData(mockReadCellData(""),
  186 + mockContentProperty("arrayValues"), null);
  187 + // 断言
  188 + assertArrayEquals(new Integer[0], (Integer[]) result);
  189 + }
  190 +
  191 + @Test
  192 + @SuppressWarnings("unchecked")
  193 + public void testConvertToJavaData_list() throws NoSuchFieldException {
  194 + // mock 数据
  195 + mockDictDatas();
  196 +
  197 + // 调用
  198 + Object result = convert.convertToJavaData(mockReadCellData("男、女"),
  199 + mockContentProperty("listValues"), null);
  200 + // 断言
  201 + assertEquals(ListUtil.of(1, 2), (List<Integer>) result);
  202 + }
  203 +
  204 + @Test
  205 + @SuppressWarnings("unchecked")
  206 + public void testConvertToJavaData_set() throws NoSuchFieldException {
  207 + // mock 数据
  208 + mockDictDatas();
  209 +
  210 + // 调用
  211 + Object result = convert.convertToJavaData(mockReadCellData("男,女"),
  212 + mockContentProperty("setValues"), null);
  213 + // 断言
  214 + assertEquals(new LinkedHashSet<>(Arrays.asList(1L, 2L)), (Set<Long>) result);
  215 + }
  216 +
  217 + @Test
  218 + @SuppressWarnings("SimplifiableAssertion")
  219 + public void testConvertToJavaData_hashSet() throws NoSuchFieldException {
  220 + // mock 数据
  221 + mockDictDatas();
  222 +
  223 + // 调用
  224 + Object result = convert.convertToJavaData(mockReadCellData("男,女"),
  225 + mockContentProperty("hashSetValues"), null);
  226 + // 断言
  227 + assertTrue(result instanceof HashSet);
  228 + assertEquals(new HashSet<>(Arrays.asList(1L, 2L)), result);
  229 + }
  230 +
  231 + @Test
  232 + public void testConvertToJavaData_array() throws NoSuchFieldException {
  233 + // mock 数据
  234 + mockDictDatas();
  235 +
  236 + // 调用
  237 + Object result = convert.convertToJavaData(mockReadCellData("男,女"),
  238 + mockContentProperty("arrayValues"), null);
  239 + // 断言
  240 + assertArrayEquals(new Integer[]{1, 2}, (Integer[]) result);
  241 + }
  242 +
  243 + @Test
  244 + public void testConvertToJavaData_unknownLabel() throws NoSuchFieldException {
  245 + // mock 数据
  246 + mockDictDatas();
  247 +
  248 + // 调用
  249 + Object result = convert.convertToJavaData(mockReadCellData("男、未知"),
  250 + mockContentProperty("stringValues"), null);
  251 + // 断言
  252 + assertNull(result);
  253 + }
  254 +
  255 + private void mockDictDatas() {
  256 + List<DictDataRespDTO> dictDatas = ListUtil.of(
  257 + randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("男")),
  258 + randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("2").setLabel("女"))
  259 + );
  260 + when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(dictDatas);
  261 + }
  262 +
  263 + private static ReadCellData<?> mockReadCellData(String value) {
  264 + ReadCellData<?> readCellData = mock(ReadCellData.class);
  265 + when(readCellData.getStringValue()).thenReturn(value);
  266 + return readCellData;
  267 + }
  268 +
  269 + private static ExcelContentProperty mockContentProperty(String fieldName) throws NoSuchFieldException {
  270 + Field field = TestExcelVO.class.getDeclaredField(fieldName);
  271 + ExcelContentProperty contentProperty = mock(ExcelContentProperty.class);
  272 + when(contentProperty.getField()).thenReturn(field);
  273 + return contentProperty;
  274 + }
  275 +
  276 + static class TestExcelVO {
  277 +
  278 + @DictFormat(DICT_TYPE)
  279 + private String stringValues;
  280 +
  281 + @DictFormat(DICT_TYPE)
  282 + private List<Integer> listValues;
  283 +
  284 + @DictFormat(DICT_TYPE)
  285 + private Set<Long> setValues;
  286 +
  287 + @DictFormat(DICT_TYPE)
  288 + private HashSet<Long> hashSetValues;
  289 +
  290 + @DictFormat(DICT_TYPE)
  291 + private Integer[] arrayValues;
  292 +
  293 + }
  294 +
  295 +}