# 评论系统(服务端项目)从设计、实践开发到云端部署

TIP

从本节开始,我们正式完善和开发评论系统的服务端项目。之前熟悉了什么是服务端,相关的前置技术点 和 工具。已实现了部分评论系统的功能,但还还不够系统。

通过本项目把此前所有的知识点串联起来,将知识点变为项目输出。

  • 需求分析
  • 接口(路由)设计
  • 技术选型,架构设计,环境搭建
  • 数据库设计
  • 业务逻辑开发
  • 单元测试 和 集成测试
  • 部署和上线
  • 项目总结

# 一、项目开发过程

TIP

当下主流的项目开发基本都是 前后端分离开发。

日常项目开发过程如下

image-20240413124100664

注:

  • 一起做需求评审,做接口设计(前后端人员共同参与 和 确定)
  • 技术选型,架构设计,环境搭建
  • 前后端分离开发(前端 和 后端开发人员各自负责自己的部分)
  • 自测
  • 前后端联调

# 1、实战项目的重要性

TIP

  • 实战项目是对所学知识点的真实反馈,反馈很重要
  • 实战项目增加自己的成就感
  • 实战项目是企业面试时非常看重的

# 2、重视业务 和 需求

TIP

  • 工作了,老板给你发工资不是因为你会写代码,而是做出来功能
  • 代码只是一个工具,产出决定价值
  • 要积参与其中,不要排斥,且要力争深刻理解(要将代码、需求 和 业务也结合起来)

# 3、重视技术方案设计

TIP

  • 包括:接口设计,数据库模型设计等
  • 设计、决定了以后怎么开发,设计比开发难
  • 越难的越有价值,越要重视

# 4、不要讨厌 “重复”

TIP

  • 学习就是一个重复的过程,课程中也是同样
  • 大量重复之后,由量变到质变,再进行下一轮重复
  • 切记:“一直重复,从未质变”

# 二、需求分析

TIP

在实际工作中,需求很重要。一定要搞清楚项目的需求是什么,需求的背景、流程是什么,一定要搞清楚才能进入后边的流程

明确项目的需求,包括业务需求、用户需求以及技术需求等。确保充分理解和定义系统的功能和模块。

# 1、需求的重要性

TIP

  • 需求:描述项目最终效果的文字 和 图(正常情况下会有 原型图 和 文字描述)
  • 产品经理写需求,然后要经过各个项目角色的评审通过
  • 需求一般包括:原型图,功能描述

# 2、评论系统 - 原型图

# 2.1、原型图 - 注册页

TIP

注册页面的原型图,用户注册时需要填写以下信息

  • 用户名
  • 密码
  • 年龄
  • 城市
  • 性别

如已经注册过了,直接点击登录进入登录页面,进行用户登录

image-20240411094451896

# 2.2、原型图 - 登录页

TIP

登录页面的原型图,用户登录时只需要填写用户名 和 密码即可。如为注册过,点击登录链接进入注册页注册新用户即可

image-20240411094835290

# 2.3、原型图 - 主页

TIP

用户注册登录成功后,即可进入主页

  • 主页中有评论输入框,提交评论按钮
  • 评论列表,包含(用户头像、用户名、评论时间、评论内容,编辑、删除按钮)
  • 列表上边有评论列表的筛选条件:看全部(默认) 和 只看自己

image-20240411113919039

# 3、功能描述 - 注册 和 登录

TIP

  • 用户名唯一,不能重复注册
  • 用户名 和 密码匹配,即可登录
  • 登录成功后,跳转到首页

# 4、功能描述 - 首页

TIP

  • 非登录用户不能进入首页,登录用户可发布评论
  • 可查看全部评论 或 查看自己的评论
  • 只能编辑 和 删除自己的评论信息,无权操作他人的评论

目前只是简单的描述,实际工作中,会有完整的 word 文档 或 在线的协作文档等(图文并茂)文档一定要出现一些关键信息。

# 三、接口(路由)设计

TIP

按照需求的顺序,分析了每个页面需要哪些功能 和 接口,经过需求分析之后就需要做接口设计。正常情况下接口设计是需要 和 前端一起来完成。

当然,这个一起完成并非要 前端 和后端开发人员坐在一起办公(可以是由任意一方来完成接口的设计,设计完成后双方的人员在一起开会讨论最终确定下来)

假定,本次设计是由我们后端开发来完成接口设计,最后与前端人员讨论确定。在设计接口之前,我们先回顾一下 RESTful API 的设计范式

# 1、RESTful API

TIP

  • 一种新的 API 设计方法(早已推广使用,目前又有新的 API 设计方式,如:GraphQL AP)
  • 传统 API 设计:把每个 url 当做一个功能
  • RESTfulAPI 设计:把每个 url 当做一个唯一的资源

GraphQL API:GraphQL 是一种用于 API 的查询语言,它允许客户端精确地指定它们需要的数据,而不是依赖于预定义的端点。这使得 API 调用更加灵活和高效,能够减少多次请求的网络开销,提高响应速度。

# 2、如何设计成一个资源

TIP

  • 尽量不用 url 参数
  • 用 method 表示操作类型

# 2.1、不使用 url 参数

API 设计 URL 描述
传统 API 设计 /api/list?pageNum=1&pageSize=10 典型的功能描述型,从 URL 中的参数可以看出,查询第一页的数据,每页显示 10 条记录。无法表示 文件 和 文件夹的层次关系
RESTful API 设计 /api/list/1/10 可以很好的表示文件夹 和 文件的层次关系,是一种资源

