目 录CONTENT

文章目录

通用权限控制系统设计与实现

Administrator
2023-05-29 / 0 评论 / 0 点赞 / 113 阅读 / 114402 字 / 正在检测是否收录...

通用权限控制系统

一、功能图

image-20230524140746601

按照正常的情况下来设计,应该是5张表,分别是用户表,角色表,用户角色表,权限表,角色权限表,用户授予角色,角色再包含权限,这里我们简化一下,直接由用户到权限,然后再根据用户权限表来完成操作

同时这里的权限,我们直接使用以下的等级与分类

  1. token权限,用户登录与未登录的区别
  2. 页面权限,登录以后的用户还要细分是否可以进入某一个页面,也就是根据权限来生成动态路由
  3. API请求权限,这一个权限其实与前台的权限基本保持一致,但仍然细分
    • 无需权限:即这个API接口不需要任何权限就可以直接 使用,如登录,注册,上传头像等(一般称之为游客权限)
    • 泛权限:这一个权限即代表用户登录以后具备一定的身份,这个身份可以访问一些需要经过token认证的API
    • 具体权限:这一个权限是强验证权限,即只有授权了这个权限的用户才可以访问这个API接口

二、数据库设计

  1. 用户表

    列名 默认值 是否为空 类型 索引 其他信息 注释
    id NO int PRI auto_increment
    account NO varchar(50) 账号
    password NO varchar(50) 密码
    avatar varchar(255) 头像
    phone_number varchar(30) 手机号
    status 0 NO int 0代表正常,1代表禁用
    login_time datetime 最后一次登录时间
    remark text 备注说明
    is_del 0 NO tinyint 软删除
    create_time datetime 创建时间
    update_time datetime 修改时间
  2. 权限表

    列名 默认值 是否为空 类型 索引 其他信息 注释
    id NO int PRI auto_increment 主键
    permission_name NO varchar(50) 权限名称
    icon varchar(50) 图标
    parent_id NO int 上级菜单
    order_num 0 NO int 排序
    font_path varchar(255) 前端路径
    component varchar(255) 组件名称
    permission_type NO int 权限类型 0代表目录 1代表页面 2代表按钮
    permission_key NO varchar(255) 权限标识,不能重复
    create_time datetime 创建时间
    update_time datetime 修改时间
    remark 备注
    is_del 0 NO tinyint 软删除
  3. 用户权限表

    列名 默认值 是否为空 类型 索引 其他信息 注释
    id NO int PRI auto_increment 主键
    user_id NO int 用户id
    permission_id NO int 权限id
    create_time datetime 创建时间
    update_time datetime 修改时间
    is_del NO int 软删除

三、安装项目所需要的依赖包

本次项目使用express+typeorm的方式来创建,使用typescript语法来完成

  1. 先准备一个文件夹,取名为premission_express_20230523

  2. 打开控制台,执行npm init --yes快速进行初始化

  3. 安装项目所需要使用的依赖,本次项目的依赖主要有以下几个

    • nodemon用于项目热启动的
    • ts-node使用node编译typescript的
    • typescript 这是TS的编译核心包
    • express这是express项目的核心包
    • body-parser进行post数据提交的时候接收参数的
    • mysql2这是连接mysql数据库需要的
    • typeorm操作数据库的ORM框架
  4. 创建tsconfig.json的配置文件

    {
        "compilerOptions": {
            "emitDecoratorMetadata": true,
            "experimentalDecorators": true,
            "target": "ES2015",
            "module": "CommonJS",
            "outDir": "./dist",
            "lib": [
                "DOM",
                "ES2015",
                "ESNext"
            ],
        },
        "include": [
            "./src/**/*"
        ],
        "exclude": [
            "node_modules"
        ]
    }
    

    tsconfig.json 文件是 TypeScript 项目的配置文件,它包含了编译器的配置选项。常用的配置属性如下:

    • compilerOptions: 编译器的选项,如语言版本、目标 JavaScript 版本、生成的 sourcemap 等。
    • include: 指定需要编译的文件路径或文件夹路径。
    • exclude: 指定不需要编译的文件路径或文件夹路径。
    • files: 指定需要编译的文件列表。
    • extends: 指定继承自另一个 tsconfig.json 文件。
    • compileOnSave : 指定是否在保存时编译文件。
    • buildOnSave: 指定是否在保存时编译文件。
    • target:编译目标 JavaScript 版本,可以是 “ES3”,“ES5” 或 “ES2015” 等。
    • module:指定模块系统,可以是 “CommonJS”,“AMD” 或 “System” 等。
    • sourceMap:是否生成 sourcemap 文件。
    • outDir:编译输出目录。
    • rootDir:设置项目的根目录。
    • strict:是否开启严格类型检查。
    • noImplicitAny:是否禁止隐式 any 类型。
    • lib:指定要包含在编译中的库文件,如 “es2015”。
    • paths: 指定模块路径别名。
    • baseUrl: 指定基础目录。
    • jsx: 指定 JSX 的处理方式。
    • allowJs: 是否允许编译 JavaScript 文件。
    • checkJs: 是否检查 JavaScript 文件。
    • declaration: 是否生成声明文件。
    • declarationMap: 是否生成声明文件的 sourcemap。
    • emitDecoratorMetadata: 是否支持装饰器。
    • experimentalDecorators: 是否支持实验性装饰器。
    • listEmittedFiles: 是否列出所有输出的文件。
    • listFiles: 是否列出所有编译过的文件。
    • locale: 指定本地化语言。
    • mapRoot: 指定 sourcemap 文件的根目录。
    • moduleResolution: 指定模块解析策略。
    • noEmit: 是否禁止输出 JavaScript 代码。
    • noEmitHelpers: 是否禁止输出辅助函数。
    • noEmitOnError: 是否在发生错误时禁止输出 JavaScript 代码。
    • noImplicitReturns: 是否禁止隐式返回。
    • noUnusedLocals: 是否检查未使用的局部变量。
    • noUnusedParameters: 是否检查未使用的参数。
    • preserveConstEnums: 是否保留 const 枚举。
    • pretty: 是否格式化输出的 JavaScript 代码。
    • removeComments: 是否移除注释。
    • skipLibCheck: 是否跳过检查库文件。
    • sourceRoot: 指定源文件的根目录。
    • suppressExcessPropertyErrors: 是否禁止过多属性错误。
    • suppressImplicitAnyIndexErrors: 是否禁止隐式 any 类型索引错误。
    • typeRoots: 指定类型声明文件的根目录。
    • types: 指定需要包含在编译中的类型声明文件。
    • watch: 是否监视文件变化并重新编译。

这里要说明一点,因为我们使用的是typescript在进行开发,所以我们在写代码的时候是需要类型支持的,这个时候我们可以安装各个包的类型包

"devDependencies": {
    "@types/express": "^4.17.14",
    "@types/jsonwebtoken": "^8.5.9",
    "@types/multer": "^1.4.7",
    "@types/node": "^18.11.11",
    "nodemon": "^2.0.20",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.3"
  },
  "dependencies": {
    "@types/body-parser": "^1.19.2",
    "body-parser": "^1.20.1",
    "class-validator": "^0.13.2",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "express-async-errors": "^3.1.1",
    "jsonwebtoken": "^8.5.1",
    "md5": "^2.3.0",
    "multer": "^1.4.5-lts.1",
    "mysql2": "^2.3.3",
    "reflect-metadata": "^0.1.13",
    "typeorm": "^0.3.11"
  }

在上面的依赖中,那个@type开始的就是类型包

四、创建数据库连接的配置文件

本次项目使用的是typeorm来完成,所以需要按typeorm进行数据库连接的配置

在src的目录下面创建config文件夹,再创建DBConfig.ts的文件

/**
 * 数据库的配置文件
 */

export default {
    name: "default",
    type: "mysql",
    host: "127.0.0.1",
    port: 3306,
    database: "scenic_spot",
    username: "dev",
    password: "123456",
    multipleStatements: true,
    synchronize: false,
    entities: [
    ]
}

五、创建app.ts项目启动文件

import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";

const app: Express = express();
app.use(bodyParser.urlencoded({extended:false,limit:"30m"}));
app.use(bodyParser.json({limit:'30m'}));


app.listen(9999,"[::]",()=>{
    console.log("服务器启动成功");
})

六、编写package.json的启动命令

package.jsonscript选项下面,创建下面的启动命令

"main": "app.ts",
"scripts": {
    "dev": "nodemon --watch ./src",
    "build": "tsc -p ./",
    "start": "node dist/app.js"
  },
  1. dev代表开发模式
  2. build代表生产模式
  3. start是启动生产模式下面的代码

同时这里要注意,要将main选项下面的index.js换成src/app.ts,这代表项目的默认启动文件

注:如果项目没有安装nodemon的,通过下面的命令安装

$ npm install nodemon --save-dev

当编写完启动命令以后,我们的项目就可以启动了,如下所示

image-20230524145603210

七、编写entity文件

项目使用了typeorm的框架,所以这里需要编译数据表的entity文件,同时使用class-validator进行最终的数据校验

$ npm install class-validator

UserInfo.ts

/**
 * 数据表user_info
 */

import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from "typeorm";
import {IsNotEmpty} from "class-validator";
import {UserPermissionInfo} from "./UserPermissionInfo";

@Entity({
    name:"user_info"
})
export class UserInfo{
    @PrimaryGeneratedColumn()
    id:number;

    @IsNotEmpty()
    @Column({
        nullable:false
    })
    account:string;

    @IsNotEmpty()
    @Column({
        select:false,
        nullable:false
    })
    password:string;

    @Column()
    avatar:string;

    @IsNotEmpty()
    @Column()
    phone_number:string;

    @IsNotEmpty()
    @Column({
        type:"int"
    })
    status:number;


    @Column({
        type:"datetime"
    })
    login_time:Date;

    @Column()
    remark:string;

    @Column({
        type:"datetime"
    })
    create_time:Date;

    @Column({
        type:"datetime"
    })
    update_time:Date;

    @Column({
        default:false,
        select:false
    })
    is_del:boolean;

    @OneToMany(()=>UserPermissionInfo,userPermissionInfo=>userPermissionInfo.userInfo)
    userPermissionInfoList:UserPermissionInfo[];
}

PermissionInfo.ts

/**
 * 数据表permission_info
 */
import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from "typeorm";
import {IsInt, IsNotEmpty} from "class-validator";
import {UserPermissionInfo} from "./UserPermissionInfo";

