三、全栈项目-个人博客服务端设计

郁子大约 14 分钟约 4108 字笔记渡一教育强化课谢杰

(一)三层架构

1.简介

引用自百度百科的解释

  • 三层架构,3-tier architecture
  • 通常意义上的三层架构就是将整个业务应用划分为:
    • 界面层 User Interface layer
    • 业务逻辑层 Business Logic Layer
    • 数据访问层 Data access layer
  • 区分层次的目的是为了 高内聚低耦合 的思想
  • 在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构
  • 微软推荐的分层式结构一般分为三层,从上至下分别为
    • 表示层
    • 业务逻辑层
    • 数据访问层(也称之为持久层)

作用
表示层接收用户请求,返回数据,为客户端提供应用程序的访问
业务逻辑层负责操作数据层,组合数据层的操作
数据访问层取决于数据层是否包含逻辑处理,各个函数主要完成对各个数据文件的操作,不必管其他操作

2.简单实现

(二)项目准备

1.使用 Express 脚手架搭建项目

# 全局安装 express 脚手架
npm i -g express-generator

# 查看脚手架版本
express --version

# 创建项目
express 项目名
// package.json
{
  "name": "00mypractice",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "express": "~4.16.0",
    "http-errors": "~1.6.2",
    "jade": "~1.11.0",
    "morgan": "~1.9.0"
  }
}

2.修改端口和启动输出

// bin/www
var port = normalizePort(process.env.PORT || "3001");

function onListening() {
  var addr = server.address();
  var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
  debug("Listening on " + bind);
  console.log("服务器端已启动,监听 3001 端口...");
}

3.按照三层架构修改目录

目录含义
routes表示层
service业务逻辑层
dao数据访问层
public静态文件目录

1)移除模板引擎

  • 删除 views 目录
  • 删除 app.js 中对 views 目录的引用

2)全局安装 nodemon

npm i -g nodemon
nodemon npm start

# npm start # 需要配置启动脚本
  • 配置启动脚本
"scripts": {
  "start": "nodemon -x npm run server",
  "server": "node ./bin/www"
}

4.连接 MySQL 数据库

1)安装 Sequelize 和 MySQL2 驱动

"dependencies": {
  "mysql2": "^2.3.3",
  "sequelize": "^6.13.0"
}

2)创建数据库并配置连接

// dao/dbConnect.js
const { Sequelize } = require("sequelize");

// 创建数据库连接
const sequelize = new Sequelize("mysite", "root", "123123", {
  host: "localhost",
  dialect: "mysql",
  logging: false,
});

module.exports = sequelize;

// 测试连接
(async () => {
  try {
    await sequelize.authenticate();
    console.log("Connection has been established successfully.");
  } catch (error) {
    console.error("Unable to connect to the database:", error);
  }
})();

3)入口文件引入数据库

// app.js
require("./dao/dbConnect");

5.提取配置信息到单独文件

1)安装第三方库

npm i dotenv

2)根目录新建环境变量文件

# .env
DB_NAME=mysite
DB_USER=root
DB_PASS=123123
DB_HOST=localhost

3)入口文件引入数据库

// app.js
// 默认读取项目根目录下的 .env 环境变量文件
require("dotenv").config();
// 引入数据库
require("./dao/dbConnect");

4)数据库配置文件中使用

const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
  host: process.env.DB_HOST,
  dialect: "mysql",
  logging: false,
});

(三)编写错误处理类

1.业务处理错误基类

// utils/errors.js

const { formatResponse } = require("./tool");
class ServiceError extends Error {
  /**
   * 构造函数
   * @param {String} message 错误消息
   * @param {Number} code 错误消息码
   */
  constructor(message, code) {
    super(message);
    this.code = code;
  }

  /**
   * 以JSON格式将错误信息响应给客户端
   */
  toResponseJSON() {
    return formatResponse(this.code, this.message, null);
  }
}

2.自定义错误

/**
 * 文件上传错误
 */