# 2.2、两种 API 设计对比

传统 API 设计

请求方式 URL(路由) 描述
POST /api/create-comment 创建评论
POST /api/update-comment?id=1001 根据 id 修改评论
GET /api/get-comment?id=1001 根据 id 获取评论
GET /api/del-comment?id=1001 根据 id 删除评论

以上 URL 中,都带有明确的功能性描述 createupdategetdel

对比 RESTful API 设计

请求方式 URL(路由) 描述
POST /api/comment 创建评论
PATCH /api/comment/1001 根据 id 修改评论
GET /api/comment/1001 根据 id 获取评论
DELETE /api/comment/1001 根据 id 删除评论

注:

  • 通过 RESTful API 设计风格,以上同样的路由地址,只需要通过不同的请求方式来得到不同的响应结果。
  • 而传统的 API 设计,在 URL 路由中(createupdategetdel)是有明确的功能描述的
  • 在 RESTful API 中是将功能描述放在了 method 中(POST、PATCH、GET、DELETE ... 等),但 URL 是单纯的资源标识,它表示的永远是一个资源。
  • 可以理解成 文件夹 和 文件的层次关系 ,同样的 URL 会根据不同的请求来区分对应的功能

因此,RESTful API 几乎不会用 URL 参数(类似 ?id=1001 的形式),它会用 method 来表示操作类型(功能描述)

即:RESTful API 的设计方式中 URL 表示是一个资源标识,传统的 API 设计方式中 URL 表示功能描述。

# 3、接口设计作用

TIP

  • 前后端交互的桥梁。前后端一起设计,一起评审
  • 前后端分离开发,都要按照该设计,不得违约
  • 如发现问题,及时反馈,及时修改,及时通知,及时更新接口设计文档

# 4、接口设计按功能模块分类

TIP

按照前面需求分析来看,可将 评论系统项目 接口设计分为两类

  • 用户接口设计
  • 评论接口设计

接下来就需要按照功能模块来编写接口设计文档

# 5、项目接口(路由)设计步骤

TIP

项目接口(路由)设计步骤有:需求分析、接口定义、参数与响应设计、权限与认证、接口文档编写、接口实现与测试

# 5.1、需求分析

TIP

  • 深入理解项目需求,明确各个接口的功能和目的。
  • 与产品经理、前端开发人员等相关人员沟通,确保对接口需求有准确的理解。

# 5.2、接口定义

TIP

为每个接口定义唯一的 URL 路径,例如:

  • 注册接口:POST /users/register
  • 登录接口:POST /users/login
  • 修改密码接口:PATCH /users
  • 获取评论列表接口:GET /comments
  • 创建评论接口:POST /comments(使用 POST 方法)
  • 更新评论接口:PUT /comments/:commentId(使用 PUT 或 PATCH 方法)
  • 删除评论接口:DELETE /comments/:commentId(使用 DELETE 方法)

确定每个接口的请求方法(GET、POST、PUT、PATCH、DELETE 等)

# 5.3、参数与响应设计

TIP

  • 为每个接口定义请求参数,包括参数名称、数据类型、是否必填、格式要求等。
  • 定义接口的响应格式,包括成功和失败的情况,以及对应的 HTTP 状态码和数据结构。

# 5.4、权限与认证

TIP

  • 根据项目需求,为每个接口设计合适的权限控制机制。
  • 确定是否需要认证,如使用 JWT(JSON Web Tokens)进行用户身份验证。

# 5.5、接口文档编写

TIP

  • 编写详细的接口文档,包括接口名称、路径、请求方法、请求参数、响应格式等信息。
  • 使用易于理解的语言和格式,确保团队成员能够准确理解和实现接口功能。

# 5.6、接口实现与测试

TIP

  • 根据接口设计文档,在后端实现相应的接口逻辑。
  • 编写测试用例,对接口进行功能测试和性能测试,确保接口的正确性和稳定性。

# 6、接口设计文档

TIP

编写注册、登录、获取评论列表、创建评论、更新评论、删除评论接口设计相关文档

# 6.1、注册接口

TIP

  • URL/users/register
  • 请求方法POST
  • 请求参数
    • username:用户名(必填,字符串,唯一)
    • password:密码(必填,字符串,加密存储)
    • email:邮箱(必填,字符串,格式验证)
    • age:年龄(非必填,数字)
    • city:所在城市(非必填,字符串)
    • gender:性别(非必填,数字, 0 - 保密<默认值>,1 - 男,2 - 女)
    • phone:手机号(非必填,字符串,验证手机号格式,必须是 11 位数字)
    • email:邮箱(非必填,字符串,验证邮箱格式)
    • role:角色(非必填,数字)限制 role 字段只能为 0 - 普通用户(默认) 或 1 - 管理员
    • createdAt:创建时间(时间戳,系统自动添加)
    • updatedAt:更新时间(时间戳,系统自动添加)
  • 响应

注册成功响应结果

{
  "code": 0,
  "message": "用户注册成功",
  "data": {
    "id": "新创建的用户ID",
    "username": "arry老师"
  }
}

注册失败响应结果

{
  "code": "10003",
  "message": "用户注册失败",
  "data": ""
}

# 6.2、登录接口

