mirror of
https://gitee.com/zhijiantianya/ruoyi-vue-pro.git
synced 2026-07-09 06:50:32 +08:00
feat: 新增 Excel 多值字典转换器
- 新增 MultiDictConvert,支持半角逗号存储的多值字典字段 - 支持 String、Collection、Set、具体集合类和数组字段 - 未知字典 label/value 时整体转换失败,避免返回部分结果导致数据缺失 - 补充单值、多值、空值、顺序保持和失败场景单元测试
This commit is contained in:
@ -0,0 +1,207 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.convert;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Excel 多值数据字典转换器
|
||||
*
|
||||
* 数据库存储值使用半角逗号分隔,例如 {@code 1,2}
|
||||
* Excel 展示使用顿号分隔,例如 {@code 男、女}
|
||||
* 使用时,需要在字段上同时配置
|
||||
* {@code @ExcelProperty(converter = MultiDictConvert.class)} 和 {@link DictFormat}
|
||||
*
|
||||
* @author NaCl
|
||||
*/
|
||||
@Slf4j
|
||||
public class MultiDictConvert implements Converter<Object> {
|
||||
|
||||
private static final String EXCEL_SEPARATOR = "、";
|
||||
private static final String DB_SEPARATOR = ",";
|
||||
private static final String EXCEL_SEPARATOR_REGEX = "[、,,]";
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空值
|
||||
Field field = contentProperty.getField();
|
||||
String labels = readCellData.getStringValue();
|
||||
if (StrUtil.isBlank(labels)) {
|
||||
return convertToFieldValue(field, new ArrayList<>());
|
||||
}
|
||||
|
||||
// 使用字典解析
|
||||
String type = getType(contentProperty);
|
||||
String[] labelArray = labels.split(EXCEL_SEPARATOR_REGEX);
|
||||
List<String> values = new ArrayList<>(labelArray.length);
|
||||
for (String item : labelArray) {
|
||||
String label = item.trim();
|
||||
if (StrUtil.isBlank(label)) {
|
||||
continue;
|
||||
}
|
||||
String value = DictFrameworkUtils.parseDictDataValue(type, label);
|
||||
if (value == null) {
|
||||
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
|
||||
return null;
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
// 将 String 的 value 转换成对应的属性
|
||||
return convertToFieldValue(field, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空
|
||||
if (object == null) {
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
|
||||
// 使用字典格式化
|
||||
String type = getType(contentProperty);
|
||||
List<String> values = convertToStringList(object);
|
||||
List<String> labels = new ArrayList<>(values.size());
|
||||
for (String value : values) {
|
||||
String label = DictFrameworkUtils.parseDictDataLabel(type, value);
|
||||
if (label == null) {
|
||||
log.error("[convertToExcelData][type({}) 转换不了 value({})]", type, value);
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
labels.add(label);
|
||||
}
|
||||
// 生成 Excel 小表格
|
||||
return new WriteCellData<>(String.join(EXCEL_SEPARATOR, labels));
|
||||
}
|
||||
|
||||
private static Object convertToFieldValue(Field field, List<String> values) {
|
||||
Class<?> fieldClazz = field.getType();
|
||||
if (String.class == fieldClazz) {
|
||||
return String.join(DB_SEPARATOR, values);
|
||||
}
|
||||
if (fieldClazz.isArray()) {
|
||||
return convertToArray(fieldClazz.getComponentType(), values);
|
||||
}
|
||||
if (Collection.class.isAssignableFrom(fieldClazz)) {
|
||||
return convertToCollection(field, values);
|
||||
}
|
||||
return Convert.convert(fieldClazz, String.join(DB_SEPARATOR, values));
|
||||
}
|
||||
|
||||
private static Object convertToArray(Class<?> componentType, List<String> values) {
|
||||
Object array = Array.newInstance(componentType, values.size());
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
Array.set(array, i, Convert.convert(componentType, values.get(i)));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static Collection<?> convertToCollection(Field field, List<String> values) {
|
||||
Class<?> elementClazz = getCollectionElementClazz(field);
|
||||
Collection<Object> result = createCollection(field.getType());
|
||||
for (String value : values) {
|
||||
result.add(Convert.convert(elementClazz, value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Collection<Object> createCollection(Class<?> fieldClazz) {
|
||||
if (!fieldClazz.isInterface() && !Modifier.isAbstract(fieldClazz.getModifiers())) {
|
||||
try {
|
||||
return (Collection<Object>) fieldClazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception ignored) {
|
||||
// 使用默认集合实现
|
||||
}
|
||||
}
|
||||
return Set.class.isAssignableFrom(fieldClazz) ? new LinkedHashSet<>() : new ArrayList<>();
|
||||
}
|
||||
|
||||
private static Class<?> getCollectionElementClazz(Field field) {
|
||||
Type genericType = field.getGenericType();
|
||||
if (!(genericType instanceof ParameterizedType)) {
|
||||
return String.class;
|
||||
}
|
||||
Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
|
||||
if (actualType instanceof Class<?>) {
|
||||
return (Class<?>) actualType;
|
||||
}
|
||||
if (actualType instanceof ParameterizedType
|
||||
&& ((ParameterizedType) actualType).getRawType() instanceof Class<?>) {
|
||||
return (Class<?>) ((ParameterizedType) actualType).getRawType();
|
||||
}
|
||||
return String.class;
|
||||
}
|
||||
|
||||
private static List<String> convertToStringList(Object object) {
|
||||
List<String> values = new ArrayList<>();
|
||||
if (object instanceof String) {
|
||||
String[] valueArray = ((String) object).split(DB_SEPARATOR);
|
||||
for (String value : valueArray) {
|
||||
addStringValue(values, value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
if (object instanceof Collection<?>) {
|
||||
for (Object item : (Collection<?>) object) {
|
||||
addStringValue(values, item);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
if (object.getClass().isArray()) {
|
||||
int length = Array.getLength(object);
|
||||
for (int i = 0; i < length; i++) {
|
||||
addStringValue(values, Array.get(object, i));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
addStringValue(values, object);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void addStringValue(List<String> values, Object value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
String str = String.valueOf(value).trim();
|
||||
if (StrUtil.isBlank(str)) {
|
||||
return;
|
||||
}
|
||||
values.add(str);
|
||||
}
|
||||
|
||||
private static String getType(ExcelContentProperty contentProperty) {
|
||||
return contentProperty.getField().getAnnotation(DictFormat.class).value();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,295 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.convert;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
|
||||
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link MultiDictConvert} 的单元测试
|
||||
*
|
||||
* @author NaCl
|
||||
*/
|
||||
public class MultiDictConvertTest extends BaseMockitoUnitTest {
|
||||
|
||||
private static final String DICT_TYPE = "test_dict";
|
||||
|
||||
@Mock
|
||||
private DictDataCommonApi dictDataApi;
|
||||
|
||||
private MultiDictConvert convert;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
convert = new MultiDictConvert();
|
||||
DictFrameworkUtils.init(dictDataApi);
|
||||
DictFrameworkUtils.clearCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_string() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1,2", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_single() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_order() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("2,1", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("女、男", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_null() {
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(null, null, null);
|
||||
// 断言
|
||||
assertEquals("", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_collection() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(ListUtil.of(1, 2),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_array() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(new int[]{1, 2},
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_unknownValue() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1,3", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_string() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("1,2", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_single() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("1", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blank() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankList() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof List);
|
||||
assertTrue(((List<?>) result).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankSet() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("setValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof LinkedHashSet);
|
||||
assertTrue(((Set<?>) result).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankArray() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("arrayValues"), null);
|
||||
// 断言
|
||||
assertArrayEquals(new Integer[0], (Integer[]) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConvertToJavaData_list() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertEquals(ListUtil.of(1, 2), (List<Integer>) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConvertToJavaData_set() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("setValues"), null);
|
||||
// 断言
|
||||
assertEquals(new LinkedHashSet<>(Arrays.asList(1L, 2L)), (Set<Long>) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("SimplifiableAssertion")
|
||||
public void testConvertToJavaData_hashSet() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("hashSetValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof HashSet);
|
||||
assertEquals(new HashSet<>(Arrays.asList(1L, 2L)), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_array() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("arrayValues"), null);
|
||||
// 断言
|
||||
assertArrayEquals(new Integer[]{1, 2}, (Integer[]) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_unknownLabel() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、未知"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
private void mockDictDatas() {
|
||||
List<DictDataRespDTO> dictDatas = ListUtil.of(
|
||||
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("男")),
|
||||
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("2").setLabel("女"))
|
||||
);
|
||||
when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(dictDatas);
|
||||
}
|
||||
|
||||
private static ReadCellData<?> mockReadCellData(String value) {
|
||||
ReadCellData<?> readCellData = mock(ReadCellData.class);
|
||||
when(readCellData.getStringValue()).thenReturn(value);
|
||||
return readCellData;
|
||||
}
|
||||
|
||||
private static ExcelContentProperty mockContentProperty(String fieldName) throws NoSuchFieldException {
|
||||
Field field = TestExcelVO.class.getDeclaredField(fieldName);
|
||||
ExcelContentProperty contentProperty = mock(ExcelContentProperty.class);
|
||||
when(contentProperty.getField()).thenReturn(field);
|
||||
return contentProperty;
|
||||
}
|
||||
|
||||
static class TestExcelVO {
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private String stringValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private List<Integer> listValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private Set<Long> setValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private HashSet<Long> hashSetValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private Integer[] arrayValues;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user