exports.UploadError = class extends ServiceError {
  constructor(message) {
    super(message, 413);
  }
};

/**
 * 禁止访问错误
 */
exports.ForbiddenError = class extends ServiceError {
  constructor(message) {
    super(message, 401);
  }
};

/**
 * 验证错误
 */
exports.ValidationError = class extends ServiceError {
  constructor(message) {
    super(message, 406);
  }
};

/**
 * 无资源错误
 */
exports.NotfoundError = class extends ServiceError {
  constructor() {
    super("Not found", 406);
  }
};

/**
 * 未知错误
 */
exports.UnknownError = class extends ServiceError {
  constructor() {
    super("Server internal error", 500);
  }
};

module.exports.ServiceError = ServiceError;

(四)实现登录功能

1.定义管理员数据模型

// dao/model/adminModel.js
const { DataTypes } = require("sequelize");
const sequelize = require("../dbConnect");

// 定义数据模型
module.exports = sequelize.define(
  "admin",
  {
    loginId: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    loginPwd: {
      type: DataTypes.STRING,
      allowNull: false,
    },
  },
  {
    freezeTableName: true,
    createdAt: false,
    updatedAt: false,
  },
);

2.初始化数据库

// dao/db.js
// 数据库连接实例
const sequelize = require("./dbConnect");

// 数据模型
const adminModel = require("./model/adminModel");

const md5 = require("md5");

(async () => {
  // 同步数据模型和数据表
  await sequelize.sync({
    alter: true,
  });

  // 同步完成后部分表生成初始化数据
  const adminCount = await adminModel.count();
  if (!adminCount) {
    // 该表没有数据
    await adminModel.create({
      loginId: "admin",
      name: "超级管理员",
      loginPwd: md5("123456"),
    });
    console.log("初始化管理员数据完毕...");
  }

  console.log("数据库数据同步完毕...");
})();

3.定义路由层

1)定义 API 接口

// routes/admin.js
const express = require("express");
const router = express.Router();

const { loginService } = require("../service/adminService");

// 管理员登录
router.post("/login", async (req, res, next) => {
  // 请求时会拼接 app.js 中的一级路由 —— /api/admin/login

  // 校验验证码
  if (req.body.captcha.toLowerCase() !== req.session.captcha.toLowerCase()) {
    throw new ValidationError("验证码错误");
  }

  const result = await loginService(req.body);
  console.log(result);
});

module.exports = router;

2)引入

  • 要在初始化数据库后引入
// app.js
const adminRouter = require("./routes/admin");
app.use("/api/admin", adminRouter);

4.定义业务逻辑层

// service/adminService.js
const md5 = require("md5");

const { loginDao } = require("../dao/adminDao");

// 登录
module.exports.loginService = async (loginInfo) => {
  loginInfo.loginPwd = md5(loginInfo.loginPwd);

  // 数据验证 —— 查询数据库
  let data = await loginDao(loginInfo);
  if (data && data.dataValues) {
    // TODO 添加token
  }

  return {
    data,
  };
};

5.定义数据层

// dao/adminDao.js
const adminModel = require("./model/adminModel");

// 登录
module.exports.loginDao = async (loginInfo) => {
  return await adminModel.findOne({
    where: {
      loginId: loginInfo.loginId,
      loginPwd: loginInfo.loginPwd,
    },
  });
};

三层架构流程

  1. 客户端请求 API 接口 /api/admin/login
  2. 先执行服务器路由层 routes/admin.js
  3. 如果需要处理业务逻辑,转去执行业务逻辑层 service/adminService.js
  4. 如果需要操作数据库,转去执行数据层 dao/adminDao.js
  5. 数据层通过模型操作数据库 dao/model/adminModel.js
  6. 最后数据从数据层依次返回到路由层,响应给客户端

(五)token 的生成和验证

1.安装第三方库

"dependencies": {
  "jsonwebtoken": "^8.5.1",
  "express-jwt": "^6.1.0"
}

