项目重构,从零开始搭建一套新的后台管理系统(后端版)

简介: 项目重构,从零开始搭建一套新的后台管理系统(后端版)

背景介绍


这篇文章是之前写的文章的补充,由于之前写的文章有人评论说没有鉴权、动态路由、权限控制等功能,但是这些功能都是需要后台来配合的,这其中牵扯很多:


  1. 鉴权:也就是登录,需要单独写一篇文章来说;
  2. 动态路由:也就是根据用户的角色来动态生成路由,这个也需要单独写一篇文章来说;
  3. 权限控制:也就是根据用户的角色权限来控制页面功能,这个也需要单独写一篇文章来说;
  4. 项目架构:也需要单独拿出来介绍;
  5. 项目内部的细节实现:也需要单独拿出来介绍。


所以之前的文章只有前端相关的内容,任何涉及后端的内容都没有,其中包括登录页面的相关的功能都没有写;


写的都是架构相关的,例如技术栈、项目结构、页面布局、代码规范等等;


因为没有牵扯到后端,所以至少也是必须的网络请求封装没有写,路由跳转拦截控制没有写,状态管理控制也没有写,因为这些都是需要数据来支持的,也就是后端相关的内容;


所以这篇文章就是之前文章的补充,主要是后端相关的内容,但是这其中还得牵扯到前端的内容,前端是后端数据的消费者,所以这些内容必须配合的来写;


技术选型


这一篇是后端的内容,所以主要是后端的技术选型,前端的技术选型在之前的文章中已经介绍过了,这里就不再赘述了;


  • 后端技术栈


  • 语言:nodejs
  • 框架:express
  • 数据库:postgresql
  • 鉴权:JWT


比较简单,没有上什么花里胡哨的技术,本来就是重构,并不是炫技。


目录结构

├── api                             # 三层架构
│   ├── dao                         # 数据访问层
│   │   └──  system                 # 系统管理
│   │        └── user.js            # 用户管理
│   ├── models                      # 数据模型
│   │   ├──  system                 # 系统管理
│   │   │    └── user.js            # 用户管理
│   │   └── associate.js            # 统一维护关联关系
│   └── routers                     # 路由
│       ├──  system                 # 系统管理
│       │    └── user.js            # 用户管理
│       └──  index.js               # 路由统筹注册文件
├── assets                          # 静态资源
├── common                          # 全局公共文件
├── public                          # 公开资源
├── utils                           # 工具函数
│   ├──  jwt                        # jwt
│   │    ├── jwt_middleware.js      # jwt 中间件
│   │    └── jwt_store.js           # jwt 存储
│   └──  result                     # 全局返回结果
│        ├── code_enum.js           # 全局返回枚举
│        └── index.js               # 全局返回结果
├── .gitignore                      # git忽略文件
├── app.js                          # 入口文件
├── package.json                    # 项目依赖
├── README.md                       # 项目介绍
└── webpack.config.js               # webpack配置文件

命名规范


后台管理API为三层架构,分别为daomodelsrouters


commonutils为全局公共文件,他们的作用差不多,可以合并到一起,也可以根据自己的喜好来。


公司要求变量名全都是小写,单词之间用下划线分割,例如:user_name


其他的命名规范,例如文件夹、文件名等,都是根据功能模块来命名的,例如:system(系统管理功能模块)。


dao


dao层为数据访问层,主要负责数据库的增删改查操作,以及一些复杂的业务逻辑。


models


models层为数据模型层,主要负责数据的校验,以及数据的转换。


routers


routers层为路由层,主要负责路由的注册,以及路由的处理。



上面的三层架构,每一层根据功能模块进行划分,每一个功能模块都有一个对应的文件夹,文件夹下面有对应功能的文件。


例如上面的目录结构中:


  • dao层 -> system(系统管理功能模块)文件夹 -> user.js(用户管理功能)文件
  • models层 -> system(系统管理功能模块)文件夹 -> user.js(用户管理功能)文件
  • routers层 -> system(系统管理功能模块)文件夹 -> user.js(用户管理功能)文件


禁止在router层直接操作数据库,所有的数据库操作都在dao层进行。


代码规范


这里没有安装eslint,所以代码规范需要自己遵守。


数据库配置


公司要求使用的是postgresql数据库,所以这里就使用postgresql数据库,数据库连接使用sequelize来连接。


sequelize是一个ORM框架,可以让我们不用写sql语句,就可以操作数据库。


我这里是将数据库相关写到了common文件夹下的sequelize目录下,然后在app.js中引入,然后在app.js中进行初始化。