TIP

  • URL/users/login
  • 请求方法POST
  • 请求参数
    • username:用户名(必填,字符串)
    • password:密码(必填,字符串)
  • 响应

登录成功响应结果

{
  "code": 0,
  "message": "用户登录成功",
  "data": {
    "token": "登录令牌"
  }
}

登录失败响应结果

{
  "code": "10006",
  "message": "登录失败",
  "data": ""
}

# 6.3、创建评论接口

TIP

  • URL/comments
  • 请求方法POST
  • 权限要求:用户登录
  • 请求参数
    • content:评论内容(必填,字符串)
    • username:用户名(必填,字符串)
    • createdAt:创建时间(时间戳,系统自动添加)
    • updatedAt:更新时间(时间戳,系统自动添加)
  • 响应

创建评论成功响应结果

{
  "code": 0,
  "message": "创建评论成功",
  "data": {
    "id": "新创建的评论ID"
  }
}

创建评论失败响应结果

{
  "code": "10201",
  "message": "创建评论失败",
  "data": ""
}

未登录

{
  "code": -2,
  "message": "用户未登录,请登录后再试",
  "data": {}
}

# 6.4、获取评论列表接口

TIP

  • URL/comments
  • 请求方法GET
  • 权限要求:用户登录
  • 请求参数
    • page:页码(可选,默认值为 1)
    • size:每页评论数(可选,默认值为 10)
    • sort:排序字段和顺序(可选,例如createTime:desc表示按创建时间降序排序)
  • 响应

获取评论列表成功响应结果

{
  "code": 0,
  "message": "获取评论列表成功",
  "data": {
    "total": "评论总数",
    "list": [
      {
        "commentId": "评论ID",
        "content": "评论内容",
        "userId": "用户ID",
        "createTime": "创建时间"
      }
      // ...其他评论
    ]
  }
}

获取评论列表失败响应结果

{
  "code": "10202",
  "message": "获取评论列表失败",
  "data": ""
}

未登录

{
  "code": -2,
  "message": "用户未登录,请登录后再试",
  "data": {}
}

# 6.5、更新评论接口

TIP

  • URL/comments/:commentId
  • 请求方法PUTPATCH
  • 权限要求:用户登录(且通常是评论的创建者或具有编辑权限的用户)
  • 请求参数
    • content:新的评论内容(可选)
    • 其他需要更新的字段(可选)
  • 响应

更新评论成功响应结果

{
  "code": 0,
  "message": "更新评论成功",
  "data": {}
}

更新评论失败响应结果

{
  "code": "10205",
  "message": "编辑评论失败",
  "data": ""
}

未登录或无权限

{
  "code": -2,
  "message": "用户未登录或无权限更新该评论",
  "data": {}
}

# 6.6、删除评论接口

TIP

  • URL/comments/:commentId
  • 请求方法DELETE
  • 权限要求:用户登录(且通常是评论的创建者或具有删除权限的用户)
  • 请求参数:无
  • 响应

删除评论成功响应结果

{
  "code": 0,
  "message": "删除评论成功",
  "data": {}
}

删除评论失败响应结果

{
  "code": "10204",
  "message": "删除评论失败",
  "data": ""
}

未登录或无权限:

{
  "code": -2,
  "message": "用户未登录 或 无权限删除该评论",
  "data": {}
}

评论不存在

{
  "code": "10203",
  "message": "该评论不存在,无效的ID",
  "data": ""
}

注意事项:

  • 在删除评论之前,建议检查评论的关联关系,例如是否有其他用户对该评论进行了回复或点赞,如果有,则可能需要先处理这些关联数据。
  • 删除评论时,还需要考虑是否保留评论的历史记录,这取决于项目的具体需求。如果需要保留历史记录,可以将评论标记为已删除而不是物理删除。
  • 在删除评论的操作中,要确保删除操作的原子性,避免因为操作失败而导致数据不一致的问题。

请根据项目实际情况和需求,对删除评论接口设计文档进行适当调整和补充。同时,也请确保在整个接口设计过程中,遵循项目的统一规范,确保接口的一致性和可维护性。

# 四、数据库设计

TIP

数据库设计目的:根据需求分析系统需要存储哪些数据,数据应该怎样存储

在整个后端项目开发过程中,数据库设计是一个关键环节,它直接影响到系统的性能和稳定性。因此,在进行数据库设计时,需要充分考虑数据的完整性、安全性以及可维护性等因素。同时,随着项目的进展和需求的变化,可能需要对数据库设计进行迭代和优化,以适应新的业务场景和需求。

数据库设计步骤

  • 需求分析:准确了解和分析用户对数据库的需求,包括数据的存储、检索和处理需求。这是数据库设计的基础,对后续设计过程有决定性影响。

  • 概念结构设计:根据需求分析的结果,通过抽象和归纳,形成独立于具体 DBMS 的概念模型。通常使用 E-R 图(实体-关系图)来描述数据与数据之间的关系。

  • 逻辑结构设计:将概念模型转换为具体 DBMS 支持的数据模型,并对其进行优化。这通常涉及到表的设计、关系的定义以及约束的添加等。

  • 物理结构设计:为逻辑数据模型选择适合的存储结构和存取方法,确保数据的存储和检索效率。

  • 数据库实施:根据逻辑设计和物理设计的结果,使用 DBMS 提供的数据语言、工具及宿主语言建立数据库,编制与调试应用程序,组织数据入库,并进行试运行。

  • 编写数据库设计文档:数据库设计文档是项目开发过程中的一个重要组成部分,它详细描述了数据库的结构、表的设计、关系定义、约束条件、索引策略、数据完整性和安全性、变更记录 等,为开发团队提供了清晰、准确的数据库设计指南。