@Entity({
    name:"permission_info"
})
export class PermissionInfo {
    @PrimaryGeneratedColumn()
    id:number;

    @IsNotEmpty()
    @Column()
    permission_name:string;

    @Column()
    icon:string;

    @IsNotEmpty()
    @IsInt()
    @Column()
    parent_id:number;

    @IsNotEmpty()
    @IsInt()
    @Column()
    order_num:number;

    @Column()
    front_path:string;

    @Column()
    api_path:string;

    @Column()
    component:string;

    @IsNotEmpty()
    @IsInt()
    @Column()
    permission_type:number;

    @IsNotEmpty()
    @Column()
    permission_key:string;

    @Column({
        type:"datetime"
    })
    create_time:Date;

    @Column({
        type:"datetime"
    })
    update_time:Date;

    @Column()
    remark:string;

    @Column({
        default:false
    })
    is_del:boolean;


    @OneToMany(()=>UserPermissionInfo,userPermissionInfo=>userPermissionInfo.permissionInfo)
    userPermissionInfoList:UserPermissionInfo[];

}

UserPermissionInfo.ts

import {Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
import {IsNotEmpty} from "class-validator";
import {UserInfo} from "./UserInfo";
import {PermissionInfo} from "./PermissionInfo";

@Entity({
    name:"user_permission_info"
})
export class UserPermissionInfo{
    @PrimaryGeneratedColumn()
    id:number;

    @IsNotEmpty()
    @Column()
    user_id:number;

    @IsNotEmpty()
    @Column()
    permission_id:number;

    @Column()
    create_time:Date;

    @Column()
    update_time:Date;

    @Column({
        default:false
    })
    is_del:boolean;

    @ManyToOne(()=>UserInfo,userInfo=>userInfo.userPermissionInfoList)
    @JoinColumn({name:"user_id"})
    userInfo:UserInfo;

    @ManyToOne(()=>PermissionInfo,permissionInfo=>permissionInfo.userPermissionInfoList)
    @JoinColumn({name:"permission_id"})
    permissionInfo:PermissionInfo;
}

八、将实例类文件添加到DBConfig.ts

所有的entity实体对象都要添加到typeorm的配置文件,随着项目的启动去启动

image-20230524153044827

后续如果还有其它的新增的实体对象,也要添加进去

九、express与typeorm结合

之前我们已经编写了app.ts的 express项目启动文件,现在需要将typeorm结合进去,如下

app.ts

import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";
import DBConfig from "./config/DBConfig";
import {createConnection, DataSourceOptions} from "typeorm";

createConnection(DBConfig as DataSourceOptions).then(()=>{
    const app: Express = express();
    app.use(bodyParser.urlencoded({extended:false,limit:"30m"}));
    app.use(bodyParser.json({limit:'30m'}));

    app.listen(9999,"[::]",()=>{
        console.log("服务器启动成功");
    })
})

十、添加跨域

当前的接口是没有跨域的,现在的项目如果进行前后端分离式开始是需要添加跨域的请求头的,这里直接在后端处理了

在这里我们需要使用一个第三方的依赖包cors

$ npm install cors --save

image-20230524154329913

十一、编辑DBUtils.ts

正常情部钙,DBUtils.ts是数据库的连接文件,但是我们现在使用typeorm来完成以后,这里就不需这个文件了,所以这个文件可以简写,如下

/**
 * 数据库操作的核心对象
 */

import {getConnection} from "typeorm";

export default class DBUtils {
    public getConnection = getConnection
}

十二、编写Services

数据库的操作还是离不开Service的

BaseService各数据表的Service

BaseService.ts

/**
 * 数据库操作的基类BaseService
 */

import DBUtils from "../utils/DBUtils";
import {validate} from "class-validator";

export default class BaseService<T> extends DBUtils {

    public repositoryType: any;

    constructor(repositoryType: any) {
        super();
        this.repositoryType = repositoryType;
    }

    /**
     *  根据id去查询
     */
    async findById(id: number, ...relations) {
        let connection = this.getConnection();
        let repository = connection.getRepository(this.repositoryType);
        let result = await repository.find({
            where: {
                id,
                is_del:false
            },
            relations
        });
        return result;
    }


    /**
     * 根据id删 除
     * @param id
     * @returns
     */
    async deleteById(id: number): Promise<boolean> {
        let connection = this.getConnection();
        let result = await connection.getRepository(this.repositoryType)
            .update({ id }, { id, is_del: true });
        return result.affected > 0;
    }

    /**
     * 查询所有数据
     * @returns
     */
    async getAllList(): Promise<any> {
        let connection = this.getConnection();
        let result = await connection.getRepository(this.repositoryType)
            .find({
                where: {
                    is_del: false
                }
            });
        return result;
    }

    async add(model: T): Promise<any> {
        const errors = await validate(model as any);
        if (errors.length > 0) {
            throw new Error("表单验证失败");
        }
        else {
            let connection = this.getConnection();
            let result = await connection.getRepository(this.repositoryType)
                .save(model);
            return result;
        }
    }

    async update(model: T): Promise<boolean> {
        let connection = this.getConnection();
        let result = await connection.getRepository(this.repositoryType)
            .update({ id: (model as any).id,is_del:false }, model);
        return result.affected > 0;
    }
}

UserInfoService.ts

/**
 * 数据表user_info的操作
 */
import BaseService from "./BaseService";
import {UserInfo} from "../entity/UserInfo";

export default class UserInfoService extends BaseService<UserInfo>{
    constructor() {
        super(UserInfo);
    }
}

PermissionService.ts

/**
 * 数据表permission_info的操作
 */
import BaseService from "./BaseService";
import {PermissionInfo} from "../entity/PermissionInfo";
export default class PermissionInfoService extends BaseService<PermissionInfo>{
    constructor() {
        super(PermissionInfo);
    }
}

UserPermissionInfoService.ts

/**
 * 数据表user_permission_info的操作
 */
import BaseService from "./BaseService";
import {UserPermissionInfo} from "../entity/UserPermissionInfo";

export default class UserPermissionInfoService extends BaseService<UserPermissionInfo>{
    constructor() {
        super(UserPermissionInfo);
    }
}

十三、创建Service的工厂

为了更好的实现服务层高类聚低耦合,这里使用工厂模式

  1. 首先在项目的src目录下面创建factory文件夹

  2. 在文件夹下面创建ServiceFactory.ts文件,代码如下

/**
 * 服务层工厂
 */
import UserPermissionInfoService from "../services/UserPermissionInfoService";
import UserInfoService from "../services/UserInfoService";
import PermissionInfoService from "../services/PermissionInfoService";

export default class ServiceFactory{
    static createUserInfoService(){
        return new UserInfoService();
    }
    static createPermissionInfoService(){
        return new PermissionInfoService();
    }
    static createUserPermissionInfoService(){
        return new UserPermissionInfoService();
    }
}

十四、服务器返回数据的编写

在服务端返回数据给客户端的时候,格式是需要固定的,这里我们列举以下常见的格式

  1. ResultJson.ts这个文件是专门用于返回JSON格式给前端的

    /**
     * @description 返回结果
     */
    export default class ResultJson {
      public status: string;
      public msg: string;
      public data: any;
    
      constructor(status: boolean, msg: string, data?: any) {
        this.status = status ? "success" : "fail";
        this.msg = msg;
        this.data = data;
      }
    }
    
  2. PageList.ts这是分页以后的数据,当进行分页查询的时候,我们可以使用这个格式的数据

    /**
     * @description 分页的数据模型
     * @author 杨标
     * @version 1.0
     */
    
    export default class PageList {
        public pageIndex: number;
        public totalCount: number;
        public pageCount: number;
        public listData: any[]
        public pageStart: number;
        public pageEnd: number;
        /**
         * @param {number} pageIndex 当前第几页
         * @param {number} totalCount 共多少条数据
         * @param {number} pageSize  每页显示多少
         * @param {any[]} listData 查询的分页列表数据 
         */
        constructor(pageIndex: number, totalCount: number, pageSize: number, listData: any[]) {
            this.pageIndex = pageIndex;
            this.totalCount = totalCount;
            this.pageCount = Math.ceil(totalCount / pageSize);
            this.listData = listData;
            this.pageStart = this.pageIndex - 3 > 0 ? this.pageIndex - 3 : 1;
            this.pageEnd = this.pageStart + 6 > this.pageCount ? this.pageCount : this.pageStart + 6;
        }
    }
    

十五、进行userInfoRouter.ts的编写

后端路由是用于处理不同的HTTP请求的,路由的使用可以更好的实现模块的低耦合,我们在项目的src目录下面新建一个router的目录,然后新建一个userInfoRouter.ts的文件,代码如下

import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import {checkPermission} from "../utils/permissionUtils";
import ServiceFactory from "../factory/ServiceFactory";
const router = express.Router();


router.get("/findById/:id", checkPermission("userInfo:findById"), (req: Request, resp: Response) => {

});


export default router;

上面我们列举了一个findById的请求,同时使用了一个中间件,这个中间件叫checkPermission,这是我们本次项目的核心,做于做权限验证的,这个中间件的代码如下

src/utils/checkPermission.ts

import {NextFunction, Request, Response} from "express";

export const checkPermission = (permissionKey: string) => {
    console.log(permissionKey);
    return (req: Request, resp: Response, next: NextFunction) => {
        next();
    }
}

将路由的框架搭建起来以后,后面就可以在app.ts里面导入了

src/app.ts

import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import * as bodyParser from "body-parser";
import DBConfig from "./config/DBConfig";
import {createConnection, DataSourceOptions} from "typeorm";
import * as cors from "cors";
import userInfoRouter from "./router/userInfoRouter";

const app: Express = express();


createConnection(DBConfig as DataSourceOptions).then(() => {
    app.use(cors({
        origin: "*"
    }));
    app.use(bodyParser.urlencoded({extended: false, limit: "30m"}));
    app.use(bodyParser.json({limit: '30m'}));
    app.use("/userInfo", userInfoRouter);


    app.listen(9999, "[::]", () => {
        console.log("服务器启动成功");
    })
})

十六、全局异常处理

在整个程序里面,如果在每个请求里面,每个模块里面都处理异常,这样会显得非常麻烦 ,我们可以使用一个模块来实现全局异常的处理,模块的名子叫express-async-errors

$ npm install express-async-errors

image-20230525091850701

十七、完成的userInfoRouter.ts

整个路由的基本结构完成以后,现在就开始实现具体业务请求(通俗一点说就是增删改查的请求操作),这里我先写一个模板,后面其它的模块与此大同小异

import * as express from "express";
import {Express, Request, Response, NextFunction} from "express";
import {checkPermission} from "../utils/permissionUtils";
import ServiceFactory from "../factory/ServiceFactory";
import ResultJson from "../dto/ResultJson";

const router = express.Router();


router.get("/findById/:id", checkPermission("userInfo:findById"), async (req: Request, resp: Response) => {
    let result = await ServiceFactory.createUserInfoService().findById(Number(req.params.id));
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "获取数据成功" : "获取数据失败", result));
});

