node+koa+mysql(四)

上文定义好了所有的表结构,本篇开始写接口逻辑啦~

用户模块

用户模块只设置了一个登录接口,未注册的直接注册然后登录,已注册的直接登录。

登录接口需要校验用户填写的手机号和密码,本文采用第三方库 joi 进行接口校验,先进行安装:

npm install joi

新建文件 /app/validators/validator.js

css 复制代码
const Joi = require('joi');

const registerSchema = Joi.object({
  telephone: Joi.string()
    .required()
    .pattern(new RegExp("^1[3456789][0-9]{9}$"))
    .messages({
      "string.base": "手机号码必须是字符串",
      "string.empty": "手机号码不能为空",
      "string.pattern.base": "手机号码格式不正确",
    }),

  password: Joi.string()
    .required()
    .pattern(new RegExp("^[a-zA-Z0-9]{3,30}$"))
    .messages({
      "string.base": "密码必须是字符串",
      "string.empty": "密码不能为空",
      "string.pattern.base": "密码格式不正确",
    }),
})

module.exports = {
    registerSchema
}

校验通过之后需要判断用户是否已经注册过,如果注册过的话直接生成 token 返回即可,没有注册过的需要先进行注册,然后再生成 token 返回。

user 模型中新增方法,判断用户是否已经注册,已经注册过的需要判断密码是否输入正确,最终返回用户相关信息。

修改文件 /app/models/user.js

scala 复制代码
class User extends Model {
    static async verifyTelephone(params){
        // 手机号是唯一的,所以根据手机号来查询用户信息是否存在
        const user = await User.findOne({
          where: {
            telephone: params.telephone
          }
        })
        // 用户信息存在,则说明该用户已经注册
        if(user){
          const correct = bcryct.compareSync(params.password, user.password)
          if(!correct){
            throw new AuthFailed("密码不正确")
          }
        }
        
        return user
    }
}

修改文件 /app/api/v1/user.js

csharp 复制代码
router.post("/login", async (ctx, next) => {
  const params = ctx.request.body;
  // 校验接口参数
  await registerSchema.validateAsync(params);
  let user;
  // 验证用户是否已经注册
  user = await User.verifyTelephone(params)
  // 没有注册过的话先注册创建用户
  if(!user){
    user = await User.create({
        telephone: params.telephone,
        password: params.password,
    });
  }
  // 根据用户id生成token
  const token = generateToken(user.id)
  // 返回token
  ctx.body = {
    token
  }
});

生成 token 采用的是第三方库 jsonwebtoken。先来配置下生成 token 的密钥和过期时间:

修改配置文件 /config/config.js,增加如下配置:

perl 复制代码
security: {
    secretKey: "aeq12@#$89.&", // 这个key要比较复杂,丢失的话jwt令牌就可能会被破解
    expiresIn: 60 * 60 * 24 * 30, // 令牌过期时间
},

然后新建文件 /core/util.js

ini 复制代码
const jwt = require("jsonwebtoken");
const { security } = require("../config/config");

// 使用用户id生成token
const generateToken = function (uid) {
  const secretKey = security.secretKey;
  const expiresIn = security.expiresIn;
  const token = jwt.sign({ uid }, secretKey, { expiresIn });
  return token;
};

module.exports = {
    generateToken
}

用户登录接口已完成,可以用 Postman 来测试我们的接口:

然后打开 phpMyAdmin,可以看到 user 表中新增了一条用户数据:

书籍模块

书籍模块设置了获取书籍列表的接口。

修改文件 /app/api/v1/book.js

ini 复制代码
const Router = require("koa-router");
const { Book } = require("../../models/book");

const router = new Router({
  prefix: "/v1/book",
});

router.get("/list", async (ctx, next) => {
  const books = await Book.getAll();
  ctx.body = {
    books,
  };
});

module.exports = router;

书籍列表接口已完成,可以使用 phpMyAdminbook 表中导入一些数据或者手动创建几条,然后使用 Postman 测试接口 http://localhost:3000/v1/book/list

评论模块

部分接口需要登录才能访问,所以接口需要对 token 进行校验,校验它是否合法,是否有效。新增一个中间件用来校验 token,校验其是否合法采用的是第三方库 basic-auth

先安装依赖:

npm install basic-auth

新建文件 /middlewares/auth.js

ini 复制代码
const basicAuth = require("basic-auth");
const jwt = require("jsonwebtoken");
const { Forbbiden } = require("../core/http-exception");
const { security } = require("../config/config");

class Auth {
  get m() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req);
      let errMsg = "token不合法";
      if (!userToken || !userToken.name) {
        throw new Forbaiden(errMsg);
      }
      try {
        // 如果token是有效的jwt.verify函数会返回JWT中的数据,如果无效或者过期,该函数会抛出一个错误。
        var decode = jwt.verify(userToken.name, security.secretKey);
      } catch (error) {
        if (error.name === "TokenExpiredError") {
          errMsg = "token已过期";
        }
        throw Forbbiden(errMsg);
      }

      ctx.auth = {
        uid: decode.uid,
      };

      await next();
    };
  }
}

修改文件 /core/http-exception.js,新增一个 Forbbiden 类并导出:

scala 复制代码
class Forbbiden extends HttpException {
  constructor(msg, errorCode) {
    super();
    this.code = 403;
    this.msg = msg || "禁止访问";
    this.errorCode = errorCode || 10006;
  }
}

module.exports = {
  // ... 其它的
  Forbbiden
}

评论模块设置了获取评论列表,新增评论的接口。

获取评论列表接口,修改文件 /app/api/v1/comment.js,新增路由如下:

csharp 复制代码
router.get("/:id/list", new Auth().m, async (ctx, next) => {
  const { id } = ctx.params; // 获取路由里的id参数
  // 根据书籍id获取评论
  const commentList = await Comment.getBookCommentList(id);
  ctx.body = {
    commentList,
  };
});

获取评论列表需要先校验权限,所以加了 auth 中间件,用户信息验证通过之后,才会执行下一个路由处理中间件函数。

修改文件 /app/models/comment.js,新增 getBookCommentList 方法:

csharp 复制代码
static async getBookCommentList(id) {
  const commentList = await Comment.findAll({
    order: [["created_at", "DESC"]], // 按照创建时间降序返回
    where: {
      bookId: id, // 查找该id的所有评论
    },
  });
  return commentList;
}

评论列表接口已完成,可以使用 phpMyAdmincomment 表中导入一些数据或者手动创建几条,注意 userIdbookId 需要从 user 表和 book表中拿取, 然后使用 Postman 测试接口 http://localhost:3000/v1/comment/2/list,注意需要配置好 Authorization

接下来我们来实现新增评论接口,该接口需要先校验用户输入信息,校验通过之后才能新增,先来实现校验函数,修改文件 /app/validators/validator.js,增加如下:

css 复制代码
const addCommentSchema = Joi.object({
  bookId: Joi.number().required().messages({
    "number.base": "书籍id必须是数字",
    "string.empty": "书籍id不能为空",
  }),
  content: Joi.string().required().min(3).max(30).messages({
    "string.base": "评论内容必须是字符串",
    "string.empty": "评论内容不能为空",
    "string.min": "最少3个字符",
    "string.max": "最多30个字符",
  }),
});

修改文件 /app/api/v1/comment.js,新增路由如下:

csharp 复制代码
router.post("/add", new Auth().m, async (ctx, next) => {
  // 获取body中传递的参数
  const params = ctx.request.body;
  // 先校验参数
  await addCommentSchema.validateAsync(params);
  await Comment.addComment({
    ...params,
    userId: ctx.auth.uid, // 校验token时我们有将用户id存在ctx.auth.uid中
  });
  success();
});

先来新增公共成功返回方法 success,新增文件 /app/lib/helper.js

javascript 复制代码
const { Success } = require("../../core/http-exception");

function success(msg, errorCode) {
  throw new Success(msg, errorCode);
}

module.exports = {
  success,
};

修改文件/app/models/comment.js,新增方法 addComment

csharp 复制代码
static async addComment(params) {
    // 先校验传入的书籍id是否存在
    const book = await Book.findOne({
      where: {
        id: params.bookId,
      },
    });
    
    if (book) {
      // 存在则新增评论
      return await Comment.create({
        ...params,
      });
    } else {
      // 不存在抛出错误提示
      throw new NotFound();
    }
}