# 1、MongoDB 创建数据库 和 集合

TIP

  • 创建数据库 icoding-comment-system
  • 创建集合 comments,存储评论数据
  • 创建集合 users,存储用户数据

以及建立每个集合中根据实际需求对应的字段信息

image-20240402233016123

# 2、Mongoose 定义 Schema 和 Model

TIP

  • 使用 Mongoose 连接 和 操作 MongoDB 数据库
  • 定义 Schema
  • 定义 Model

src/model/user.model.js 中定义用户 Schema 和 模型

// 数据模型(规范数据格式)
const { Schema } = require("mongoose");
// 导入数据库连接对象
const mongoose = require("../db/db");

// 定义 Schema
const userSchema = new Schema(
  {
    username: {
      type: String,
      required: true, // 必填
      unique: true, // 唯一,不重复
    },
    password: String,
    age: Number,
    city: String,
    // 性别
    gender: {
      type: Number,
      // 默认值
      default: 0, // 0 - 保密,1 - 男,2 - 女
    },
    phone: {
      type: String,
      required: false,
      validate: {
        validator: function (v) {
          // 这里使用正则表达式来验证手机号格式
          // 假设你的手机号格式是11位数字
          return /^\d{11}$/.test(v);
        },
        message: "手机号格式不正确,请输入11位数字",
      },
    },
    email: {
      type: String,
      required: false,
      validate: {
        validator: function (v) {
          // 这里使用正则表达式来验证邮箱格式
          return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
        },
        message: "邮箱格式不正确",
      },
    },
    // 角色
    role: {
      type: Number,
      enum: [0, 1], // 枚举类型,限制 role字段只能为 0 - 普通用户(默认) 或 1 - 管理员
      default: 0, // 默认值为 0,即普通用户
    },
  },
  {
    timestamps: true, // 时间戳,自动添加文档的创建时间和更新时间
  }
);

// 创建模型
const User = mongoose.model("user", userSchema);

// console.log(User)

// 导出 User 对象
module.exports = User;

src/model/comment.model.js 中定义评论信息 Schema 和 模型

// 数据模型(规范数据格式)
const { Schema } = require("mongoose");
// 导入数据库连接对象
const mongoose = require("../db/db");

// 定义 Schema
const commentSchema = new Schema(
  {
    // 评论内容
    content: {
      type: String,
      required: true, // 必填
    },
    username: String, // 用户名
  },
  {
    timestamps: true, // 时间戳,自动添加文档的创建时间和更新时间
  }
);

// 创建 comment 模型
const Comment = mongoose.model("comment", commentSchema);

// console.log(Comment)

// 导出 Comment 对象
module.exports = Comment;

# 五、开发环境准备

TIP

经过了需求分析、数据库设计、接口(路由)设计,复杂的大型项目还有架构的设计(架构设计分为四个部分:业务架构设计、应用架构设计、数据架构设计、技术架构设计)

image-20240404103822816

# 1、初始化开发环境

TIP

  • 初始化 koa2 环境
  • 规范目录 和 层级
  • 连接数据库

详细步骤可查阅 koa2 框架操作 MongoDB 数据库,项目实践基础准备 (opens new window)

image-20240404112350310

注:

上图清楚了描述了本项目的目录 和 层级结构,我们任何的项目设计阶段都要能够清晰的画出来,能对项目有一览众山小的全局观。

  • 路由:定义 URL 模式 与 处理程序(如控制器中的方法)之间的映射关系
  • controller:在 Web 应用或 MVC(模型 - 视图 - 控制器)架构中,控制器(Controller)负责处理用户输入(如 HTTP 请求),并基于这些输入与模型(Model)和视图(View)进行交互。控制器决定使用哪个模型来处理请求,以及选择哪个视图来显示结果。
  • Service:Service 是一种可以在后台执行长时间运行操作而没有用户界面的应用组件。它通常用于处理网络事务、执行文件 I/O 或 与内容提供程序交互等任务。Service 可以由其他应用组件启动,并在后台持续运行,即使启动它的组件已经销毁。简单说即:业务逻辑,操作数据库
  • db:连接数据库
  • 中间件:中间件是一种独立的系统软件服务程序,在项目用到 2 次 或 2 次以上的功能函数都可以拆分成中间件,实现可复用
  • Model:在 MVC 架构中,模型(Model)代表应用的数据和业务逻辑。它通常与数据库交互,负责数据的存取和验证等操作。模型不关心数据的展示方式,它只关心数据本身和业务规则。当前项目中 Model 即 数据库字段对应的模型

# 2、规范目录 和 层级

观察下图中的箭头

image-20240404161405068

注:

  • 箭头指向从 路由 -> controller -> service -> db -> 数据库
  • 发送 HTTP 请求,先经过路由处理 ,路由会将逻辑分发给 controller 层,再到 service 层完成业务逻辑处理,再到 db 连接数据库完成数据库的操作
