Java练习-设计并实现一个字典功能模块

前言

很多Java开发的系统中,可能都需要做一些配置呀,字典之类的设计。
这里我提供一个思路,以及针对这个思路做了具体的实现,以供大家参考学习。

代码仓库:https://gitee.com/fengsoshuai/custom-dict

采用java8,springboot2.7.7,mysql 进行实现。

正文

因为代码文件较多,就不粘贴全部了,这里粘贴部分重要文件。
基本的查询(树形结构),插入,导入,导出等也都是实现了。

一、maven依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.7</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.feng</groupId>
            <artifactId>custom-dict-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.8.19</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.11</version>
        </dependency>

    </dependencies>

二、SQL脚本

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80100
 Source Host           : localhost:3306
 Source Schema         : test_custom_dict

 Target Server Type    : MySQL
 Target Server Version : 80100
 File Encoding         : 65001

 Date: 06/12/2023 11:15:58
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for custom_dict
-- ----------------------------
DROP TABLE IF EXISTS `custom_dict`;
CREATE TABLE `custom_dict`
(
    `id`              bigint                                                        NOT NULL AUTO_INCREMENT COMMENT 'id',
    `parent_id`       bigint                                                        NOT NULL DEFAULT '0' COMMENT '父id',
    `name`            varchar(100)                                                  NOT NULL DEFAULT '' COMMENT '含义',
    `dict_val`        varchar(100)                                                  NOT NULL DEFAULT '' COMMENT '字典val',
    `dict_key`        varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '字典key',
    `weight`          int                                                           NOT NULL DEFAULT '0' COMMENT '权重',
    `status`          tinyint                                                       NOT NULL DEFAULT '1' COMMENT '状态(1 启用;0禁用)',
    `create_user`     varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci  NOT NULL DEFAULT '' COMMENT '创建用户',
    `create_username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci  NOT NULL DEFAULT '' COMMENT '创建用户名',
    `create_time`     datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`     datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 4
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_0900_ai_ci;

ALTER TABLE `custom_dict`
    ADD UNIQUE INDEX `uk_dict_key` (`dict_key`) USING HASH COMMENT '唯一索引,dict_key';

SET FOREIGN_KEY_CHECKS = 1;

三、树形结构查寻

这里的功能,借助hutool里的TreeUtil工具类。
具体使用情况如下。

3.1 调用时

根据传入的参数,决定最终的查询结果。
查到结果后,使用DictUtil.convertToTree转换树形结构。

    /**
     * 依据ID查字典,并转换数据为树形
     *
     * @param id id为负数,查所有数据;否则查对应ID、对应父ID
     * @return 树形字典数据
     */
    public List<Tree<Long>> listDictTree(Long id) {
        CustomDict dict = new CustomDict();
        if (id >= 0) {
            dict.setId(id);
        }
        // 查库
        List<CustomDict> customDicts = dictClient.list(dict);
        if (CollUtil.isEmpty(customDicts)) {
            return Collections.emptyList();
        }

        long minId = customDicts.stream().mapToLong(CustomDict::getParentId).min().orElse(0L);
        // 转换为树形
        return DictUtil.convertToTree(customDicts, minId, null);
    }

3.2 DictUtil.convertToTree(…)

入参中的biConsumer,作为扩展字段的处理逻辑。可以直接传空。当需要额外增加自己需要的其他字段时,可以使用它,而不用去修改现有代码。

public static List<Tree<Long>> convertToTree(List<CustomDict> customDictList, Long rootId, BiConsumer<Map<String, Object>, CustomDict> biConsumer) {
        if (CollUtil.isEmpty(customDictList)) {
            return Collections.emptyList();
        }

        List<TreeNode<Long>> treeNodes = new ArrayList<>();
        for (CustomDict customDict : customDictList) {
            TreeNode<Long> treeNode = new TreeNode<>();
            treeNode.setId(customDict.getId());
            treeNode.setName(customDict.getName());
            treeNode.setParentId(customDict.getParentId());
            treeNode.setWeight(customDict.getWeight());
            Map<String, Object> extra = new LinkedHashMap<>();
            extra.put("dictKey", customDict.getDictKey());
            extra.put("dictVal", customDict.getDictVal());
            extra.put("status", customDict.getStatus().getValue());
            extra.put("createUser", customDict.getCreateUser());
            extra.put("createUsername", customDict.getCreateUsername());
            extra.put("createTime", customDict.getCreateTime());
            extra.put("updateTime", customDict.getUpdateTime());
            if (customDict.hasSystemOperation()) {
                extra.put("systemCode", customDict.getSystemCode());
                extra.put("subSystemCode", customDict.getSubSystemCode());
            }
            if (biConsumer != null) {
                biConsumer.accept(extra, customDict);
            }
            treeNode.setExtra(extra);
            treeNodes.add(treeNode);
        }

        return TreeUtil.build(treeNodes, rootId);
    }

四、导出和导入

实现了导入和导出功能,以及必要的枚举转换器,时间转换器。
使用的技术点:EasyExcel

想看全部代码的,可以从代码仓库拉取。

/**
     * 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
     *
     * @since 2.1.1
     */
    @GetMapping("/download")
    public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {

        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
            String fileName = URLEncoder.encode("字典导出", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + System.currentTimeMillis() + ".xlsx");
            // 这里需要设置不关闭流
            EasyExcel.write(response.getOutputStream(), CustomDictExcelVo.class).autoCloseStream(Boolean.FALSE).sheet("字典")
                    .doWrite(customDictManager.customDictExcelVos());
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, String> map = new HashMap<>(4);
            map.put("status", "failure");
            map.put("message", "下载文件失败" + e.getMessage());
            response.getWriter().println(map);
        }
    }

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), CustomDictExcelVo.class, new CustomDictExcelListener(customDictManager))
                .sheet()
                .doRead();

        return "success";
    }