router.post("/add", checkPermission("userInfo:add"), async (req: Request, resp: Response) => {
    let result = await ServiceFactory.createUserInfoService().add(req.body);
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "新增成功" : "新增失败"));
});

router.put("/update/:id", checkPermission("userInfo:update"), async (req: Request, resp: Response) => {
    let result = await ServiceFactory.createUserInfoService().update({...req.body, ...req.params});
    resp.json(new ResultJson(result, result ? "修改成功" : "修改失败"));
});

router.delete("/deleteById/:id", checkPermission("userInfo:deleteById"), async (req: Request, resp: Response) => {
    let result = await ServiceFactory.createUserInfoService().deleteById(Number(req.params.id));
    resp.json(new ResultJson(result, result ? "删除成功" : "删除失败"));
});


export default router;

十八、密码的md5加密

在上面,我们新增用户信息的时候,用户这个时候的密码是不能进行明文存储的,应该要进行加密,目前加密的方法有很多,这里我们使用md5加密再加盐的方式去处理

  1. 先在config的目录下面新建一个文件APPConfig.ts
  2. 在APPConfig.ts里面进行相应的md5盐的配置

src/config/APPConfig.ts

export default {
    md5Salt:"098lskdf.!@#09sdfj"
}

完成上面的配置以后,接下来我们就可以安装md5的包了

$ npm install md5

最后在UserInfoService.ts重写add的方法

image-20230525093332108

十九、头像上传

在新增用户的时候,有一个头像上传的环节,这里我们也是第三方包multer来进行完成

当然这里也可以使用七牛云进行图片的存储,后面讲到

$ npm install multer 

依赖包安装完成以后,我们在项目的根目录下面创建 uploadImages/avatar用于存放用户上传的头像

image-20230525095131977

router.post("/uploadAvatar", [checkPermission("userInfo:uploadAvatar"), uploadAvatar.single("avatar")], async (req: Request, resp: Response) => {
    let file = req.file;
    if (file) {
        let newFileName = file.filename + file.originalname
        fs.renameSync(file.path, path.join(__dirname, "../../uploadImages/avatar", newFileName));
        resp.json(new ResultJson(true, "图片上传成功", `/uploadImages/avatar/${newFileName}`))
    }
    else {
        resp.json(new ResultJson(false, "未接收到文件"))
    }
});

完成上面的路由操作以后,接下来其它的模块的路由编写就差这多了,这里就不再细述了,接下来的工作转向前端的操作


二十、使用vite来构建vue的项目

在空文件夹下面输入如下命令,建构项目

$ npm create vite@latest

项目创建完成以后,使用npm install来安装项目的依赖信息

image-20230525095714315

二十一、安装element-plus

项目依然使用element-ui这个组件库来进行页面的构建

$ npm install element-plus --save

element-plus的导入方式有2种,这里我们配置按需引入

按需导入

您需要使用额外的插件来导入要使用的组件。

自动导入推荐

首先你需要安装unplugin-vue-componentsunplugin-auto-import这两款插件

$ npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到你的 ViteWebpack 的配置文件中

Vite

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

Webpack

// webpack.config.js
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  // ...
  plugins: [
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
}

image-20230525100321658

全局配置

import {createApp} from 'vue'
import App from './App.vue'
import ElementPlus from "element-plus";

const app = createApp(App)
app.use(ElementPlus).mount('#app')

二十二、配置pinia及持久化插件

vue在之前的版本里面使用的全局状态管理是vuex,这里我们更新到pinia,同时添加持久化插件

$ npm install pinia pinia-plugin-persistedstate web-storage-cache

上面的pinia-plugin-persistedstate是持久化插件,同时使用web-storage-cache来进行缓存的处理

src/store/index.js下面,来进行pinia 的配置

/**
 * pinia的存储区域
 */

import { defineStore } from "pinia";
import WebStorageCache from "web-storage-cache";

const cache = new WebStorageCache({
    storage: window.localStorage
})

export const mainStore = defineStore("main", {
    state() {
        return {
            token: null,
        }
    },
    actions: {

    },
    // 配置状态的持久化
    persist: {
        key: "permission_vite_vue_20230525",
        storage: {
            getItem: key => cache.get(key),
            // 到期时间默认以秒为单位
            setItem: (key, value) => cache.set(key, value, { exp: 60 * 60 * 24 }),
            removeItem: key => cache.delete(key),
            clear: () => cache.clear()
        },
        //指定需要持久化的字段
        paths: ["token"]
    }
});

将pinia配置好了以后,需要在main.js里面进行加载

image-20230525101308628

二十三、配置项目的路径及文件别名

当我们在使用import进行文件的导入的时候,有时候为了方便需要快速使用路径,同时也需要快速导入文件,这个时候我们可以在vite.config.js里面进行别名的处理

image-20230525101859181

上面的extensions配置是后缀名,如果在上面配置了以后,则在导入文件的时候,以上的后缀名就可以省略掉,下面的alias代表的是别名配置,如果配置了以后可以直接 使用@来代表src的目录,这样更方便快速

二十四、配置vue-router前端路由

默认情况下,vite并没有帮我们添加路由,这里我们需要自己手动来进行

通过下面的命令安装路由依赖

$ npm install vue-router

src/store/index.js下面进行配置

import {createRouter,createWebHashHistory} from "vue-router";

const router = createRouter({
    history:createWebHashHistory(),
    routes:[

    ]
})

export default router;

最后在src/main.js下面加载这个配置文件

image-20230525102048487

二十五、第一个页面-登录

在项的src目录下面,我们新建一个views的目录,这是用于存放页面的,然后我们在里面创建一个Login.vue的文件,用于完成登录页面的布局

image-20230525102334242

做了一个简单的页面以后,这个时候需要在路由的配置文件里面配置路径

image-20230525102555509

当我们去配置完路由信息以后,这个时候控制台会报错,如下所示

image-20230525102440677

这个时候我们会发现是因为之前的Login.vue这个文件里面,我们使用了Sass,但是vite在创建项目的时候并没有直接帮我们安装sass的依赖包,我们需要安装一下

$ npm install sass --save-dev

安装包依赖包以后,这个时候就不会有错误了

当路径配置完成以后,我们就要在App.vue里面配置路由的出口了

<template>
  <router-view></router-view>
</template>
<script setup>
</script>

<style >

</style>

这个时候启动项目,在浏览器的地址栏输入地址以后就可以访问了

image-20230525102729159

最后,我们还需要配置默认路径

image-20230525103030400

这样当我们在打开项目上以后,默认就会跳转到Login的页面了

二十六、配置项目的样式初始化

我们刚刚打开Login的登录页以后,可以看到这里面的样式是没有初始化的,所以这里我们来进行初始化一下

assets的目录下面新建一个css目录,然后建立一个comm.css的文件,代码如下

@charset "utf-8";
*{
    margin: 0;
    padding: 0;
    list-style-type: none;
}

将上面的文件在main.js里面进行导入

image-20230525103504833

二十七、配置tailwind.css

在进行页面样式与布局的时候,我们要频繁的使用CSS,这里我推荐大家在项目里面使用tailwind.css这个框架,中文官网Tailwind CSS - 只需书写 HTML 代码,无需书写 CSS,即可快速构建美观的网站。 | TailwindCSS中文文档 | TailwindCSS中文网

