搏彩网站开发建设,制作宣传片视频,晋中路桥建设集团网站,网页的设计与制作数据字典是系统中基本的必不可少的功能#xff0c;在多种多样的系统中#xff0c;数据字典表的设计都大同小异。但是使用方式确是多种多样#xff0c;设计好一套易用的数据字典功能模块#xff0c;可以使开发事半功倍。
常用的数据字典使用方式#xff1a;
直接在SQL语句… 数据字典是系统中基本的必不可少的功能在多种多样的系统中数据字典表的设计都大同小异。但是使用方式确是多种多样设计好一套易用的数据字典功能模块可以使开发事半功倍。
常用的数据字典使用方式
直接在SQL语句中LEFT JOIN 当然是不推荐这样用的查询出原始数据然后再根据原始数据需要的字典编码批量查询字典表并赋值到原始数据的字典值字段。后台提供通用数据字典接口前端根据需求统一查询数据字典数据并缓存在前台。当业务返回后台原始数据时前台通过类似于FilterVUE功能进行字典值对应。自定义数据字典注解当接口返回原始数据时通过切面分析返回对象中的数据字典字段并将数据字典赋值到数据字典值字段。提供数据字典通用工具手动处理单个或批量需要进行数据字典转换的数据。 我们为了更好的满足多样的业务需求那么我们肯定是需要支持多种多样的方式来实现数据字典转换的功能接下来我们以1注解2工具3前端转换的方式来支持数据字典转换。三种方式相辅相成、可以单独使用也可以结合起来使用。
可注解在Controller可注解在Service支持的集合类型List、Set、Queue 引用类型Array一维数组单独的bean支持递归赋值不支持复杂数据递归后台提供通用数据字典接口前端页面提供通用转换方法。只注解在普通字段上不要注解到复杂对象上
数据字典转换流程
1、在Service或者Controller添加DictAuto注解用于切面判断此方法是需要进行数据字典转换的方法。 2、切面发现此方法是需要数据字典转换的方法之后那么解析方法的返回参数返回参数有多种数据类型这里只处理集合类型List、Set、Queue 引用类型Array一维数组还有普通对象类型自定义实体bean。 3、无论是集合类型还是普通对象类型都需要进行遍历、递归等操作因为List里面是普通对象对象中也有可能是集合类型。此处需要注意请不要在对象中的字段嵌套自己这样会造成死循环。当然对象中可以嵌套自己的对象类型可以引用非自己的对象实例因为递归操作中我们会判断如果是null那么终止递归 4、对返回类型进行递归时通过注解获取到数据字典类型system、business等、数据字典CODE一级数据字典CODE作为数据字典的分类通过此条件去Redis数据库查询数据字典列表。将查询的数据字典列表存储在Map中。在循环遍历过程中增加判断如果Map中有了那么不再查询Redis数据库而是直接从Map中取。 5、在遍历递归对象的同时根据数据字典注解获取本对象中用于映射数据字典的字段值作为数据字典的CODE值二级数据字典CODE对应具体的数据字典然后赋值到数据字典值上。
一、通过注解实现数据字典转换功能
1、新增数据字典注解定义
package com.gitegg.platform.base.annotation.dict;import java.lang.annotation.*;/*** 数据字典注解注解在方法上自动设置返回参数的字典数据* 1、可以注解在 service和 controller上只注解返回值支持引用类型和常用的集合类型* 2、具体的实体类中如果是引用类型那么递归赋值* 3、支持的集合类型List Set Queue 引用类型Array一维数组普通对象类型自定义实体bean。* author GitEgg*/
Target({ ElementType.METHOD })
Retention(RetentionPolicy.RUNTIME)
Documented
public interface DictAuto {
}package com.gitegg.platform.base.annotation.dict;import java.lang.annotation.*;/*** 数据字典注解注解在字段上* 如果dictCode为空且此字段是对象类型那么表示此字段对象中拥有字典类型* 也就是只有注解了此字段的数据才会去递归设置字典值不去随便做无所谓的遍历** author GitEgg*/
Target(ElementType.FIELD)
Retention(RetentionPolicy.RUNTIME)
Documented
public interface DictField {/*** 数据字典类型 系统字典 system 业务字典 business 地区字典 areas 其他字典直接表名,例 t_sys_role* 1、确定选择哪种类型的数据字典*/String dictType() default business;/*** 数据字典编码就是取哪些数据字典的值* 2、确定需要匹配数据字典的集合*/String dictCode() default ;/*** 要最终转换最终数据字典的键是实体类中的一个字段通常配置为此字段的定义名称通过此字段作为key来转换数据字典的值* 3、确定需要把实体中哪个字段转换为字典值*/String dictKey() default ;/*** 如果是自定义表数据时此字段作为字典code对应数据表的字段* 4、表中作为数据字典的键*/String dictFiled() default ;/*** 如果是自定义表数据时此字段作为字典value对应数据表的字段* 5、表中作为数据字典的值*/String dictValue() default ;
}
2、新增切面处理数据字典注解
package com.gitegg.platform.boot.aspect;import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gitegg.platform.base.annotation.dict.DictAuto;
import com.gitegg.platform.base.annotation.dict.DictField;
import com.gitegg.platform.base.constant.DictConstant;
import com.gitegg.platform.base.constant.GitEggConstant;
import com.gitegg.platform.base.result.Result;
import com.gitegg.platform.boot.util.GitEggAuthUtils;
import com.gitegg.platform.redis.lock.IDistributedLockService;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import jodd.util.StringPool;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Consumer;/*** 数据字典切面* author GitEgg* date 2022-4-10*/
Log4j2
Component
Aspect
RequiredArgsConstructor(onConstructor_ Autowired)
ConditionalOnProperty(name enabled, prefix dict, havingValue true, matchIfMissing true)
public class DictAspect {/*** 是否开启租户模式*/Value(${tenant.enable})private Boolean enable;private final RedisTemplate redisTemplate;/*** 后置通知 解析返回参数进行字典设置* AfterReturning 只有存在返回值时才会执行 After 无论有没有返回值都会执行 所以这里使用 AfterReturning 只有存在返回值时才执行字典值注入操作* param dictAuto 注解配置*/AfterReturning(pointcut annotation(dictAuto), returning returnObj)public void doAfterReturning( DictAuto dictAuto, Object returnObj){// 返回不为null时进行数据字典处理if (null ! returnObj) {doDictAuto(dictAuto, returnObj);}}/*** key的组成为: dict:userId:sessionId:uri:method:(根据spring EL表达式对参数进行拼接)* 此处要考虑多种返回类型集合类型、引用类型、对象类型和基本数据类型这里只处理 集合类型List Set Queue 引用类型Array数组Array只支持一维数组。* 对于对象中的子对象为了提升性能同样需要加DictField注解才去填充否则每个子对象都去递归判断影响性能* 我们要考虑此处的逻辑* 1、判断返回数据类型如果是集合类型那么取出包含实体对象的集合类然后进行对象解析* 2、如果是对象类型那么直接进行对象解析* 3、如果是IPage类型那么取出其中的list数据判断是否为空不为空执行 1 步骤* 4、如果是Result类型判断Result的data是IPage还是集合类型分别执行对应的 1 步骤 或 3 步骤如果不是IPage也不是集合类型直接执行第 2 步骤* param dictAuto 注解* param objectReturn 方法返回值*/private void doDictAuto(NonNull DictAuto dictAuto, Object objectReturn) {// 临时存储数据字典mapMapString, MapObject, Object dictMap new HashMap();this.translationObjectDict(objectReturn, dictMap);}/*** 找到实际的对象或对象列表* 此处要考虑多种返回类型集合类型、引用类型、对象类型和基本数据类型这里只处理 集合类型List Set Queue 引用类型Array一维数组。* param objectReturn* param dictMap* return*/private void translationObjectDict(Object objectReturn, MapString, MapObject, Object dictMap) {if (Objects.isNull(objectReturn)){return;}// 判断返回值类型是Result、IPage、List、Objectif (objectReturn instanceof Result) {Object objectTarget ((Result) objectReturn).getData();translationObjectDict(objectTarget, dictMap);} else if (objectReturn instanceof IPage) {ListObject objectTargetList ((IPage) objectReturn).getRecords();translationObjectDict(objectTargetList, dictMap);} else if (objectReturn instanceof Collection) {((Collection) objectReturn).forEach(object- translationObjectDict(object, dictMap));} else if (ArrayUtil.isArray(objectReturn)) {// 数组这里需要处理((Collection) objectReturn).forEach(object- translationObjectDict(object, dictMap));} else {parseObjectFieldCodeValue(objectReturn, dictMap);}}/*** 取出对象中需要进行字典转换的字段** param targetObj : 取字段的对象* param dictMap : 存储数据字典* author liam*/private void parseObjectFieldCodeValue(Object targetObj, MapString, MapObject, Object dictMap) {if (Objects.isNull(targetObj)){return;}// 获取当前对象所有的fieldField[] declaredFields targetObj.getClass().getDeclaredFields();// 构造填充映射关系Arrays.stream(declaredFields).forEach(field -// 递归处理parseFieldObjDict(field, targetObj,fieldObj - parseObjectFieldCodeValue(fieldObj, dictMap),// 解析注解字段信息() - parseDictAnnotation(targetObj, field, dictMap)));}/*** 解析field对象对基本数据类型和复杂类型直接根据注解赋值对于对象或集合类型继续进行递归遍历** param field : 字段对象* param obj : 字段所属的obj对象* param recursiveFunc : 递归处理方法* author liam*/private static void parseFieldObjDict(Field field, Object obj, ConsumerObject recursiveFunc,NestedFunction parseAnnotationFunc) {Class cls field.getType();// 不处理map数据if (Map.class.isAssignableFrom(cls)) {return;}// 需要数据字典转换的属性有Dict注解 DictField只注解在普通字段上不要注解到复杂对象上if (field.isAnnotationPresent(DictField.class)) {// 分析注解并转换数据字典值parseAnnotationFunc.run();}// 没有注解的属性判断else {try {// 获取字段值且非空处理field.setAccessible(true);Optional.ofNullable(field.get(obj)).ifPresent(fieldValue - {// 集合类型如果泛型的类型是JavaBean继续递归处理if (Collection.class.isAssignableFrom(cls)) {// 如果是list-map结果则这里返回nullClass generic getGeneric(obj.getClass(), field.getName());if (null ! generic notInFilterClass(generic)) {// 循环递归处理((Collection) fieldValue).forEach(recursiveFunc::accept);}}// 非基本数据类型else if (notInFilterClass(cls)) {recursiveFunc.accept(fieldValue);}});} catch (Exception e) {log.error(e.getMessage(), e);}}}/*** 获取一个类的属性的泛型如果没有泛型则返回null* P.s 如果有多个取第一个如果有多层泛型也返回null比如ListMap** param cls :* param property : 属性名* author liam*/public static Class getGeneric(Class cls, String property) {try {Type genericType cls.getDeclaredField(property).getGenericType();// 如果是泛型参数的类型if (null ! genericType genericType instanceof ParameterizedType) {ParameterizedType pt (ParameterizedType) genericType;Type type pt.getActualTypeArguments()[GitEggConstant.Number.ZERO];// 这里type也可能是 ParameterizedType 直接不考虑if (type instanceof Class) {return (Class) type;}}} catch (Exception e) {log.error(e.getMessage(), e);}return null;}/*** 解析含有注解DictField并赋值** param obj : 对象* param field : 字段* param dictMap : 数据字典* author liam*/private void parseDictAnnotation(Object obj, Field field, MapString, MapObject, Object dictMap) {// 读取注解信息获取编码类型DictField dictField field.getAnnotation(DictField.class);String fieldName field.getName();// 根据Dict的codeName属性或者字段名称获取字典编码codeString code getFieldValue(obj, dictField, fieldName);if (!Strings.isNullOrEmpty(code)) {String dictType dictField.dictType();String dictCode dictField.dictCode();String dictKey dictType StringPool.COLON dictCode;// 首先判断是否开启多租户String redisDictKey DictConstant.DICT_TENANT_MAP_PREFIX;if (enable) {redisDictKey GitEggAuthUtils.getTenantId() StringPool.COLON dictKey;} else {redisDictKey DictConstant.DICT_MAP_PREFIX dictKey;}MapObject, Object dictKeyValue dictMap.get(redisDictKey);// 首先从dictMap中获取值如果没有再从Redis数据库中获取值if (null dictKeyValue) {// 从Redis数据库获取值MapObject, Object dictCodeMap redisTemplate.opsForHash().entries(redisDictKey);dictMap.put(redisDictKey, dictCodeMap);}if (null ! dictKeyValue.get(code)){try {// 给Field赋值最终的数据字典field.setAccessible(true);field.set(obj, dictKeyValue.get(code));} catch (Exception e) {log.error(e.getMessage(), e);}}}}/*** 根据Dict的codeName属性或者字段名称获取字段值* 注意如果当前字段没有以Name结尾那就取当前字段的值也就是根据当前字段的值转换。** param obj : 对象* param dictField : 字段注解对象* param fieldName : 字段名称* return java.lang.String* author liam*/private String getFieldValue(Object obj, DictField dictField, String fieldName) {String codeName dictField.dictKey();if (Strings.isNullOrEmpty(codeName)) {// 如果当前字段是Name结尾进行截取否则取当前字段名称int endNameIndex fieldName.lastIndexOf(DictConstant.NAME_SUFFIX);if (endNameIndex ! -1) {codeName fieldName.substring(0, endNameIndex);} else {codeName fieldName;}}return getPropertyValue(obj, codeName);}/*** 获取对象里指定属性的值并转化为字符串** param obj : 对象* param propertyName : 对象里面的属性名称* author liam*/private String getPropertyValue(Object obj, String propertyName) {BeanWrapperImpl beanWrapper new BeanWrapperImpl(obj);if (beanWrapper.isReadableProperty(propertyName)) {Object propertyValue beanWrapper.getPropertyValue(propertyName);if (null ! propertyValue) {return propertyValue.toString();}}return ;}/*** 判断不在过滤类(常用基本数据类型)中*/private static boolean notInFilterClass(Class cls) {return !DictConstant.baseTypeList.contains(cls);}/*** 函数式接口类似freemarker中的#nested处理*/FunctionalInterfacepublic interface NestedFunction {/*** 无参无返回值的方法运行*/void run();}}
二、实现自定义工具手动进行数据字典转换
比较灵活选择需要转换的数据即可
三、前端转换数据字典
定义通用接口首先从缓存查缓存没有再查询数据库在登录后重置数据字典。
1、前端新增dictUtils用于dictCode的查询、缓存等操作
import { getAuthCache, setAuthCache } from //utils/auth;
import { DICT_SYSTEM_CACHE_KEY, DICT_BUSSINESS_CACHE_KEY } from //enums/cacheEnum;
import { listDict, batchListDict } from //api/system/base/dict;
import { listDictBusiness, batchListDictBusiness } from //api/system/base/dictBusiness;// System default cache time
export const DICT_CACHE_TIME 60 * 60 * 2 * 1000;// Dict
export interface Dict {// dictCodedictCode: string;// dictListdictList?: [];// filterMapfilterMap?: {};
}// DictMap
export interface DictMap {// dictListdictMap: {};
}export function getDictCacheOnly(dict: Dict) {let dictMap getAuthCache(DICT_SYSTEM_CACHE_KEY) as any;if (!dictMap) {dictMap {};}if (dictMap[dict.dictCode]) {return dictMap[dict.dictCode] as Dict;} else {getDict(dict).then(function (dictReturn) {dictMap[dict.dictCode] dictReturn;// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_SYSTEM_CACHE_KEY, dictMap);});return dict;}
}export function getDictBusinessCacheOnly(dict: Dict) {let dictBusinessMap getAuthCache(DICT_BUSSINESS_CACHE_KEY) as any;if (!dictBusinessMap) {dictBusinessMap {};}if (dictBusinessMap[dict.dictCode]) {return dictBusinessMap[dict.dictCode] as Dict;} else {getDictBusiness(dict).then(function (dictReturn) {dictBusinessMap[dict.dictCode] dictReturn;// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_BUSSINESS_CACHE_KEY, dictBusinessMap);});return dict;}
}export async function getDictCache(dict: Dict) {let dictMap getAuthCache(DICT_SYSTEM_CACHE_KEY) as any;if (!dictMap) {dictMap {};}if (dictMap[dict.dictCode]) {return dictMap[dict.dictCode] as Dict;} else {const dictReturn await getDict(dict);dictMap[dict.dictCode] dictReturn;// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_SYSTEM_CACHE_KEY, dictMap);return dictReturn;}
}export async function getDictBusinessCache(dict: Dict) {let dictBusinessMap getAuthCache(DICT_BUSSINESS_CACHE_KEY) as any;if (!dictBusinessMap) {dictBusinessMap {};}if (dictBusinessMap[dict.dictCode]) {return dictBusinessMap[dict.dictCode] as Dict;} else {const dictReturn await getDictBusiness(dict);dictBusinessMap[dict.dictCode] dictReturn;// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_BUSSINESS_CACHE_KEY, dictBusinessMap);return dictReturn;}
}// 批量初始化系统字典
export async function initDictCache(dictCodeList: string[]) {let dictMap getAuthCache(DICT_SYSTEM_CACHE_KEY) as any;if (!dictMap) {dictMap {};}const dictResultMap await batchListDict(dictCodeList);if (dictResultMap) {dictCodeList.forEach(function (dictCode) {if (dictResultMap[dictCode]) {const dict {} as Dict;dict.dictList dictResultMap[dictCode];dict.filterMap {};dict.dictList.forEach((item) {const itemDict item as any;dict.filterMap[itemDict.dictCode] itemDict.dictName;});dictMap[dictCode] dict;}});// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_SYSTEM_CACHE_KEY, dictMap);}
}// 批量初始化业务字典
export async function initDictBusinessCache(dictCodeList: string[]) {let dictMap getAuthCache(DICT_BUSSINESS_CACHE_KEY) as any;if (!dictMap) {dictMap {};}const dictResultMap await batchListDictBusiness(dictCodeList);if (dictResultMap) {dictCodeList.forEach(function (dictCode) {if (dictResultMap[dictCode]) {const dict {} as Dict;dict.dictList dictResultMap[dictCode];dict.filterMap {};dict.dictList.forEach((item) {const itemDict item as any;dict.filterMap[itemDict.dictCode] itemDict.dictName;});dictMap[dictCode] dict;}});// 数据字典默认缓存2小时重新登陆后失效setAuthCache(DICT_BUSSINESS_CACHE_KEY, dictMap);}
}export async function getDict(dict: Dict) {const dictList await listDict(dict.dictCode);if (dictList dictList.length 0) {dict.dictList dictList;dict.filterMap {};dictList.forEach((item) {dict.filterMap[item.dictCode] item.dictName;});}return dict;
}export async function getDictBusiness(dict: Dict) {const dictBusinessList await listDictBusiness(dict.dictCode);if (dictBusinessList dictBusinessList.length 0) {dict.dictList dictBusinessList;dict.filterMap {};dictBusinessList.forEach((item) {dict.filterMap[item.dictCode] item.dictName;});}return dict;
}
2、登录成功后重新数据字典缓存也就是每次在后台数据字典修改之后前端需要重新登录才能刷新数据字典缓存。 // 重新初始化系统数据字典setAuthCache(DICT_SYSTEM_CACHE_KEY, {});// 重新初始化业务数据字典setAuthCache(DICT_BUSSINESS_CACHE_KEY, {});3、在需要用到数据字典时直接调用即可根据utils的实现首先会从缓存查询如果缓存中没有才会从后台查询。
import { getDictBusinessCache } from //utils/gitegg/dictUtils;
......{label: 状态,field: status,component: ApiRadioGroup,required: true,defaultValue: 2,componentProps: {api: getDictBusinessCache,params: { dictCode: USER_STATUS },resultField: dictList,// use name as labellabelField: dictName,// use id as valuevalueField: dictCode,},},
......数据字典在系统中的使用非常频繁所以在设计和使用时既要保证实时更新获取最新配置又要保证不能引发系统性能问题。在系统设计的时候既要考虑到后台数据字典转换还需要考虑到前端数据字典转换这两种转换方式在使用过程中我们根据具体业务需求和使用条件具体选择其中一种即可。
GitEgg-Cloud是一款基于SpringCloud整合搭建的企业级微服务应用开发框架开源项目地址:
Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg
欢迎感兴趣的小伙伴Star支持一下。