前言
大家好,这里是藤原豆腐店,最近在学习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
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
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
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
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中配置