https://www.tailwindcss.cn/
  1. 安装依赖包

    $ npm install -D tailwindcss postcss autoprefixer
    
  2. 初始化配置文件

    $ npx tailwindcss init -p
    

    初始化配置文件完成以后,在项目的根目录下面分多出2个文件,一个是postcss.config.js,另一个是tailwind.config.js

  3. 编辑tailwind.config.js文件

    /** @type {import('tailwindcss').Config} */
    export default {
      content: ["./index.html", "./src/*.{vue,js}", "./src/**/*.{vue,js}"],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    
  4. 在之前创建的src/assets/css/comm.css下面建立tailwindcss 的指令,如下

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  5. 编写测试样式

    在Login.vue的文件下面,编写代码

    <template>
      <div class="w-[100px] h-[100px] bg-red-400"></div>
    </template>
    
    <script setup>
    
    </script>
    
    <style lang="scss"  scoped>
    
    </style>
    

    打开页面,可以看到如下效果,说明成功

    image-20230525104853919

至此,我们以后在书写项目的时候就可以直接 使用tailwind.css来完成了

二十八、加载animate.css

在项目当中经常会使用动画 ,这里我们加载animate.css

$ npm install animate.css

安装完成以后,我们就在main.js里面进行加载配置

image-20230525111539863

二十八、完成登录页面布局

image-20230525133035108

页面完成以后,现在需要进行前后端的数据请求

在进行前后端的数据请求的时候,我们要使用axios来进行,这里我们来安装

二十九、axios的安装与配置

通过以下的命令安装依赖包

$ npm install axios 

安装完成以后,我们在src/utils下面新建一个axiosInstance.js的文件,进行相应的配置

import axios from "axios";

const axiosInstance = axios.create({
    baseURL: "http://127.0.0.1:9999"
});


export default axiosInstance;

三十、配置前端请求的接口

现在是前后端分离的开发,前后端为了实现低耦合,需要进行前端的接口配置,我们在src/api下面新建api的文件夹,同时根据不同的模块来编写API接口请求

import axiosInstance from "@/utils/axiosInstance.js";


const userInfo = {
    findById(id) {
        return axiosInstance.get(`/userInfo/findById/${id}`);
    },
    add({account, password, avatar, phone_number, status, login_time, remark}) {
        return axiosInstance.post(`/userInfo/add`, {
            account, password, avatar, phone_number, status, login_time, remark
        });
    },
    update({id, account, password, avatar, phone_number, status, login_time, remark}) {
        return axiosInstance.put(`/userInfo/update/${id}`, {
            id, account, password, avatar, phone_number, status, login_time, remark
        })
    },
    deleteById(id) {
        return axiosInstance.delete(`/userInfo/deleteById/${id}`);
    },
    uploadAvatar(fileFormData) {
        return axiosInstance.post(`/userInfo/uploadAvatar`, fileFormData, {
            headers: {
                "Content-Type": "multipart/form-data"
            }
        });
    }
}

export default userInfo;

完成上面的操作以后,其它的模块依次类型。

当所有的的模块完成以后,后面就会有很多的文件,我们要将这些模块进行高内聚的操作,把这些接口对象全部集合在index.js里面

image-20230525134355231

将上面的东西设置完成以后,这个时候我们还发现一个问题,基本上所有的的表当中都会有create_timeupdate_time,一个是新增的时候时间,一个是更新的时候的时间,这两个时间应该是在后端处理,所以我们需要在后端来进行调整

三十一、调整create_time与update_time

image-20230525134728602

我们分别在UserInfoServicePermissionInfoService里面重写了addupdate的方法,如上所示

三十二、编写登录的后台接口

src/services/UserInfoServices.ts

/**
 * 数据表user_info的操作
 */
import BaseService from "./BaseService";
import {UserInfo} from "../entity/UserInfo";
import * as md5 from "md5";
import APPConfig from "../config/APPConfig";

export default class UserInfoService extends BaseService<UserInfo> {
    constructor() {
        super(UserInfo);
    }

    async add(model: UserInfo): Promise<any> {
        model.password = <string>md5(model.password + APPConfig.md5Salt);
        model.create_time = new Date();
        return super.add(model);
    }

    async update(model: UserInfo): Promise<boolean> {
        model.update_time = new Date();
        return super.update(model);
    }

    async checkLogin(account, password) {
        password = md5(password+APPConfig.md5Salt);
        let connection = this.getConnection();
        let result =  await connection.getRepository(UserInfo).find({
            where:{
                account,
                password,
                is_del:false
            }
        });
        return result;
    }
}

上面的checkLogin就是用于检测登录的,account代表账号,password代表的是密码,同时这里要注意因为之前的密码是进行了md5的加密的,这里登录的时候密码也是要进行md5加密

service完成以后,现在就要开始进行路由的编写

src/router/userInfoRouter.ts

router.post(`/checkLogin`,async (req:Request,resp:Response)=>{
    let {account,password} = req.body;
    let result = await ServiceFactory.createUserInfoService().checkLogin(account,password);
    let flag = Boolean(result);
    if(flag){
        //登录成功
    }
    else{
        //登录失败
    }
});

上面就是路由的方法,这里我们可以通过判断来指定用户登录是否成功。

当然,再登录成功以后,我们需要向用户颁发令牌,这个时候我们就要使用jsonwebtoken这个依赖包了

三十三、jsonwebtoken的安装与使用

jsonwebtoken前后端分离开发的过程当中经常使用的一个包,用于向客户颁发令牌

$ npm install jsonwebtoken

安装完成以后,我们就要开始配置

  1. 先在config的目录下面的AppConfig.ts里面配置token的secret

    export default {
        md5Salt:"098lskdf.!@#09sdfj",
        tokenSecret:"asdfk90/.,!@#$213"
    }
    
  2. 在登录成功以后向用户发放令牌

    router.post(`/checkLogin`, async (req: Request, resp: Response) => {
        let {account, password} = req.body;
        let result = await ServiceFactory.createUserInfoService().checkLogin(account, password);
        let flag = Boolean(result);
        if (flag) {
            //登录成功
            let token = jwt.sign({
                ...result,
            }, APPConfig.tokenSecret, {
                expiresIn: "7d"
            });
            resp.json(new ResultJson(true, "登录成功", {
                token,
                userInfo: result
            }))
        } else {
            //登录失败
            resp.json(new ResultJson(false, "登录失败,用户名或密码不正确"));
        }
    });
    

在上面的令牌颁发过程当中,我们可以看到我们将令牌设置了一个到期时间,expiresIn就是一个到期时间

三十四、前端登录

我们现在回到前端的项目

api/userInfo.js下面,新增下面的接口请求方法

image-20230525140743371

回到Login.vue的页面

<script setup>
import {ref, reactive} from "vue";
import {Loading} from "@element-plus/icons-vue";
import Api from "@/api/index.js";

const loginFormData = reactive({
  account: "",
  password: ""
})
const loginFormDataRules = {
  account:[{required:true,message:"账号不能为空",trigger:"blur"}],
  password:[{required:true,message:"密码不能为空",trigger:"blur"}],
}

const isShowLoginBox = ref(true);

const loginFormEl = ref(null)

const submitLoginForm = () => {
  loginFormEl.value.validate(valid => {
    if (valid) {
      checkLogin();
    } else {
      console.log("验证失败");
    }
  })
}

const checkLogin = () => {
  Api.userInfo.checkLogin(loginFormData.account,loginFormData.password)
      .then(resp=>{
        console.log(resp.data);
      })
}
</script>

image-20230525152150256

通过上面的东西我们可以看出,已经可以请求接口,并获取到数据了,但是数据的格式是有问题的,因为我们可以看到,我们真正需要的数据其实是在data下面,所以我们可以在响应的时候,做一次响应拦截

三十五、实现axios的响应拦截

src/utils/axiosIntance.js

image-20230525152458454

上面我们设置了一个请求拦截,我们直接将resp.data的数据返回了出来,这个我们就可以直接拿到数据了

三十六、登录以后的pinia存储用户令牌

当用户登录成功以后,我们要将用户的token以及用户的基本令牌保存起来,并做持久化处理,这个时候我们就可以使用pinia来进行了

src/store/index.js

image-20230525152757668

我们添加了上面的代码以后,就可以在登录的时候使用pinia

image-20230525153534831

当我们登录成功以后,我们可以在浏览器里面看到相应的登录用户信息,如下

image-20230525153617381

三十七、后端初始鉴权

当我们登录成功以后,这个时候服务器会颁发一个令牌给用户,用户以后的每次ajax请求都需要携带这个令牌,我们可以通过axios来设置请求拦截,完成这个功能

image-20230525153933239

当我们完成上面的操作以后,这样在每次请求的时候,都会通过header里面的authorization进行token的携带,到服务端

服务端在接收到token以后就可以对token进行鉴权了

现在我们回到后台的程序,在之前的src/utils/permissionUtils.ts下面进行处理

import {NextFunction, Request, Response} from "express";
import ResultJson from "../dto/ResultJson";
import * as jwt from "jsonwebtoken";
import APPConfig from "../config/APPConfig";
import ServiceFactory from "../factory/ServiceFactory";

export const checkPermission = (permissionKey: string) => {
    console.log(permissionKey);
    return async (req: Request, resp: Response, next: NextFunction) => {
        //第1步:如果是options的预检请求,这个时候我们可以直接放行
        if (req.method.toUpperCase() === "OPTIONS") {
            return next();
        } else {
            //第2步:从header或query里面得到authorization的token
            let token = req.header("authorization") || req.query.authorization;
            //第3步:如果有token
            if (token) {
                try {
                    //第4步:对token进行鉴权,看token是否有效
                    let decode = jwt.verify(String(token), APPConfig.tokenSecret);
                    //第5步:如果token有效,我们就将得到的token保存在header方向后面继续使用
                    req.header["userInfo"] = decode;
                    //第5步:检测一下当前这个用户是否有这个权限
                    let flag = await ServiceFactory.createUserPermissionInfoService().checkUserPermission((decode as any).id, permissionKey)
                    if (flag) {
                        //说明当前用户有这个API的权限
                        return next();
                    }
                    else{
                        //说明当前用户没有这个API的权限
                        let resultJson = new ResultJson(false, "请求未授权");
                        resp.status(403).json(resultJson);
                    }
                } catch (e) {
                    let resultJson = new ResultJson(false, "令牌验证失败");
                    resp.status(403).json(resultJson);
                }
            } else {
                //如果没有token
                let resultJson = new ResultJson(false, "请求未授权");
                resp.status(403).json(resultJson);
            }
        }
    }
}

我们在上面用到了一个Service,就是根据当前用户检测当前是否有权限,代码如下

src/services/UserPermissionInfoService.ts

 /**
     * 检查某一个用户是否有某一个权限
     * @param user_id
     * @param permission_key
     */
    async checkUserPermission(user_id, permission_key):Promise<boolean> {
        let connection = this.getConnection();
        let result = await connection.getRepository(UserPermissionInfo).createQueryBuilder("user_permission_info")
            .innerJoinAndSelect("user_permission_info.permissionInfo", "permission_info")
            .where("user_permission_info.is_del=false")
            .andWhere("permission_info.permission_key=:permission_key", {permission_key})
            .andWhere("user_permission_info.user_id=:user_id", {user_id})
            .getOne();
        return result ? true : false;
    }

目前阶段,我们先暂时不开启权限控制,我们会把权限控制的中间件放开,方便后面的开发,等到最后再打开

image-20230525162412531

三十八、后面控制面板创建

src/views目录下面新建一个Manager.vue的文件,并配置路由

image-20230525162603596

三十九、注册所有图标

因为我们使用的是element-plus框架,同时使用的图标也是element-plus里面的图标,我们现在将所有的图标都注册一下,方便后面使用

image-20230525165106287

四十、左边菜单的完成

我们可以将左边的菜单单独放在一个组件里面进行完成,如下操作

  1. 在components里面创建一个LeftMenu.vue的文件
  2. 在Manager.vue里面导入这个组件,并使用

image-20230525165434650

image-20230525165452924

有了上面的操作以后,我们就可以看到下面的效果了

image-20230525165510191

现在我们只需要将左边的菜单完成即可

<template>
  <div class="menu-box">
    <div class="h-[60px] bg-[#263445] font-bold flex flex-row justify-center items-center text-white italic">
      <el-icon size="28" class="is-loading mr-[10px]">
        <Loading/>
      </el-icon>
      通用权限管理系统
    </div>
    <el-menu
        active-text-color="#ffd04b"
        background-color="#304156"
        default-active="2"
        router
        text-color="#fff">
      <el-sub-menu index="1">
        <template #title>
          <el-icon>
            <setting/>
          </el-icon>
          <span>系统管理</span>
        </template>
        <el-menu-item index="/Manager/UserInfoList" >
          <el-icon>
            <user/>
          </el-icon>
          用户管理
        </el-menu-item>
        <el-menu-item index="1-2">
          <el-icon><user-filled /></el-icon>
          角色管理
        </el-menu-item>
        <el-menu-item index="1-3">
          <el-icon><stamp /></el-icon>
          菜单管理
        </el-menu-item>
      </el-sub-menu>
    </el-menu>
  </div>
</template>

<script setup>

</script>

<style scoped lang="scss">
.menu-box {
  @apply bg-[#304156] w-full h-full overflow-hidden;
}
</style>

四十一、用户列表页面

本期项目的核心是通用权限管理,管理的是用户,所以我们主要围绕的仍然是用户,权限这两个点,这里我们先完成用户列表的页面

后端项目的src/services/UserInfoServices.ts文件中,编写分页查询的方法

 /**
     * 分页查询用户信息
*/
async getListByPage({pageIndex, account, phone_number}) {
        let connection = this.getConnection();
        let queryBuilder = connection.getRepository(UserInfo).createQueryBuilder("user_info")
            .innerJoinAndSelect("user_info.userPermissionInfoList", "user_permission_info")
            .where("user_info.is_del=false");
        if (account) {
            queryBuilder.andWhere("user_info.account like :account", {account: `%${account}%`});
        }
        if (phone_number) {
            queryBuilder.andWhere("user_info.phone_number like :phone_number", {phone_number: `%${phone_number}%`});
        }
        let [listData,totalCount] = await queryBuilder.orderBy("id","ASC").skip((pageIndex-1)*10).take(10).getManyAndCount();
        return new PageList(pageIndex, totalCount, 10, listData);
}

后端项目的src/services/userInfoRouter.ts文件中,编写分页查询的方法

router.get("/getListByPage",checkPermission("userInfo:getListByPage"),async(req:Request,resp:Response)=>{
   let pageList = await ServiceFactory.createUserInfoService().getListByPage(req.query as any);
    resp.json(new ResultJson(true, "获取数据成功", pageList));
});

当后端的功能完成了以后,现在我们就在要前端的项目里面编写API接口请求了

前端项目api/userInfo.js下面

image-20230525175410634

完成以后,就可以进行相应的页面布局了

<template>
  <el-card>
    <template #header>
      <div class="font-bold">用户列表</div>
    </template>
    <el-form :inline="true">
      <el-form-item label="用户名">
        <el-input placeholder="输入用户名查询" v-model="queryParamsData.account"></el-input>
      </el-form-item>
      <el-form-item label="手机号">
        <el-input placeholder="输入手机号查询" v-model="queryParamsData.phone_number"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="queryData" :loading="isLoading">查询</el-button>
        <el-button type="success" icon="CirclePlus" @click="$router.push({name:'AddUserInfo'})">新增用户</el-button>
      </el-form-item>

    </el-form>
    <div v-loading="isLoading">
      <el-table
          :data="listData"
          border
          stripe>
        <el-table-column label="ID" prop="id" width="50" align="center"></el-table-column>
        <el-table-column label="账号" prop="account" width="180"></el-table-column>
        <el-table-column label="头像" width="100" align="center">
          <template #default="{row}">
            <el-image class="w-55px h-[55px] object-fit" :src="BASEURL+row.avatar"></el-image>
          </template>
        </el-table-column>
        <el-table-column label="手机号" prop="phone_number" align="center" width="180"></el-table-column>
        <el-table-column label="状态" width="100" align="center">
          <template #default="{row}">
            <el-tag type="success" v-if="row.status==0">启用</el-tag>
            <el-tag type="danger" v-else-if="row.status==1">禁用</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注" prop="remark"></el-table-column>
        <el-table-column label="操作">
          <el-button type="warning" size="small" icon="EditPen">编辑</el-button>
          <el-button type="primary" size="small" icon="Key">分配权限</el-button>
        </el-table-column>
      </el-table>
      <div class="mt-[10px] flex flex-row justify-end">
        <el-pagination :total="totalCount" background layout="prev, pager, next"></el-pagination>
      </div>
    </div>
  </el-card>
</template>

<script setup>
import {reactive, ref, inject, onMounted} from "vue";
import Api from "@/api/index.js";
import {ElNotification} from "element-plus";

const BASEURL = inject("BASEURL");
const queryParamsData = reactive({
  account: "",
  phone_number: "",
  pageIndex: 1
});


const listData = ref([]);
const totalCount = ref(0);
const isLoading = ref(false);
const queryData = () => {
  Api.userInfo.getListByPage(queryParamsData)
      .then(result => {
        totalCount.value = result.data.totalCount;
        listData.value = result.data.listData;
        queryParamsData.pageIndex = +result.data.pageIndex;
      }).catch(error => {
    ElNotification.error(error.msg);
  })
}


onMounted(() => {
  queryData();
})
</script>

<style scoped lang="scss">

</style>

四十二、新增用户

src/views/userInfo下面新建AddUserInfo.vue这个文件

<template>
  <el-card>
    <template #header>
      <div class="flex flex-row justify-between items-center">
        <span class="font-bold">新增用户</span>
        <el-link type="primary" @click="$router.back()" underline>返回列表</el-link>
      </div>
    </template>
    <el-form ref="formEl" label-width="150" :model="formParamsData" :rules="formParamsDataRules">
      <el-form-item label="账号" prop="account">
        <el-input placeholder="请输入账号" v-model="formParamsData.account"/>
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input placeholder="请输入密码" v-model="formParamsData.password" show-password/>
      </el-form-item>
      <el-form-item label="确认密码" prop="secondPassword">
        <el-input placeholder="请输入确认密码" v-model="formParamsData.secondPassword" show-password/>
      </el-form-item>
      <el-form-item label="手机号">
        <el-input placeholder="请输入手机号" v-model="formParamsData.phone_number"/>
      </el-form-item>
      <el-form-item label="头像">
        <el-upload
            class="avatar-uploader"
            :action="`${BASEURL}/userInfo/uploadAvatar`"
            name="avatar"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload"
        >
          <img v-if="formParamsData.avatar" :src="BASEURL+formParamsData.avatar" class="avatar"/>
          <el-icon v-else class="avatar-uploader-icon">
            <Plus/>
          </el-icon>
        </el-upload>
      </el-form-item>
      <el-form-item label="备注">
        <el-input :rows="2" type="textarea" placeholder="备注信息"/>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm">保存数据</el-button>
        <el-button type="danger" @click="formEl.resetFields()">重置表单</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script setup>
import {reactive, inject, ref} from "vue";
import {ElMessage, ElNotification} from "element-plus";
import Api from "@/api/index.js";
import {useRouter} from "vue-router";

const router = useRouter();
const BASEURL = inject("BASEURL");
const formParamsData = reactive({
  account: "",
  password: "",
  secondPassword: "",
  phone_number: "",
  avatar: "",
  status: 0,
  remark: ""
});

const formParamsDataRules = {
  account: [{required: true, message: "账号不能为空", trigger: "blur"}],
  password: [{required: true, message: "密码不能为空", trigger: "blur"}],
  secondPassword: [{required: true, message: "确认密码不能为空", trigger: "blur"}, {
    validator(rule, value, callBack) {
      if (value === "") {
        callback(new Error('请输入确认密码'))
      }
      if (value != formParamsData.password) {
        callBack(new Error("再次密码必须相同"))
      }
      callBack();
    }, trigger: "blur"
  }],
}

const handleAvatarSuccess = (response, uploadFile) => {
  if (response.status == "success") {
    formParamsData.avatar = response.data;
  } else {
    ElMessage.error(response.msg);
  }
}

const beforeAvatarUpload = (rawFile) => {
  if (rawFile.size / 1024 / 1024 > 2) {
    ElMessage.error('头像大小不能超过2M!')
    return false
  }
  return true
}
const formEl = ref(null);
const submitForm = () => {
  formEl.value.validate(valid => {
    if (valid) {
      addUserInfo();
    } else {
      console.log("表单验证失败");
    }
  })
}

const addUserInfo = () => {
  Api.userInfo.add(formParamsData).then(result => {
    router.replace({name: "UserInfoList"});
  }).catch(error => {
    ElNotification.error(error.msg);
  })
}
</script>

<style lang="scss" scoped>
.avatar-uploader {
  width: 100px;
  height: 100px;
  border: 1px dashed #808080;
  border-radius: 6px;
  display: flex;
  justify-content: center;
  align-content: center;

  .avatar-uploader-icon {
    font-size: 36px;
    color: #808080;
    width: 100%;
    height: 100%;
  }

  .avatar {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
}
</style>

页面新增完成以后,这个时候我们就要进行路由配置了

src/router/index.js

import {createRouter, createWebHashHistory} from "vue-router";
import Login from "@/views/Login.vue";
import Manager from "@/views/Manager.vue";

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: "/",
            redirect: {
                name: "Login"
            }
        },
        {
            path: "/Login",
            name: "Login",
            component: Login,
            meta: {
                title: "登录信息"
            }
        }, {
            path: "/Manager",
            name: "Manager",
            component: Manager,
            children:[
                {
                    path: "/Manager/UserInfoList",
                    name: "UserInfoList",
                    component: () => import("@/views/userInfo/UserInfoList.vue")
                },
                {
                    path: "/Manager/AddUserInfo",
                    name: "AddUserInfo",
                    component: () => import("@/views/userInfo/AddUserInfo.vue")
                }
            ]
        },
    ]
})