const {Sequelize} = require("sequelize");
const pg = require("pg");
const minimist = require("minimist");
const argv = minimist(process.argv.slice(2), {string: ["_"]});
let sequelize = {};
const _config = {
  database: "db_cms",
  username: "postgres",
  password: "123456",
  options: {
    host: "127.0.0.1",
    port: 5432,
    dialect: "postgres", /* 选择 'mysql' | 'mariadb' | 'postgres' | 'mssql' 其一 */
    logging: false,
    define: {
      freezeTableName: true,
      createdAt: "created_time",
      updatedAt: "updated_time",
      timestamps: true,
    },
    // dialectOptions: {
    //   charset: 'utf8',
    //   dateStrings: true,
    //   typeCast: true
    // },
    // timezone: '+08:00'
  }
};
const init_sequelize = (config) => {
  config = config || _config;
  sequelize = new Sequelize(
    config.database,
    config.username,
    config.password,
    {
      ...config.options,
      dialectModule: pg
    }
  );
  // 异步挂载关联关系
  Promise.resolve().then(() => {
    require("../../api/models/associate");
  });
  return sequelize;
};
init_sequelize();
if (argv.NODE_ENV && argv.NODE_ENV === "development") {
  sequelize.sync().then(() => {
    console.log("数据库初始化成功");
  });
}
module.exports = {
  init_sequelize,
  sequelize
};

这个代码就不需要解释了吧,就是初始化数据库连接,看不明白的可以去看看sequelize的文档:sequelize


sequelize有一个很棒的特性就是可自定义实体,也就是模型,这一块上面的目录结构中的api/models文件夹就是用来存放模型的。


并且可以对模型(表空间)创建关联关系,这一块上面的目录结构中的api/models/associate.js文件就是用来维护关联关系的。


上面导出了init_sequelize方法,但是其实可以不需要导出,因为在这个里面已经自执行了,因为我是准备做多数据源切换的,所以会导出用于重置数据库连接,当然示例代码将这一块移除了,因为这一块不是重点;


数据库的模型啥的都不用说了,就是定义模型,然后定义关联关系,这里就不贴代码了,大家可以自己去看看,文末会有仓库地址。


路由配置


项目使用的技术栈是express,所以路由配置也是使用express的路由配置;


express也就不多介绍了吧,看的懂的不用说,看不懂的不是一两句话能说清楚的,文档地址:express


路由配置在api/routers文件夹下,当我们写完路由相关代码之后,都需要导出然后在app.js中引入,然后在app.js中进行注册。


我这里把注册的一步全都放在了api/routers/index.js文件中,然后在app.js中引入,然后在app.js中进行注册。

// 系统管理
const user = require("./system/user");
const role = require("./system/role");
const menu = require("./system/menu");
const dict = require("./system/dict");
module.exports = function (app) {
  // 系统管理
  app.use("/api/user", user);
  app.use("/api/role", role);
  app.use("/api/menu", menu);
  app.use("/api/dict", dict);
};

这里导出的是一个方法,这个方法接收一个参数,就是expressapp对象,然后在这个方法中进行路由的注册。


全局返回


全局返回是为了统一返回格式,我这里在utils文件夹下有一个result文件夹,这里面的文件就是用来统一返回格式的。


  • code_enum.js:这个文件是用来定义返回码的,这里面的返回码是根据业务来定义的,这里面的返回码是我自己定义的,大家可以根据自己的业务来定义返回码;
  • result.js:这个文件是用来定义返回格式的,这里面的返回格式是我自己定义的,大家可以根据自己的业务来定义返回格式;


code_enum.js文件代码如下,这个就是模仿TSenum的语法写的,其实完全没必要:

