Nest实战 - 组织架构

347 阅读5分钟

前言

本系列主要通过实现一个后台管理系统作为切入点,帮助掘友熟悉nest的开发套路

本系列作为前后端分离项目,主要讲解nest相关知识,前端项目知识一笔带过

完整的示例代码在最后,sql文件在/doc下的nest-study.sql文件中

本章属于实战系列第三章,主要讲解部门模块和组织架构模块相关内容,包括

  • 部门模块CRUD操作
  • 组织架构执行位置移动时,添加事务处理
  • 上移、下移、置顶、置底如何修改数据库数据
  • 子查询构建组织部门树结构

预览

image.png

image.png

image.png

image.png

规则

  • 本系列完全遵循RestFul风格进行开发,即Get Post Put Delete
  • 本系列的调用链 controller --> service --> 三方服务数据库
  • 本系列的三方服务被消费时,统一注入到nestIOC中,统一风格;其他函数的使用直接调用即可

部门模块

表设计

CREATE TABLE `department` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `name` varchar(255) NOT NULL COMMENT '部门名称',
  `status` int NOT NULL DEFAULT '1' COMMENT '状态 0 禁用; 1启用',
  `avatar` varchar(255) DEFAULT NULL COMMENT '部门头像',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `create_user` bigint NOT NULL COMMENT '创建人',
  `update_user` bigint NOT NULL COMMENT '修改人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 COMMENT='部门信息表';

说明

  • 部门模块和员工模块基本一致,大家参考开发就行
  • 本项目部门模块存放在src/department下,这是gitee仓库demo/v5

组织架构

表设计

sql设计

CREATE TABLE `organization` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `name` varchar(255) DEFAULT NULL COMMENT '组织架构名',
  `sort` int DEFAULT NULL COMMENT '组织架构顺序',
  `d_id` bigint DEFAULT NULL COMMENT '部门ID',
  `e_id` bigint DEFAULT NULL COMMENT '员工ID',
  `p_id` bigint DEFAULT NULL COMMENT '父级ID',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `create_user` bigint NOT NULL COMMENT '创建人',
  `update_user` bigint NOT NULL COMMENT '修改人',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8 COMMENT='组织架构表';

说明

  • 组织架构表主要关注三个字段d_id、e_id、p_id,其中d_id、e_id分别对应部门表和员工表,主要用来做作表关联,p_id表示当前组织架构属于哪个部门
  • 剩余字段中规中矩

创建部门