修改文件 /core/http-exception.js,新增 SuccessNotFound 类:

scala 复制代码
class Success extends HttpException {
  constructor(msg, errorCode) {
    super();
    this.code = 201;
    this.msg = msg || "ok";
    this.errorCode = errorCode || 0;
  }
}

class NotFound extends HttpException {
  constructor(msg, errorCode) {
    super();
    this.code = 404;
    this.msg = msg || "资源未找到";
    this.errorCode = errorCode || 10000;
  }
}

module.exports = {
  // ....
  Success,
  NotFound
};

新增评论已完成,可自行进行测试~

收藏模块

收藏模块设置了获取用户收藏列表,新增收藏、取消收藏的接口。

收藏列表接口,修改文件 /app/api/v1/favor.js

csharp 复制代码
static async getMyFavorList(id) {
  const FavorList = await Favor.findAll({
    order: [["created_at", "DESC"]],
    where: {
      userId: id,
    },
  });
  return FavorList;
}

收藏接口,需要先进行校验传参是否正确,所以我们先来新增校验函数,修改文件 /app/validators/validator.js,增加如下:

css 复制代码
const favorSchema = Joi.object({
  bookId: Joi.number().required().messages({
    "number.base": "书籍id必须是数字",
    "number.empty": "书籍id不能为空",
  }),
  likeFlag: Joi.string().required().valid("0", "1").messages({
    "string.valid": "likeFlag传参错误",
  }),
});

然后新增路由,修改文件 /app/api/v1/favor.js

csharp 复制代码
router.post("/like", new Auth().m, async (ctx, next) => {
  const params = ctx.request.body;
  await favorSchema.validateAsync(params);
  await Favor.handleLikeOrDisLike({
    ...params,
    userId: ctx.auth.uid,
  });
  success();
});

修改文件 /app/models/favor.js,新增方法 handleLikeOrDisLike,我们需要先判断是否能找到该用户对这本书籍的收藏信息,如果有收藏信息的话,并且数据库存储的 like_flag 字段和我们传递的 likeFlag 字段都为"0"的话,需要提示用户还没有点赞,都为"1"的话需要提示用户已经点赞过。如果没有收藏信息,但是用户传递的 likeFlag 字段为"0"的话需要提示用户还没有点赞过所以不需要取消点赞。新增和修改 favor 表信息的同时还需要通过修改对应书籍的收藏数量。

csharp 复制代码
static async handleLikeOrDisLike(params) {
  const { bookId, userId, likeFlag } = params;
  // 查找favor中是否有相应信息
  const favor = await Favor.findOne({
    where: {
      bookId: bookId,
      userId: userId,
    },
  });

  // 有查询到信息并且传递的likeFlag字段和数据库中的相等,则需要提示报错
  if (
    favor &&
    favor.getDataValue("likeFlag") === likeFlag 
  ) {
    if (likeFlag === "0") {
      throw new DislikeError();
    } else {
      throw new LikeError();
    }
  }

  // 没有查询到信息,则不能进行取消收藏操作
  if(!favor && likeFlag === "0"){
    throw new DislikeError();
  }

  // 使用事务功能,若其中的所有promise链都执行成功的话,则自动提交,若有一个执行失败,则自动回滚
  return sequelize.transaction(async (t) => {
    if (!favor) {
      // 没有该用户该书籍的收藏信息,则新增
      await Favor.create({
        bookId,
        userId,
        likeFlag,
      });
    }else{
      // 有该用户该书籍的收藏信息,则更新likeFlag字段
      favor.update({
        likeFlag
      })
    }
    // 获取相应书籍的信息
    const book = await Book.getBookDetail(bookId);
    if (likeFlag) {
      // 如果是收藏,则相应书籍的收藏数量加一
      await book.increment("favorNums", { by: 1, transaction: t });
    } else {
      // 如果是取消收藏,则相应书籍的收藏数量减一
      await book.decrement("favorNums", { by: 1, transaction: t });
    }
  });
}

收藏接口已完成,可自行测试~

到此所有的接口都已经实现啦,还木有在实际项目中实战过,有错误的话欢迎大家指出,一起学习交流,天天向上~~

相关推荐
吕彬-前端9 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱11 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
许野平19 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
guai_guai_guai21 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端