node+koa2搭建通用api服务器

104 阅读18分钟

前言

大家好,这里是藤原豆腐店,最近在学习node的koa框架,下面我总结了一篇使用koa搭建通用api服务器的流程,包括用户注册,登录,权限控制,图片上传,商品和购物车的增删改查,主要拆分了以下几个层级:

  • model层用于数据库的数据表模型
  • router层用于配置接口路径和中间件
  • controller层用于编写业务代码
  • service层用于编写数据库操作代码
  • middleware层用于编写中间件

项目地址:随风/koa_server

项目初始化

生成package.json文件同时进行git初始化

npm init -y
git init

安装koa框架

npm i koa

添加辅助开发工具

nodemon-自动重启服务

nodemon会监视该文件以及其他相关的文件,并在它们发生变化时自动重新启动应用程序。除了应用程序文件,nodemon还可以监视配置文件、模板文件和其他任何你希望它监视的文件。

nodemon还提供了许多其他的配置选项,它们可以通过命令行参数或在项目的nodemon.json配置文件中进行设置。这些选项包括忽略特定文件、延迟重新启动、监视目录中的特定文件等等。

安装nodemon

npm i nodemon -D

在package.json中配置

"scripts": {
  "test": "echo "Error: no test specified" && exit 1",
  "dev": "nodemon app.js"
},

最后通过npm run dev启动

dotenv-读取配置文件

dotenv 是一个 npm 包,它用于在 Node.js 应用程序中加载环境变量。环境变量通常用于存储应用程序的配置信息,例如数据库连接字符串、API 密钥等。使用 dotenv 可以帮助我们管理和加载这些环境变量。

dotenv可以去读取根目录中的.env文件,将配置写到process.env中

npm i detenv

创建.env文件

APP_PORT=8888

创建config/config_default.js的配置文件

const dotenv = require("dotenv");
dotenv.config();
// console.log(process.env.APP_PORT)

module.exports = process.env;

编写main.js

const Koa = require("koa");
const { APP_PORT } = require("./config/config.default");
const app = new Koa();
app.use((ctx,next)=>{
  ctx.body = 'hello world'
})
// 绑定端口
app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

目录解构优化

添加路由

koa-router

安装koa-router

npm i koa-router

创建router目录用于存储不同的路由

在user_route.js按步骤创建路由

// 导入包
const Router = require('koa-router');
// 实例化对象
const router=new Router({prefix:'/users'});

// 编写路由 /users/
router.get('/',(ctx,next)=>{
  ctx.body='hello users'
})

// 导出对象
module.exports = router

在main.js中导入相应路由,并注册中间件

const userRouter = require("./router/user_route");
// 注册中间件
app.use(userRouter.routes())

拆分http服务和app业务

创建app/index.js

// 业务模块
// 引入外部包
const Koa = require("koa");
const userRouter = require("../router/user_route");

// 创建koa的实例对象
const app = new Koa();
// 注册中间件
app.use(userRouter.routes())

module.exports = app

改写main.js

// 服务器入口文件
// 引入外部包
const { APP_PORT } = require("./config/config_default");
const app = require("./app/index");

// 绑定端口
app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

拆分路由和控制器

路由:解析URL,分布给控制器对应的方法

改写user_route.js

const Router = require("koa-router");
const { register, login } = require("../controller/user_controller");
const router = new Router({ prefix: "/user" });

// 注册接口
router.post("/register", register);
// 登录接口
router.post("/login", login);

module.exports = router;

控制器:处理不同的业务

创建controller用于存储业务代码

class UserController{
  async register(ctx,next){
    ctx.body = '用户注册成功'
  }

  async login(ctx,next){
    ctx.body = '用户登录成功'
  }
}

module.exports = new UserController()

解析body

安装并使用koa-body

koa-body

npm i koa-body

在app/index.js中引入并注册中间件

// 业务模块
const Koa = require("koa");
const { koaBody } = require('koa-body');
const userRouter = require("../router/user_route");

// 创建koa的实例对象
const app = new Koa();
// 注册中间件
app.use(koaBody());
app.use(userRouter.routes())

module.exports = app

解析请求数据