export default router;

四十三、权限列表

这一个其它就是我们的菜单管理页面

image-20230526091122060

这里我们模仿软帝邱芬芬老师的界面来完成,模拟的界面效果如下

image-20230526091212231

展开以后的效果

image-20230526091226336

新增权限的界面

image-20230526091246198

编辑权限的界面

image-20230526091302605

在这个界面里面,我们首先要完成的应该是新增权限的过程,这里的新增权限有三种类型,一个是目录,一个是页面,还有一个 就是按钮操作了

四十四、新增权限

首先在之前的PermissionList.vue里面完成基本的布局,使用el-dialog完成模态框的弹出

<template>
  <el-card>
    <template #header>
      <div class="font-bold">权限列表</div>
    </template>
    <div>
      <el-button type="success" icon="CirclePlus" @click="isModelVisible=true">新增权限菜单</el-button>
    </div>
    <el-dialog
        v-model="isModelVisible"
        title="新增权限菜单"
        :isModelVisible="true"
        :close-on-click-modal="false"
        :close-on-press-escape="false">
      
    </el-dialog>
  </el-card>
</template>

<script setup>
import {ref} from "vue";

const isModelVisible = ref(false);
</script>

<style scoped>

</style>

再到src/views/permissionInfo下面新建AddPermissionInfo.vue的文件