comment-system
├─ .gitignore
├─ package-lock.json
├─ package.json
├─ README.md
└─ src
   ├─ app
   │  ├─ errHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  └─ user.controller.js
   ├─ db
   │  └─ db.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  ├─ comment.model.js
   │  └─ user.model.js
   ├─ router
   │  └─ user.route.js
   └─ service
      └─ user.service.js

# 3、为什么要分层设计

TIP

  • 高内聚低耦合:分层设计使得每一层都专注于自己的职责,层与层之间的依赖关系清晰明了。每一层都尽可能独立,减少对其他层的依赖,从而实现了高内聚低耦合的目标。这样的设计有助于减少代码的复杂性,提高代码的可读性和可维护性。
  • 便于开发和管理:分层设计使得项目的开发和管理更加有序。不同的开发团队可以专注于不同的层次进行开发,降低了团队协作的难度。同时,每一层都有明确的职责和接口,方便进行单元测试和功能验证。
  • 易于扩展和维护:随着项目的不断发展和变化,可能需要添加新的功能或修改现有的功能。分层设计使得这些变化可以在特定的层次中进行,而不会影响到其他层次。这使得项目的扩展和维护变得更加容易。
  • 提高系统性能:通过分层设计,我们可以根据每一层的特性进行性能优化。例如,对于数据处理层,我们可以使用高效的算法和数据库操作来提高数据处理的速度;对于展示层,我们可以优化界面渲染和交互逻辑,提高用户体验。
  • 安全性考虑:在某些项目中,安全性是一个非常重要的考虑因素。通过分层设计,我们可以将敏感的操作和数据访问限制在特定的层次中,并通过加密、权限控制等手段提高系统的安全性。

因此,分层设计是一种有效的项目结构设计方法,它有助于提高代码质量、降低开发难度、便于扩展和维护、提高系统性能以及增强安全性。

# 4、连接数据库

TIP

  • 安装 Mongoose
  • 连接 MongoDB
  • Schema 和 Model

详细查阅 Koa2 集成 Mongoose 操作 MongoDB 数据库 (opens new window)

# 六、评论系统项目开发实践

TIP

  • 用户注册,密码修改
  • 用户登录,用户认证
  • 创建评论信息
  • 获取评论列表
  • 删除评论信息
  • 更新评论信息

关于用户注册、登录 和 相关功能前面已经完成,接下来只需要完成评论相关功能即可

# 1、创建评论信息

TIP

开发过程可以按照下图的箭头顺序来书写

  • 定义创建评论信息路由
  • Controller 控制器
  • 权限认证中间件(校验必须登录才能创建评论)
  • Service 操作数据库
  • postman 测试创建评论信息接口

image-20240404161405068

创建 src/router/comment.route.js 评论路由相关文件

// 评论功能的路由

// 导入 koa-router
const Router = require("koa-router");
// 导入登录验证中间件
const { auth } = require("../middleware/auth.middleware");
// 导入 createComment 方法
const { create } = require("../controller/comment.controller");

// 实例化 router 路由对象
// 给路由设置一个统一的前缀
const router = new Router({ prefix: "/comments" });

// 创建评论
// auth 登录验证中间件
// create 创建评论的 Controller
// 方式一
// router.post('/create', auth, create)

// 或 方式二
router.post("/", auth, create);

// 导出 Router 对象,这样其他模块就可以引入并使用这个路由对象了
module.exports = router;

src/app/index.js 中,注册评论路由

const Koa = require("koa");
const { koaBody } = require("koa-body");
const errHandler = require("./errHandler");

const userRouter = require("../router/user.route");
// 导入评论路由模块
const commentRouter = require("../router/comment.route");

const app = new Koa();
app.use(koaBody());

app.use(userRouter.routes());
// 注册 评论路由
app.use(commentRouter.routes());

app.on("error", errHandler);
module.exports = app;

创建 src/controller/comment.controller.js 评论控制器

/**
 * 评论信息 Controller
 * @author arry老师
 * @version V1.0
 */
class CommentController {
  // 创建评论
  async create(ctx, next) {
    // 1、获取数据

    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username;
    // 评论数据
    const { content } = ctx.request.body;

    console.log(content);
    console.log("username:" + username);

    // 2、操作数据库

    // 3、返回结果
    ctx.body = {
      status: 0,
      message: "创建评论成功",
      data: {},
    };
  }
}

// 导出 CommentController 实例(其他模块可以通过 require 导入这个实例,并调用其方法)
module.exports = new CommentController();

启动运行

npm run dev

先登录,然后在 postman 中创建评论(方式一)

image-20240405013704280

或(方式二)

image-20240408180226015

VSCode 控制台打印输出 获取到的数据

image-20240405014121899

创建 src/service/comment.service.js 用于编写评论相关操作数据库逻辑

// 导入 Comment 模型
const Comment = require("../model/comment.model");

/**
 * 评论信息 Service
 * @author arry老师
 * @version V1.0
 */
class CommentService {
  /**
   * 创建评论,操作数据库
   * @param { String } username 用户名
   * @param { String } content 评论内容
   * @returns 评论信息
   */
  async createComment(username, content) {
    return await Comment.create({ username, content });
  }
}

module.exports = new CommentService();

src/controller/comment.controller.js 中,调用 service 中的 createComment 方法

// 导入 Service 中的方法
const { createComment } = require("../service/comment.service");
// 导入统一返回错误信息
const { createCommentError } = require("../constant/err.type");

/**
 * 评论信息 Controller
 * @author arry老师
 * @version V1.0
 */