const { createUser } = require("../service/user.service");
class UserController {
  async register(ctx, next) {
    // 获取数据
    // console.log(ctx.request.body);
    const { user_name, password } = ctx.request.body;
    // 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res);
    // 返回结果
    ctx.body = ctx.request.body;
  }

拆分service层,主要是做数据库处理

class UserService{
  async createUser(user_name,password){
    // todo:写入数据库
    return '写入数据库成功'
  }
}
module.exports = new UserService()

集成sequelize

Sequelize 简介 | Sequelize中文文档 | Sequelize中文网

安装sequelize

npm i sequelize mysql2

sequelize是ORM数据库工具,ORM是对象关系映射

  • 数据表映射对应一个类
  • 数据表中的数据行(记录)对应一个对象
  • 数据表字段对应对象的属性
  • 数据表的操作对应对象的方法

在.env中定义数据库相关配置

APP_PORT=8888

MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_USER = root
MYSQL_PASSWORD = 123456
MYSQL_DB = yy_visualization

在src目录下创建db文件夹用于存储数据库操作相关文件

创建seq.js用于连接数据库

const {
  MYSQL_HOST,
  MYSQL_USER,
  MYSQL_PASSWORD,
  MYSQL_DB,
} = require("../config/config_default");
const { Sequelize } = require("sequelize");

const sequelize = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PASSWORD, {
  host: MYSQL_HOST,
  dialect: "mysql",
});

// 测试连接
// sequelize
//   .authenticate()
//   .then(() => {
//     console.log("数据库连接成功");
//   })
//   .catch((error) => {
//     console.error("数据库连接失败", error);
//   });

module.exports = sequelize;

路由的自动加载

如果在router目录中添加新的路由文件,每次都需要在app/index.js中添加路由

接下来在router目录下添加index.js文件,统一引入路由

const fs = require('fs')
const Router = require('koa-router')
const router = new Router()

// 统一处理路由
fs.readdirSync(__dirname).forEach(file => { 
  // console.log(file)
  if (file !== "index.js") {
    let r = require('./' + file)
    router.use(r.routes())
  }
})

module.exports = router

改写app/index.js,实现路由的动态加载

const router = require("../router")
// 注册路由
app.use(router.routes()).use(router.allowedMethods());

用户注册

创建User模型

拆分Model层,sequelize主要通过Model对应数据表

创建src/model/user_model.js

const { DataTypes } = require("sequelize");
const seq = require("../db/seq");

// 创建模型(Model yy_user)
const User = seq.define(
  "yy_user",
  {
    // id会被sequelize自动创建
    user_nama: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
      commient: "用户名,唯一",
    },
    password: {
      type: DataTypes.CHAR(64),
      allowNull: false,
      comment: "密码",
    },
    is_admin: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
      defaultValue: 0,
      comment: "是否是管理员,0:不是管理员(默认)1:是管理员",
    },
  },
);

// 强制同步数据库(会重新创建表)
// User.sync({ force: true });

module.exports = User;

添加用户入库

所有数据库的操作都在 Service 层完成, Service 调用 Model 完成数据库操作

改写user_service.js

onst User = require("../model/user_model");
class UserService {
  async createUser(user_name, password) {
    // 插入数据
    const res = await User.create({ user_name, password });
    console.log("service_user", res);
    return res.dataValues;
  }
}
module.exports = new UserService();

改写user_contorller.js