2.完善登录业务逻辑

  • 生成 token
module.exports.loginService = async (loginInfo) => {
  loginInfo.loginPwd = md5(loginInfo.loginPwd);

  // 数据验证 —— 查询数据库
  let data = await loginDao(loginInfo);
  if (data && data.dataValues) {
    // 添加token
    data = {
      id: data.dataValues.id,
      loginId: data.dataValues.loginId,
      name: data.dataValues.name,
    };
    // 是否登录七天
    const loginPeriod = loginInfo.remember ? +loginInfo.remember : 1;
    // 生成token
    const token = jwt.sign(data, md5(process.env.JWT_SECRET), {
      expiresIn: 24 * 60 * 60 * loginPeriod,
    });
    return {
      token,
      data,
    };
  }

  return {
    data,
  };
};

3.完善登录路由处理

1)定义格式化响应信息工具函数

// utils/tool.js
/**
 * 格式化响应信息
 * {
 *    "code": code,
 *    "msg": msg,
 *    "data": data
 * }
 */
module.exports.formatResponse = (code, msg, data) => {
  return {
    code,
    msg,
    data,
  };
};

2)路由层处理

const { formatResponse } = require("../utils/tool");
router.post("/login", async (req, res, next) => {
  // 请求时会拼接 app.js 中的一级路由 —— /api/admin/login

  // TODO 校验验证码

  const result = await loginService(req.body);
  if (result.token) {
    res.setHeader("authentication", result.token);
  }
  res.send(formatResponse(0, "", result.data));
});

3)验证 token

  • 在路由中间件之前配置验证接口
  • 如果验证失败,不会调用路由中间件
  • 直接执行错误处理中间件
// app.js
const expressJWT = require("express-jwt");
// 引入路由
const adminRouter = require("./routes/admin");
// 配置验证token接口
app.use(
  expressJWT({
    secret: md5(process.env.JWT_SECRET),
    algorithms: ["HS256"], // 新版本express-jwt要求必须指定算法
  }).unless({
    // 需要排除token验证的路由
    path: [
      {
        url: "/api/admin/login",
        methods: ["POST"],
      },
    ],
  }),
);
// 使用路由中间件
app.use("/api/admin", adminRouter);
  • 解析 token
// utils/tool.js
const jwt = require("jsonwebtoken");
const md5 = require("md5");
module.exports.analysisToken = (token) => {
  return jwt.verify(token.split(" ")[1], md5(process.env.JWT_SECRET));
};
  • 路由层恢复登录
// routes/admin.js
const { analysisToken } = require("../utils/tool");
router.get("/whoami", async (req, res, next) => {
  // 解析token
  const token = analysisToken(req.get("Authorization"));
  res.send(
    formatResponse(0, "", {
      id: token.id,
      loginId: token.loginId,
      name: token.name,
    }),
  );
});
  • 错误处理中间件
// app.js
const { ForbiddenError } = require("./utils/errors");
app.use(function (err, req, res, next) {
  if (err.name === "UnauthorizedError") {
    // token验证错误
    res.send(new ForbiddenError("未登录,或者登录凭证已过期").toResponseJSON());
  } else if (err instanceof ServiceError) {
    res.send(err.toResponseJSON());
  } else {
    res.send(new UnknownError().toResponseJSON());
  }
});

(六)完成管理员模块

1.定义更新管理员信息接口

router.put("/", async (req, res, next) => {
  res.send(await updateAdminService(req.body));
});

2.定义业务逻辑

module.exports.updateAdminService = async (accountInfo) => {
  // 根据传入的账号信息查询管理员
  const adminInfo = await loginDao({
    loginId: accountInfo.loginId,
    loginPwd: md5(accountInfo.oldLoginPwd),
  });
  if (adminInfo && adminInfo.dataValues) {
    // 修改密码
    const result = await updateAdminDao({
      loginId: accountInfo.loginId,
      loginPwd: md5(accountInfo.loginPwd),
      name: accountInfo.name,
    });
    return formatResponse(0, "", {
      id: adminInfo.dataValues.id,
      loginId: accountInfo.loginId,
      name: accountInfo.name,
    });
  } else {
    // 密码不正确
    throw new ValidationError("旧密码不正确");
  }
};