class CommentController {
  // 创建评论
  async create(ctx, next) {
    // 1、获取数据

    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username;
    // 评论数据
    const { content } = ctx.request.body;

    // console.log(content)
    // console.log("username:" + username)

    // 2、操作数据库
    try {
      const newComment = await createComment(username, content);
      // 3、返回结果
      ctx.body = {
        code: 0,
        message: "创建评论成功",
        data: newComment,
      };
    } catch (err) {
      console.error("创建评论失败", err);
      ctx.app.emit("error", createCommentError, ctx);
    }
  }
}

// 导出 CommentController 实例(其他模块可以通过 require 导入这个实例,并调用其方法)
module.exports = new CommentController();

改写 src/constant/err.type.js ,新增 createCommentError 统一错误处理

createCommentError: {
    code: '10201',
    message: '创建评论失败',
    data: ''
}

在 postman 中测试(方式一)

image-20240406015929637

或(方式二)

image-20240408175959711

# 2、获取评论列表

TIP

  • 定义获取评论列表路由
  • Controller 控制器
  • 权限认证中间件(校验必须登录才能获取评论列表)
  • Service 操作数据库
  • postman 测试分页查询接口

src/router/comment.route.js 中,定义获取评论列表路由

// 导入 createComment,findList 方法
const { create, findList } = require("../controller/comment.controller");

// 获取评论列表
// auth 登录验证中间件
// findList 获取评论列表的 Controller
// 方式一(http://localhost:8000/comments/list?pageNum=1&pageSize=2):
// router.get('/list', auth, findList)

// 方式二(http://localhost:8000/comments/1/2):
router.get("/:pageNum/:pageSize", auth, findList);

注:

路由地址的定义,不仅仅要遵守 RESTful API 的风格,更要考虑 SEO 搜索引擎优化。相比方式一,方式二更有利于 SEO 优化。

src/controller/comment.controller.js Controller 控制器 中 定义获取评论列表的方法 findList

// 获取评论列表(分页查询)
async findList(ctx, next) {

    // 1、获取数据(方式一)
    // 解析 pageNum 和 pageSize
    // const { pageNum = 1, pageSize = 10 } = ctx.request.query
    // console.log("pageNum:" + pageNum, "pageSize:" + pageSize)

    // 1、获取数据(方式二)
    // parseInt 函数用于将一个字符串转换为整数。这个函数接受两个参数:要转换的字符串和基数(或称为进位制),10 表示将字符串视为一个十进制数来解析
    const pageNum = parseInt(ctx.params.pageNum, 10)
    const pageSize = parseInt(ctx.params.pageSize, 10)
    console.log("pageNum:" + pageNum, "pageSize:" + pageSize)

    // 2、操作数据库

    // 3、返回结果
    ctx.body = {
        code: 0,
        message: '获取评论列表成功',
        data: {}
    }
}

启动运行

npm run dev

先登录,然后在 postman 中访问路由(方式一)

image-20240406130424348

postman 中访问路由(方式二)

image-20240408171843950

VSCode 控制台打印输出 获取到的请求参数

image-20240406130740294

src/service/comment.service.js 中,编写评论列表相关操作数据库逻辑

/**
 * 获取评论列表(分页查询)
 * @param { Number } pageNum 请求的页码,默认值 1
 * @param { Number } pageSize 每页显示的文档数量, 默认值 10
 * @returns 评论列表信息
 */
async getCommentList(pageNum, pageSize) {
    // 计算需要跳过的文档数量
    const skip = (pageNum - 1) * pageSize;
    // 执行查询并获取数据
    // sort({ createdAt: -1 }) 按创建时间降序排列
    return await Comment.find().sort({ createdAt: -1 }).skip(skip).limit(pageSize).exec();
}

src/controller/comment.controller.js 中,调用 service 中的 getCommentList 方法

// 导入 Service 中的方法
const { createComment, getCommentList } = require('../service/comment.service')
// 导入统一返回错误信息
const { createCommentError, getCommentListError } = require('../constant/err.type')


// 获取评论列表(分页查询)
async findList(ctx, next) {

    // 1、获取数据(方式一)
    // 解析 pageNum 和 pageSize
    // const { pageNum = 1, pageSize = 10 } = ctx.request.query
    // console.log("pageNum:" + pageNum, "pageSize:" + pageSize)

    try {
        // 1、获取数据(方式二)
        // parseInt 函数用于将一个字符串转换为整数。这个函数接受两个参数:要转换的字符串和基数(或称为进位制),10 表示将字符串视为一个十进制数来解析
        const pageNum = parseInt(ctx.params.pageNum, 10)
        const pageSize = parseInt(ctx.params.pageSize, 10)
        console.log("pageNum:" + pageNum, "pageSize:" + pageSize)

        // 2、操作数据库
        const commentList = await getCommentList(pageNum, pageSize)
        // 3、返回结果
        ctx.body = {
            code: 0,
            message: '获取评论列表成功',
            data: commentList
        }
    } catch (err) {
        console.error('获取评论列表失败', err)
        ctx.app.emit('error', getCommentListError, ctx)
    }
}

改写 src/constant/err.type.js ,新增 getCommentListError 统一错误处理

getCommentListError: {
    code: '10202',
    message: '获取评论列表失败',
    data: ''
}

postman 测试获取评论列表(方式一)