代码

  • 写入 src/organization/organization.controller.ts
     @ApiOperation({
        summary: '创建组织结构',
      })
      @Post()
      create(@Body() organization: Organization) {
        return this.organizationService.create(organization);
      }
    
  • 写入src/organization/organization.service.ts
          /**
           *
           * @param organization
           * @returns 新增组织结构
           */
          async create(organization: Organization) {
            // 设置默认sort 自增
            let { maxSort } = await this.organizationRepository
              .createQueryBuilder('organization')
              .select('MAX(organization.sort)', 'maxSort')
              .getRawOne();
    
            const _organization = classAssign(new Organization(), organization);
            if (!maxSort) {
              maxSort = 1;
            } else {
              maxSort++;
            }
            _organization.sort = maxSort;
            return this.organizationRepository.save(_organization);
          }
        ```
    

说明

  • 由于需要对组织结构进行位置移动操作,需要在新增组织结构的时候添加sort排序字段
  • 如果当前表中没有sort默认填充1,否则的话++即可

返回树结构

代码

  • 写入 src/organization/organization.controller.ts
     @ApiOperation({
        summary: '组织结构 - 🌲结构',
      })
      @Get('tree')
      @isPublic()
      tree() {
        return this.organizationService.tree();
      }
    
      @ApiOperation({ summary: '根据ID查询组织架构' })
      @Get(':id')
      findOne(@Param('id') id: string) {
        return this.organizationService.findById(id);
      }
    
  • 写入 src/organization/organization.service.ts
      /**
       *
       * @returns 树结构
       */
      async tree() {
        return listToTree(
          await this.organizationRepository
            .createQueryBuilder('o1')
            .select()
            .leftJoinAndSelect('organization', 'o2', 'o1.id = o2.p_id')
            .where('o1.d_id IS NOT NULL')
            .orderBy('o1.sort', 'ASC')
            .getMany(),
        );
      }
    
  • 写入 src/common/utils/index.ts
        /**
         *
         * @param data
         * @returns 列表转树结构
         */
        export function listToTree<T extends { id: string; pId: string }>(data: T[]) {
          const obj = {};
          data.forEach((w) => (obj[w.id] = w));
          type TParent = T & { children?: T[] };
          const parentList: TParent[] = [];
          data.forEach((w) => {
            const parent: TParent = obj[w.pId];
            if (parent) {
              parent.children = parent.children || [];
              parent.children.push(w);
            } else {
              parentList.push(w);
            }
          });
          return parentList;
        }
    

说明

  • OrganizationService中,通过子查询的方式获取到组织架构信息
  • orderBy设置asc表示,按照sort字段进行升序进行排序
  • 执行后的sql语句如下 image.png
  • 然后只需要通过listToTree 方法,把数组转化成树结构的数组返回即可

删除部门

代码

  • 写入 src/organization/organization.controller.ts
      @ApiOperation({ summary: '删除部门' })
      @Delete()
      delete(@Query('id') id: string) {
        return this.organizationService.delete(id);
      }
    
  • 写入 src/organization/organization.service.ts
      /**
       *
       * @param id
       * @returns 删除组织架构
       */
      async delete(id: string) {
        // 如果当前组织架构下存在客户或者组织结构,无法删除
        const count = await this.organizationRepository.countBy({
          pId: id,
        });
        if (count > 0) {
          throw new CustomException('此部门组织架构下存在组织或者员工,无法删除');
        }
        return !!(await this.organizationRepository.delete({ id })).affected;
      }
    

说明

  • 如果部门中存在员工信息,无法删除(这个看具体的业务场景)
  • 最后返回删除的状态即可true or false

置顶

代码

  • 写入 src/organization/organization.controller.ts
      @ApiOperation({
        summary: '置顶',
      })
      @Put('toTop')
      toTop(@Body('id') id: string) {
        return this.organizationService.toTop(id);
      }
    
  • 写入 src/organization/organization.service.ts
      /**
       *
       * @param id
       * @returns 置顶
       */
      @Transactional()
      async toTop(id: string) {
        // 查询到需要置顶的信息
        const organization = await this.organizationRepository.findOneBy({ id });
        if (!organization) {
          throw new CustomException('此部门数据不存在');
        }
        // 查询同级上一条数据
        const prev = await this.organizationRepository.findOneBy({
          sort: LessThan(organization.sort),
          pId: organization.pId,
        });
        if (!prev) {
          throw new CustomException('此部门已经处于置顶状态了');
        }
        // 置顶
        await this.organizationRepository
          .createQueryBuilder()
          .update(Organization)
          .set({
            sort: () => 'sort + 1',
          })
          .where('sort < :sort', { sort: organization.sort })
          .execute();
        await this.organizationRepository.update({ id }, { sort: 1 });
        return true;
      }
    

说明

  • 这块逻辑比较复杂
  • 第一步查看当前数据是否存在
  • 然后判断当前数据是否是第一条,已经置顶的数据不需要再次置顶
  • 接下来就是置顶操作 思路如下
  • 首先,需要获取需要置顶的数据POJO,将表中sort字段小于POJO顺序的所有数据的sort增加1,再将POJO的sort设置为1,最后在查询表时,按seq增序排列,就能看到置顶的效果

置底

代码

  • 写入 src/organization/organization.controller.ts
      @ApiOperation({
        summary: '置底',
      })
      @Put('toBottom')
      toBottom(@Body('id') id: string) {
        return this.organizationService.toBottom(id);
      }
    
  • 写入 src/organization/organization.service.ts
      /**
       *
       * @param id
       * @returns 置底
       */
      @Transactional()
      async toBottom(id: string) {
        // 查询需要置底的数据
        const organization = await this.organizationRepository.findOneBy({ id });
        // 查询同级下一条数据
        const last = await this.organizationRepository.findOne({
          where: {
            sort: MoreThan(organization.sort),
            pId: organization.pId,
          },
        });
        if (!last) {
          throw new CustomException('此部门已经处于最后了,无法下移');
        }
    
        // 获取最大sort
        const maxOrganization = await this.organizationRepository.findOne({
          where: {},
          order: {
            sort: 'DESC',
          },
        });
        if (!maxOrganization) {
          throw new CustomException('xxx');
        }
        // 置底
        await this.organizationRepository
          .createQueryBuilder()
          .update(Organization)
          .set({ sort: () => 'sort - 1' })
          .where('sort > :sort', { sort: organization.sort })
          .execute();
        await this.organizationRepository.update(
          {
            id: organization.id,
          },
          {
            sort: maxOrganization.sort,
          },
        );
        return true;
      }
    

说明

  • 整体思路大体和置顶功能一致
  • 唯一有区别的就是置底功能的实现,思路如下
  • 首先,拿到数据库中sort最大的数据maxOrganization,执行update操作,将大于当前数据的数据库中sort字段进行-1操作,最后将maxOrganizationsort值赋值给当前数据,最后更新数据库即可

上移

代码

  • 写入 src/organization/organization.controller.ts
      @ApiOperation({
        summary: '上移',
      })
      @Put('moveUp')
      moveUp(@Body('id') id: string) {
        return this.organizationService.moveUp(id);
      }
    
  • 写入 src/organization/organization.service.ts
      /**
       *
       * @param id
       * @returns 上移
       */
      @Transactional()
      async moveUp(id: string) {
        const organization = await this.organizationRepository.findOneBy({ id });
        if (!organization) {
          throw new CustomException('此部门id不存在');
        }
        // 拿到同级上一条数据
        const prev = await this.organizationRepository.findOne({
          where: {
            sort: LessThan(organization.sort),
            pId: organization.pId,
          },
          order: {
            id: 'DESC',
          },
        });
        if (!prev) {
          throw new CustomException('此部门已经置顶了,无法上移');
        }
        // 交换 sort
        await this.organizationRepository.update(
          { id: organization.id },
          { sort: prev.sort },
        );
        await this.organizationRepository.update(
          { id: prev.id },
          { sort: organization.sort },
        );
        return true;
      }
    

下移

代码

  • 写入 src/organization/organization.controller.ts

      @ApiOperation({
        summary: '下移',
      })
      @Put('moveDown')
      movDown(@Body('id') id: string) {
        return this.organizationService.moveDown(id);
      }
    
  • 写入 src/organization/organization.service.ts

      /**
       *
       * @param id
       * @returns 下移
       */
      @Transactional()
      async moveDown(id: string) {
        const organization = await this.organizationRepository.findOneBy({ id });
        if (!organization) {
          throw new CustomException('此部门id不存在');
        }
        // 拿到同级下一条数据
        const next = await this.organizationRepository.findOne({
          where: {
            sort: MoreThan(organization.sort),
            pId: organization.pId,
          },
        });
        if (!next) {
          throw new CustomException('此部门已经处于最后了,无法下移');
        }
        // 交换 sort
        await this.organizationRepository.update(
          {
            id: organization.id,
          },
          {
            sort: next.sort,
          },
        );
        await this.organizationRepository.update(
          {
            id: next.id,
          },
          {
            sort: organization.sort,
          },
        );
        return true;
      }
    

说明

  • 下移就比较简单了,拿到下一条数据,交换两条sort的值即可

写在最后