3.定义数据交互

module.exports.updateAdminDao = async (newInfo) => {
  return await adminModel.update(newInfo, {
    where: {
      loginId: newInfo.loginId,
    },
  });
};

4.express 捕获异步错误

  • 安装第三方库
"dependencies": {
  "express-async-errors": "^3.1.1"
}
  • app.js 引入
// 捕获异步错误
require("express-async-errors");

(七)制作验证码

1.获取验证码并保存到 session

express-sessionopen in new window

// routes/captcha.js
router.get("/", async (req, res, next) => {
  // 生成验证码
  const captcha = await getCaptchaService();
  // 保存到session
  req.session.captcha = captcha.text;
  // 设置响应头
  res.setHeader("Content-Type", "images/svg+xml");
  res.send(captcha.data);
});
  • 使用 session
// app.js
const session = require("express-session");
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: true,
    saveUninitialized: true,
  }),
);

2.生成验证码

svg-captchaopen in new window

// service/captchaService.js
const svgCaptcha = require("svg-captcha");
module.exports.getCaptchaService = async () => {
  return svgCaptcha.create({
    size: 4,
    ignoreChars: "iIl10Oo",
    noise: 6,
    color: true,
  });
};

(八)完成首页标语模块

(九)实现上传文件功能

1.工具函数

// utils/tool.js
const multer = require("multer");
const storage = multer.diskStorage({
  // 文件存储位置
  destination: function (req, file, cb) {
    cb(null, __dirname + "/../public/static/uploads");
  },
  // 处理上传文件的文件名
  filename: function (req, file, cb) {
    // 获取后缀名
    const extname = path.extname(file.originalname);
    // 获取文件名
    const basename = path.basename(file.originalname, extname);
    const newName = basename + new Date().getTime() + Math.floor(Math.random() * 9000 + 1000) + extname;
    cb(null, newName);
  },
});
module.exports.uploading = multer({
  storage,
  limits: {
    fileSize: 2 * 1024 * 1024,
    files: 1,
  },
});

2.API 接口

const multer = require("multer");
const { UploadError } = require("../utils/errors");
const { uploading, formatResponse } = require("../utils/tool");

router.post("/", async (req, res, next) => {
  // 参数是前端上传控件的name值
  const upload = uploading.single("file");
  upload(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      next(new UploadError("上传文件失败,请检查文件大小,控制在2MB内"));
    }
    res.send(formatResponse(0, "", "/static/uploads/" + req.file.filename));
  });
});

(十)完成文章类型模块

Validate.jsopen in new window

1.安装验证第三方库

"dependencies": {
  "validate.js": "^0.13.1"
}

2.业务逻辑层验证数据

// service/blogTypeService.js
const { validate } = require("validate.js");

// 新增博客分类
module.exports.addBlogTypeService = async (newBlogTypeInfo) => {
  // 数据验证规则
  const blogTypeRule = {
    name: {
      presence: {
        allowEmpty: false,
      },
      type: "string",
    },
    order: {
      presence: {
        allowEmpty: false,
      },
      type: "string",
    },
  };
  // 进行数据验证
  const validateResult = validate.validate(newBlogTypeInfo, blogTypeRule);
  if (!validateResult) {
    // 验证通过
    newBlogTypeInfo.articleCount = 0; // 因为是新增的文章分类,所以一开始文章数量为 0
    const data = await addBlogTypeDao(newBlogTypeInfo);
    return formatResponse(0, "", data);
  } else {
    // 验证失败
    throw new ValidationError("数据验证失败");
  }
};

(十一)完成文章模块

1.定义模型之间的关联关系

