Koa2入门 https://www.bilibili.com/video/BV18h411H7GE?spm_id_from=333.999.0.0
Node+Koa2搭建API
服务 教程来源:https://www.bilibili.com/video/BV13A411w79h?spm_id_from=333.999.0.0
初始化
npm init -y
git init
,并新建.gitignore
,添加node_modules
新建README.md
文档
项目初始化
npm i koa
根目录新建src/main.js
1 2 3 4 5 6 7 8 9 10 11 const Koa = require ('koa' ) const app = new Koa () app.use ((ctx, next ) => { ctx.body = 'hello world' }) app.listen (3000 , () => { console .log ('server is running on http://localhost:3000 !' ); })
启动开发服务:
node .\src\main.js
:node
方式启动,是常驻内存,不是热加载的
开发优化 自动重启服务
读取配置文件
添加路由 所谓的api
,就是根据不同url
返回不同的数据
目前http://localhost:8000
和http://localhost:8000/users
返回的内容都是一样的
需要使用路由,来根据不同的url
,调用不同的处理函数
安装koa-router
官网案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 const Koa = require ('koa' );const Router = require ('@koa/router' ); const app = new Koa ();const router = new Router (); router.get ('/' , (ctx, next ) => { }); app .use (router.routes ()) .use (router.allowedMethods ());
配置多个路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const Koa = require ('koa' ) const Router = require ('@koa/router' ) const {APP_PORT } = require ('./config/config.default' )const app = new Koa () const indexRouter = new Router ()indexRouter.get ('/' , (ctx, next ) => { ctx.body = 'hello world' }) const userRouter = new Router ()userRouter.get ('/user' , (ctx, next ) => { ctx.body = 'user' }) app.use (indexRouter.routes ()) app.use (userRouter.routes ()) app.listen (APP_PORT , () => { console .log (`server is running on http://localhost:${APP_PORT} !` ); })
但是代码都写在一起肯定不行,需要拆分一下路由
新建src/router
文件夹
新建user.routes.js
1 2 3 4 5 6 7 8 9 10 const Router = require ('@koa/router' )const router = new Router ({prefix : '/user' })router.get ('/' , (ctx, next ) => { ctx.body = 'user' }) module .exports = router
修改main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Koa = require ('koa' ) const Router = require ('@koa/router' ) const {APP_PORT } = require ('./config/config.default' )const app = new Koa () const userRouter = require ('./router/user.route' )const indexRouter = new Router ()indexRouter.get ('/' , (ctx, next ) => { ctx.body = 'hello world' }) app.use (indexRouter.routes ()) app.use (userRouter.routes ()) app.listen (APP_PORT , () => { console .log (`server is running on http://localhost:${APP_PORT} !` ); })
目录结构优化 拆分http
服务和业务代码 我们在main.js
里面写了太多的功能
拆分http
服务与业务相关代码
新建src/app/index.js
,专门放业务代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Koa = require ('koa' ) const Router = require ('@koa/router' ) const userRouter = require ('../router/user.route' )const app = new Koa () const indexRouter = new Router ()indexRouter.get ('/' , (ctx, next ) => { ctx.body = 'hello world' }) app.use (indexRouter.routes ()) app.use (userRouter.routes ()) module .exports = app
修改man.js
1 2 3 4 5 6 7 8 9 10 const {APP_PORT } = require ('./config/config.default' )const app = require ('./app' )app.listen (APP_PORT , () => { console .log (`server is running on http://localhost:${APP_PORT} !` ); })
抽离控制层 将路由router
中的处理函数,单独抽离成控制层controller
解析body
、拆分service
层 解析body
完整的注册接口
请求参数
响应
成功:
1 2 3 4 5 6 7 8 { "code": 0, "message": "用户注册成功", "result": { "id": 2, "user_name": "user" } }
原型图:
koa
需要借助中间件,来解析参数
将之前apifox
的注册接口完善下,由于是post
请求,我们在body
里面,设置参数
安装koa-body
在app/index.js
中添加koa-body
相关代码
app/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Koa = require ('koa' )const Router = require ('@koa/router' )const koaBody = require ('koa-body' ) const userRouter = require ('../router/user.route' )const app = new Koa ()app.use (koaBody ()) const indexRouter = new Router ()indexRouter.get ('/' , (ctx, next ) => { ctx.body = `Request Body: ${JSON .stringify(ctx.request.body)} ` }) app.use (indexRouter.routes ()) app.use (userRouter.routes ()) module .exports = app
控制层(处理函数)user.config.js
1 2 3 4 5 6 7 8 9 10 11 12 class UserController { async register (ctx, next ) { console .log (ctx.request .body ) ctx.body = ctx.request .body } async login (ctx, next ) { ctx.body = '用户登录成功' } } module .exports = new UserController ()
回到apifox
中,我们生成下body
请求体,发送下注册的请求
后台也打印结果了
控制器里一般做这些事
1.获取数据
2.操作数据库
如果操作数据库的逻辑很复杂,也会单独抽出这一部分(service
层)
3.返回结果
抽取servcie
层 和数据库相关的,根据客户端传递的不同参数,来操作数据库
新建src/service
目录
新建user.service.js
1 2 3 4 5 6 7 8 9 10 11 class UserService { async createUser (user_name, password ) { return '写入数据库成功' } } module .exports = new UserService ()
修改user.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const {createUser} = require ('../service/user.service' )class UserController { async register (ctx, next ) { const {user_name, password} = ctx.request .body const res = await createUser (user_name, password) ctx.body = ctx.request .body } async login (ctx, next ) { ctx.body = '用户登录成功' } } module .exports = new UserController ()
后台打印结果:
ORM
工具集成sequelize
介绍ORM
:对象关系映射
数据表映射(对应)一个类
数据表中的数据行(记录)对应一个对象
数据表字段对应对象的属性
数据表的操作,对应对象的方法
就是使用面向对象的方式,来操作数据库
使用sequelize
ORM
数据库工具:https://github.com/demopark/sequelize-docs-Zh-CN/tree/master
基于Promise
的ORM
工具
Sequelize 是一个基于 promise
的 Node.js ORM
工具, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server, Amazon Redshift 和 Snowflake’s Data Cloud
. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能.
安装sequelize
和mysql2
(支持Promise
)
得注意下安装的sequelize
支持的最低版本的mysql
,目前默认安装的sequlize
版本是6.21.3
,对应的mysql
版本至少是5.7
及以上:https://github.com/demopark/sequelize-docs-Zh-CN/tree/v6
安装数据库 windows
安装在正式连接之前,我们需要装下mysql
数据库,这里暂时安装windows
版本,参照:https://blog.csdn.net/jsugs/article/details/124143762
启动mysql
服务
1 输入net start mysql或sc start mysql
改密码后再用navicat连接
,会报错Authentication plugin 'caching_sha2_password' cannot be loaded
,参照:https://www.jianshu.com/p/465a444ad846
1 2 3 ALTER USER 'root' @'localhost' IDENTIFIED BY '123123' PASSWORD EXPIRE NEVER; ALTER USER 'root' @'localhost' IDENTIFIED WITH mysql_native_password BY '123123' ; FLUSH PRIVILEGES;
查询mysql进程,并杀掉
1 2 3 netstat -aon|findstr "3306" taskkill /pid 26372 -t -f
成功进入后,新建数据库
一开始是没有选中下面两个的,设置名称后直接确定
linux
安装使用docker
安装,参见
最后一步报语法错误,可以不用管
连接数据库 官方示例:https://github.com/demopark/sequelize-docs-Zh-CN/blob/v6/core-concepts/getting-started.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const { Sequelize } = require ('sequelize' );const sequelize = new Sequelize ('sqlite::memory:' ) const sequelize = new Sequelize ('postgres://user:pass@example.com:5432/dbname' ) const sequelize = new Sequelize ({ dialect : 'sqlite' , storage : 'path/to/database.sqlite' }); const sequelize = new Sequelize ('database' , 'username' , 'password' , { host : 'localhost' , dialect : });
新建src/db/seq.js
该文件中实现数据库的连接,并导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const {Sequelize } = require ('sequelize' )const seq = new Sequelize ('mytest' , 'root' , '123123' , { host : 'localhost' , dialect : 'mysql' }) seq.authenticate ().then (res => { console .log ('数据库连接成功' , res) }).catch (error => { console .log ('数据库连接失败' , error) }) module .exports = seq
在db
目录下,使用node
测试下:
开发环境我们这样搞没事,生产环境可能会用连接池
配置文件 使用dotenv
将参数提取成配置文件
修改.env
1 2 3 4 5 6 7 APP_PORT = 8000 MYSQL_HOST = localhost MYSQL_PORT = 3306 MYSQL_USER = root MYSQL_PASSWORD = 123123 MYSQL_DATABASE = mytest
seq.js
中导入并使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const { Sequelize } = require ('sequelize' )const { MYSQL_HOST , MYSQL_PORT , MYSQL_USER , MYSQL_PASSWORD , MYSQL_DATABASE } = require ('../config/config.default' ) console .log (MYSQL_HOST ,MYSQL_DATABASE )const seq = new Sequelize (MYSQL_DATABASE , MYSQL_USER , MYSQL_PASSWORD , { host : MYSQL_HOST , dialect : 'mysql' , }) module .exports = seq
此时需要在根目录下测试,不然读不到.env
文件
测试完将测试代码注释掉
创建User
模型 模型创建 新建src/model
文件夹
service
层通过model
层来具体操作数据库
新建user.model.js
,使用define
方法来创建模型:https://www.sequelize.com.cn/core-concepts/model-basics#%E4%BD%BF%E7%94%A8-sequelizedefine
全局定义表名等于模型名,seq.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const { Sequelize } = require ('sequelize' )const { MYSQL_HOST , MYSQL_PORT , MYSQL_USER , MYSQL_PASSWORD , MYSQL_DATABASE } = require ('../config/config.default' ) console .log (MYSQL_HOST ,MYSQL_DATABASE )const seq = new Sequelize (MYSQL_DATABASE , MYSQL_USER , MYSQL_PASSWORD , { host : MYSQL_HOST , dialect : 'mysql' , define : { freezeTableName : true } }) module .exports = seq
根据表设计文档,定义模型属性:
用户表
表名:sai_users
字段名
字段类型
说明
id
int
主键,自增(sequelize会自动维护)
user_name
varchar(255)
用户名,unique
password
char(64)
密码
is_admin
tinyint(1)
0:不是管理员,1:是管理员
定义模型属性时的数据类型,参见:https://www.sequelize.com.cn/core-concepts/model-basics#%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B
user.model.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const { DataTypes } = require ("sequelize" ) const seq = require ('../db/seq' )const User = seq.define ('sai_user' , { user_name : { type : DataTypes .STRING , allowNull : false , unique : true , comment : '用户名唯一' }, password : { type : DataTypes .CHAR (64 ), allowNull : false , comment : '密码' }, is_admin : { type : DataTypes .BOOLEAN , allowNull : false , defaultValue : 0 , comment : '是否为管理员,0:不是管理员(默认值),1:是管理员' } }) User .sync ({force : true })
有关模型同步:https://www.sequelize.com.cn/core-concepts/model-basics#%E6%A8%A1%E5%9E%8B%E5%90%8C%E6%AD%A5
根目录下,执行node src/model/user.model.js
就是执行了sql
语句
1 2 3 4 5 6 PS D:\workspace\github\code\project-workshop\code-prac\koa\01> node .\src\model\user.model.js localhost mytest Executing (default): DROP TABLE IF EXISTS `sai_user`; Executing (default): CREATE TABLE IF NOT EXISTS `sai_user` (`id ` INTEGER NOT NULL auto_increment , `user_name` VARCHAR(255) NOT NULL UNIQUE COMMENT '用户名唯一' , `password` CHAR(64) NOT NULL COMMENT ' 密码' , `is_admin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为管理员,0:不是管理员(默认值),1:是管理员' , `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id `)) ENGINE=InnoDB;Executing (default): SHOW INDEX FROM `sai_user`
可以看到,数据库中多了一个表:
其中,createAt
和updatedAt
是sequelize
自动给我们维护的,如果不需要时间戳,在define
函数中,添加配置项:{timestamps: false}
,但是一般情况下,都是保留的
导出User
模型,并注释掉sync
的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const { DataTypes } = require ("sequelize" )const seq = require ('../db/seq' )const User = seq.define ('sai_user' , { user_name : { type : DataTypes .STRING , allowNull : false , unique : true , comment : '用户名唯一' }, password : { type : DataTypes .CHAR (64 ), allowNull : false , comment : '密码' }, is_admin : { type : DataTypes .BOOLEAN , allowNull : false , defaultValue : 0 , comment : '是否为管理员,0:不是管理员(默认值),1:是管理员' } }) module .exports = User
添加用户 我们继续完善写入数据库的代码
需要通过ORM
实现标准的CRUD
:https://www.sequelize.com.cn/core-concepts/model-querying-basics
新建src/service/user.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const User = require ('../model/user.model' )class UserService { async createUser (user_name, password ) { const res = await User .create ({ user_name, password }) console .log (res) return res } } module .exports = new UserService ()
使用apifox
发送register
接口,成功后查看数据库
成功注册,注意下时区慢8小时
再看下后台打印:
执行的了insert
语句
User.create
返回的是一个sai_user
的表模型对象,dataValues
对应着表里面的一条记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Executing (default): INSERT INTO `sai_user` (`id `,`user_name`,`password`,`is_admin`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?,?); sai_user { dataValues: { is_admin: false , id : 1, user_name: '邵洋' , password: 'do' , updatedAt: 2022-07-26T15:08:52.849Z, createdAt: 2022-07-26T15:08:52.849Z }, _previousDataValues: { user_name: '邵洋' , password: 'do' , id : 1, is_admin: false , createdAt: 2022-07-26T15:08:52.849Z, updatedAt: 2022-07-26T15:08:52.849Z }, uniqno: 1, _changed: Set(0) {}, _options: { isNewRecord: true , _schema: null, _schemaDelimiter: '' , attributes: undefined, include: undefined, raw: undefined, silent: undefined }, isNewRecord: false }
我们要返回给用户dataValues
的结果
对于其他的值,在service
层就可以直接过滤掉,直接返回res.dataValues
user.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const User = require ('../model/user.model' )class UserService { async createUser (user_name, password ) { const res = await User .create ({ user_name, password }) return res.dataValues } } module .exports = new UserService ()
那么controller
层拿到返回的数据后,再根据接口文档,构建最终要返回给客户端的数据格式
注册接口:
成功
1 2 3 4 5 6 7 8 { "code" : 0 , "message" : "用户注册成功" , "result" : { "id" : 2 , "user_name" : "user" } }
失败
1 2 3 4 5 { "code" : "10001" , "message" : "用户名或密码不能为空" , "result" : "" }
修改控制层
user.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const {createUser} = require ('../service/user.service' )class UserController { async register (ctx, next ) { const {user_name, password} = ctx.request .body const res = await createUser (user_name, password) ctx.body = { code : 0 , message : '用户注册成功' , result : { id : res.id , user_name : res.user_name } } } async login (ctx, next ) { ctx.body = '用户登录成功' } } module .exports = new UserController ()
再次使用apifox
测试下register
接口,注意要使用新的样例(不要重复注册,目前没做异常处理)
整个的流程小结:
用户发送请求,koa
服务接受到请求,先导入各种中间件,然后处理路由,根据路由调用处理函数(控制层),处理函数中涉及业务逻辑及数据库操作(服务层),服务层根据模型层,返回给控制层操作数据库的结果,控制层根据该结果封装接口数据,返回给路由,最后koa
将路由的结果,作为接口响应发送到服务端
错误处理 重复注册和没有用户名,目前都会返回500
,错误类型不够细致
后台是可以看到两次操作的错误提示的
对于不同的错误类型,我们要分别处理
在控制层接受到用户参数时,要进行验证
合法性验证
user.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const {createUser} = require ('../service/user.service' )class UserController { async register (ctx, next ) { const {user_name, password} = ctx.request .body if (!user_name || !password) { console .error ('用户名或密码为空' ) ctx.status = 400 ctx.body = { code : '10001' , 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 ()
参数只写一个字段,测试一下注册接口:
可以看到后台,打印的错误日志
合理性验证
错误处理函数封装 我们可以把格式的验证,单独封装成一个中间件(处理函数)
在controller
层拆分中间件
新建middleware/user.middleware.js
,中间件里定义各种函数,然后导出
user.middleware.js
从controller
里把合法性验证的代码抽离出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const userValidator = async (ctx, next ) => { const { user_name, password } = ctx.request .body if (!user_name || !password) { console .error ('用户名或密码为空' , ctx.request .body ) ctx.status = 400 ctx.body = { code : '10001' , message : '用户名或者密码为空' , result : '' } return } await next () } module .exports = { userValidator }
那么什么时候执行中间件呢?
在路由匹配的时候,只要路由一匹配上,就立刻调用校验的中间件
user.route.js
1 2 3 4 5 6 7 8 9 10 11 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/user' })const { register, login } = require ('../controller/user.conroller' )const { userValidator } = require ('../middleware/user.middleware' )router.post ('/register' , userValidator, register) router.post ('/login' , login) module .exports = router
用apifox
测试一下,可以正常打印错误日志
再抽离查询用户的代码(合理性验证)
user.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const { getUserInfo } = require ('../service/user.service' )const userValidator = async (ctx, next ) => { const { user_name, password } = ctx.request .body if (!user_name || !password) { console .error ('用户名或密码为空' , ctx.request .body ) ctx.status = 400 ctx.body = { code : '10001' , message : '用户名或者密码为空' , result : '' } return } await next () } const verifyUser = async (ctx, next ) => { const { user_name } = ctx.request .body if (getUserInfo ({ user_name })) { ctx.status = 409 ctx.body = { code : '10002' , message : '用户名已经存在' , result : '' } return } await next () } module .exports = { userValidator, verifyUser }
user.route.js
1 2 3 4 5 6 7 8 9 10 11 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/user' })const { register, login } = require ('../controller/user.conroller' )const { userValidator, verifyUser } = require ('../middleware/user.middleware' )router.post ('/register' , userValidator, verifyUser, register) router.post ('/login' , login) module .exports = router
测试下用户名已存在的情况,正常
至此,逻辑已经很清晰了,但我们还可以进一步管理错误信息
统一错误管理 在koa
中,怎么进行错误管理呢?
通过ctx.app
可以拿到实例化的koa
对象,它有一个emit
方法用来提交错误,我们可以对此做一个检测
ctx.app.emit('error', {}, ctx)
该对象是自定义的错误类型,可以统一放在一个新的文件里管理
新建src/constant
常量文件夹
新建error.type.js
1 2 3 4 5 6 7 8 9 10 11 12 13 module .exports = { userFormateError : { code : '10001' , message : '用户名或者密码为空' , result : '' }, userAlreadyExists : { code : '10002' , message : '用户名已经存在' , result : '' } }
修改user.middle.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const { getUserInfo } = require ('../service/user.service' )const { userFormateError, userAlreadyExists } = require ('../constant/error.type' )const userValidator = async (ctx, next ) => { const { user_name, password } = 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 } = ctx.request .body if (getUserInfo ({ user_name })) { ctx.app .emit ('error' , userAlreadyExists, ctx) return } await next () } module .exports = { userValidator, verifyUser }
在app/index.js
中统一进行错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Koa = require ('koa' )const Router = require ('@koa/router' )const koaBody = require ('koa-body' )const userRouter = require ('../router/user.route' )const errHandler = require ('./errorHandler' )const app = new Koa ()app.use (koaBody ()) const indexRouter = new Router ()indexRouter.get ('/' , (ctx, next ) => { ctx.body = `Request Body: ${JSON .stringify(ctx.request.body)} ` }) app.use (indexRouter.routes ()) app.use (userRouter.routes ()) app.on ('error' , errHandler) module .exports = app
同级目录下,新建errHandler.js
错误处理函数
errHandler.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 }
小问题 verifyUser
中间件调用的getUserInfo
返回的是一个Promise
对象,恒为真,正常注册流程也走不下去了
user.middleware.js
1 2 3 4 5 6 7 8 9 10 11 const verifyUser = async (ctx, next ) => { const { user_name } = ctx.request .body if (getUserInfo ({ user_name })) { ctx.app .emit ('error' , userAlreadyExists, ctx) return } await next () }
修改
添加await
,相当于判断条件是一个表达式
1 2 3 4 5 6 7 8 9 10 11 const verifyUser = async (ctx, next ) => { const { user_name } = ctx.request .body if (await getUserInfo ({ user_name })) { ctx.app .emit ('error' , userAlreadyExists, ctx) return } await next () }
另外假设中间件都没问题,到了控制层createUser
这一步了,目前我们对这一步做任何的异常处理,使用try-catch
来处理下
虽说这一步是sequelize
自己操作的数据库,不大可能会出错,但为了代码的健壮性,还是要处理下的
user.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const { createUser } = require ('../service/user.service' )const { userRegisterError } = require ('../constant/error.type' )class UserController { async register (ctx, next ) { const { user_name, password } = ctx.request .body try { const res = await createUser (user_name, password) console .log (res) ctx.body = { code : 0 , message : '用户注册成功' , result : { id : res.id , user_name : res.user_name } } } catch (err) { console .log (err) ctx.app .emit ('error' , userRegisterError, ctx) return } } async login (ctx, next ) { ctx.body = '用户登录成功' } } module .exports = new UserController ()
定义错误类型
constant/error.type.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module .exports = { userFormateError : { code : '10001' , message : '用户名或者密码为空' , result : '' }, userAlreadyExists : { code : '10002' , message : '用户名已经存在' , result : '' }, userRegisterError : { code : '10003' , message : '用户注册错误' , result : '' } }
我们在createUser
方法中,模拟下写入数据库时发生了错误
user.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const User = require ('../model/user.model' )class UserService { async createUser (user_name, password ) { const res = await User .create ({ user_name, password }) console .log (aa) return res.dataValues } } module .exports = new UserService ()
测试下
后台日志
建议调用service
层所有的函数时,都加上错误处理
继续完善verifyUser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const verifyUser = async (ctx, next ) => { const { user_name } = ctx.request .body try { const res = await getUserInfo ({ user_name }) if (res) { console .error ('用户名已存在' , { user_name }) ctx.app .emit ('error' , userAlreadyExists, ctx) return } } catch (err) { console .err ('获取用户信息错误' , err) ctx.app .emit ('error' , userQueryError, ctx) return } await next () }
error.type.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 module .exports = { userFormateError : { code : '10001' , message : '用户名或者密码为空' , result : '' }, userAlreadyExists : { code : '10002' , message : '用户名已经存在' , result : '' }, userRegisterError : { code : '10003' , message : '用户注册错误' , result : '' }, userQueryError : { code : '10004' , message : '用户查询错误' , result : '' } }
可以看到,真正要写代码的部分,其实是比较少的
很重要的一部分,在于提高代码的质量上,都在一些异常捕获和错误处理上,这一块是需要下功夫的,平时写代码的时候,要有这样的意识
这样当代码上线后,如果有问题,我们调试错误会很方便
ctx.app.emit
提交的错误,最后会以接口的形式返回给客户端,如果服务器自己想记录错误信息,可以使用console.error()
来记录日志信息
目前来说,这里还是有一个问题,就是重复注册直接走到了register
,验证重复用户名的中间件直接就通过了!!
原因是getUserInfo
函数里的User.findOne
是一个异步函数,需要加一个await
,否则会出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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}) const res = await User .findOne ({ attributes : ['id' , 'user_name' , 'password' , 'is_admin' ], where : whereOpt }) return res ? res.dataValues : null } }
这样重复注册走到verifyUser
中间件时,就不会被当做异常处理
加密 在将密码保存到数据库之前,要对密码进行加密处理
md5
加密还是有可能被破解的,使用bcrypt
加密
第一个依赖也多,这里我们使用第二个bcryptjs
:在 JavaScript 中优化了 bcrypt
,零依赖关系。
安装
用法:有同步和异步的用法:https://www.npmjs.com/package/bcryptjs
使用:
将代码加密功能,也抽离成一个中间件(单一职责原则)
不想bcrypt
这种加密方式,耦合到代码中
后面有可能会换成其他加密方式:hash
、md5
user.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const bcrypt = require ('bcryptjs' )const scyptPassword = async (ctx, next ) => { const { password } = ctx.request .body const salt = bcrypt.genSaltSync (10 ); const hash = bcrypt.hashSync (password, salt); ctx.request .body .password = hash await next () } module .exports = { userValidator, verifyUser, scyptPassword }
user.route.js
导入并使用
测试注册接口成功后,查看user
表password
字段,已加密
仍存在的问题:密码在前端传给后端的过程中,是明文的,应该前端先加密,后端存储密文;而登录时,后面采用和前端一样的加密算法解密即可
小结:注册接口 梳理一下整理流程,及每个模块的功能
登录接口 user.controller.js
1 2 3 4 async login (ctx, next ) { const { user_name } = ctx.request .body ctx.body = `欢迎回来,${user_name} ` }
完善apifox
添加样例,应为数据库有已有的数据
但事实上,现在login
对任何的输入都是可以的,我们需要对数据进行一个校验
是否为空(合法性校验)
是否存在(合理性校验)
验证登录
user.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const verifyLogin = async (ctx, next ) => { const { user_name, password } = ctx.request .body try { const res = await getUserInfo ({ user_name }) if (!res) { console .log ('用户不存在' , res) return ctx.app .emit ('error' , userNotFound, ctx) } if (!bcrypt.compareSync (password, res.password )) { return ctx.app .emit ('error' , userInvalidPassword, ctx) } } catch (err) { console .error (err) return ctx.app .emit ('error' , userLoginFailed, ctx) } await next () }
登录成功后记录用户状态
用户认证和授权 颁发token
登录成功后,给用户颁发一个令牌token
,用户在以后的每一次请求中,携带这个令牌
前后端分离中,使用jwt
:json web token
header
:头部
payload
:载荷
signature
:签名
如何使用
用户认证 我们新建一个修改密码的接口
修改的操作
写对应的路由
user.route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/user' })const { register, login } = require ('../controller/user.conroller' )const { userValidator, verifyUser, scyptPassword, verifyLogin } = require ('../middleware/user.middleware' )router.post ('/register' , userValidator, verifyUser, scyptPassword, register) router.post ('/login' , userValidator, verifyLogin, login) router.patch ('/modifyPassword' , (ctx, next ) => { ctx.body = '修改密码成功' }) module .exports = router
先简单测试下
我们在上面颁发token
后,后续的请求(如修改密码)需要携带这个token
请求头新增Authorization
,值一开始固定的是Bearer
,有一个空格
后面发送请求时,需要加上签发的token
新建auth.middleware.js
文件,将认证相关的验证放在这里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const jwt = require ('jsonwebtoken' )const { JWT_SECRET } = require ('../config/config.default' )const { tokenExpiredError, invalidToken} = require ('../constant/error.type' )const auth = async (ctx, next ) => { const { authorization = 'Bearer ' } = ctx.request .header const token = authorization.replace ('Bearer ' ,'' ) console .log ('token' , token) try { const user = jwt.verify (token, JWT_SECRET ) ctx.state .user = user } catch (err) { switch (err.name ) { case 'TokenExpiredError' : console .error ('token已过期' , err) ctx.app .emit ('error' , tokenExpiredError, ctx) return case 'JsonWebTokenError' : console .error ('无效的token' , err) return ctx.app .emit ('error' , invalidToken, ctx) default : console .error ('token错误' , err) return } } await next () } module .exports = { auth }
error.type.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module .exports = { tokenExpiredError : { code : '10101' , message : 'token已过期' , result : '' }, invalidToken : { code : '10102' , message : '无效的token' , result : '' } }
在路由上,加上token
认证的中间件
user.route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/user' })const { register, login } = require ('../controller/user.conroller' )const { userValidator, verifyUser, scyptPassword, verifyLogin } = require ('../middleware/user.middleware' )const { auth } = require ('../middleware/auth.middleware' )router.post ('/register' , userValidator, verifyUser, scyptPassword, register) router.post ('/login' , userValidator, verifyLogin, login) router.patch ('/modifyPassword' , auth, (ctx, next ) => { ctx.body = '修改密码成功' }) module .exports = router
接口测试工具中,将登录成功后颁发的token
,存为全局变量
这样在修改密码的接口里,就可以不用复制token
了
但这里有个不好的地方,我想测试下错误token
的响应,是不支持修改的,测试的时候还是需要手动复制
正常接口返回的user
1 2 3 4 5 6 7 8 user { id : 46, user_name: 'sai' , is_admin: false , iat: 1659173449, exp: 1659259849 }
测试无效的token
把token
失效时间改为5s
,测试过期token
(记得测试完改回去)
重新登录后,测试下
修改密码 测试没问题后,我们再加上加密的中间件,并正式写修改密码接口对应的处理函数
user.route.js
1 2 router.patch ('/modifyPassword' , auth, scyptPassword, modifyPassword)
user.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const { createUser, getUserInfo, updateById } = require ('../service/user.service' )async modifyPassword (ctx, next ) { const id = ctx.state .user .id const password = ctx.request .body .password try { const res = await updateById ({ id, password }) if (res) { ctx.body = { code : 0 , message : '修改密码成功' , result : '' } } else { ctx.app .emit ('error' , modifyPasswordFailed, ctx) return } } catch (err) { console .error ('修改密码更新数据库失败' , err) ctx.app .emit ('error' , modifyPasswordFailed, ctx) return } console .log (id, password) }
user.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 }) return res[0 ] > 0 ? true : false }
小问题:入参为空对象时,应该做处理;修改密码还是原密码时,应该做处理
商品模块 整体流程打通 路由
新建router/goods.route.js
1 2 3 4 5 6 7 8 9 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/goods' })const { upload } = require ('../controller/goods.controller' )router.post ('/upload' , upload) module .exports = router
控制器
新建controller/goods/controller.js
1 2 3 4 5 6 7 8 class GoodsController { async upload (ctx, next ) { ctx.body = '商品上传成功' } } module .exports = new GoodsController ()
app/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Koa = require ('koa' )const Router = require ('@koa/router' )const koaBody = require ('koa-body' )const userRouter = require ('../router/user.route' )const goodsRouter = require ('../router/goods.route' )const errHandler = require ('./errHandler' )const app = new Koa ()app.use (koaBody ()) app.use (userRouter.routes ()) app.use (goodsRouter.routes ()) app.on ('error' , errHandler) module .exports = app
测试接口
至此,整个流程已经打通
自动加载路由 上面我们在app/index.js
注册路由时,需要手动一个个导入
可以使用fs
模块实现自动导入
新建router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 const fs = require ('fs' )const Router = require ('@koa/router' )const router = new Router ()fs.readdirSync (__dirname).forEach (file => { if (file !== 'index.js' ) { let r = require ('./' + file) router.use (r.routes ()) } }) module .exports = router
改写app/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Koa = require ('koa' )const koaBody = require ('koa-body' )const router = require ('../router' ) const errHandler = require ('./errHandler' )const app = new Koa ()app .use (koaBody ()) .use (router.routes ()) .use (router.allowedMethods ()) .on ('error' , errHandler) module .exports = app
use(router.allowedMethods())
,请求类型不支持的话,会更友好的响应
封装管理员权限 准备工作
goods.route.js
1 2 3 4 5 6 7 8 9 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/goods' })const { upload } = require ('../controller/goods.controller' )const { auth, hasAdminPermission } = require ('../middleware/auth.middleware' )router.post ('/upload' , auth, hasAdminPermission, upload) module .exports = router
auth.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const { tokenExpiredError, invalidToken, hasNoAdminPermission } = require ('../constant/error.type' )const hasAdminPermission = async (ctx, next ) => { const { is_admin } = ctx.state .user if (!is_admin) { console .log ('该用户无管理员权限' , ctx.state .user ) ctx.app .emit ('error' , hasNoAdminPermission, ctx) return } await next () } module .exports = { auth, hasAdminPermission }
error.type.js
1 2 3 4 5 6 hasNoAdminPermission : { code : '10103' , message : '无管理员权限' , result :'' }
然后注册admin
用户,在数据库里is_admin
字段的值改为1
,接着用测试工具登录,然后测一下上传图片接口
图片上传 koa-body
是支持文件上传的(参数配置项,见npm
)
multipart
配置项默认是false
,要改成true
formidable
是一些关于文件上传的配置信息,multipart
依赖fomidabel
这个包
uploadDir
:上传路径
keepExtensions
:保持后缀名
会把上传成功的文件信息,挂载到ctx.request.files
中
修改app.index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const path = require ('path' )const Koa = require ('koa' )const koaBody = require ('koa-body' )const router = require ('../router' ) const errHandler = require ('./errHandler' )const app = new Koa ()app .use (koaBody ({ multipart : true , formidable : { uploadDir : path.join (__dirname, '../upload' ), keepExtensions : true } })) .use (router.routes ()) .use (router.allowedMethods ()) .on ('error' , errHandler) module .exports = app
我们在upload
处理函数里,打印下文件信息
1 2 3 4 5 6 7 8 9 class GoodsController { async upload (ctx, next ) { console .log ('file' , ctx.request .files .file ) ctx.body = '商品上传成功' } } module .exports = new GoodsController ()
信息如下,有用的几个字段:lastModifiedDate
、mimetype
、size
等
注意:不同版本的koa-body
,里面的字段可能不一样,需要打印看下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 file PersistentFile { _events: [Object: null prototype] { error: [Function (anonymous)] }, _eventsCount: 1, _maxListeners: undefined, lastModifiedDate: 2022-07-30T23:43:02.773Z, filepath: 'D:\\workspace\\github\\code\\project-workshop\\code-prac\\koa\\01\\src\\upload\\8d12dbfca3833df9df3594700.png' , newFilename: '8d12dbfca3833df9df3594700.png' , originalFilename: 'blog.png' , mimetype: 'image/png' , hashAlgorithm: false , size: 257457, _writeStream: WriteStream { fd: null, path: 'D:\\workspace\\github\\code\\project-workshop\\code-prac\\koa\\01\\src\\upload\\8d12dbfca3833df9df3594700.png' , flags: 'w' , mode: 438, start: undefined, pos: undefined, bytesWritten: 257457, closed: false , _writableState: WritableState { objectMode: false , highWaterMark: 16384, finalCalled: false , needDrain: true , ending: true , ended: true , finished: true , destroyed: true , decodeStrings: true , defaultEncoding: 'utf8' , length: 0, writing: false , corked: 0, sync : false , bufferProcessing: false , onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true , allNoop: true , pendingcb: 0, constructed: true , prefinished: true , errorEmitted: false , emitClose: true , autoDestroy: true , errored: null, closed: false , closeEmitted: false , [Symbol(kOnFinished)]: [] }, _events: [Object: null prototype] { error: [Function (anonymous)] }, _eventsCount: 1, _maxListeners: undefined, [Symbol(kFs)]: { appendFile: [Function: appendFile], appendFileSync: [Function: appendFileSync], access: [Function: access], accessSync: [Function: accessSync], chown : [Function: chown ], chownSync: [Function: chownSync], chmod : [Function: chmod ], chmodSync: [Function: chmodSync], close: [Function: close], closeSync: [Function: closeSync], copyFile: [Function: copyFile], copyFileSync: [Function: copyFileSync], cp : [Function: cp ], cpSync: [Function: cpSync], createReadStream: [Function: createReadStream], createWriteStream: [Function: createWriteStream], exists: [Function: exists], existsSync: [Function: existsSync], fchown: [Function: fchown], fchownSync: [Function: fchownSync], fchmod: [Function: fchmod], fchmodSync: [Function: fchmodSync], fdatasync: [Function: fdatasync], fdatasyncSync: [Function: fdatasyncSync], fstat: [Function: fstat], fstatSync: [Function: fstatSync], fsync: [Function: fsync], fsyncSync: [Function: fsyncSync], ftruncate: [Function: ftruncate], ftruncateSync: [Function: ftruncateSync], futimes: [Function: futimes], futimesSync: [Function: futimesSync], lchown: [Function: lchown], lchownSync: [Function: lchownSync], lchmod: undefined, lchmodSync: undefined, link : [Function: link ], linkSync: [Function: linkSync], lstat: [Function: lstat], lstatSync: [Function: lstatSync], lutimes: [Function: lutimes], lutimesSync: [Function: lutimesSync], mkdir : [Function: mkdir ], mkdirSync: [Function: mkdirSync], mkdtemp: [Function: mkdtemp], mkdtempSync: [Function: mkdtempSync], open: [Function: open], openSync: [Function: openSync], opendir: [Function: opendir], opendirSync: [Function: opendirSync], readdir: [Function: readdir], readdirSync: [Function: readdirSync], read : [Function: read ], readSync: [Function: readSync], readv: [Function: readv], readvSync: [Function: readvSync], readFile: [Function: readFile], readFileSync: [Function: readFileSync], readlink : [Function: readlink ], readlinkSync: [Function: readlinkSync], realpath : [Function], realpathSync: [Function], rename: [Function: rename], renameSync: [Function: renameSync], rm : [Function: rm ], rmSync: [Function: rmSync], rmdir : [Function: rmdir ], rmdirSync: [Function: rmdirSync], stat : [Function: stat ], statSync: [Function: statSync], symlink: [Function: symlink], symlinkSync: [Function: symlinkSync], truncate : [Function: truncate ], truncateSync: [Function: truncateSync], unwatchFile: [Function: unwatchFile], unlink : [Function: unlink ], unlinkSync: [Function: unlinkSync], utimes: [Function: utimes], utimesSync: [Function: utimesSync], watch: [Function: watch], watchFile: [Function: watchFile], writeFile: [Function: writeFile], writeFileSync: [Function: writeFileSync], write: [Function: write], writeSync: [Function: writeSync], writev: [Function: writev], writevSync: [Function: writevSync], Dir: [class Dir], Dirent: [class Dirent], Stats: [Function: Stats], ReadStream: [Getter/Setter], WriteStream: [Getter/Setter], FileReadStream: [Getter/Setter], FileWriteStream: [Getter/Setter], _toUnixTimestamp: [Function: toUnixTimestamp], F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1, constants: [Object: null prototype], promises: [Getter] }, [Symbol(kIsPerformingIO)]: false , [Symbol(kCapture)]: false }, hash : null, [Symbol(kCapture)]: false }
完善upload
处理函数
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const path = require ('path' )const { fileUploadFailed } = require ('../constant/error.type' )class GoodsController { async upload (ctx, next ) { const { file } = ctx.request .files if (file) { ctx.body = { code : 0 , message : '商品图片上传成功' , result : { goods_img : path.basename (file.filepath ) } } } else { return ctx.app .emit ('error' , fileUploadFailed, ctx) } } } module .exports = new GoodsController ()
error.type.js
每一种类型可以进一步携带status
,这里没有做那么细
1 2 3 4 5 6 fileUploadFailed : { code : '10201' , message : '商品图片上传失败' , result :'' }
测试:
静态资源回显 现在我们希望对静态资源做一个回显,将某个目录设置成静态资源文件夹
安装koa-static
app/index.js
中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const path = require ('path' )const Koa = require ('koa' )const koaBody = require ('koa-body' )const KoaStatic = require ('koa-static' )const router = require ('../router' ) const errHandler = require ('./errHandler' )const app = new Koa ()app .use (koaBody ({ multipart : true , formidable : { uploadDir : path.join (__dirname, '../upload' ), keepExtensions : true } })) .use (KoaStatic (path.join (__dirname, '../upload' ))) .use (router.routes ()) .use (router.allowedMethods ()) .on ('error' , errHandler) module .exports = app
重启项目,可以通过http://localhost:8000/4f791979542fe84e316506b00.png
访问静态资源(后面跟实际的文件名)
前端实现上传图片 后面会讲到
图片类型判断 不能在upload
处理函数里面校验,走到这一步时图片已经上传了
下面的方式不建议
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async upload (ctx, next ) { const { file } = ctx.request .files console .log (file) const fileTypes = ['image/png' , 'image/jpg' , 'image/jpeg' , 'image/webp' ] if (file) { if (!fileTypes.includes (file.mimetype )) { return ctx.app .emit ('error' , unSupportedFileType, ctx) } ctx.body = { code : 0 , message : '商品图片上传成功' , result : { goods_img : path.basename (file.filepath ) } } } else { return ctx.app .emit ('error' , fileUploadFailed, ctx) } }
error.type.js
1 2 3 4 5 unSupportedFileType : { code : '10202' , message : '商品图不支持的文件类型' , result :'' }
测试接口上传txt
文件
但我们看到,后台文件其实已经上传了
更好的方式失去配置formidable
,但如果要统一错误处理,还需要写专门的中间件去返回错误类型
发布商品接口
路由
请求参数
1 goods_name, goods_price, goods_num, goods_img
响应
成功
1 2 3 4 5 6 7 8 9 10 11 { "code" : 0 , "message" : "发布商品成功" , "result" : { id: "" , goods_name: "" , goods_price: "" , goods_img: "" , goods_num: "" } }
新增路由
goods.route.js
1 2 3 4 const { upload, release } = require ('../controller/goods.controller' )router.post ('/release' , auth, hasAdminPermission, release)
参数格式校验 发布商品,除了需要认证、授权中间件,还需要参数格式校验的中间件
新建goods.middleware.js
1 2 3 4 5 6 7 const validator = async (ctx, next ) => {} module .exports = { validator }
之前写user
模块的时候,我们是自己写的参数校验,不是说不好,实际写业务的时候,可以直接用社区里稳定的包
koa-parameter
或者其它(先跟着教程里的来,这个库是5年前的,并且周下载量 不高了)https://www.npmjs.com/package/koa-parameter,是基于`parameter`这个库
安装
在app/index.js
中导入,并在路由注册之前,导入该中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const path = require ('path' )const Koa = require ('koa' )const koaBody = require ('koa-body' )const KoaStatic = require ('koa-static' )const parameter = require ('koa-parameter' ) const router = require ('../router' )const errHandler = require ('./errHandler' )const app = new Koa ()app .use (koaBody ({ multipart : true , formidable : { uploadDir : path.join (__dirname, '../upload' ), keepExtensions : true } })) .use (KoaStatic (path.join (__dirname, '../upload' ))) .use (parameter (app)) .use (router.routes ()) .use (router.allowedMethods ()) .on ('error' , errHandler) module .exports = app
完善校验逻辑
goods.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const {goodsParamsError} = require ('../constant/error.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 (err) { console .error (err) goodsParamsError.result = err ctx.app .emit ('error' , goodsParamsError, ctx) return } await next () } module .exports = { validator }
error.type.js
1 2 3 4 5 goodsParamsError : { code : '10203' , message : '商品参数格式错误' , result :'' }
新建发布商品接口,并测试
我们将价格参数写成字符串,错误提示正确:
发布商品写入数据库 goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const path = require ('path' )const { fileUploadFailed, unSupportedFileType, publishGoodsError } = require ('../constant/error.type' )const {createGoods} = require ('../service/goods.service' )class GoodsController { async upload (ctx, next ) { const { file } = ctx.request .files console .log ("file" , file) const fileTypes = ['image/png' , 'image/jpg' , 'image/jpeg' , 'image/webp' ] if (file) { if (!fileTypes.includes (file.mimetype )) { return ctx.app .emit ('error' , unSupportedFileType, ctx) } ctx.body = { code : 0 , message : '商品图片上传成功' , result : { goods_img : path.basename (file.filepath ) } } } else { return ctx.app .emit ('error' , fileUploadFailed, ctx) } } async release (ctx, next ) { try { const {createdAt, updatedAt, ...res} = await createGoods (ctx.request .body ) ctx.body = { code : 0 , message : '发布商品成功' , result : res } } catch (err) { console .error (err) return ctx.app .emit ('error' , publishGoodsError, ctx) } } } module .exports = new GoodsController ()
error.type.js
1 2 3 4 5 publishGoodsError : { code : '10204' , message : '商品发布失败' , result :'' }
goods.service.js
1 2 3 4 5 6 7 8 9 const Goods = require ('../model/goods.model' ) class goodsService { async createGoods (goods ) { const res = await Goods .create (goods) return res.dataValues } } module .exports = new goodsService ()
goods.model.js
生成表结构后,记得注释掉seq.sync()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const {DataTypes } = require ('sequelize' )const seq = require ('../db/seq' )const Goods = seq.define ('sai_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地址' } }) module .exports = Goods
测试接口
修改商品接口
请求参数
1 goods_name, goods_price, goods_num, goods_img
响应
成功
1 2 3 4 5 6 7 8 9 10 { "code" : 0 , "message" : "修改商品成功" , "result" : { "id" : "" , "goods_name" : "" , "goods_prcie" : "" , "goods_img" : "" } }
新建修改商品的测试接口
goods.route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/goods' })const { upload, release, update } = require ('../controller/goods.controller' )const { auth, hasAdminPermission } = require ('../middleware/auth.middleware' )const { validator } = require ('../middleware/goods.middleware' )router.post ('/upload' , auth, hasAdminPermission, upload) router.post ('/release' , auth, hasAdminPermission, validator, release) router.put ('/update/:id' ,auth, hasAdminPermission, validator, update) module .exports = router
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 async update (ctx, next ) { try { 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) } } catch (err) { console .error (err) return ctx.app .emit ('error' , updateGoodsError, ctx) } }
error.type.js
1 2 3 4 5 6 7 8 9 10 invalidGoodsId : { code : '10205' , message : '待修改的商品不存在' , result :'' }, updateGoodsError : { code : '10206' , message : '更新商品失败' , result :'' }
goods.service.js
1 2 3 4 5 async updateGoods (id, goods ) { const res = await Goods .update (goods, { where : { id } }) return res[0 ] > 0 ? true : false } }
测试接口
成功:
失败:
删除商品接口
硬删除(直接从数据库中删除)
软删除(通过字段标识是否删除)
1 DELETE /goods/remove/:id
请求参数
响应
成功
1 2 3 4 5 { "code" : 0 , "message" : "删除商品成功" , "result" : "" }
硬删除 goods.router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/goods' })const { upload, release, update, remove } = require ('../controller/goods.controller' )const { auth, hasAdminPermission } = require ('../middleware/auth.middleware' )const { validator } = require ('../middleware/goods.middleware' )router.post ('/upload' , auth, hasAdminPermission, upload) router.post ('/release' , auth, hasAdminPermission, validator, release) router.put ('/update/:id' ,auth, hasAdminPermission, validator, update) router.delete ('/remove/:id' , auth, hasAdminPermission, remove) module .exports = router
goods.controller.js
1 2 3 4 5 6 7 8 async remove (ctx, next ) { await removeGoods (ctx.params .id ) ctx.body = { code : 0 , message : '商品删除成功' , result : '' } }
goods.service.js
1 2 3 4 5 async removeGoods (id ) { const res = await Goods .destroy ({ where : { id } }) return res[0 ] > 0 ? true : false } }
软删除
扩展
可以做成上下架的状态,考虑加一个状态字段,删除商品做成下架,下架商品更新字段值
一般不会硬删除的
修改goods.model.js
define
函数新增第三个参数,然后取消sync
注释重新创建表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const {DataTypes } = require ('sequelize' )const seq = require ('../db/seq' )const Goods = seq.define ('sai_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 }) Goods .sync ({force : true })module .exports = Goods
重新执行后,商品表会多一个字段,删除时会更新一个时间戳,表示删除
将硬删除改为软删除接口
goods.router.js
1 2 router.post ('/remove/:id/off' , auth, hasAdminPermission, remove)
新建软删除测试接口,deleteAt
字段有值,表示下架
调通后,完善错误处理
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const { fileUploadFailed, unSupportedFileType, publishGoodsError, invalidGoodsId } = require ('../constant/error.type' ) async remove (ctx, next ) { const res = await removeGoods (ctx.params .id ) if (res) { ctx.body = { code : 0 , message : '商品下架成功' , result : '' } } else { return ctx.app .emit ('error' , invalidGoodsId, ctx) } }
goods.service.js
1 2 3 4 async removeGoods (id ) { const res = await Goods .destroy ({ where : { id } }) return res > 0 ? true : false }
测试下架商品接口,
对于重复下架、下架不存在的商品,给出错误提示
上架接口
goods.router.js
1 2 router.post ('/remove/:id/on' , auth, hasAdminPermission, restore)
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 async restore (ctx, next ) { const res = await restoreGoods (ctx.params .id ) if (res) { ctx.body = { code : 0 , message : '商品上架成功' , result : '' } } else { return ctx.app .emit ('error' , invalidGoodsId, ctx) } }
goods.service.js
1 2 3 4 async restoreGoods (id ) { const res = await Goods .restore ({ where : { id } }) return res > 0 ? true : false }
测试上架接口,
对于重复上架、上架不存在的商品,给出错误提示
商品列表接口
请求参数
1 2 pageNum(default=1) pageSize(default=10)
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "code" : 0 , "message" : "获取商品成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_name" : "" , "goods_price" : "" , "goods_img" : "" } , { "id" : 2 , "goods_name" : "" , "goods_price" : "" , "goods_img" : "" } , ] } }
goods.route.js
1 2 3 router.get ('/lists' , findAll)
goods.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async findAll (ctx, next ) { try { console .log (ctx.params .query ) const { pageNum = 1 , pageSize = 10 } = ctx.request .query const res = await findGoods (pageNum, pageSize) ctx.body = { code : 0 , message : '获取商品列表成功' , result : res } } catch (err) { console .error (err) } }
goods.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async findGoods (pageNum, pageSize ) { const count = await Goods .count () const offset = (pageNum - 1 ) * pageSize const rows = await Goods .findAll ({ offset, limit : pageSize * 1 }) return { pageNum, pageSize, total : count, list : rows } }
这个接口还是有很多问题的,如参数校验、异常处理等
购物车模块 添加购物车
请求参数
计算登录用户的user_id
如果该用户下的goods_id
不存在,新建一条记录
如果该用户下的goods_id
已存在,更新数量+1
响应
购物车表
表名:sai_carts
字段名
字段类型
说明
id
int
主键,自增
goods_id
int
商品id
user_id
int
用户id
number
int
数量
selected
tinyint
0:没选中;1:选中
cart.route.js
1 2 3 4 5 6 7 8 9 10 11 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/carts' })const { validator } = require ('../middleware/cart.middleware' )const { auth } = require ('../middleware/auth.middleware' )const { add } = require ('../controller/cart.controller' )router.post ('/' , auth, validator, add) module .exports = router
cart.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const {createOrUpdate} = require ('../service/cart.service' )class CartController { async add (ctx, next ) { try { const user_id = ctx.state .user .id const goods_id = ctx.request .body .goods_id const res = await createOrUpdate (user_id, goods_id) ctx.body = { code : 0 , message : '添加到购物车成功' , result : res } } catch (err) { console .error (err) } } } module .exports = new CartController ()
cart.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const Cart = require ('../model/cart.model' )const {Op } = require ('sequelize' ) class CartService { async createOrUpdate (user_id, goods_id ) { const res = await Cart .findOne ({ [Op .and ]: { user_id, goods_id } }) if (res) { await res.increment ('number' ) return await res.reload () } else { return await Cart .create ({ user_id, goods_id }) } } } module .exports = new CartService ()
cart.model.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const seq = require ('../db/seq' )const {DataTypes } = require ('sequelize' )const Cart = seq.define ('sai_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 , defaultValue : 1 , comment : '商品的数量' }, selected : { type : DataTypes .BOOLEAN , allowNull : false , defaultValue : true , comment : '是否选中' } }) module .exports = Cart
小问题:这里=我们没有对goods_id
是否存在做校验,并且已经下架的商品是不能够进行操作的
要做一个真实的、可以商用的接口,不是那么简单的,会有很多的细节要考虑
获取购物车列表
请求参数
1 2 pageNum(default=1) pageSize(default=10)
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } , { "id" : 2 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } ] } }
表关联:https://www.sequelize.com.cn/core-concepts/assocs
cart.route.js
1 router.get ('/' , auth, findAll)
cart.controller.js
1 2 3 4 5 6 7 8 9 async findAll (ctx, next ) { const {pageNum = 1 , pageSize = 10 } = ctx.request .query const res = await findCarts (pageNum, pageSize) ctx.body = { code : 0 , message : '获取购物车列表成功' , result : res } }
cart.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 async findCarts (pageNum, pageSize ) { const offset = (pageNum - 1 ) * pageSize const { count, rows } = await Cart .findAndCountAll ({ attributes : ['id' , 'number' , 'selected' ], offset : offset, limit : pageSize * 1 }) return { pageNum, pageSize, total : count, list : rows } }
添加几件商品到购物车后,测试获取购物车列表的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : "1" , "pageSize" : "10" , "total" : 4 , "list" : [ { "id" : 1 , "number" : 4 , "selected" : true } , { "id" : 3 , "number" : 1 , "selected" : true } , { "id" : 4 , "number" : 2 , "selected" : true } , { "id" : 5 , "number" : 2 , "selected" : true } ] } }
现在需要做一个联表查询,查询具体的商品信息
现在是cart
表要关联goods
表(根据cart
表里的goods_id
去goods
表里查询)
cart.model.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const seq = require ('../db/seq' )const {DataTypes } = require ('sequelize' )const Goods = require ('./goods.model' ) const Cart = seq.define ('sai_carts' , { }) Cart .belongsTo (Goods , { foreignKey : 'goods_id' , as : 'goods_info' }) module .exports = Cart
cart.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Goods = require ('../model/goods.model' ) async findCarts (pageNum, pageSize ) { const offset = (pageNum - 1 ) * pageSize const { count, rows } = await Cart .findAndCountAll ({ attributes : ['id' , 'number' , 'selected' ], offset : offset, limit : pageSize * 1 , include : { model : Goods , as : 'goods_info' , attributes : ['id' , 'goods_name' , 'goods_price' , 'goods_img' ] } }) return { pageNum, pageSize, total : count, list : rows } }
测试接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : "1" , "pageSize" : "10" , "total" : 4 , "list" : [ { "id" : 1 , "number" : 4 , "selected" : true , "goods_info" : { "id" : 3 , "goods_name" : "证断影然度叫" , "goods_img" : "http://dummyimage.com/400x400" , "goods_price" : "53.00" } } , { "id" : 3 , "number" : 1 , "selected" : true , "goods_info" : { "id" : 2 , "goods_name" : "酸这争取" , "goods_img" : "http://dummyimage.com/400x400" , "goods_price" : "13.00" } } , { "id" : 4 , "number" : 2 , "selected" : true , "goods_info" : { "id" : 3 , "goods_name" : "证断影然度叫" , "goods_img" : "http://dummyimage.com/400x400" , "goods_price" : "53.00" } } , { "id" : 5 , "number" : 2 , "selected" : true , "goods_info" : { "id" : 2 , "goods_name" : "酸这争取" , "goods_img" : "http://dummyimage.com/400x400" , "goods_price" : "13.00" } } ] } }
如果自己写SQL
也可以,可以用LEFT OUTER JOIN
做联表查询
1 SELECT `sai_carts`.`id`, `sai_carts`.`number`, `sai_carts`.`selected`, `goods_info`.`id` AS `goods_info.id`, `goods_info`.`goods_name` AS `goods_info.goods_name`, `goods_info`.`goods_img` AS `goods_info.goods_img`, `goods_info`.`goods_price` AS `goods_info.goods_price` FROM `sai_carts` AS `sai_carts` LEFT OUTER JOIN `sai_goods` AS `goods_info` ON `sai_carts`.`goods_id` = `goods_info`.`id` AND (`goods_info`.`deletedAt` IS NULL ) LIMIT 0 , 10 ;
一个接口把前端想要的所有信息都返回,避免两次网络请求(否则将根据goods_id
又重新发送请求获取商品信息)
要多看看sequelize
官方文档
更新购物车 通过更新接口可以修改购物车中商品的选中状态和数量
请求参数
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } , { "id" : 2 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } ] } }
cart.route.js
1 2 3 4 5 6 7 8 9 10 router.patch ( '/:id' , auth, validator ({ number : { type : 'number' , required : false }, selected : { type : 'bool' , required : false } }), update )
cart.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 async update (ctx, next ) { const { id } = ctx.request .params const { number, selected } = ctx.request .body 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 } }
error.type.js
1 2 3 4 5 6 cartFormatError : { code : '10301' , message : '购物车数据格式错误' , result : '' }
cart.service.js
1 2 3 4 5 6 7 8 9 10 11 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 () }
刪除购物车
请求参数
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } , { "id" : 2 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } ] } }
cart.route.js
1 2 3 router.delete ('/' , auth, validator ({ ids : 'array' }), remove)
app.index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const app = new Koa ()app .use (koaBody ({ multipart : true , formidable : { uploadDir : path.join (__dirname, '../upload' ), keepExtensions : true }, parsedMethods : ['POST' , 'PUT' , 'PATCH' , 'DELETE' ] })) .use (KoaStatic (path.join (__dirname, '../upload' ))) .use (parameter (app)) .use (router.routes ()) .use (router.allowedMethods ()) .on ('error' , errHandler) module .exports = app
cart.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async remove (ctx, next ) { try { const { ids } = ctx.request .body console .log (ids) const res = await removeCarts (ids) ctx.body = { code : 0 , message : '删除购物车成功' , result : res } } catch (err) { console .error (err) } }
cart.service.js
1 2 3 4 5 6 7 8 9 async removeCarts (ids ) { return await Cart .destroy ({ where : { id : { [Op .in ]: ids } } }) }
全选中接口
请求参数
无
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } , { "id" : 2 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } ] } }
全不选中接口
请求参数
无
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "code" : 0 , "message" : "获取购物车列表成功" , "result" : { "pageNum" : 1 , "pageSize" : 10 , "total" : 2 , "list" : [ { "id" : 1 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } , { "id" : 2 , "goods_info" : { "id" : 2 , "goods_name" : "蓝牙耳机" , "goods_price" : 199.00 , "goods_img" : "./32048091210.jpg" } , "number" : 1 , "selected" : 1 } ] } }
cart.route.js
1 2 3 4 router.post ('/selectAll' , auth, selectAll) router.post ('/unselectAll' , auth, unSelectAll)
cart.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 async selectAll (ctx, next ) { try { const user_id = ctx.state .user .id const res = await selectAllCarts (user_id) ctx.body = { code : 0 , message : '全部选中' , result : res } } catch (err) { console .error (err) } } async unSelectAll (ctx, next ) { const user_id = ctx.state .user .id const res = await unSelectAllCarts (user_id) ctx.body = { code : 0 , message : '全不选' , result : res } }
cart.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async selectAllCarts (user_id ) { return await Cart .update ({ selected : true }, { where : { user_id } }) } async unSelectAllCarts (user_id ) { return await Cart .update ({ selected : false }, { where : { user_id } }) }
获取购物车商品总数量接口
请求参数
无
响应
成功
1 2 3 4 5 6 7 { "code" : 0 , "message" : "获取购物车商品数量成功" , "result" : { "total" : 10 } }
这个接口可以不写,直接在前端计算显示
地址模块 添加地址接口 这里我们做一下限制,假设只支持3个地址
请求参数
1 consignee, phone, address
响应
成功
1 2 3 4 5 6 7 { "code" : 0 , "message" : "添加地址成功" , "result" : { } }
地址表
表名:sai_address
字段名
字段类型
说明
id
int
主键,自增
user_id
int
用户id
consignee
varchar(255)
收货人
phone
char(11)
手机号
address
varchar(255)
收货地址
is_default
tinyint
0:不是默认,1:默认地址
address.route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/address' })const { auth } = require ('../middleware/auth.middleware' )const { validator } = require ('../middleware/address.middleware' )const {create} = require ('../controller/address.controller' )router.post ('/' , auth, validator ({ consignee : 'string' , phone : { type : 'string' , format : /^1\d{10}$/ }, address : 'string' }), create) module .exports = router
address.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const { addressFormatError } = require ('../constant/error.type' )const validator = (rules ) => { return async (ctx, next) => { try { await ctx.verifyParams (rules) } catch (err) { console .error (err) addressFormatError.result = err ctx.app .emit ('error' , addressFormatError, ctx) return } await next () } } module .exports = { validator }
不同模块的参数校验中间件,可以进一步封装的
address.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const {createAddr} = require ('../service/address.service' )class AddressController { async create (ctx, next ) { const user_id = ctx.state .user .id const { consignee, phone, address } = ctx.request .body const res = await createAddr ({user_id, consignee, phone, address}) ctx.body = { code : 0 , message : '添加地址成功' , result : res } } } module .exports = new AddressController ()
address.service.js
1 2 3 4 5 6 7 8 9 const Address = require ('../model/address.model' )class AddressService { async createAddr (addr ) { return await Address .create (addr) } } module .exports = new AddressService ()
address.model.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const seq = require ('../db/seq' )const {DataTypes } = require ('sequelize' )const Address = seq.define ('sai_address' , { user_id : { type : DataTypes .INTEGER , allowNull : false , comment : '用户id' }, consignee : { type : DataTypes .STRING , allowNull : false , comment : '收货人姓名' }, phone : { type : DataTypes .CHAR (11 ), allowNull : false , comment : '收货人手机号' }, address : { type : DataTypes .STRING , allowNull : false , comment : '收货人地址' }, is_default : { type : DataTypes .BOOLEAN , allowNull : false , defaultValue : false , comment : '是否为默认地址,0:不是,1:是' } }) module .exports = Address
获取地址列表接口
请求 参数
无
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "code" : 0 , "message" : "获取列表成功" , "result" : [ { "id" : 1 , "consignee" : "enim Duis id" , "phone" : "18167553843" , "address" : "澳门特别行政区贵港市滨湖区" } , { "id" : 2 , "consignee" : "nisi ullamco" , "phone" : "13157913140" , "address" : "陕西省揭阳市玛多县" } ] }
address.route.js
1 2 router.get ('/' , auth, findAll)
address.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 async findAll (ctx, next ) { const user_id = ctx.state .user .id const res = await findAllAddr (user_id) ctx.body = { code : 0 , message : '获取地址成功' , result : res } }
address.service.js
1 2 3 4 5 6 async findAllAddr (user_id ) { return await Address .findAll ({ attributes : ['id' , 'consignee' , 'phone' , 'address' , 'is_default' ], where : { user_id } }) }
正确响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 { "code" : 0 , "message" : "获取地址成功" , "result" : [ { "id" : 1 , "consignee" : "enim Duis id" , "phone" : "18167553843" , "address" : "澳门特别行政区贵港市滨湖区" , "is_default" : false } , { "id" : 2 , "consignee" : "commodo adipisicing officia" , "phone" : "18661881741" , "address" : "安徽省广元市青神县" , "is_default" : false } , { "id" : 3 , "consignee" : "nisi ullamco" , "phone" : "13157913140" , "address" : "陕西省揭阳市玛多县" , "is_default" : false } ] }
修改地址接口
请求参数
1 consignee, phone, address
响应
成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "code" : 0 , "message" : "修改列表成功" , "result" : [ { "id" : 1 , "consignee" : "enim Duis id" , "phone" : "18167553843" , "address" : "澳门特别行政区贵港市滨湖区" } , { "id" : 2 , "consignee" : "nisi ullamco" , "phone" : "13157913140" , "address" : "陕西省揭阳市玛多县" } ] }
address.route.js
1 2 3 4 5 6 7 router.put ('/:id' , auth, validator ({ consignee : 'string' , phone : { type : 'string' , format : /^1\d{10}$/ }, address : 'string' }), update)
address.controller.js
1 2 3 4 5 6 7 8 9 10 11 async update (ctx, next ) { const id = ctx.request .params .id const res = await updateAddr (id, ctx.request .body ) ctx.body = { code : 0 , message : '更新地址成功' , result : res } }
address.service.js
1 2 3 async updateAddr (id, addr ) { return await Address .update (addr, { where : { id } }) }
正确响应
1 2 3 4 5 6 7 { "code" : 0 , "message" : "更新地址成功" , "result" : [ 1 ] }
如果修改后的地址需要回填,就需要进一步完善
删除接口
请求参数
响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "code" : 0 , "message" : "获取列表成功" , "result" : [ { "id" : 1 , "consignee" : "enim Duis id" , "phone" : "18167553843" , "address" : "澳门特别行政区贵港市滨湖区" } , { "id" : 2 , "consignee" : "nisi ullamco" , "phone" : "13157913140" , "address" : "陕西省揭阳市玛多县" } ] }
address.route.js
1 2 3 router.delete ('/:id' , auth, remove)
address.controller.js
1 2 3 4 5 6 7 8 9 10 11 async remove (ctx, next ) { const id = ctx.request .params .id const res = await removeAddr (id) ctx.body = { code : 0 , message : '删除地址成功' , result : res } }
address.service.js
1 2 3 async removeAddr (id ) { return await Address .destroy ({ where : { id } }) }
成功响应
1 2 3 4 5 { "code" : 0 , "message" : "删除地址成功" , "result" : 1 }
设为默认接口
请求参数
address.route.js
1 2 router.delete ('/:id' , auth, remove)
address.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 async setDefault (ctx, next ) { const user_id = ctx.state .user .id const id = ctx.request .params .id const res = await setDefaultAddr (user_id, id) ctx.body = { code : 0 , message : '设置默认地址成功' , result : res } }
address.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async setDefaultAddr (user_id, id ) { await Address .update ( { is_default : false }, { where : { user_id } } ) return await Address .update ( { is_default : true }, { where : { id } } ) }
成功响应:
1 2 3 4 5 6 7 { "code" : 0 , "message" : "设置默认地址成功" , "result" : [ 1 ] }
订单模块 生成订单接口
请求参数
1 address_id, goods_info, total
响应
成功
1 2 3 4 5 6 7 8 9 10 11 { "code" : 0 , "message" : "生成订单成功" , "result" : { "id" : 1 , "address_id" : 1 , "goods_info" : "" , "total" : "" , "order_number" : "" } }
订单表
表名:sai_orders
字段名
字段类型
说明
id
int
主键,自增
user_id
int
用户id
address_id
int
地址id
goods_info
text
商品信息,json字符
total
decimal(10,2)
订单总金额
order_number
char(16)
订单号,唯一订单标识
status
tinyint
订单状态( 0:未支付,1:已支付,2:已发货,3:已签收,4:取消 )
order.route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Router = require ('@koa/router' )const router = new Router ({ prefix : '/orders' })const { auth } = require ('../middleware/auth.middleware' )const { validator } = require ('../middleware/order.middleware' )const { create } = require ('../controller/order.controller' )router.post ( '/' , auth, validator ({ address_id : 'int' , goods_info : 'string' , total : 'string' }), create ) module .exports = router
order.middleware.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const { orderFormatError } = require ('../constant/error.type' )const validator = (rules ) => { return async (ctx, next) => { try { await ctx.verifyParams (rules) } catch (err) { console .error (err) orderFormatError.result = err ctx.app .emit ('error' , orderFormatError, ctx) return } await next () } } module .exports = { validator }
error.type.js
1 2 3 4 5 orderFormatError : { code : '10401' , message : '数据格式错误' , result : '' }
order.controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const { createOrder } = require ('../service/order.service' )class OrderController { async create (ctx, next ) { const user_id = ctx.state .user .id const { address_id, goods_info, total } = ctx.request .body const order_number = 'sai' + Date .now () const res = await createOrder ({ user_id, address_id, goods_info, total, order_number }) ctx.body = { code : 0 , message : '生成订单成功' , result : res } } } module .exports = new OrderController ()
order.service.js
1 2 3 4 5 6 7 8 const Order = require ('../model/order.model' )class OrderService { async createOrder (order ) { return await Order .create (order) } } module .exports = new OrderService ()
order.model.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const seq = require ('../db/seq' )const {DataTypes } = require ('sequelize' )const Order = seq.define ('sai_orders' , { user_id : { type : DataTypes .INTEGER , allowNull : false , comment : '用户id' }, address_id : { type : DataTypes .INTEGER , allowNull : false , comment : '地址id' }, goods_info : { type : DataTypes .TEXT , allowNull : false , comment : '商品信息' }, total : { type : DataTypes .DECIMAL (10 ,2 ), allowNull : false , comment : '订单总金额' }, order_number : { type : DataTypes .CHAR (16 ), allowNull : false , comment : '订单号' }, status : { type : DataTypes .TINYINT , allowNull : false , defaultValue : 0 , comment : '订单状态( 0:未支付,1:已支付,2:已发货,3:已签收,4:取消 )' } }) module .exports = Order
正确响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "code" : 0 , "message" : "生成订单成功" , "result" : { "status" : 0 , "id" : 2 , "user_id" : 48 , "address_id" : 2 , "goods_info" : "[{}, {}, {}]" , "total" : "199.99" , "order_number" : "sai1659969030037" , "updatedAt" : "2022-08-08T14:30:30.041Z" , "createdAt" : "2022-08-08T14:30:30.041Z" } }
订单列表接口
请求参数
1 2 3 4 5 { "pageNum" : 1 , "pageSize" : 10 , "status" : 0 }
响应
1 2 3 4 5 6 7 8 9 10 11 12 13 { "code" : 0 , "messaeg" : "获取订单列表成功" , "result" : [ { "id" : 1 , "goods_info" : "" , "total" : "" , "order_number" : "" , "status" : 0 } ] }
order.route.js
1 2 router.get ('/' , auth, findAll)
order.controller.js
1 2 3 4 5 6 7 8 9 async findAll (ctx, next ) { const {pageNum = 1 ,pageSize = 10 , status = 0 } = ctx.request .query const res = await findAllOrder (pageNum, pageSize, status) ctx.body = { code : '' , message : '获取订单列表成功' , result : res } }
order.service.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async findAllOrder (pageNum, pageSize, status ) { const { count, rows } = await Order .findAndCountAll ({ attributes : ['goods_info' , 'total' , 'order_number' , 'status' ], where : { status }, offset : (pageNum - 1 ) * pageSize, limit : pageSize * 1 , }) return { pageNum, pageSize, total : count, list : rows } }
修改订单接口
请求参数
对于会员,可以取消订单(取消订单也是更新状态)
order.route.js
1 2 3 4 router.patch ('/:id' , auth, validator ({ status : 'number' }), update)
order.controller.js
1 2 3 4 5 6 7 8 9 10 11 async update (ctx, next ) { const id = ctx.request .params .id const {status} = ctx.request .body const res = await updateOrder (id, status) ctx.body = { code : 0 , message : '更新订单状态成功' , result : res } }
order.service.js
1 2 3 async updateOrder (id, status ) { return await Order .update ({ status }, { where : { id } }) }
成功响应
1 2 3 4 5 6 7 { "code" : 0 , "message" : "更新订单状态成功" , "result" : [ 1 ] }