springboot 数据字典设计思路:字典表+字典枚举 两者兼故方案

方式一:使用字典表存取 

        说明:适合于前端页面使用的下拉框数据值、或者字典数据不固定有变化调整的字典,建议放在数据表中维护。

       直接借鉴Ruoyi框架提供的2张字典表,sys_dict_type(字典类型定义表)、sys_dict_data(字典数据表)

CREATE TABLE `sys_dict_type` (
  `dict_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '字典主键',
  `dict_name` varchar(100) DEFAULT '' COMMENT '字典名称',
  `dict_type` varchar(100) DEFAULT '' COMMENT '字典类型',
  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`dict_id`),
  UNIQUE KEY `dict_type` (`dict_type`)
) COMMENT='字典类型表';

CREATE TABLE `sys_dict_data` (
  `dict_code` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '字典编码',
  `dict_sort` int(4) DEFAULT '0' COMMENT '字典排序',
  `dict_label` varchar(100) DEFAULT '' COMMENT '字典标签',
  `dict_value` varchar(100) DEFAULT '' COMMENT '字典键值',
  `dict_type` varchar(100) DEFAULT '' COMMENT '字典类型',
  `css_class` varchar(100) DEFAULT NULL COMMENT '样式属性(其他样式扩展)',
  `list_class` varchar(100) DEFAULT NULL COMMENT '表格回显样式',
  `is_default` char(1) DEFAULT 'N' COMMENT '是否默认(Y是 N否)',
  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`dict_code`)
) COMMENT='字典数据表';

方式二:自定义枚举变量

        说明:适用于系统级别、固定不变的字典;可以考虑直接写在代码中,方便其它人在代码中使用。

@DictType("sys_disable")
public enum SysDisableEnum {
    ENABLE("0", "启用"),
    DISABLE("1", "停用");

    private String code;
    private String desc;

    public static List toList() {
        return (List)Stream.of(values()).map((row) -> {
            DictOptions options = new DictOptions();
            options.setDictLabel(row.getDesc());
            options.setDictValue(row.getCode());
            return options;
        }).collect(Collectors.toList());
    }

    private SysDisableEnum(final String code, final String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return this.code;
    }

    public String getDesc() {
        return this.desc;
    }
}

        对于字典表,在使用时很简单,无非是提供好添加、更新、删除及查询的API即可;但对于枚举字典,怎么能不重复配置到字典表中,同时又能融入到 查询字典的API中,这里就需要好好封装一下代码了!

        具体实现代码参考如下:  

/**
 * 字典类型枚举:@DictType(value="sys_sex")
 */
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DictType {

    String value();//字典类型别名
}


/**
 * 字典翻译注解:@DictEnum(value="sex", dictType="sys_sex")
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DictEnum {

    String value();//实体类字段

    String dictType() default "";//字典code;指的是dict_type

    String target() default "";//返回目标属性,如 sexDesc
}

/**
 * 字典类型注解:用于多个字典翻译
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DictEnums {

    DictEnum[] value();//多个字典值
}
package com.geline.cloud.core.dict;

import com.geline.cloud.core.domain.DictOptions;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 字典枚举缓存
 * @author: mengxin
 * @date: 2022/9/25 10:14
 */
public class DictTypeCache {

    private static Map> dictMapCache = new HashMap<>();

    public static void put(String dictType, List optionsList) {
        dictMapCache.put(dictType, optionsList);
    }

    public static Map> getAll(){
        return dictMapCache;
    }

    /**
     * 获取字典值对应的标签名
     * @param dictType
     * @param dictValue
     * @return
     */
    public static String getDictLabel(String dictType, String dictValue){
        List options = dictMapCache.get(dictType);
        if(options != null){
            for (DictOptions row : options){
                if(row.getDictValue().equals(dictValue)){
                    return row.getDictLabel();
                }
            }
        }
        return null;
    }
}
package com.geline.cloud.core.dict;

import com.geline.cloud.core.dict.annotation.DictType;
import com.geline.cloud.core.domain.DictOptions;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

/**
 * 扫描自定义字典枚举类 @DictType
 * @author: mengxin
 * @date: 2022/9/25 10:16
 */
@Component
public class DictTypeHandler implements CommandLineRunner, ResourceLoaderAware {
    private ResourceLoader resourceLoader;

    private ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
    private static final String FULLTEXT_SACN_PACKAGE_PATH = "com.geline.cloud";

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void run(String... args) throws Exception {
        String concat = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                .concat(ClassUtils.convertClassNameToResourcePath(FULLTEXT_SACN_PACKAGE_PATH)
                        .concat("/**/*Enum*.class")); //只扫描文件名中包含Enum的文件,会快点
        Resource[] resources = resolver.getResources(concat);
        MetadataReader metadataReader = null;
        for (Resource resource : resources) {
            if (resource.isReadable()) {
                metadataReader = metadataReaderFactory.getMetadataReader(resource);
                boolean flag = metadataReader.getAnnotationMetadata().hasAnnotation(DictType.class.getName());
                String className = metadataReader.getClassMetadata().getClassName();

                if(flag) {
                    Map annotationAttributes = metadataReader.getAnnotationMetadata()
                            .getAnnotationAttributes(DictType.class.getName());
                    String dictType = annotationAttributes.get("value").toString();

                    try {
                        Class aClass = Class.forName(className);
                        Method listDict = aClass.getDeclaredMethod("toList");
                        Object[] oo = aClass.getEnumConstants();
                        List list = (List) listDict.invoke(oo[0]);
                        if(list != null){
                            DictTypeCache.put(dictType, list);
                        }
                    } catch (ClassNotFoundException | NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
package com.geline.cloud.core.domain;

import lombok.Getter;
import lombok.Setter;

/**
 * 查询字典列表
 * @author: mengx
 * @date: 2021/9/14 16:18
 */
@Getter
@Setter
public class DictOptions {

    private String dictType;
    private String dictLabel;
    private String dictValue;
    private String isDefault;
    private String listClass;
}
package com.geline.cloud.core.dict;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.geline.cloud.core.dict.annotation.DictEnum;
import com.geline.cloud.core.dict.annotation.DictEnums;
import com.geline.cloud.core.domain.Result;
import com.geline.cloud.core.domain.TableDataInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * 使用方式:子类继承后加 @ControllerAdvice
 * @author: mengxin
 * @date: 2022/9/17 10:05
 */
@Slf4j
public abstract class AbstractDictEnumHandler implements ResponseBodyAdvice {

    /**
     * 从DB中查询字典标签名
     * @param dictType 字典类型code
     * @param dictValue 字典值code
     * @return
     */
    public abstract String selectDictLabelByDatabase(String dictType, String dictValue);

    /**
     * 从枚举字典中取字典标签名
     * @param dictType
     * @param dictValue
     * @return
     */
    public String selectDictLabelByEnum(String dictType, String dictValue){
        return DictTypeCache.getDictLabel(dictType, dictValue);
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return methodParameter.hasMethodAnnotation(DictEnum.class) || methodParameter.hasMethodAnnotation(DictEnums.class);
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class clazz,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (ObjectUtil.isEmpty(o)) {
            return null;
        }
        // 获取方法 注解参数
        Method method = methodParameter.getMethod();
        final String methodName = method == null ? "" : (method.getDeclaringClass().getSimpleName() + "." + method.getName());
        DictEnum[] sensibles;
        if (methodParameter.hasMethodAnnotation(DictEnum.class)) {
            DictEnum sensible = methodParameter.getMethodAnnotation(DictEnum.class);
            sensibles = new DictEnum[]{sensible};
        } else {
            DictEnums typeSensibles = methodParameter.getMethodAnnotation(DictEnums.class);
            sensibles = typeSensibles == null ? new DictEnum[]{} : typeSensibles.value();
        }
        // 处理返回值
        this.dis(o, sensibles, methodName);
        return o;
    }

    /**
     * 处理返回数据
     *
     * @param obj       对象
     * @param sensibles 字典类型
     */
    private void dis(Object obj, DictEnum[] sensibles, String methodName) {
        // 处理返回类型
        if (obj instanceof TableDataInfo) {
            this.dis(((TableDataInfo) obj).getRows(), sensibles, methodName);
            return;
        }
        if (obj instanceof Result) {
            this.dis(((Result) obj).getData(), sensibles, methodName);
            return;
        }
        if (obj instanceof IPage) {
            ((IPage) obj).getRecords().forEach(e -> this.dis(e, sensibles, methodName));
            return;
        }
        if (obj instanceof Iterable) {
            ((Iterable) obj).forEach(e -> this.dis(e, sensibles, methodName));
            return;
        }
        if (obj instanceof Map) {
            Map map = (Map) obj;
            for (DictEnum sensible : sensibles) {
                if (!map.containsKey(sensible.value())) {
                    log.warn("--- 方法 {},字典 {} 进行忽略,不存在 ------", methodName, sensible.value());
                    continue;
                }
                Object value = map.get(sensible.value());
                if (value instanceof Iterable) {
                    this.dis(value, sensibles, methodName);
                    continue;
                }
                String dictType = StrUtil.isEmpty(sensible.dictType()) ? StrUtil.toUnderlineCase(sensible.value()) : sensible.dictType();
                String label = selectDictLabelByEnum(dictType, String.valueOf(value));
                if(label == null){
                    label = selectDictLabelByDatabase(dictType, String.valueOf(value));
                }
                String target = sensible.target();
                map.put(StrUtil.isNotBlank(target) ? target : sensible.value(), label);
            }
            return;
        }
        Class clazz = obj.getClass();
        for (DictEnum sensible : sensibles) {
            try {
                Field field = ReflectUtil.getField(clazz, sensible.value());
                if (field == null || !field.getType().getSimpleName().contains("String")) {
                    log.warn("--- 方法 {},字典 {} 进行忽略,不存在 || 非String类型 ------", methodName, sensible.value());
                    continue;
                }
                Object val = ReflectUtil.getFieldValue(obj, field);
                if (ObjectUtil.isEmpty(val)) {
                    continue;
                }
                String dictType = StrUtil.isEmpty(sensible.dictType()) ? StrUtil.toUnderlineCase(sensible.value()) : sensible.dictType();
                String label = selectDictLabelByEnum(dictType, String.valueOf(val));
                if(label == null){
                    label = selectDictLabelByDatabase(dictType, String.valueOf(val));
                }
                ReflectUtil.setFieldValue(obj, field, label);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
 
  
package com.geline.cloud.util.dict;

import com.geline.cloud.core.dict.AbstractDictEnumHandler;
import com.geline.cloud.modules.system.entity.SysDictData;
import com.geline.cloud.modules.system.service.SysDictDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ControllerAdvice;

/**
 * 自定义字典处理
 * @author: mengxin
 * @date: 2022/9/17 10:43
 */
@ControllerAdvice
@Slf4j
public class MyDictEnumHandler extends AbstractDictEnumHandler {

    @Autowired
    private SysDictDataService sysDictDataService;

    @Override
    public String selectDictLabelByDatabase(String dictType, String dictValue) {
        SysDictData dictData = sysDictDataService.getDictData(dictType, dictValue);
        if(dictData != null){
            return dictData.getDictLabel();
        }
        return null;
    }
}

具体使用方法:

在controller方法上添加注解,扩展返回字典字段,如查询返回 sex, 增加一个sexDesc;

举例:分页查询返回 Page处理字典,多个字典字段使用 @DictEnums,一个字典字段使用 @DictEnum 。

    /**
     * 分页查询
     * localhost:9501/reportData/pageMaps?pageNum=1&pageSize=10&orderByColumn=create_time&isAsc=desc&reportTitle=标题
     * @param pageNum
     * @param pageSize
     * @param orderByColumn
     * @param isAsc
     * @return
     */
    @DictEnums({
            @DictEnum(value = "reportType", dictType = "sys_report_type"),
            @DictEnum(value = "status", dictType = "sys_disable", target = "statusDesc")
    })
    @GetMapping({"/pageMaps"})
    public TableDataInfo pageMaps(String reportTitle,
                                  int pageNum, int pageSize, String orderByColumn, String isAsc) {
        ReportDataQueryVO queryVO = new ReportDataQueryVO();
        queryVO.setReportTitle(reportTitle);
        QueryWrapper query = QueryWrapperUtil.build(queryVO, orderByColumn, isAsc);
        Page page = this.getBaseService().pageMaps(new Page(pageNum, pageSize), query);
        return this.getTableDataInfo(page);
    }


    /**
     * 地图数据 - 分页查询
     * localhost:9501/visualMap/pageMaps?pageNum=1&pageSize=10&orderByColumn=create_time&isAsc=desc&name={名称}
     * @return
     */
    @DictEnum(value = "disable", dictType = "sys_disable")
    @GetMapping({"/pageMaps"})
    public TableDataInfo pageMaps(int pageNum, int pageSize, String orderByColumn, String isAsc, String name) {
        VisualMapQueryVO queryVO = new VisualMapQueryVO();
        queryVO.setName(name);
        QueryWrapper query = QueryWrapperUtil.build(queryVO, orderByColumn, isAsc);
        Page page = this.visualMapService.pageMaps(new Page(pageNum, pageSize), query);
        return this.getTableDataInfo(page);
    }

        至于 添加、更新相关代码,直接用mybatis生成的 Enitity.java,不用在实体类中额外添加 @Dict注解,前端添加、更新时传入字典code直接保存及更新,字典问题-只考虑查询返回的字典翻译问题。

        最后,再提供2个查询返回字典列表方法:

package com.geline.cloud.system;

import com.geline.cloud.core.dict.DictTypeCache;
import com.geline.cloud.core.domain.DictOptions;
import com.geline.cloud.core.domain.Result;
import com.geline.cloud.modules.system.service.SysDictDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

/**
 * 字典管理 Controller
 * @author mengx
 * @date 2020/7/26 21:45
 */
@RestController
@RequestMapping("/system/dict")
public class SysDictController {

    @Autowired
    private SysDictDataService sysDictDataService;

    /**
     * 查询所有枚举字典Map
     * /system/dict/listEnums
     * @return
     */
    @GetMapping("/listEnums")
    public Result listEnums(){
        return Result.ok(DictTypeCache.getAll());
    }

    /**
     * 根据字典类型查询字典列表
     * /system/dict/list/{dictType}
     * @param dictType
     * @return
     */
    @GetMapping("/list/{dictType}")
    public Result list(@PathVariable String dictType){
        Map> all = DictTypeCache.getAll();
        List options = all.get(dictType);
        if(options != null){
            return Result.ok(options);
        }
        return Result.ok(sysDictDataService.getListByDictType(dictType));
    }

}

 

 

你可能感兴趣的:(java,spring,spring,boot,后端,java)