// dao/db.js
const blogTypeModel = require("./model/blogTypeModel");
const blogModel = require("./model/blogModel");

(async () => {
  // 定义模型之间的关联关系
  blogTypeModel.hasMany(blogModel, {
    foreignKey: "categoryId",
    targetKey: "id",
  });
  blogModel.belongsTo(blogTypeModel, {
    foreignKey: "categoryId",
    targetKey: "id",
    as: "category",
  });
  console.log("数据库数据同步完毕...");
})();

2.扩展验证规则

// service/blogService.js
const { validate } = require("validate.js");

validate.validators.categoryIdIsExist = async (value) => {
  const blogTypeInfo = blogTypeModel.findByPk(value);
  if (blogTypeInfo) {
    return;
  }
  return "CategoryId Is Not Exist";
};

3.异步验证

// service/blogService.js
module.exports.addBlogService = async (newBlogInfo) => {
  // TODO 处理 TOC

  // 将处理好的TOC格式转为字符串
  newBlogInfo.toc = JSON.stringify('["a":"b"]');

  // 初始化新文章的其他信息
  newBlogInfo.scanNumber = 0; // 阅读量初始化为 0
  newBlogInfo.commentNumber = 0; // 评论数初始化为 0

  // 定义验证规则
  const blogRule = {
    // ......
    categoryId: {
      presence: true,
      type: "integer",
      categoryIdIsExist: true,
    },
  };

  // 验证数据
  try {
    // 因为扩展的验证规则里面涉及到异步的操作,所以要采用异步的验证方式
    await validate.async(newBlogInfo, blogRule);
    const data = await addBlogDao(newBlogInfo); // 新增
    // 文章新增了,对应的文章分类也应该新增
    await addBlogToType(newBlogInfo.categoryId);
    return formatResponse(0, "", data);
  } catch (e) {
    // 验证未通过
    throw new ValidationError("数据验证失败");
  }
};

4.根据 id 新增文章分类

// dao/blogTypeDao.js
module.exports.addBlogToType = async (id) => {
  const data = await blogTypeModel.findByPk(id);
  data.articleCount++;
  await data.save();
  return;
};

(十二)处理 TOC 目录

1.安装第三方库

markdown-tocopen in new window

"dependencies": {
  "markdown-toc": "^1.2.0"
}

2.定义业务逻辑

// service/blogService.js
module.exports.addBlogService = async (newBlogInfo) => {
  // 处理TOC
  newBlogInfo = handleTOC(newBlogInfo);
  // 将处理好的TOC格式转为字符串
  newBlogInfo.toc = JSON.stringify(newBlogInfo.toc);

  // ......
};

3.定义工具函数