image-20230526095300398

完成上面的布局以垢,我们现在就要开始去填充数据了,如上级菜单中的那个树形结构图,如下所示

image-20230526095327345

image-20230526100516523

这里没有数据,所以我们现在先将填充一个根目录

完成上面的界面布局以后,我们现在就开始后端接口的编写 了

后台src/router/permissionInfoRouter.ts代码

image-20230526100830217

后台src/app.ts代码

image-20230526100953859

现在回到前台,我们来进行接口请求接口的编写

src/api目录下面,新建 permissionInfo.js的文件

import axiosInstance from "@/utils/axiosInstance.js";

const permissionInfo = {
    add({permission_name, icon, parent_id, order_num, front_path, component, permission_type, permission_key,remark}){
        return axiosInstance.post(`/permissionInfo/add`,{
            ermission_name, icon, parent_id, order_num, front_path, component, permission_type, permission_key,remark
        });
    }
}

export default permissionInfo;

最后仍然将这个对象聚合在src/api/index.js这个文件里面

image-20230526104522579

完成上面的操作以后,我们就可以新增权限菜单了,这里我们只添加目录与页面的菜单

<template>
  <el-form label-width="120px" :model="permissionFormData" :rules="permissionFormDataRules" ref="formEl">
    <el-form-item label="上级菜单" prop="parent_id">
      <el-select placeholder="选择上级菜单" v-model="permissionFormData.parent_id">
        <el-option :value="0" label="根目录"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="菜单类型" prop="permission_type">
      <el-radio-group v-model="permissionFormData.permission_type">
        <el-radio :label="0">目录</el-radio>
        <el-radio :label="1">菜单</el-radio>
        <el-radio :label="2">按钮</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="菜单图标">
      <el-input placeholder="使用element-plus里面的图标" v-model="permissionFormData.icon"/>
    </el-form-item>
    <el-form-item label="菜单名称" prop="permission_name">
      <el-input placeholder="请输入菜单的名称" v-model="permissionFormData.permission_name"/>
    </el-form-item>
    <el-form-item label="权限标识" prop="permission_key">
      <el-input placeholder="输入权限标识 ,如userInfo:findById" v-model="permissionFormData.permission_key"/>
    </el-form-item>
    <el-form-item label="路由地址">
      <el-input placeholder="如果是菜单页面,则要输入路由地址" v-model="permissionFormData.front_path"/>
    </el-form-item>
    <el-form-item label="组件路径">
      <el-input placeholder="如果是菜单页面,则要输入组件的路径,实现动态加载"
                v-model="permissionFormData.component"></el-input>
    </el-form-item>
    <el-form-item label="显示顺序" prop="order_num">
      <el-input-number :min="0" :step="1" v-model="permissionFormData.order_num"></el-input-number>
    </el-form-item>
    <el-form-item label="备注信息">
      <el-input type="textarea" :rows="3" v-model="permissionFormData.remark"/>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm" :loading="isSubmiting" loading-icon="loading">保存</el-button>
      <el-button type="default" @click.stop="$emit('closeDialog')">关闭</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import {ref, reactive,defineEmits} from "vue";
import Api from "@/api/index.js";
import {ElMessage, ElNotification} from "element-plus";

const emit = defineEmits(["closeDialog"])
const permissionFormData = reactive({
  permission_name: "",
  icon: "",
  parent_id: 0,
  order_num: 0,
  front_path: "",
  component: "",
  permission_type: 0,
  permission_key: "",
  remark: ""
});
const permissionFormDataRules = {
  permission_name: [{required: true, message: "权限名称不能为空", trigger: "blur"}],
  parent_id: [{required: true, message: "父级菜单不能为空", trigger: "blur"}],
  order_num: [{required: true, message: "排列序号不为能为空", trigger: "blur"}],
  permission_type: [{required: true, message: "权限类型不能为空", trigger: "blur"}],
  permission_key: [{required: true, message: "权限标识不能为空", trigger: "blur"}]
}

const isSubmiting = ref(false);
const formEl = ref(null);
const submitForm = () => {
  formEl.value.validate(valid => {
    if (valid) {
      addPermissionInfo();
    } else {
      console.log("表单验证错误");
    }
  })
}
const addPermissionInfo = () => {
  isSubmiting.value = true;
  Api.permissionInfo.add(permissionFormData)
      .then(result => {
        ElMessage.success("新增成功");
        emit("closeDialog");
      }).catch(error => {
    ElNotification.error(error.msg);
  }).finally(() => {
    isSubmiting.value = false;
  })
}
</script>

<style scoped>

</style>

插入的结果如下

image-20230526111922621

四十五、获取菜单目录

菜单的目录其实就是permission_type为0和1的类型,所以现在我们在后台先编写相应的接口

src/services/PermissionInfoServices.ts代码

image-20230526111801266

src/router/permissionInfoRouter.ts代码

router.get("/getAllFolderAndPageList", checkPermission("permissionInfo:getAllFolderAndPageList"), async (req: Request, resp: Response) => {
    let result = await ServiceFactory.createPermissionInfoService().getAllFolderAndPageList();
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});

测试以求以后,得到如果的数据结果

{
    "status": "success",
    "msg": "获取成功",
    "data": [
        {
            "id": 1,
            "permission_name": "系统管理",
            "icon": "icon",
            "parent_id": 0,
            "order_num": 0,
            "front_path": "",
            "component": "",
            "permission_type": 0,
            "permission_key": "manager",
            "create_time": "2023-05-26T02:53:27.000Z",
            "update_time": null,
            "remark": "系统管理目录",
            "is_del": false,
            "children": [
                {
                    "id": 2,
                    "permission_name": "用户管理",
                    "icon": "user",
                    "parent_id": 1,
                    "order_num": 0,
                    "front_path": "/Manager/UserInfoList",
                    "component": "uesrInfo/UserInfoList.vue",
                    "permission_type": 1,
                    "permission_key": "page:userInfo:list",
                    "create_time": "2023-05-26T02:58:28.000Z",
                    "update_time": null,
                    "remark": "用户管理的页面",
                    "is_del": false
                },
                {
                    "id": 3,
                    "permission_name": "新增用户",
                    "icon": "CirclePlus",
                    "parent_id": 1,
                    "order_num": 1,
                    "front_path": "/Manager/AddUserInfo",
                    "component": "userInfo/AddUserInfo.vue",
                    "permission_type": 1,
                    "permission_key": "page:userInfo:add",
                    "create_time": "2023-05-26T03:01:09.000Z",
                    "update_time": null,
                    "remark": "新增用户的页面",
                    "is_del": false
                },
                {
                    "id": 4,
                    "permission_name": "菜单管理",
                    "icon": "key",
                    "parent_id": 1,
                    "order_num": 3,
                    "front_path": "/Manager/PermissionInfoList",
                    "component": "permissionInfo/PermissionInfoList.vue",
                    "permission_type": 1,
                    "permission_key": "page:permissionInfo:list",
                    "create_time": "2023-05-26T03:04:10.000Z",
                    "update_time": null,
                    "remark": "菜单管理的页面",
                    "is_del": false
                }
            ]
        }
    ]
}

我们现在就可以通过这个接口来完成如下图的功能

image-20230526112156554

在前端src/api/permissionInfo.js下面编写请求接口

image-20230526112245746

src/views/permissionInfo/AddPermissionInfo.vue

image-20230526115953870

image-20230526120148876

最终的效果如下

image-20230526120211434

四十六、权限列表

效果图如下

image-20230526120234056

后台代码src/services/PermissionInfoServices.ts

  /**
     * 获取所有权限,并实现table的级联
     */
    async getAllPermissionList() {
        let connection = this.getConnection();
        let result = await connection.getRepository(PermissionInfo).find({
            where: {
                is_del: false
            }
        });
        //开始构建层级数据
        let level0 = [];
        let level1 = [];
        let level2 = [];
        result.forEach(item=>{
            //@ts-ignore
            item.children = [];
            if(item.permission_type===0){
                level0.push(item);
            }
            else if(item.permission_type===1){
                level1.push(item);
            }
            else if(item.permission_type===2){
                level2.push(item);
            }
        });
        level2.forEach(item=>{
            let parent = level1.find(itemParent=>itemParent.id===item.parent_id);
            if(parent){
                parent.children.push(item);
            }
        });

        level1.forEach(item=>{
            let parent = level0.find(itemParent=>itemParent.id===item.parent_id);
            if(parent){
                parent.children.push(item);
            }
        });

        return level0;

    }

后台代码src/services/PermissionInfoServices.ts

/**
 * 获取所有权限,并实现table的级联
 */
router.get("/getAllPermissionList",checkPermission("permissionInfo:getAllPermission"),async(req,resp)=>{
    let result = await ServiceFactory.createPermissionInfoService().getAllPermissionList();
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});

前台代码src/api/permissionInfo.js里面编写接口请求

image-20230526140058583

完成接口请求以后,我们就可以开始使用这个接口来渲染数据了【此处的代码就省略了,具体可以看permissionInfoList.vue】

image-20230526140043536

四十七、编辑权限列表

在权限列表里面,我们还可以对某个权限列表进行编辑,这里我们仍然使用弹窗的形式去完成

编辑权限列表的代码与新增权限列表的代码基本上大同事小异,前端页面的布局,后端接口的编写等这里就省略掉了,具体可以看代码