const code_enum = {
  SUCCESS: 0, // 成功
  0: "SUCCESS",
  ERROR: 500, // 通用错误
  500: "ERROR",
  ERROR_PARAM: 501, // 参数错误
  501: "ERROR_PARAM",
  ERROR_DB: 502, // 数据库错误
  502: "ERROR_DB",
  ERROR_AUTH: 503, // 权限错误
  503: "ERROR_AUTH",
  ERROR_TOKEN: 504, // token错误
  504: "ERROR_TOKEN",
  ERROR_FILE: 505, // 文件错误
  505: "ERROR_FILE",
  ERROR_UNKNOWN: 506, // 未知错误
  506: "ERROR_UNKNOWN",
  ERROR_NOT_FOUND: 507, // 未找到
  507: "ERROR_NOT_FOUND",
  ERROR_NOT_SUPPORT: 508, // 不支持
  508: "ERROR_NOT_SUPPORT",
  ERROR_NOT_LOGIN: 509, // 未登录
  509: "ERROR_NOT_LOGIN",
  ERROR_NOT_PERMISSION: 510, // 无权限
  510: "ERROR_NOT_PERMISSION",
  ERROR_NOT_EXIST: 511, // 不存在
  511: "ERROR_NOT_EXIST",
  ERROR_EXIST: 512, // 已存在
  512: "ERROR_EXIST",
  ERROR_NOT_ALLOW: 513, // 不允许
  513: "ERROR_NOT_ALLOW",
  ERROR_NOT_VALID: 514, // 无效
  514: "ERROR_NOT_VALID",
  ERROR_NOT_MATCH: 515, // 不匹配
  515: "ERROR_NOT_MATCH",
  ERROR_NOT_ENOUGH: 516, // 不足
  516: "ERROR_NOT_ENOUGH",
};
const message = {
  SUCCESS: "成功",
  0: "成功",
  ERROR: "服务器内部错误",
  500: "服务器内部错误",
  ERROR_PARAM: "参数错误",
  501: "参数错误",
  ERROR_DB: "数据库错误",
  502: "数据库错误",
  ERROR_AUTH: "权限错误",
  503: "权限错误",
  ERROR_TOKEN: "token错误",
  504: "token错误",
  ERROR_FILE: "文件错误",
  505: "文件错误",
  ERROR_UNKNOWN: "未知错误",
  506: "未知错误",
  ERROR_NOT_FOUND: "未找到",
  507: "未找到",
  ERROR_NOT_SUPPORT: "不支持",
  508: "不支持",
  ERROR_NOT_LOGIN: "未登录",
  509: "未登录",
  ERROR_NOT_PERMISSION: "无权限",
  510: "无权限",
  ERROR_NOT_EXIST: "不存在",
  511: "不存在",
  ERROR_EXIST: "已存在",
  512: "已存在",
  ERROR_NOT_ALLOW: "不允许",
  513: "不允许",
  ERROR_NOT_VALID: "无效",
  514: "无效",
  ERROR_NOT_MATCH: "不匹配",
  515: "不匹配",
  ERROR_NOT_ENOUGH: "不足",
  516: "不足",
};
code_enum.getMsg = function (code) {
    return message[code] || message[Code_enum.ERROR_NOT_FOUND];
};
module.exports = code_enum;

result.js文件代码如下:

class Result {
  /**
   * 构造函数
   * 重载:
   * 1. new Result(code, message, data)
   * 2. new Result(code, data)
   *
   * @param {number|string} code
   * @param {string|object?} message
   * @param {any?} data
   */
  constructor(code, message, data) {
    if (code == null) {
      throw new Error("code is null");
    }
    if (typeof message === "object") {
      const temp = data;
      data = message;
      if (typeof data === "string") {
        message = temp;
      } else {
        message = null;
      }
    }
    if (message == null) {
      message = code_enum.getMsg(code);
    }
    this.code = code;
    this.message = message;
    this.data = data;
    if (this.code === code_enum.ERROR) {
      console.error(this.message, this.data);
    }
  }
  /**
   * 静态函数,成功返回
   * 重载:
   * 1. success(data)
   * 2. success(data, message)
   * 3. success(message, data)
   *
   * @param {string|object} message 消息内容
   * @param {any?} data 数据
   * @return {Result}
   */
  static success(message, data) {
    if (typeof message === "object") {
      if (typeof message === "object") {
        const temp = data;
        data = message;
        if (typeof data === "string") {
          message = temp;
        } else {
          message = null;
        }
      }
    }
    if (message == null) {
      message = code_enum.getMsg(code_enum.SUCCESS);
    }
    return new Result(code_enum.SUCCESS, message, data);
  }
  /**
   * 静态函数,失败返回
   * 重载:
   * 1. fail(data)
   * 2. fail(data, message)
   * 3. fail(message, data)
   *
   * @param {string|object} message 消息内容
   * @param {any?} data 数据
   * @return {Result}
   */
  static fail(message, data) {
    if (typeof message === "object") {
      if (typeof message === "object") {
        const temp = data;
        data = message;
        if (typeof data === "string") {
          message = temp;
        } else {
          message = null;
        }
      }
    }
    if (message == null) {
      message = code_enum.getMsg(code_enum.ERROR);
    }
    return new Result(code_enum.ERROR, message, data);
  }
  send(res) {
    res.send(this);
  }
}
module.exports = Result;

这两个文件都直接挂在全局下,不需要导出,直接使用即可。