const { createUser, getUserInfo } = require("../service/user.service");
class UserController {
  async register(ctx, next) {
    // 获取接口数据
    // console.log(JSON.parse(ctx.request.body));
    const {user_name,password} = JSON.parse(ctx.request.body);
    // 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res);
    // 返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }

  async login(ctx, next) {
    ctx.body = "用户登录成功";
  }
}

module.exports = new UserController();

添加错误处理

在controller中, 对不同的错误进行处理, 返回不同的提示错误提示, 提高代码质量

const { createUser, getUserInfo } = require("../service/user.service");
class UserController {
  async register(ctx, next) {
    // 获取接口数据
    // console.log(JSON.parse(ctx.request.body));
    const {user_name,password} = JSON.parse(ctx.request.body);
    // 校验数据(合法性,合理性)
    if (!user_name || !password) {
      console.error("用户名或密码不能为空", ctx.request.body);
      ctx.status = 400;
      ctx.body = {
        code: "10001",
        message: "用户名或密码不能为空",
        result: "",
      };
      return;
    }
    if (await getUserInfo({ user_name })) {
      console.log("用户名已存在", ctx.request.body);
      ctx.status = 409;
      ctx.body = {
        code: "10002",
        message: "用户名已存在",
        result: "",
      };
      return;
    }
    // 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res);
    // 返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }

  async login(ctx, next) {
    ctx.body = "用户登录成功";
  }
}

module.exports = new UserController();

在service中封装函数

// 查询用户信息
  async getUserInfo({ id, user_name, password, is_admin }) {
    const whereOpt = {};
    id && Object.assign(whereOpt, { id });
    user_name && Object.assign(whereOpt, { user_name });
    password && Object.assign(whereOpt, { password });
    is_admin && Object.assign(whereOpt, { is_admin });
    if (Object.keys(whereOpt).length > 0) {
      const res = await User.findOne({
        attributes: ["id", "user_name", "password", "is_admin"],
        where: whereOpt,
      });
      return res ? res.dataValues : null;
    } else {
      return null;
    }
}

拆分中间件,统一错误处理

添加constant目录用于存储常量,添加err_type.js用于存储 常见的错误消息

module.exports = {
  userFormateError: {
    code: "10001",
    message: "用户名或密码不能为空",
    result: "",
  },
  userAlreadyExited: {
    code: "10002",
    message: "用户名已存在",
    result: "",
  },
  userRegisterError: {
    code: "10003",
    message: "用户注册失败",
    result: "",
  },
};

添加middleware目录用于存储中间件,添加user_middleware.js用于集成错误处理,在出错的地方使用ctx.app.emit提交错误

const { getUserInfo } = require("../service/user.service");
const {
  userFormateError,
  userAlreadyExited,
  userRegisterError,
} = require("../constant/err_type");
// 校验数据合法性
const userValidator = async (ctx, next) => {
  const { user_name, password } = JSON.parse(ctx.request.body);
  if (!user_name || !password) {
    console.error("用户名或密码不能为空", ctx.request.body);
    // 提交错误信息
    ctx.app.emit("error", userFormateError, ctx);
    return;
  }
  // 执行下一个中间件
  await next();
};

// 验证用户是否已存在
const verifyUser = async (ctx, next) => {
  const { user_name } = JSON.parse(ctx.request.body);
  const res = await getUserInfo({ user_name });
  if (res) {
    console.error("用户名已存在", { user_name });
    ctx.app.emit("error", userAlreadyExited, ctx);
    return;
  }
  await next();
};
module.exports = {
  userValidator,
  verifyUser,
};

在app.js中通过app.on进行监听,统一错误处理

const errHandler = require('./errHandler')
// 统一的错误处理
app.on('error', errHandler)

创建errHandler.js用于定制响应体

module.exports = (err, ctx) => {
  let status = 500
  switch (err.code) {
    case '10001':
      status = 400
      break
    case '10002':
      status = 409
      break
    default:
      status = 500
  }
  ctx.status = status
  ctx.body = err
}

最后在user_route.js中添加对应的中间件

const { userValidator, verifyUser } = require("../middleware/user_middleware");
// 注册接口
router.post("/register", userValidator, verifyUser, register);

密码加密

安装bcryptjs

npm i bcryptjs

编写加密中间件

// 密码加密
const crpytPassword = async (ctx, next) => {
  const requestBody = JSON.parse(ctx.request.body);
  const salt = bcrypt.genSaltSync(10);
  // hash保存的是密文
  const hash = bcrypt.hashSync(requestBody.password, salt);
  requestBody.password = hash;
  ctx.request.body = JSON.stringify(requestBody);
  console.log('密码加密',ctx.request.body)
  await next();
};

在 user_route.js 中添加该中间件

const {
  crpytPassword,
} = require("../middleware/user_middleware");

// 注册接口
router.post("/register", userValidator, verifyUser, crpytPassword, register);

用户登录

登录验证

给err_type.js增加三个错误消息

userDoesNotExist: {
  code: "10004",
  message: "用户不存在",
  result: "",
},
userLoginError: {
  code: "10005",
  message: "用户登录失败",
  result: "",
},
invalidPassword: {
  code: "10006",
  message: "密码不匹配",
  result: "",
}

在user_middleware.js中添加登录验证中间件

// 验证登录
const verifyLogin = async (ctx, next) => {
  // 判断用户是否存在
  const { user_name, password } = JSON.parse(ctx.request.body);
  try {
    const res = await getUserInfo({ user_name });
    if (!res) {
      console.error("用户不存在", { user_name });
      ctx.app.emit("error", userDoesNotExist, ctx);
      return
    }
    // 密码是否匹配
    if (!bcrypt.compareSync(password, res.password)) {
      console.error('密码不匹配')
      ctx.app.emit("error", invalidPassword, ctx);
      return
    }
  }
  catch (err) {
    console.error(err);
    return ctx.app.emit('error', userLoginError, ctx)
  }
  await next();
}

改写路由

// 登录接口
router.post("/login", userValidator, verifyLogin, login);

颁发token

登录成功后,给用户颁发一个令牌token,用户在以后的每一次请求中携带这个令牌

安装jsonwebtoken

jsonwebtoken

npm i jsonwebtoken

在.env定义私钥

JWT_SECRET = xzd

在controller中改写login方法

const jwt = require('jsonwebtoken')
const { JWT_SECRET } = require('../config/config_default')
// 登录
async login(ctx, next) {
  const { user_name } = JSON.parse(ctx.request.body);
  // ctx.body = `欢迎回来,${user_name}`;
  // 获取用户信息(在token的payload中,记录id,user_name,is_admin)
  try {
    const { password, ...res } = await getUserInfo({ user_name })
    ctx.body = {
      code: 0,
      message: "用户登录成功",
      result: {
        token: jwt.sign(res, JWT_SECRET, { expiresIn: '1d' })
      }
    }
  } catch (error) {
    console.error('用户登录失败',err)
  }
}

用户认证

创建auth中间件

const jwt = require('jsonwebtoken')
const { JWT_SECRET } = require('../config/config_default')
const { tokenExpiredError, invalidToken } = require('../constant/err_type')
// 验证token是否有效
const auth = async (ctx, next) => {
  const { authorization } = ctx.request.header;
  // console.log('验证token的header',ctx.request.header)
  const token = authorization.replace('Bearer ', '');
  try {
    // user中包含了payload的信息
    const user = jwt.verify(token, JWT_SECRET)
    // 待处理 user没有携带到ctx.state.user上
    console.log('验证token的user',user)
    ctx.state.user = user
  } catch (err) {
    switch (err.name) {
      case 'TokenExpiredError':
        console.error('token已过期', err)
        return ctx.app.emit('error', tokenExpiredError, ctx)
      case 'JsonWebTokenError':
        console.error('无效的token', err)
        return ctx.app.emit('error', invalidToken, ctx)
    }
  }
  await next()
}
module.exports = {
  auth
}

添加错误信息

tokenExpiredError: {
  code: "10101",
  message: "token已过期",
  result: "",
},
invalidToken: {
  code: '10102',
  message: '无效的token',
  result: ''
}

改写router

// 修改密码
router.patch('/', auth, (ctx, next) => {
  ctx.body = '修改密码成功';
})

实现修改密码

在service中根据id更新用户信息

// 根据id更新用户信息
async updateById({ id, user_name, password, is_admin }) {
  const whereOpt = {id};
  const newUser = {};
  user_name && Object.assign(newUser, { user_name });
  password && Object.assign(newUser, { password });
  is_admin && Object.assign(newUser, { is_admin });

  const res = await User.update(newUser, { where: whereOpt });
  console.log('操作数据库更新密码', res)
  return res[0]
}

在controller中实现修改密码的方法