<template>
  <el-form label-width="120px" :model="permissionFormData" :rules="permissionFormDataRules" ref="formEl">
    <el-form-item label="上级菜单" prop="parent_id">
      <el-tree-select
          v-model="permissionFormData.parent_id"
          :data="allFolderAndPageList" check-strictly
          :render-after-expand="false"></el-tree-select>
    </el-form-item>
    <el-form-item label="菜单类型" prop="permission_type">
      <el-radio-group v-model="permissionFormData.permission_type">
        <el-radio :label="0">目录</el-radio>
        <el-radio :label="1">菜单</el-radio>
        <el-radio :label="2">按钮</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="菜单图标">
      <el-input placeholder="使用element-plus里面的图标" v-model="permissionFormData.icon"/>
    </el-form-item>
    <el-form-item label="菜单名称" prop="permission_name">
      <el-input placeholder="请输入菜单的名称" v-model="permissionFormData.permission_name"/>
    </el-form-item>
    <el-form-item label="权限标识" prop="permission_key">
      <el-input placeholder="输入权限标识 ,如userInfo:findById" v-model="permissionFormData.permission_key"/>
    </el-form-item>
    <el-form-item label="路由地址">
      <el-input placeholder="如果是菜单页面,则要输入路由地址" v-model="permissionFormData.front_path"/>
    </el-form-item>
    <el-form-item label="组件路径">
      <el-input placeholder="如果是菜单页面,则要输入组件的路径,实现动态加载"
                v-model="permissionFormData.component"></el-input>
    </el-form-item>
    <el-form-item label="显示顺序" prop="order_num">
      <el-input-number :min="0" :step="1" v-model="permissionFormData.order_num"></el-input-number>
    </el-form-item>
    <el-form-item label="备注信息">
      <el-input type="textarea" :rows="3" v-model="permissionFormData.remark"/>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm" :loading="isSubmiting" loading-icon="loading">保存</el-button>
      <el-button type="default" @click.stop="$emit('closeDialog')">关闭</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import {ref, reactive,  onMounted} from "vue";
import Api from "@/api/index.js";
import {ElMessage, ElNotification} from "element-plus";

const emit = defineEmits(["closeDialog"]);
const props = defineProps({
  currentEditPermissionId: {
    required: true,
  }
})

const permissionFormData = ref({
  id:"",
  permission_name: "",
  icon: "",
  parent_id: 0,
  order_num: 0,
  front_path: "",
  component: "",
  permission_type: 0,
  permission_key: "",
  remark: ""
});
const permissionFormDataRules = {
  permission_name: [{required: true, message: "权限名称不能为空", trigger: "blur"}],
  parent_id: [{required: true, message: "父级菜单不能为空", trigger: "blur"}],
  order_num: [{required: true, message: "排列序号不为能为空", trigger: "blur"}],
  permission_type: [{required: true, message: "权限类型不能为空", trigger: "blur"}],
  permission_key: [{required: true, message: "权限标识不能为空", trigger: "blur"}]
}

//获取菜单目录
const allFolderAndPageList = ref([]);
const getAllFolderAndPageList = () => {
  Api.permissionInfo.getAllFolderAndPageList().then(result => {
    //生成el-tree-select的数据
    buildElTreeData(result.data);
    allFolderAndPageList.value = [{
      value: 0,
      label: "根目录",
      children: result.data
    }];
  }).catch(error => {
    ElNotification.error(error.msg);
  })
}

/**
 * 构建el-tree-select的数据
 * @param arrData {Array}
 */
const buildElTreeData = (arrData) => {
  if (Array.isArray(arrData)) {
    arrData.forEach(item => {
      item.value = item.id,
          item.label = item.permission_name;
      if (item.children) {
        buildElTreeData(item.children);
      }
    })
  }
}

const findCurrentEditItem = ()=>{
  Api.permissionInfo.findById(props.currentEditPermissionId)
      .then(result=>{
        permissionFormData.value = result.data;
      }).catch(error=>{
        ElNotification.error(error.msg);
  })
}

onMounted(() => {
  getAllFolderAndPageList();
  findCurrentEditItem();
})

const isSubmiting = ref(false);
const formEl = ref(null);
const submitForm = () => {
  formEl.value.validate(valid => {
    if (valid) {
      updatePermissionInfo();
    } else {
      console.log("表单验证错误");
    }
  })
}

const updatePermissionInfo = ()=>{
  isSubmiting.value = true;
  Api.permissionInfo.update(permissionFormData.value)
      .then(result=>{
        emit("closeDialog");
      }).catch(error=>{
        ElNotification.error(error.msg);
  })
}

</script>

<style scoped>

</style>

后面关于编辑与新增的操作,代码将会省略一些

四十八、用户权限分配

这是本次项目的核心了,界面如下图所示

image-20230526161154251

点击“分配权限”以后将会进行下面的操作

image-20230526161240664

弹出类似的模态框

在当前这个页面,使用的是element-plus里面的el-tree来完成的,这里主要有2个点需要完成

  1. 获取所有的权限列表,并形成这样的一个树形结构
  2. 获取当前这个用户已经具备的权限列表,对时在树形结构上面钩选已经具备的权限

有了这2点思路以后,我们先从后台开始

后台src/services/PermissionInfoService.ts文件

/**
     * 获取所有权限,并实现table的级联
     */
    async getAllPermissionList() {
        let connection = this.getConnection();
        let result = await connection.getRepository(PermissionInfo).find({
            where: {
                is_del: false
            }
        });
        //开始构建层级数据
        let level0 = [];
        let level1 = [];
        let level2 = [];
        result.forEach(item=>{
            //@ts-ignore
            item.children = [];
            //@ts-ignore
            item.label = item.permission_name;
            if(item.permission_type===0){
                level0.push(item);
            }
            else if(item.permission_type===1){
                level1.push(item);
            }
            else if(item.permission_type===2){
                level2.push(item);
            }
        });
        level2.forEach(item=>{
            let parent = level1.find(itemParent=>itemParent.id===item.parent_id);
            if(parent){
                parent.children.push(item);
            }
        });

        level1.forEach(item=>{
            let parent = level0.find(itemParent=>itemParent.id===item.parent_id);
            if(parent){
                parent.children.push(item);
            }
        });

        return level0;

    }

上面的代码是获取所有的权限,并形成数形结构

后台src/services/UserPermissionInfoService.ts代码

  /**
     * 根据user_id来查询他的权限
     */
    async getUserPermissionByUserId(user_id: number) {
        let connection = this.getConnection();
        let result = await connection.getRepository(UserPermissionInfo).find({
            where: {
                is_del: false,
                user_id
            }
        });
        return result;
    }

完成Services的操作以后,接下来是后台的路由操作

后台src/router/userPermissionInfoRouter.ts文件

router.get("/getUserPermissionByUserId/:user_id", checkPermission("userPermissionInfo:getUserPermissionByUserId"), async (req, resp) => {
    let result = await ServiceFactory.createUserPermissionInfoService().getUserPermissionByUserId(+req.params.user_id);
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});

后台src/router/permissionInfoRouter.ts文件

/**
 * 获取所有权限,并实现table的级联
 */
router.get("/getAllPermissionList", checkPermission("permissionInfo:getAllPermission"), async (req, resp) => {
    let result = await ServiceFactory.createPermissionInfoService().getAllPermissionList();
    let flag = Boolean(result);
    resp.json(new ResultJson(flag, flag ? "获取成功" : "获取失败", result));
});

完成后台的操作以后,接下来,我们就会转向前台操作

前台src/views/userInfo/ControlAuth.vue文件

<template>
  <div>
    <el-tree
        :check-strictly="true"
        ref="authorElTree"
        :data="allPermissionList"
        show-checkbox
        node-key="id"
        :default-checked-keys="currentUserPermissionList"
        default-expand-all></el-tree>
    <div class="mt-[10px] flex flex-row justify-end">
      <el-button type="primary" @click="saveAuth" :loading="isSubmiting" loading-icon="loading">确定</el-button>
      <el-button type="default" @click="$emit('closeDialog')">关闭</el-button>
    </div>
  </div>
</template>

<script setup>
/**
 * 在这个页面上面必须有2个操作,第1个是获取所有的权限,形成树形图,第2个就是把自己的权限钩上
 */
import {onMounted, ref} from "vue";
import Api from "@/api/index.js";
import {ElNotification} from "element-plus";

const emit = defineEmits(["closeDialog"]);

const allPermissionList = ref([]);
const props = defineProps({
  currentControlAuthorUserId: {
    required: true
  }
})
const getAllPermissionList = () => {
  Api.permissionInfo.getAllPermissionList()
      .then(result => {
        allPermissionList.value = result.data;
      }).catch(error => {
    ElNotification.error(error.msg || "操作失败");
  })
}

//当前用户的权限
const currentUserPermissionList = ref([]);

const getUserPermissionByUserId = () => {
  Api.userPermissionInfo.getUserPermissionByUserId(props.currentControlAuthorUserId)
      .then(result => {
        currentUserPermissionList.value = result.data.map(item=>item.permission_id);
      }).catch(error => {
    ElNotification.error(error.msg);
  })
}

onMounted(() => {
  getAllPermissionList();
  getUserPermissionByUserId();
})

//点击确定授权以后
const isSubmiting = ref(false);
const authorElTree = ref(null);
const saveAuth = () => {
  //第一步:先获取选中的结点
  let keys = authorElTree.value.getCheckedKeys();
  //第二步:发送请求
  Api.userPermissionInfo.controlAuth(props.currentControlAuthorUserId, keys)
      .then(result => {
        ElNotification.success("授权成功");
        emit("closeDialog");
      }).catch(error => {
    ElNotification.error(error.msg);
  })
}

</script>

<style lang="scss" scoped>

</style>

完成上面的操作以后,我们就可以动态的用户的权限进行分配了

四十九、保存用户分配的权限

这里承接上面的章节,当我们完成el-tree的数据渲染以后,我们肯定是需要保存用户的数据,这个时候,要将前台选中的权限传递到后台,这个操作有几点要注意

  1. 前台选择的可能是有多个权限,这个时候要将选中的权限组成一个数组,传递到后台,问题就在于怎么样向后台传递一个数组
  2. 后台接收到这个数组以后如果操作这个数组。初步的方案是数据库里面原来用户的权限全部删除掉,最后再重新添加权限,这样就会涉及到数据的一致性,所以这个地方要使用数据库的事务进行操作

前端src/api/userPermissionInfo.js代码

image-20230529090921620

在上面的代码里面,我们可以看到,我们使用axios的时候更改了它的请求头,将Content-Type 设置成了application/json,这样就方便我们向后台传递数据了

后台src/router/userPermissionRouter.ts代码

router.post("/controlAuth", checkPermission("userPermissionInfo:controlAuth"), async (req: Request, resp: Response) => {
    let {user_id, permissionList} = req.body;
    let flag = await ServiceFactory.createUserPermissionInfoService().controlAuth(user_id, permissionList);
    resp.json(new ResultJson(flag, flag ? "授权成功" : "授权失败"));
});

请求到达路由以后,再次调用Services里面的方法,如下

后台src/services/UserPermissionInfoService.ts文件