URL 请求地址:http://localhost:8000/comments/list?pageNum=1&pageSize=2

GIF-2024-4-7-11-17-36

postman 测试获取评论列表(方式二)

URL 请求地址:http://localhost:8000/comments/1/2

GIF-2024-4-8-17-26-04

# 3、删除评论信息

TIP

需求:只能删除自己的评论信息,无权操作他人的评论

  • 定义删除评论列表路由
  • Controller 控制器
  • 权限认证中间件(校验必须登录才能删除评论)
  • Service 操作数据库
  • postman 测试根据 id 删除评论接口

src/router/comment.route.js 中,定义删除评论列表路由

// 导入 createComment,findList,remove 方法
const {
  create,
  findList,
  remove,
} = require("../controller/comment.controller");

// 删除评论
// auth 登录验证中间件
// remove 删除评论的 Controller
// 方式一
// router.post('/del', auth, remove)

// 方式二
router.delete("/:commentId", auth, remove);

注:

删除评论路由,也可以使用 delete 请求方法,即:router.delete('/del', auth, remove)

src/controller/comment.controller.js Controller 控制器 中 定义删除评论的方法 remove

// 删除评论
async remove(ctx, next) {

    // 1、获取数据
    // 解析 _id,根据 id 删除用户评论
    // 方式一
    // const { _id } = ctx.request.body

    // 方式二
    // 获取 URL 中的 commentId
    const _id = ctx.params.commentId

    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username

    console.log("_id:" + _id, "username:" + username)

    // 2、操作数据库

    // 3、返回结果
    ctx.body = {
        code: 0,
        message: '删除评论成功',
        data: {}
    }
}

启动运行

npm run dev

先登录,然后在 postman 中访问删除路由(同步 DELETE 请求),方式一

image-20240407143656464

方式二

image-20240408213557391

VSCode 控制台打印输出 获取到的请求参数

image-20240407143931364

src/service/comment.service.js 中,编写删除评论相关操作数据库逻辑

/**
 * 根据 id 删除评论信息,且 只能删除自己的评论信息,无权操作他人的评论
 * @param { String } _id 评论信息 id
 * @param { String } username 用户名
 * @returns 删除影响的记录数,大于 0 为 true,小于 0 为 false
 */
async removeCommentById(_id, username) {
    // deleteOne方法用于删除与指定查询条件匹配的第一个文档
    // 删除当前登录用户 对应 id 的评论信息(即:只能删除自己的评论信息,无权操作他人的评论)
    // _id 和 username 两个条件同时满足时,才能删除该评论信息(就保证了只能删除自己的评论)
    const res = await Comment.deleteOne({ _id, username })
    // 返回值为代码改动记录数
    return res.deletedCount > 0 ? true : false;
}

src/controller/comment.controller.js 中,调用 service 中的 removeCommentById 方法

// 导入 Service 中的方法
const {
    createComment,
    getCommentList,
    removeCommentById } = require('../service/comment.service')
// 导入统一返回错误信息
const {
    createCommentError,
    getCommentListError,
    invalidCommentID,
    deleteCommentError } = require('../constant/err.type')


// 删除评论
async remove(ctx, next) {

    // 1、获取数据
    // 解析 _id,根据 id 删除用户评论
    // 方式一
    // const { _id } = ctx.request.body

    // 方式二
    // 获取 URL 中的 commentId
    const _id = ctx.params.commentId

    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username
    // console.log("_id:" + _id, "username:" + username)

    try {
        // 2、操作数据库
        const res = await removeCommentById(_id, username)
        if (res) {
            // 3、返回结果
            ctx.body = {
                code: 0,
                message: '删除评论成功',
                data: {}
            }
        } else {
            console.error('该评论不存在,无效的 ID')
            return ctx.app.emit('error', invalidCommentID, ctx)
        }
    } catch (err) {
        console.error('删除评论失败', err)
        return ctx.app.emit('error', deleteCommentError, ctx)
    }
}

先登录,然后在 postman 中访问删除路由。根据当前登录用户 对应的 评论信息 id 可成功删除(即:能删除自己的评论信息)

方式一

image-20240408011304758

删除非当前登录用户 对应的 评论信息(即:不能删除别人的评论信息)

方式一

image-20240408011707908

注:

方式二,严格按 RESTful API 风格,评论 id 在 URL 地址栏中,无需 body 传递参数,请求方式为 DELETE

# 4、编辑评论信息

TIP

  • 定义编辑评论路由
  • Controller 控制器
  • 权限认证中间件(校验必须登录才能编辑评论)
  • Service 操作数据库
  • postman 测试编辑评论接口

src/router/comment.route.js 中,定义编辑评论路由

// 导入 createComment,findList,remove,update 方法
const {
  create,
  findList,
  remove,
  update,
} = require("../controller/comment.controller");

// 编辑评论
// auth 登录验证中间件
// update 编辑评论的 Controller
router.put("/", auth, update);

src/controller/comment.controller.js Controller 控制器 中 定义编辑评论的方法 update

// 编辑评论
async update(ctx, next) {
    // 1、获取数据
    // 解析 _id,根据 id 编辑评论信息
    // content 编辑后的评论信息
    const { _id, content } = ctx.request.body
    // 同时,只能编辑自己的评论信息,无权编辑别人的评论
    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username
    console.log("_id:" + _id, "username:" + username, "content:" + content)

    // 2、操作数据库

    // 3、返回结果
    ctx.body = {
        code: 0,
        message: '编辑评论成功',
        data: {}
    }
}