 // 修改密码 
  async changePassword(ctx, next) {
    // 获取数据
    console.log('修改密码用户信息', ctx.state.user)
    const id = ctx.state.user.id
    const password = JSON.parse(ctx.request.body).password
    // 操作数据库
    if (await updateById({ id, password })) {
      ctx.body = {
        code: 0,
        message: "密码修改成功",
        result: ''
      }
    } else {
      ctx.body = {
        code: '10007',
        message: '修改密码失败',
        result: ''
      }
    }
  }
}

改写router

// 修改密码
router.patch('/', auth, crpytPassword, changePassword)

权限控制

在auth中间件添加对应方法,通过is_admin判断是否有管理员权限

const { tokenExpiredError, invalidToken, hadNotAdminPermission } = require('../constant/err_type');
// 判断是否有管理员权限
const hadAdminPermission = async (ctx, next) => {
  const { is_admin } = ctx.state.user
  if (!is_admin) {
    console.error('该用户没有管理员的权限', ctx.state.user)
    return ctx.app.emit('error', hadNotAdminPermission, ctx)
  }
}

添加错误信息

hadNotAdminPermission: {
  code: "10103",
  message: '没有管理员权限',
  result: '',
}

在路由中使用

const { auth, hadAdminPermission } = require('../middleware/auth_middleware')
const { upload } = require('../controller/good_controller')
router.post('/upload', auth, hadAdminPermission, upload)
module.exports = router

商品模块

图片上传

在koaBody中配置文件上传的目录

app.use(koaBody({
  multipart: true,
  formidable: {
    // 在option里的相对路径,不是相对的当前文件,相对process.cwd()
    // 设置用于放置文件上传的目录
    uploadDir: path.join(__dirname, '../upload'),
    // 写入的文件将包含原始文件的扩展名
    keepExtensions: true,
  }
}));

在控制器中实现图片上传的逻辑

const path = require('path')
const { fileUploadError, unSupportedFileType } = require('../constant/err_type')
class GoodController {
  async upload(ctx, next) {
    // console.log('图片信息', ctx.request.files.img)
    const { img } = ctx.request.files
    // 支持的文件类型
    const fileTypes = ['image/jpeg', 'image/png']
    if (img) {
      // 判断文件类型
      if (!fileTypes.includes(img.mimetype)) {
        return ctx.app.emit('error', unSupportedFileType, ctx)
      }
      ctx.body = {
        code: 0,
        message: '图片上传成功',
        result: {
          goods_img: path.basename(img.filepath)
        }
      }
    } else {
      return ctx.app.emit('error', fileUploadError, ctx)
    }
  }
}

module.exports = new GoodController()

添加错误信息

fileUploadError: {
    code: "10201",
    message: '商品图片上传失败',
    result: '',
  },
unSupportedFileType: {
  code: '10202',
  message: '不支持的文件类型',
  result: ''
}

改写路由

const { auth, hadAdminPermission } = require('../middleware/auth_middleware')
const { upload } = require('../controller/good_controller')
router.post('/upload', auth, hadAdminPermission, upload)

添加koa-static包

npm i koa-static

在app/index.js中引入,并且配置访问路径

const KoaStatic = require("koa-static");
app.use(KoaStatic(path.join(__dirname, "../upload")));

可以通过这种方式访问图片

http://localhost:8888/图片名.jpg

apifox中配置接口的类型和请求参数

统一参数格式校验

koa-parameter

引入koa-parameter

npm i koa-parameter

在app/index.js引入,在路由之前注册

const parameter = require('koa-parameter')
app.use(parameter(app))
// 注册路由
app.use(router.routes()).use(router.allowedMethods());

添加good_middleware中间件,实现格式检验方法

const { goodsFormatError } = require('../constant/err_type')
const validator = async (ctx, next) => {
  try {
    // 参数格式校验
    ctx.verifyParams({
      goods_name: { type: 'string', required: true },
      goods_price: { type: 'number', required: true },
      goods_num: { type: 'number', required: true },
      goods_img: { type: 'string', required: true }
    })
  } catch (error) {
    console.error(error)
    // 赋值错误信息
    goodsFormatError.result = error
    return ctx.app.emit('error', goodsFormatError, ctx)
  }
  await next()
}
module.exports = {
  validator
}

添加错误信息

goodsFormatError: {
    code: '10203',
    message: '商品参数格式错误',
    result: ''
  }

apifox测试-响应体中返回对应错误消息

创建商品模型

在model目录下创建good_model.js

const { DataTypes } = require("sequelize");
const seq = require("../db/seq");

const Good = seq.define("yy_goods", {
  goods_name: {
    type: DataTypes.STRING,
    allowNull: false, //不允许为空
    comment: '商品名称',
  },
  goods_price: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    comment: '商品价格'
  },
  goods_num: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: '商品数量'
  },
  goods_img: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '商品图片的url'
  }
})