/**
     * 开始授权
     * @param user_id
     * @param permissionList
     */
    async controlAuth(user_id: number, permissionList: Array<any>) {
        console.log(user_id);
        console.log(permissionList);
        //注意,这里要开启事务
        let connection = this.getConnection();
        return await connection.transaction(async transactionalEntityManager => {
            try {
                //第一步:在用户权限表里面,将这个用户的所有权限全部删除掉。
                await transactionalEntityManager.getRepository(UserPermissionInfo).createQueryBuilder()
                    .update()
                    .set({is_del: true, update_time: new Date()})
                    .where({user_id})
                    .execute();
                for (let permission_id of permissionList) {
                    const userPermissionInfo = transactionalEntityManager.create(UserPermissionInfo);
                    userPermissionInfo.user_id = user_id;
                    userPermissionInfo.permission_id = permission_id;
                    userPermissionInfo.create_time = new Date();
                    await transactionalEntityManager.save(userPermissionInfo);
                }
                return true;
            } catch (e) {
                console.log(e);
                return false;
            }
        })

    }

上面的代码就是保存用户权限的时候的操作

五十、根据用户的权限生成路由信息

当我们对当同的用户分配不同的权限以后,我们就可以根据它的权限来动态的生成路由了

后台src/services/UserPermissionInfoServices.ts代码

async getUserRouterByUserId(user_id: number) {
        let connection = this.getConnection();
        let result = await connection.getRepository(UserPermissionInfo).createQueryBuilder("user_permission_info")
            .innerJoinAndSelect("user_permission_info.permissionInfo", "permission_info")
            .where("user_permission_info.is_del=false")
            .andWhere("user_permission_info.user_id=:user_id", {user_id})
            .orderBy("permission_info.order_num", "ASC")
            .getMany();
        return result;
    }

上面的代码是获取当前用户的权限(这里之前写错了,只是获取了permission_type=0或1的权限,后来将这里删除了)

image-20230529091425590

在用户登录的时候,获取用户的权限信息,并通过currentUserPagePermission返回到前端

image-20230529091510598

前端登录成功以后,将数据保存在了pinia里面

image-20230529091540936

pinia在保存数据的时候,将数据进行保存的时候调用了buildRouterFromPagePermission来动态生成路由表

现在我们回到router/index.js下面看一下buildRouterFromPagePermission方法

image-20230529091707138

image-20230529091644046

完成上面的操作以后,我们就生成了动态路由了,现在就需要到LeftMenu.vue里面来生成菜单了

<template>
  <div class="menu-box">
    <div class="h-[60px] bg-[#263445] font-bold flex flex-row justify-center items-center text-white italic">
      <el-icon size="28" class="is-loading mr-[10px]">
        <Loading/>
      </el-icon>
      通用权限管理系统
    </div>
    <el-menu
        active-text-color="#ffd04b"
        background-color="#304156"
        :default-active="$route.fullPath"
        router
        text-color="#fff">

      <el-sub-menu v-for="(item,index) in menuData" :index="index" :key="index">
        <template #title>
          <el-icon>
            <component :is="item.permissionInfo.icon"></component>
          </el-icon>
          <span>{{ item.permissionInfo.permission_name }}</span>
        </template>
        <el-menu-item
            v-for="(sub_item,sub_index) in item.children"
            :index="sub_item.permissionInfo.front_path"
            :key="sub_item.permissionInfo.front_path">
          <el-icon>
            <component :is="sub_item.permissionInfo.icon"></component>
          </el-icon>
          {{ sub_item.permissionInfo.permission_name }}
        </el-menu-item>
      </el-sub-menu>
    </el-menu>
  </div>
</template>

<script setup>
import {computed} from "vue";
import {mainStore} from "@/store";

const store = mainStore();
//这里要根据页面权限来生成菜单了

const menuData = computed(() => {
  let menuList = [];
  let subMenuLsit = [];
  console.log(store.currentUserPagePermission);
  store.currentUserPagePermission.forEach(item => {
    if (item.permissionInfo.permission_type === 0) {
      menuList.push({
        ...item,
        children: []
      })
    } else if (item.permissionInfo.permission_type === 1) {
      subMenuLsit.push(item);
    }
  })

  subMenuLsit.forEach(item => {
    let parentMenu = menuList.find(v => v.permission_id === item.permissionInfo.parent_id);
    parentMenu.children.push(item);
  })

  return menuList;
});


</script>

<style scoped lang="scss">
.menu-box {
  @apply bg-[#304156] w-full h-full overflow-hidden;
}
</style>

经过上面的代码,我们就可以在而菜单的动态加载了

五十一、解决动态路由刷新页面空白的问题

image-20230529101232673

这一个问题是一个非常常见的问题,在百度上面搜索的时候也有很多解决办法,这里我只讲解一种常见的方法

因为我们的路由数据是使用 pinia进行了持久化存储,并且存储在sessionStorage里面,在持久化的时候,我们使用的是web-storage-cache这个插件,所以我们可以在加载页面的时候使用这个插件读取数据

image-20230529101341276

image-20230529101353492

现在我们回到main.js里面进行相关的操作

image-20230529101504622

完成上面的操作以后,我们再刷新的时候,就不会存在路由加载不及时而造成的页面空白了

五十二、按钮权限的控制

在前面的操作中,我们已经通过权限列表完成了页面的权限操作,现在我们需要对页面上面的按钮进行权限操作

在之前构建权限的数据表的时候,我们在数据表里面添加了一项数据permission_key的字段,这一项字段就是用于记录每一项权限的值的,如userInfo:add代表userInfo下面的add操作

现在我们就可以通过这个permission_key来进行

前端src/Login.vue代码

image-20230529102203087

前端src/store/index.js代码

image-20230529102145151

在上面的代码里面,我们可以看到,当我们在设置用户的权限的时候,我们就已经将用户的permission_key提取出来 ,放在了一个数组里面,存放在了pinia当中

有了这个数组以后,我们就可以通过全局指令来完成相应的按钮权限控制了

前端src/utils/directive.js代码

import {mainStore} from "@/store";
export const permissionCheck = (el,binding,vnode)=>{
    const store = mainStore();
    let permission_key = binding.value;
    let index =  store.permissionKeyList.findIndex(item=>item===permission_key);
    if(index===-1){
        el.remove();
    }
}

上面我们定义了一个自定义指令,如果在permissionKeyList里面找到了这个数组我们就保存这个元素,如果没有找到,就不保留这个元素

现在去使用一下

image-20230529102547153

经过使用以后发现,按钮权限控制正常

五十三、后台权限的控制

前台完成权限控制以后,我们的后台API接口也要完成权限控制

重新回到后台src/utils/permissionUtils.ts里面

import {NextFunction, Request, Response} from "express";
import ResultJson from "../dto/ResultJson";
import * as jwt from "jsonwebtoken";
import APPConfig from "../config/APPConfig";
import ServiceFactory from "../factory/ServiceFactory";

export const checkPermission = (permissionKey?: string) => {
    console.log(permissionKey);
    return async (req: Request, resp: Response, next: NextFunction) => {
        next();
        return ;
        //第1步:如果是options的预检请求,这个时候我们可以直接放行
        if (req.method.toUpperCase() === "OPTIONS") {
            return next();
        } else {
            //第2步:从header或query里面得到authorization的token
            let token = req.header("authorization") || req.query.authorization;
            //第3步:如果有token
            if (token) {
                try {
                    //第4步:对token进行鉴权,看token是否有效
                    let decode = jwt.verify(String(token), APPConfig.tokenSecret);
                    //第5步:如果token有效,我们就将得到的token保存在header方向后面继续使用
                    req.header["userInfo"] = decode;
                    //第6步:是否是泛权限,如果不是泛权限,就进一步检测权限
                    if(permissionKey){
                        //第7步:检测一下当前这个用户是否有这个权限
                        let flag = await ServiceFactory.createUserPermissionInfoService().checkUserPermission((decode as any).id, permissionKey)
                        if (flag) {
                            //说明当前用户有这个API的权限
                            return next();
                        }
                        else{
                            //说明当前用户没有这个API的权限
                            let resultJson = new ResultJson(false, "请求未授权");
                            resp.status(403).json(resultJson);
                        }
                    }
                    else{
                        //第8部:说明是泛权限,只要登录了的就可以放行
                        return next();
                    }

                } catch (e) {
                    let resultJson = new ResultJson(false, "令牌验证失败");
                    resp.status(403).json(resultJson);
                }
            } else {
                //如果没有token
                let resultJson = new ResultJson(false, "请求未授权");
                resp.status(403).json(resultJson);
            }
        }
    }
}

上面的代码就是一个权限验证的中间件代码,在路由的路径里面,我们就可以使用这个中间件进行权限验证

在之前我们其实已经讲过了,这里的权限划分的情况,后台的权限 们划分了3个等级

image-20230529113351849

所以在checkPermission这一个中间件里面,我们也做了3 次等级的验证

在使用的时候,可以通过下面的方式来进行

无需权限

router.get("/Login",async(req:Request,resp:Response)=>{})

上面的请求我们没有使用checkPermission则代表当前这个API接口不需要进行权限验证

泛权限

router.get("/getAllPermissionInfoList",checkPermission(),async(req:Request,resp:Response)=>{});

上面的请求我们使用了checkPermission中间件,但是我们并没有指定它需要使用权限,只是要验证token,所以这一种情况我们叫泛权限验证

具体权限

router.get("/deleteById/:id",checkPermission("userInfo:deleteById"),async(req:Request,resp:Response)=>{});

上面的请求我们使用了checkPermission("userInfo:deleteById")这个中间件,并传递了具体的permission_key这就说明一定要具备这个权限的用户才能使用这个API接口


五十四、问题汇总

  1. 如果时间充足的情况下,尽量做到,用户,角色,权限的完整操作,而不是直接使用用户,权限的操作

  2. 接口的编写一定要统一,不然后期的操作会非常麻烦,如下

    新增使用add,查询使用query,编辑使用edit,删除使用delete,这样后台在编写接口的时候也可以直接使用这几个基本操作,同时前台在进行页面与按钮的权限控制的时候也不容易弄混淆

  3. 这里的用户表只有一张,这样做起来比较简单,但是不方便,如果后期有的用户分为管理员,学生,老师等,而学生与老师又需要在不同的数据表里面,这样权限操作起来就会变得非常麻烦

0

评论区