启动运行

npm run dev

先登录,然后在 postman 中访问编辑评论路由

image-20240408015848775

VSCode 控制台打印输出 获取到的请求参数

image-20240408020017664

src/service/comment.service.js 中,编写编辑评论相关操作数据库逻辑

/**
 * 根据 id 修改评论信息,且 只能修改自己的评论信息,无权操作他人的评论
 * @param { String } _id 评论 id
 * @param { String } username 当前登录的用户名
 * @param { String } content 修改后的评论内容
 * @returns newData 返回更新后的评论
 */
async updateCommentById(_id, username, content) {
    // 修改当前登录用户 对应 id 的评论信息(即:只能修改自己的评论信息,无权操作他人的评论)
    const newData = await Comment.findOneAndUpdate(
        { _id, username }, // 修改的条件(只能修改自己的评论信息,无权操作他人的评论)
        { content }, // 修改的内容
        { new: true } // 返回更新之后的最新评论
    )
    // 返回更新后的评论
    return newData
}

src/controller/comment.controller.js 中,调用 service 中的 updateCommentById 方法

// 导入 Service 中的方法
const {
    createComment,
    getCommentList,
    removeCommentById,
    updateCommentById } = require('../service/comment.service')
// 导入统一返回错误信息
const {
    createCommentError,
    getCommentListError,
    invalidCommentID,
    deleteCommentError,
    updateCommentError,
    invalidCommentIdAndAuth } = require('../constant/err.type')


// 编辑评论
async update(ctx, next) {
    // 1、获取数据
    // 解析 _id,根据 id 编辑评论信息
    // content 编辑后的评论信息
    const { _id, content } = ctx.request.body
    // 同时,只能编辑自己的评论信息,无权编辑别人的评论
    // 获取 auth 中间件中将验证后的用户数据,其中的 username
    const username = ctx.state.user.username
    // console.log("_id:" + _id, "username:" + username, "content:" + content)

    try {
        // 2、操作数据库
        const newData = await updateCommentById(_id, username, content)
        if (newData) {
            // 3、返回结果
            ctx.body = {
                code: 0,
                message: '编辑评论成功',
                data: newData
            }
        } else {
            console.error('该评论不存在 或 无权编辑')
            return ctx.app.emit('error', invalidCommentIdAndAuth, ctx)
        }
    } catch (err) {
        console.error('编辑评论失败', err)
        return ctx.app.emit('error', updateCommentError, ctx)
    }
}

先登录,然后在 postman 中访问修改路由。根据当前登录用户 对应的 评论信息 id 可成功完成修改(即:能修改自己的评论信息)

image-20240408025717872

修改非当前登录用户 对应的 评论信息(即:不能修改别人的评论信息)

image-20240408030128070

注:

按 RESTful API 风格,编辑评论直接使用 PUT 请求,URL 地址使用 http://localhost:8000/comments 即可

postman 中相关 API 接口测试

image-20240414011750881

# 七、服务端项目部署 与 实践

TIP

深入浅出 Vue 前后端分离项目(API 接口)云服务器部署 与 实践、Vue 前端项目的云服务器部署、性能优化 与 最佳实践。

# 1、API 接口服务端项目的云部署

TIP

  • 云服务器购买 与 配置
  • Linux 下 Node 后端服务部署
  • 后端服务 Nginx 端口转发与部署
  • 使用 PM2 管理进程
  • Nginx 配置 HTTPS 加密协议

详细查阅 服务端项目部署 与 实践 (opens new window),相关图文教程

# 2、Vue 前端项目的云部署

TIP

  • 云服务器 与 Nginx 基础配置
  • Linux 下 Vue 前端项目部署
  • 阿里云域名注册 与 解析
  • Nginx 部署 Vue 项目
  • Nginx 配置 HTTPS 加密协议
  • Nginx 性能优化
  • 多平台、多系统部署

详细查阅 Vue 前端项目的云服务器部署、性能优化 与 最佳实践 (opens new window),相关图文教程

# 八、前后端联调

TIP

深入浅出前后端联调的步骤 和 注意事项,联调的过程,遇到 Bug 后排查过程等。

# 1、联调时间节点

下图为前后端分离的开发过程

image-20240413124100664

注:

前后端分离开发的过程,需求评审、接口设计、前端和后端各自开发(两个团队同步进行),各自开发结束后完成自测,最后进入前后端联调。

# 2、开始联调

TIP

  • 启动前端项目服务
  • 启动后端项目服务,保证前后端都能运行成功
  • 前端 baseURL 指向后端 或 切换至测试环境、线上测试环境等

# 3、联调步骤

TIP

  • 测试主要的功能
  • 看各个 API 的返回,是否符合预期
  • 浏览器控制台是否报错、服务端控制台是否报错

# 4、注意事项

TIP

  • 联调不是测试,能跑通主要流程即可,不可陷入细节
  • 联调的目的是为了打通前后端,不可能像专业的测试人员一样,将每个细节都测试的非常完善(抓住主要矛盾即可,测试有专业的测试人员负责)
  • 遇到 Bug 不要慌,顺藤摸瓜找原因即可
  • 如自己找不到原因,可以找 前端/后端 开发人员一起排查(多方协同)
上次更新时间: 7/19/2024, 3:10:45 AM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X