Good.sync({ force: true })
module.exports = Good

添加商品

service层进行数据操作

const Good = require('../model/good_model')
class GoodService {
  async createGoods(goods) {
    const res = await Good.create(goods)
    return res.dataValues
  }
}
module.exports = new GoodService();

controller层添加create方法

 // 添加商品
async create(ctx) {
  try {
    const { createdAt, updatedAt, ...res } = await createGoods(ctx.request.body)
    ctx.body = {
      code: 0,
      message: '商品发布成功',
      result: res,
    }
  } catch (error) {
    console.error(error)
    return ctx.app.emit('error', publishGoodsError, ctx)
  }
}

改写router

const { upload,create } = require('../controller/good_controller')
//  发布商品接口
router.post('/', auth, hadAdminPermission, validator, create)

在apifox添加发布商品的接口

修改商品

在controller和service中添加修改商品逻辑

// 修改商品
async update(ctx) {
  ctx.body = '修改商品成功'
  const res = await updateGoods(ctx.params.id, ctx.request.body)
  if (res) {
    ctx.body = {
      code: 0,
      message: '商品修改成功',
      result: '',
    }
  } else {
    return ctx.app.emit('error', invalidGoodsID, ctx)
  }
}
async updateGoods(id, goods) {
  try {
    const res = await Good.update(goods, { where: { id } })
    return res[0] > 0 ? true : false
  } catch (error) {
    console.error(error)
  }
}
 invalidGoodsID: {
  code: '10205',
  message: '待修改的商品不存在',
  result: ''
}

改写router

// 修改商品接口
router.post('/:id', auth, hadAdminPermission, validator, update)

在apifox中添加修改商品接口

上下架商品

good_model添加paranoid,启用软删除

const { DataTypes } = require("sequelize");
const seq = require("../db/seq");

const Good = seq.define("yy_goods", {
  goods_name: {
    type: DataTypes.STRING,
    allowNull: false, //不允许为空
    comment: '商品名称',
  },
  goods_price: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    comment: '商品价格'
  },
  goods_num: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: '商品数量'
  },
  goods_img: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '商品图片的url'
  },
},
{
  // 启用软删除
  paranoid: true
})

// Good.sync({ force: true })
module.exports = Good

数据表会添加deletedAt属性

上下架商品controller层实现操作逻辑

// 下架商品
async remove(ctx) {
  const res = await removeGoods(ctx.params.id)
  if (res) {
    ctx.body = {
      code: 0,
      message: '商品下架成功',
      result: '',
    }
  } else {
    return ctx.app.emit('error', invalidGoodsID, ctx)
  }
}

// 上架商品
async restore(ctx) {
  const res = await restoreGoods(ctx.params.id)
  if (res) {
    ctx.body = {
      code: 0,
      message: '商品上架成功',
      result: '',
    }
  } else {
    return ctx.app.emit('error', invalidGoodsID, ctx)
  }
}

service层 下架调用destory方法,上架调用restore方法

async removeGoods(id) {
  const res = await Good.destroy({ where: { id } })
  return res > 0 ? true : false
}
async restoreGoods(id) {
  const res = await Good.restore({ where: { id } })
  return res > 0 ? true : false
}

改写路由

// 下架商品接口
router.post('/:id/off', auth, hadAdminPermission, remove)
// 上架商品
router.post('/:id/on', auth, hadAdminPermission, restore)

apifox中配置上下架接口

商品列表接口

controller层实现获取商品列表的具体逻辑

// 商品列表
async findAll(ctx) {
  const { pageNum = 1, pageSize = 10 } = ctx.request.query
  const res = await findGodds(pageNum, pageSize)
  if (res) {
    ctx.body = {
      code: 0,
      message: '商品列表获取成功',
      result: res
    }
  } else {
    return ctx.app.emit('error', findAllError, ctx)
  }
}

service层调用findAndCountAll获取总数和具体数据

async findGodds(pageNum, pageSize) {
  // 获取总数和分页的具体数据
  const offset = (pageNum - 1) * pageSize
  // offset定义跳过的数据量,limit定义单次的数据量
  const { count, rows } = await Good.findAndCountAll({ offset: offset, limit: +pageSize })
  return {
    pageNum,
    pageSize,
    total: count,
    list: rows
  }
}

定义错误信息

findAllError: {
  code: '10206',
  message: '查询列表失败',
  result: ''
}

改写路由

// 获取商品列表
router.get('/', findAll)

apifox中定义接口信息

测试接口返回信息

购物车模块

创建购物车模型

const { DataTypes } = require("sequelize");
const seq = require("../db/seq");

const Cart = seq.define("yy_carts", {
  goods_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: "商品ID",
  },
  user_id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: "用户ID",
  },
  number: {
    type: DataTypes.INTEGER,
    allowNull: false,
    comment: "商品数量",
    defaultValue: 1,
  },
  selected: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    comment: "是否选中",
    defaultValue: true,
  },
});

// Cart.sync({ force: true });
module.exports = Cart;

添加商品到购物车

添加商品到购物车之前会进行三次判断

  • 验证是否登录
  • 验证参数是否合法
  • 验证商品id是否存在

在router目录下添加cart_router.js

const Router = require("koa-router");
const { auth } = require("../middleware/auth_middleware");
const { validator } = require("../middleware/cart_middleware");
const { add } = require("../controller/cart_controller");
const { verifyGood } = require("../middleware/good_middleware");
const router = new Router({ prefix: "/carts" });

// 添加到购物车接口(登录验证,参数验证,商品验证,添加商品)
router.post("/", auth, validator, verifyGood, add);
module.exports = router;

middleware目录下添加cart_middleware.js用于参数验证

const { invalidGoodsID } = require("../constant/err_type");
const validator = async (ctx, next) => {
  try {
    console.log('cart_validator')
    ctx.verifyParams({
      goods_id: "number",
    });
  } catch (error) {
    console.error(error);
    return ctx.app.emit("error", invalidGoodsID, ctx);
  }
  await next();
};
module.exports = {
  validator
}

在good_middleware下添加该中间件用于判断商品是否已存在

// 验证商品是否已存在
const verifyGood = async (ctx, next) => {
  const { goods_id } = ctx.request.body;
  const res = await getGoodInfo({ goods_id });
  if (!res) {
    console.error("该商品不存在", { goods_id });
    ctx.app.emit("error", invalidGoodsID, ctx);
    return;
  }
  await next();
};

good_service下添加数据库的查询操作

// 获取商品信息
  async getGoodInfo({ goods_id, goods_name, goods_price, goods_num }) {
    const whereOpt = {};
    goods_id && Object.assign(whereOpt, { id: goods_id });
    goods_name && Object.assign(whereOpt, { goods_name });
    goods_price && Object.assign(whereOpt, { goods_price });
    goods_num && Object.assign(whereOpt, { goods_num });
    console.log(whereOpt);
    if (Object.keys(whereOpt).length > 0) {
      const res = await Good.findOne({
        attributes: ["id", "goods_name", "goods_price", "goods_num"],
        where: whereOpt,
      });
      console.log("获取商品信息service", res);
      return res ? res.dataValues : null;
    } else {
      return null;
    }
  }
}

在controller目录下添加cart_controller.js

const { createOrUpdate } = require("../service/cart_service");
class CartController {
  // 添加购物车
  async add(ctx) {
    // 解析数据(user_id,goods_id)
    const user_id = ctx.state.user.id;
    const goods_id = ctx.request.body.goods_id;
    // console.log(user_id, goods_id);
    const res = await createOrUpdate(user_id, goods_id);
    ctx.body = {
      code: 0,
      message: "添加到购物车成功",
      result: res,
    };
  }
}
module.exports = new CartController();

service层需要判断购物车是否已经存在商品

  • 存在的话对应的number+1
  • 不存在的话创建一条
const { Op } = require("sequelize");
const Cart = require("../model/cart_model");
class cardService {
  async createOrUpdate(user_id, goods_id) {
    // 查询购物车中是否已经存在该商品
    let res = await Cart.findOne({
      where: {
        [Op.and]: {
          user_id,
          goods_id,
        },
      },
    });
    if (res) {
      // 已经存在一条记录,number+1
      await res.increment("number");
      // 重载实例
      return await res.reload();
    } else {
      // 创建实例
      return await Cart.create({
        user_id,
        goods_id,
      });
    }
  }
}
module.exports = new cardService();

apifox中添加对应接口

获取购物车列表

修改route

const { findAll } = require("../controller/cart_controller");
// 获取购物车列表
router.get("/", auth, findAll);

在controller中添加获取列表方法

const { findAllError } = require("../constant/err_type");
const { createOrUpdate, findCarts } = require("../service/cart_service");
class CartController {
  // 获取列表
  async findAll(ctx) {
    // 解析数据
    const { pageNum = 1, pageSize = 10 } = ctx.request.query;
    // 操作数据库
    const res = await findCarts(pageNum, pageSize);
    if (res) {
      ctx.body = {
        code: 0,
        message: "获取购物车列表成功",
        result: res,
      };
    } else {
      ctx.app.emit("error", findAllError, ctx);
    }
  }
}

在service中编写数据库相关的操作,这里需要使用good_id去查询goods表的数据,需要用到关联查询

关联 | Sequelize中文文档 | Sequelize中文网

在model层指定goods_id为外键

const Goods = require("./good_model");
// 指定外键
Cart.belongsTo(Goods, { foreignKey: "goods_id", as: "goods_info" });

通过include指定关联查询的内容

const Cart = require("../model/cart_model");
const Good = require("../model/good_model");
class cardService {
  async findCarts(pageNum, pageSize) {
    const offset = (pageNum - 1) * pageSize;
    const { count, rows } = await Cart.findAndCountAll({
      attributes: ["id", "number", "selected"],
      offset: offset,
      limit: +pageSize,
      include: {
        model: Good,
        // 返回指定列表字段名
        as: "goods_info",
        // 返回特定的列表
        attributes: ["id", "goods_name", "goods_price", "goods_img"],
      },
    });
  }
}

最后在apifox配置接口

更新购物车

因为更新购物车接口同样需要验证请求参数,这里修改验证请求参数的中间件以便可以在两个接口复用,传入rules用于定制参数验证

const { invalidGoodsID, cartFormatError } = require("../constant/err_type");
const validator = (rules) => {
  return async (ctx, next) => {
    try {
      console.log("cart_validator", rules);
      ctx.verifyParams(rules);
    } catch (error) {
      console.error(error);
      cartFormatError.result=err
      return ctx.app.emit("error", invalidGoodsID, ctx);
    }
    await next();
  };
};
module.exports = {
  validator,
};

改写router

const Router = require("koa-router");
const { auth } = require("../middleware/auth_middleware");
const { validator } = require("../middleware/cart_middleware");
const { add, findAll, update } = require("../controller/cart_controller");
const { verifyGood } = require("../middleware/good_middleware");
const router = new Router({ prefix: "/carts" });

// 添加到购物车接口(登录验证,参数验证,商品验证,添加商品)
router.post("/", auth, validator({ goods_id: "number" }), verifyGood, add);
// 获取购物车列表(登录验证,获取列表)
router.get("/", auth, findAll);
// 更新购物车(登录验证,参数验证,获取列表)
router.patch(
  "/:id",
  auth,
  validator({
    number: { type: "number", required: false },
    selected: { type: "bool", required: false },
  }),
  update
);
module.exports = router;

controller中添加更新购物车的方法

 async update(ctx) {
    const { id } = ctx.request.params;
    const { number, selected } = ctx.request.body;
    // console.log('controller',id, number, selected)
    if (number === undefined && selected === undefined) {
      cartFormatError.message = "number和selected不能同时为空";
      return ctx.app.emit("error", cartFormatError, ctx);
    }
    const res = await updateCarts({ id, number, selected });
    ctx.body = {
      code: 0,
      message: "更新购物车成功",
      result: res,
    };
  }

service中添加更新数据库相关逻辑

async updateCarts(params) {
    const { id, number, selected } = params;
    const res = await Cart.findByPk(id);
    if (!res) return "";
    number !== undefined ? (res.number = number) : "";
    selected !== undefined ? (res.selected = selected) : "";
    return await res.save();
  }

apifox中添加接口

删除购物车

添加对应的路由

// 删除购物车(登录验证,参数验证)
router.delete("/", auth, validator({ ids: 'array' }), remove);

koa-body严格模式仅支持解析'POST', 'PUT', 'PATCH'的body参数,delete方法需要在koa-body中增加配置

// 注册中间件
app.use(koaBody({
  multipart: true,
  formidable: {
    // 在option里的相对路径,不是相对的当前文件,相对process.cwd()
    // 设置用于放置文件上传的目录
    uploadDir: path.join(__dirname, '../upload'),
    // 写入的文件将包含原始文件的扩展名
    keepExtensions: true,
  },
  // 定义支持解析的方法名(没生效)
  parseMethods: ['POST', 'PUT', 'PATCH', 'DELETE']
}));

controller层

const {
  removeCarts
} = require("../service/cart_service");
async remove(ctx) {
  const { ids } = ctx.request.body;
  const res = await removeCarts(ids);
  ctx.body = {
    code: 0,
    message: "删除购物车成功",
    result: res,
  }
}

service层编写数据库方法

async removeCarts(ids) {
  return await Cart.destroy({
    where: {
      id: {
        [Op.in]: ids,
      }
    }
  })
}

在apifox中配置

全选和全不选

添加路由

// 全选与全不选
router.post('/selectAll', auth, selectAll)

controller中通过selected判断是全选还是全不选

async selectAll(ctx) {
  const user_id = ctx.state.user.id
  const { selected } = ctx.request.body
  const res = await selectAllCarts(user_id, selected)
  if (selected) {
    ctx.body = {
      code: 0,
      message: "全选选中",
      result: res
    }
  } else {
    ctx.body = {
      code: 0,
      message: "取消全选",
      result: res
    }
  }
}

service层去更新数据库的selected数据

async selectAllCarts(user_id,selectd) {
  return await Cart.update({ selected: selectd }, {
    where: {
      user_id
    }
  })
}

apifox中配置