// utils/tool.js
const toc = require("markdown-toc");
module.exports.handleTOC = (info) => {
  let result = toc(info.markdownContent).json;

  // 经过上面 toc 方法的处理,就将整个 markdown 里面的标题全部提取出来了
  // 形成一个数组,数组里面是一个个对象,每个对象记录了标题的名称以及等级,如下:

  // [
  //     { content: '数值类型概述', slug: '数值类型概述', lvl: 2, i: 0, seen: 0 },
  //     { content: '整数和浮点数', slug: '整数和浮点数', lvl: 3, i: 1, seen: 0 },
  //     { content: '数值精度', slug: '数值精度', lvl: 4, i: 2, seen: 0 },
  //     { content: '数值范围', slug: '数值范围', lvl: 3, i: 3, seen: 0 },
  //     { content: '数值的表示方法', slug: '数值的表示方法', lvl: 2, i: 4, seen: 0 }
  // ]

  // 但是这不是我们想要的格式,我们想要转换为
  // [
  //     { "name": "章节1", "anchor": "title-1" },
  //     { "name": "章节2", "anchor": "title-2",
  //         "children": [
  //             { "name": "章节2-1", "anchor": "title-2-1" },
  //             { "name": "章节2-2", "anchor": "title-2-2" },
  //         ]
  //     }
  // ]

  // 转换格式
  const transfer = (flatArr) => {
    const stack = [],
      result = [];

    // 创建toc对象
    const createTOCItem = (item) => ({
      name: item.content,
      author: item.slug,
      level: item.lvl,
      children: [],
    });

    // 处理toc子级对象
    const handleTOCItem = (item) => {
      // stack为空返回undefined,stack不为空则返回最后一个元素【栈顶】
      const top = stack[stack.length - 1];
      if (!top) {
        stack.push(item);
      } else if (item.level > top.level) {
        // toc等级比栈顶(上一个toc对象)大,应该成为上一个toc对象的子级
        top.children.push(item);
        stack.push(item);
      } else {
        stack.pop();
        handleTOCItem(item);
      }
    };

    // 标题最小级别
    let min = 6;
    // 寻找最小级别的标题
    for (const i of flatArr) {
      if (i.lvl < min) min = i.lvl;
    }

    for (const item of flatArr) {
      const tocItem = createTOCItem(item);
      if (tocItem.level === min) {
        // 当前目录不会是children
        result.push(tocItem);
      }
      // 有可能是其他目录的子级
      handleTOCItem(tocItem);
    }

    return result;
  };

  info.toc = transfer(result);

  delete info.markdownContent;

  // 为各个级别的标题添加id
  for (const i of result) {
    let newStr = "";
    switch (i.lvl) {
      case 1:
        newStr = `<h1 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h1>", newStr);
        break;
      case 2:
        newStr = `<h2 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h2>", newStr);
        break;
      case 3:
        newStr = `<h3 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h3>", newStr);
        break;
      case 4:
        newStr = `<h4 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h4>", newStr);
        break;
      case 5:
        newStr = `<h5 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h5>", newStr);
        break;
      case 6:
        newStr = `<h6 id="${i.slug}">`;
        info.htmlContent = info.htmlContent.replace("<h6>", newStr);
        break;
    }
  }

  return info;
};

(十三)完成项目管理模块

(十四)完成评论管理和留言板模块

1.随机生成头像地址

// service/messageService.js
const fs = require("fs");
const dir = "./public/static/avatar";

/**
 * 读取一个目录下有多少个文件
 * @param {*} dir 目录地址
 */
const readDirLength = async (dir) => {
  return new Promise((resolve) => {
    fs.readdir(dir, (err, files) => {
      if (err) throw new UnknownError();
      resolve(files);
    });
  });
};

// 新增评论或者留言
module.exports.addMessageService = async (newMessage) => {
  // 数据验证规则
  const messageRule = {
    nickname: {
      presence: {
        allowEmpty: false,
      },
      type: "string",
    },
    content: {
      presence: {
        allowEmpty: false,
      },
      type: "string",
    },
    blogId: {
      type: "string",
    },
  };

  // 进行数据验证
  const validateResult = validate.validate(newMessage, messageRule);
  if (!validateResult) {
    newMessage.blogId = newMessage.blogId ? newMessage.blogId : null;
    newMessage.createDate = Date.now();
    // 有一个头像的地址,该头像是随机生成的
    // 读取 static 下面的 avatar 目录
    const files = await readDirLength(dir);
    // 随机摇一个文件出来
    const randomIndex = Math.floor(Math.random() * files.length);
    newMessage.avatar = "/static/avatar/" + files[randomIndex];
    // 接下来开始新增
    const data = await addMessageDao(newMessage);
    // 如果是文章的评论,那么对应文章的评论数量也要自增
    if (newMessage.blogId) {
      const blogData = await findBlogByIdDao(newMessage.blogId);
      blogData.commentNumber++;
      await blogData.save();
    }
    return formatResponse(0, "", data);
  } else {
    throw new ValidationError("数据验证失败");
  }
};

2.分页获取评论或留言

// dao/messageDao.js
module.exports.findMessageByPageDao = async (pageInfo) => {
  // 如果有 blogid,说明是获取对应 blogid 的文章评论,如果没有,说明是获取留言
  if (pageInfo.blogid) {
    // 这边又分为两种情况,获取所有的文章评论,还有一种就是获取对应文章的评论
    if (pageInfo.blogid === "all") {
      // 返回所有评论
      return await messageModel.findAndCountAll({
        offset: (pageInfo.page * 1 - 1) * pageInfo.limit,
        limit: pageInfo.limit * 1,
        where: {
          blogId: {
            [Op.ne]: null,
          },
        },
        include: [
          {
            model: blogModel,
            as: "blog",
          },
        ],
      });
    } else {
      // 返回对应文章的评论
      return await messageModel.findAndCountAll({
        offset: (pageInfo.page * 1 - 1) * pageInfo.limit,
        limit: pageInfo.limit * 1,
        where: {
          blogId: pageInfo.blogid * 1,
        },
        order: [["createDate", "DESC"]],
      });
    }
  } else {
    // 获取留言
    return await messageModel.findAndCountAll({
      offset: (pageInfo.page * 1 - 1) * pageInfo.limit,
      limit: pageInfo.limit * 1,
      where: {
        blogId: null,
      },
      order: [["createDate", "DESC"]],
    });
  }
};

(十五)完成全局设置和关于模块

(十六)完善项目细节

1.删除文章时删除相应的评论

  • blogService.js
module.exports.deleteBlogService = async (id) => {
  // 根据 id 查询到该篇文章的信息
  const data = await findBlogByIdDao(id);
  // 根据该文章对应的分类,该分类下的文章数量自减
  const categoryInfo = await findOneBlogTypeDao(data.dataValues.categoryId);
  categoryInfo.articleCount--;
  await categoryInfo.save();
  // 删除该文章下所对应的评论
  await deleteMessageByBlogIdDao(id);
  // 删除文章
  await deleteBlogDao(id);
  return formatResponse(0, "", true);
};
  • messageDao.js
// 删除评论(传入的 id 是 message 表中的 blogId)
module.exports.deleteMessageByBlogIdDao = async (blogId) => {
  return await messageModel.destroy({
    where: {
      blogId,
    },
  });
};

2.删除博客分类时返回受影响的文章数量

  • blogTypeService.js
module.exports.deleteBlogTypeService = async (id) => {
  const count = await blogCountByBlogType(id);
  await deleteBlogTypeDao(id);
  // 返回受影响的文章的数量
  return formatResponse(0, "", count);
};
  • blogDao.js
// 根据博客类别 id,统计对应该博客类型 id 的博文数量
module.exports.blogCountByBlogType = async (categoryId) => {
  return await blogModel.count({
    where: {
      categoryId,
    },
  });
};

3.修改文章时判断是否修改了博客分类

  • blogService.js
module.exports.updateBlogService = async (id, newBlogInfo) => {
  // 判断正文内容有没有改变,会影响 TOC
  if (newBlogInfo.htmlContent) {
    // 重新处理 TOC 目录
    newBlogInfo = handleTOC(newBlogInfo);
    newBlogInfo.toc = JSON.stringify(newBlogInfo.toc);
  }

  // 判断博客分类有没有修改,如果有修改,之前的文章分类对应的文章数量要自减,新的文章分类对应的文章数量要自增
  const { dataValues: oldBlogInfo } = await findBlogByIdDao(id);
  if (newBlogInfo.categoryId !== oldBlogInfo.categoryId) {
    // 旧的自减
    if (oldBlogInfo.categoryId) {
      const oldBlogType = await findOneBlogTypeDao(oldBlogInfo.categoryId);
      oldBlogType.articleCount--;
      await oldBlogType.save();
    }

    // 新的自增
    const newBlogType = await findOneBlogTypeDao(newBlogInfo.categoryId);
    newBlogType.articleCount++;
    await newBlogType.save();
  }

  const { dataValues } = await updateBlogDao(id, newBlogInfo);
  return formatResponse(0, "", dataValues);
};
上次编辑于: