打造一款适合自己的快速开发框架-字典模块设计与实现

5,018 阅读9分钟

前言

一般来说每一个系统都会设计有字典模块,而常见的表现形式就是业务系统客户端与用户交互时候使用的下拉框组件,其数据源一般就来自字典表。本文的字典模块也设计有字典表,不过还会新增另一种特殊的字典类型,该数据会来源于自定义的枚举类。

字典分类与管理方式

在这里,我把字典分为两大分类,可编辑的和不可编辑的。其中可编辑的就是那些不是很确定,后期还可能会有改动的。不可编辑的就是与业务代码紧密耦合的,如果随意修改的话,可能会对系统有影响。对不同的字典分类,采用的管理方式会不一样。

  • 可编辑的字典类型

该类型和传统字典模块一样,直接存储在数据库上的,后台可对其进行增删改查。

  • 不可编辑的字典类型

该类型可能在前面的文章中也简单提过了,就是数据库字段中使用特殊的注释,然后生成代码的时候顺便生成字典枚举类。项目启动的时候,会使用自定义扫描类收集其元数据,然后提供查询展示。

字典模块设计

  • 可编辑的字典类型

这个就直接贴数据库表设计吧,一共两张表,字典表(sys_dict)与字典项表(sys_dict_item)。

CREATE TABLE `sys_dict` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) NOT NULL COMMENT '名称',
  `dict_key` varchar(64) NOT NULL COMMENT '唯一编码',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime(3) NOT NULL COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='字典';
CREATE TABLE `sys_dict_item` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `dict_id` bigint(20) unsigned NOT NULL COMMENT '字典id',
  `name` varchar(32) NOT NULL COMMENT '名称',
  `dict_item_value` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '值',
  `sort` double(10,2) unsigned DEFAULT '10.00' COMMENT '排序',
  `remark` varchar(255) DEFAULT NULL,
  `create_time` datetime(3) NOT NULL COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='字典项';
  • 不可编辑的字典类型

先贴一个样例:

表结构

CREATE TABLE `sys_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) CHARACTER SET utf8mb4 NOT NULL COMMENT '角色名称',
  `role_key` varchar(32) DEFAULT NULL COMMENT '角色标识(唯一)',
  `role_type` int(6) DEFAULT '10' COMMENT '角色类型(10->管理员|ADMIN,20->流程审核员|WORKFLOW)',
  `is_enabled` tinyint(1) DEFAULT '2' COMMENT '是否启用(1->禁用|NO,2->启用|YES)',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime(3) NOT NULL COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '是否删除(1->未删除|YES,2->已删除|NO)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色';

实体类

package com.mldong.modules.sys.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Id;
import javax.persistence.Table;

import tk.mybatis.mapper.annotation.LogicDelete;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.mldong.common.annotation.DictEnum;
import com.mldong.common.base.CodedEnum;
import com.mldong.common.base.YesNoEnum;
/**
 * <p>实体类</p>
 * <p>Table: sys_role - 角色</p>
 * @since 2020-06-08 10:26:59
 */
@Table(name="sys_role")
@ApiModel(description="角色")
public class SysRole implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = -1L;
	@Id
	@ApiModelProperty(value="主键")
    private Long id;
    @ApiModelProperty(value = "角色名称")
    private String name;
    @ApiModelProperty(value = "角色标识(唯一)")
    private String roleKey;
    @ApiModelProperty(value = "角色类型(10->管理员|ADMIN,20->流程审核员|WORKFLOW)")
    private RoleTypeEnum roleType;
    @ApiModelProperty(value = "是否启用(1->禁用|NO,2->启用|YES)")
    private YesNoEnum isEnabled;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "创建时间")
    private Date createTime;
    @ApiModelProperty(value = "更新时间")
    private Date updateTime;
    @ApiModelProperty(value = "是否删除(1->未删除|YES,2->已删除|NO)")
	@LogicDelete(isDeletedValue=YesNoEnum.Y,notDeletedValue=YesNoEnum.N)
    private YesNoEnum isDeleted;
	// 省略 get/set
    @DictEnum(key="sys_role_role_type",name="角色类型")
    public enum RoleTypeEnum implements CodedEnum {
		/**
		 * 管理员
		 */
		ADMIN(10, "管理员"),
		/**
		 * 流程审核员
		 */
		WORKFLOW(20, "流程审核员");
		private int value;
		private String name;
		@JsonCreator // 这个是为了json反序列化用的,可以value->enum
	    public static RoleTypeEnum forValue(int value) {
	        return CodedEnum.codeOf(RoleTypeEnum.class, value).get();

	    }
		RoleTypeEnum(int value, String name) {
			this.value = value;
			this.name = name;
		}
		@JsonValue // 这个是为了json序列化用的,可以enum->value
		public int getValue() {
			return value;
		}
		public String getName() {
			return name;
		}
    }

上面的role_type/is_enabled/is_deleted都是特殊的字段类型,其注释如role_type为例:

角色类型(10->管理员|ADMIN,20->流程审核员|WORKFLOW)

然后对应的枚举项如下:

ADMIN(10, "管理员"),
WORKFLOW(20, "流程审核员");

对应的枚举类RoleTypeEnum.java

加上自定义注解

@DictEnum(key="sys_role_role_type",name="角色类型")

sys_role_role_type===>表名_字段名

拼凑起来完整的元数据如下:

{
    "name": "角色类型",
    "dictKey": "sys_role_role_type",
    "items": [
        {
            "name": "管理员",
            "dictItemValue": 10
        },
        {
            "name": "流程审核员",
            "dictItemValue": 20
        }
    ]
}

注:我们定义的mldong-mapper层是不可手工修改层,所以每次数据库变动后,都可重新生成代码覆盖。

开始编码

可编辑的字典类型,相关表设计好后,就可以生成CURD代码了,在这里就不细说了,下面主要说一下自定义字典枚举类的收集。

目录结构

├── mldong-admin  管理端接口
	├── src/main/java
		├──	com.mldong.modules.sys
			├── controller
				└── SysDictController.java
			├── dto
				└── SysDictKeyParam.java
            ├── service
            	├── impl
            		└──	SysDictServiceImpl.java
				└── SysDictService.java
├── mldong-common  工具类及通用代码
	├── src/main/java
		├──	com.mldong.common
			├── scanner
				├──	model
					├── DictItemModel.java
					└── DictModel.java
				└──	DictScanner.java
├── mldong-generator  代码生成器

核心文件说明:

  • mldong-common/src/main/java/com/mldong/common/scanner/model/DictModel.java

字典实体

package com.mldong.common.scanner.model;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.List;

public class DictModel implements Serializable{
	private static final long serialVersionUID = -832930180178912158L;
	@ApiModelProperty("字典名称")
	private String name;
	@ApiModelProperty("字典唯一编码")
	private String dictKey;
	@ApiModelProperty("字典项集合")
	private List<DictItemModel> items;
	// 省略get/set
}
  • mldong-common/src/main/java/com/mldong/common/scanner/model/DictModel.java

字典项实体

package com.mldong.common.scanner.model;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;

public class DictItemModel implements Serializable {
	private static final long serialVersionUID = -3597276140499576903L;
	@ApiModelProperty("字典项名称")
	private String name;
	@ApiModelProperty("字典项值")
	private int dictItemValue;
    // 省略get/set
}
  • mldong-common/src/main/java/com/mldong/common/scanner/model/DictScanner.java

扫描自定义字典枚举处理类

package com.mldong.common.scanner;

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

import javax.annotation.PostConstruct;

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 org.springframework.util.SystemPropertyUtils;

import com.mldong.common.annotation.DictEnum;
import com.mldong.common.base.CodedEnum;
import com.mldong.common.scanner.model.DictItemModel;
import com.mldong.common.scanner.model.DictModel;
/**
 * 扫描自定义字典枚举处理类
 * @author mldong
 *
 */
@Component
public class DictScanner implements ResourceLoaderAware{
	private ResourceLoader resourceLoader;

	private ResourcePatternResolver resolver = ResourcePatternUtils
			.getResourcePatternResolver(resourceLoader);
	private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(
			resourceLoader);
	/**
	 * 字典列表,供外部页面查看使用
	 */
	private List<DictModel> dictList = new ArrayList<DictModel>();
	/**
	 * 字典hash,方便 getDictKey获取
	 */
	private Map<String,DictModel> dictMap = new HashMap<>();
	private static final String FULLTEXT_SACN_PACKAGE_PATH = "com.mldong";
	@PostConstruct
	private void init() {
		doScan();
	}
	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}
	private void doScan() {
		String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
				.concat(ClassUtils
						.convertClassNameToResourcePath(
								SystemPropertyUtils
										.resolvePlaceholders(FULLTEXT_SACN_PACKAGE_PATH))
						.concat("/**/*.class"));
		try {
			Resource[] resources = resolver.getResources(packageSearchPath);
			MetadataReader metadataReader = null;
			for (Resource resource : resources) {
				if (resource.isReadable()) {
					metadataReader = metadataReaderFactory
							.getMetadataReader(resource);
					if (metadataReader.getClassMetadata().isFinal()) {// 当类型不是抽象类或接口在添加到集合
                    	Class<?> clazz = Class.forName(metadataReader.getClassMetadata().getClassName());
                    	DictEnum enumCode = clazz.getAnnotation(DictEnum.class);
                    	if(null != enumCode) {
                    		DictModel model = handleDictEnum(clazz, enumCode);
                    		dictList.add(model);
                    		dictMap.put(model.getDictKey(), model);
                    	}
                    }
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("字典扫描失败");
		}
	}
	/**
	 * 处理字典枚举类
	 * @param clazz
	 * @param enumCode
	 * @return
	 */
	private DictModel handleDictEnum(Class<?> clazz, DictEnum enumCode) {
		DictModel model = new DictModel();
		model.setName(enumCode.name());
		model.setDictKey(enumCode.key());
		List<DictItemModel> items = new ArrayList<>();
		model.setItems(items);
		Arrays.stream(clazz.getEnumConstants()).forEach(item->{
			DictItemModel dictItem = new DictItemModel();
			CodedEnum codedEnum = (CodedEnum)item;
			dictItem.setName(codedEnum.getName());
			dictItem.setDictItemValue(codedEnum.getValue());
			items.add(dictItem);
		});
		return model;
	}
	public List<DictModel> getDictList() {
		return dictList;
	}
	public Map<String, DictModel> getDictMap() {
		return dictMap;
	}
}
  • mldong-admin/src/main/java/com/mldong/modules/sys/service/SysDictService.java

新增两个对外查询的方法-接口(代码片段)

	/**
	 * 通过字典唯一编码查询
	 * @param param
	 * @return
	 */
	public DictModel getByDictKey(SysDictKeyParam param);
	/**
	 * 获取所有字典枚举
	 * @return
	 */
	public List<DictModel> listAllEnum();
  • mldong-admin/src/main/java/com/mldong/modules/sys/service/SysDictService.java

新增两个对外查询的方法-实现(代码片段)

	@Override
	public DictModel getByDictKey(SysDictKeyParam param) {
		if("enum".equals(param.getType())) {
			return dictScanner.getDictMap().get(param.getDictKey());
		}
		SysDict q = new SysDict();
		q.setDictKey(param.getDictKey());
		SysDict dict = sysDictMapper.selectOne(q);
		if(null == dict) {
			return null;
		}
		Condition condition = new Condition(SysDictItem.class);
		condition.orderBy("sort").asc();
		condition.createCriteria().andEqualTo("dictId",dict.getId());
		List<SysDictItem> list = sysDictItemMapper.selectByCondition(condition);
		if(list.isEmpty()) {
			return null;
		}
		DictModel dictModel =  new DictModel();
		dictModel.setDictKey(dict.getDictKey());
		dictModel.setName(dict.getName());
		BeanUtils.copyProperties(dict, dictModel);
		List<DictItemModel> items = new ArrayList<DictItemModel>();
		dictModel.setItems(items);
		list.forEach(dictItem->{
			DictItemModel item = new DictItemModel();
			item.setName(dictItem.getName());
			item.setDictItemValue(dictItem.getDictItemValue());
		});
		return dictModel;
	}
	@Override
	public List<DictModel> listAllEnum() {
		return dictScanner.getDictList();
	}
  • mldong-admin/src/main/java/com/mldong/modules/sys/dto/SysDictKeyParam.java

通过字典唯一编码查询参数实体类

public class SysDictKeyParam {
	@ApiModelProperty(value="字典唯一编码(表名_字段名)",required=true)
	@NotBlank(message="字典唯一编码不能为空")
	private String dictKey;
	@ApiModelProperty(value="类型不能为空",required=true)
	@FlagValidator(values="enum,db",message="类型只能是enum或db",required=true)
	private String type;
	// 省略 get/set
  • mldong-admin/src/main/java/com/mldong/modules/sys/controller/SysDictController.java

新增两对外接口(代码片段)

@PostMapping("getByDictKey")
	@ApiOperation(value="通过字典唯一编码查询", notes="通过字典唯一编码查询",authorizations={
		@Authorization(value="通过字典唯一编码查询",scopes={
	    	@AuthorizationScope(description="通过字典唯一编码查询",scope="sys:dict:getByDictKey")
	    })
	})
	public CommonResult<DictModel> getByDictKey(@RequestBody @Validated SysDictKeyParam param) {
		return CommonResult.success("通过字典唯一编码查询成功",sysDictService.getByDictKey(param));
	}
	@PostMapping("listAllEnum")
	@ApiOperation(value="获取所有字典枚举", notes="获取所有字典枚举",authorizations={
		@Authorization(value="获取所有字典枚举",scopes={
	    	@AuthorizationScope(description="获取所有字典枚举",scope="sys:dict:listAllEnum")
	    })
	})
	public CommonResult<List<DictModel>> listAllEnum() {
		return CommonResult.success("获取所有字典枚举成功",sysDictService.listAllEnum());
	}
}

运行效果

  • 入参
{
	"dictKey": "sys_role_role_type",
	"type": "enum"
}
  • 出参
{
  "code": 0,
  "msg": "通过字典唯一编码查询成功",
  "data": {
    "name": "角色类型",
    "dictKey": "sys_role_role_type",
    "items": [
      {
        "name": "管理员",
        "dictItemValue": 10
      },
      {
        "name": "流程审核员",
        "dictItemValue": 20
      }
    ]
  }
}
  • 获取所有字典枚举
{
  "code": 0,
  "msg": "获取所有字典枚举成功",
  "data": [
    {
      "name": "是否",
      "dictKey": "yes_no",
      "items": [
        {
          "name": "是",
          "dictItemValue": 1
        },
        {
          "name": "否",
          "dictItemValue": 2
        }
      ]
    },
    {
      "name": "角色类型",
      "dictKey": "sys_role_role_type",
      "items": [
        {
          "name": "管理员",
          "dictItemValue": 10
        },
        {
          "name": "流程审核员",
          "dictItemValue": 20
        }
      ]
    },
    {
      "name": "性别",
      "dictKey": "sys_user_sex",
      "items": [
        {
          "name": "男",
          "dictItemValue": 1
        },
        {
          "name": "女",
          "dictItemValue": 2
        },
        {
          "name": "未知",
          "dictItemValue": 3
        }
      ]
    }
  ]
}

小结

本文根据两种不同字典类型,采用了两种管理字典的方式,实现了对字典模块数据的管理。在数据库管理这个方式中暂时没有考虑完善的缓存处理方案,等到后续的缓存篇时候再展开。

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-后端脚手架搭建

打造一款适合自己的快速开发框架-集成mapper

打造一款适合自己的快速开发框架-集成swaggerui和knife4j

打造一款适合自己的快速开发框架-通用类封装之统一结果返回、统一异常处理

打造一款适合自己的快速开发框架-业务错误码规范及实践

打造一款适合自己的快速开发框架-框架分层及CURD样例

打造一款适合自己的快速开发框架-mapper逻辑删除及枚举类型规范

打造一款适合自己的快速开发框架-数据校验之Hibernate Validator

打造一款适合自己的快速开发框架-代码生成器原理及实现

打造一款适合自己的快速开发框架-通用查询设计与实现

打造一款适合自己的快速开发框架-基于rbac的权限管理

打造一款适合自己的快速开发框架-登录与权限拦截

打造一款适合自己的快速开发框架-http请求日志全局处理