global.Result = Result;
global.code_enum = code_enum;
// 使用
Result.success("成功", {name: "张三", age: 18}).send(res);
Result.success({name: "张三", age: 18}).send(res);
Result.fail("失败", {name: "张三", age: 18}).send(res);
Result.fail({name: "张三", age: 18}).send(res);
new Result(code_enum.SUCCESS, "成功", {name: "张三", age: 18}).send(res);

这样我们在路由中就可以直接使用了。

router.get("/test", (req, res) => {
    db.action().then(data => {
        Result.success(data).send(res);
    }).catch(err => {
        Result.fail(err).send(res);
    })
});

app.js


这次就写这么多,有了这个框架之后,我们才能进行后续的开发,然后附上app.js文件代码。

const express = require("express");
const http = require("http");
const Result = require("./utils/result");
const code_enum = require("./utils/result/code_enum");
const socket_io = require("./common/socket.io");
// 定义全局变量
global.Result = Result;
global.code_enum = code_enum;
global.SECRET = "xxx";
global.jwt_header_key = "token";
const app = express();
// 防止 content-encoding 请求头错误报错
app.use((req, res, next) => {
  if (req.headers["content-encoding"] === "utf-8") {
    delete req.headers["content-encoding"];
  }
  next();
  // express.json({limit: "50mb"})(req, res, next);
});
// 解析 application/json
app.use(express.json());
// 解析 urlencoded
app.use(express.urlencoded({extended: false}));
// jwt 中间件拦截器
const {
  unless,
  jwt_error_handler,
  jwt_renew
} = require("./utils/jwt/jwt_middleware");
app.use('/api', unless, jwt_error_handler);
// token 续期
app.use(jwt_renew);
// 公共资源访问
app.use("/public", express.static(__dirname + "/public"));
// 路由注册
const router = require("./api/routes");
router(app);
// 全局错误处理
process.on("uncaughtException", (e) => {
  console.error(e); // Error: uncaughtException
  // do something: 释放相关资源(例如文件描述符、句柄等)
  // process.exit(1); // 手动退出进程
});
const server = http.createServer(app);
socket_io(server);
// 启动服务
const port = process.env.PORT || 3000;
server.listen(port, () => {
  console.log("http://localhost:" + port);
});

这里会有很多上面没有提到的代码,这些代码后续再说,这里只是为了让大家知道,这个框架是怎么写的。


下一篇就开始写登录相关的代码,也就是jwt的使用,随缘更新,不想等的可以直接看仓库,里面有完整的代码。



目录
相关文章
|
9天前
|
缓存 前端开发 Go
从4开始,在后端系统中增加用户注册和登录功能
从4开始,在后端系统中增加用户注册和登录功能
12 0
|
9天前
|
JSON Go 数据格式
从1开始,扩展Go语言后端业务系统的RPC功能
从1开始,扩展Go语言后端业务系统的RPC功能
22 0
|
5月前
|
前端开发 关系型数据库 MySQL
TDesign中后台管理系统-访问后端服务
TDesign中后台管理系统-访问后端服务
|
1天前
|
数据采集 JavaScript API
第三方系统访问微搭低代码的后端API
第三方系统访问微搭低代码的后端API
|
9天前
|
缓存 NoSQL Go
从2开始,在Go语言后端业务系统中引入缓存
从2开始,在Go语言后端业务系统中引入缓存
18 0
|
9天前
|
SQL JSON 前端开发
从0开始,用Go语言搭建一个简单的后端业务系统
从0开始,用Go语言搭建一个简单的后端业务系统
27 0
|
1月前
|
安全 Java 数据库
后端进阶之路——Spring Security构建强大的身份验证和授权系统(四)
后端进阶之路——Spring Security构建强大的身份验证和授权系统(四)
|
2月前
|
缓存 关系型数据库 数据库
构建高效后端系统的数据库优化策略
在后端开发中,数据库是核心组件之一,对系统性能和稳定性有着重要影响。本文将介绍一些常见的数据库优化策略,包括数据模型设计、索引优化、查询优化和缓存策略等,在实际开发中帮助提升后端系统的性能和响应速度。
|
3月前
|
Web App开发 JavaScript 前端开发
【读书后台管理系统】—后端框架搭建(二)
【读书后台管理系统】—后端框架搭建(二)
|
3月前
|
关系型数据库 MySQL Java
Linux系统jdk&Tomcat&MySQL安装以及J2EE后端接口部署
Linux系统jdk&Tomcat&MySQL安装以及J2EE后端接口部署
40 0