Koa-从零开始搭建购物商城

202 阅读9分钟

1、项目搭建

npm init -y

2、启动服务

安装依赖

npm i dotenv koa
npm i nodemon -D

目录结构

截屏2023-03-25 19.55.47.png

.env

APP_PROT=8000

config.default.js

const dotenv = require('dotenv')
// 将.env的属性值解析到此处
dotenv.config()
module.exports = process.env

main.js

const Koa = require('koa')
const app = new Koa()
const { APP_PROT } = require('./config/config.default')

app.use((ctx, next) => {
ctx.body = 'hello api'

})

app.listen(APP_PROT, () => {
console.log(`端口号为:${APP_PROT}`);
})

package.json

"scripts": {
"dev":"nodemon ./src/main.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

3、添加路由

安装依赖

npm i koa-router

目录结构

截屏2023-03-26 13.39.04.png

user.route.js

const Router = require('koa-router')
const router = new Router({ prefix: '/users' })
// GET /users /
router.get('/', (ctx, next) => {
ctx.body = 'hello user'
})
module.exports = router

main.js

const Koa = require('koa')
const app = new Koa()
const { APP_PROT } = require('./config/config.default')
const userRouter = require('./router/user.route')

//注册路由中间件
app.use(userRouter.routes())
app.listen(APP_PROT, () => {
console.log(`端口号为:${APP_PROT}`);
})

4、目录结构优化

目录结构

截屏2023-03-26 14.20.19.png

app/index.js

const Koa = require('koa')
const app = new Koa()
const userRouter = require('../router/user.route')
app.use(userRouter.routes())
module.exports = app

controller/user.controller.js

class UserController {
async register(ctx, next) {
ctx.body = '用户注册成功'
}
async login(ctx, next) {
ctx.body = '登录成功'
}
}
module.exports = new UserController()

router/user.route.js

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

// GET /users /
router.post('/register', register)
router.post('/login', login)
module.exports = router

main.js

const app = require('./app')
const { APP_PROT } = require('./config/config.default')

app.listen(APP_PROT, () => {
console.log(`端口号为:${APP_PROT}`);
})

此处post请求可用postman来验证

截屏2023-03-26 14.28.03.png

截屏2023-03-26 14.32.36.png

5、解析body拆分service层

koa-body 是一个可以帮助解析 http 中 body 的部分的中间件,包括 json、表单、文本、文件等

安装依赖

npm i koa-body

目录结构

截屏2023-03-26 16.42.25.png



app/index.js

const Koa = require('koa')
const { koaBody } = require('koa-body')

const app = new Koa()
const userRouter = require('../router/user.route')

// 将请求的方式
app.use(koaBody())
app.use(userRouter.routes())
module.exports = app

service/user.service.js

class UserService {
async createUser(userName, passWord) {
// 写入数据库
return '写入数据库成功'
}
}

module.exports = new UserService()

controller

const { createUser } = require('../service/user.service')

class UserController {
async register(ctx, next) {
// 1、获取数据
const { userName, passWord } = ctx.request.body

// 2、操作数据库
console.log(userName, passWord);
const res = await createUser(userName, passWord)

// 3、返回结果
ctx.body = res
}

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

module.exports = new UserController()



postman发送带参数的post请求

截屏2023-03-26 16.48.25.png

6、集成sequlize

安装依赖

npm i sequelize mysql2 

目录结构

截屏2023-03-26 17.30.57.png

.env

APP_PROT=8000

MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_USER = root
MYSQL_PWD = asd15132609768.
MYSQL_DB = koa-dev

seq.js

const { Sequelize } = require('sequelize')

const { MYSQL_HOST,
        MYSQL_PORT,
        MYSQL_USER,
        MYSQL_PWD,
        MYSQL_DB } = require('../config/config.default')

const seq = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PWD, {
    host: MYSQL_HOST,
    dialect: 'mysql',
})

/**
    seq.authenticate().then(
    () => {
    console.log('数据库连接成功');
    }).catch((error) => {
    console.log(error);
    })
*/

module.exports = seq

7、创建用户模型

表模型

截屏2023-03-26 17.42.51.png




目录结构

截屏2023-03-26 18.12.34.png

model/user.model.js

const { DataTypes } = require('sequelize')
const seq = require('../db/seq')
const User = seq.define('user', {

// id会自动生成
userName: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    comment: '用户名, 唯一'
},

passWord: {
    type: DataTypes.CHAR(64),
    allowNull: false,
    comment: '‘密码'
},

isAdmin: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    defaultValue: 0,
    comment: '是否为管理员,0: 不是管理员(默认). 1: 是管理员'
}
})

// 当force为true时表示如果表已经存在,则将其首先删除
User.sync({ force: false })
module.exports = User



8、添加用户

service/user.service.js

const User = require('../model/user.model')

class UserService {
async createUser(userName, passWord) {
// 写入数据库
    const res = await User.create({ userName, passWord })
    return res.dataValues
}
}
module.exports = new UserService()

controller/user.controller.js

const { createUser } = require('../service/user.service')

  


class UserController {
    async register(ctx, next) {
        // 1、获取数据
        const { userName, passWord } = ctx.request.body

        // 2、操作数据库
        const res = await createUser(userName, passWord)

        // 3、返回结果
        ctx.body = {
            code: 0,
            message: '用户注册成功',
            result: {
                id: res.id,
                userName: res.userName
            }
         }
        console.log(ctx.body);
    }

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

module.exports = new UserController()

打印结果为

截屏2023-03-26 18.42.52.png

9、错误处理

service/user.service.js

const User = require('../model/user.model')

class UserService {

async createUser(userName, passWord) {

// 写入数据库

const res = await User.create({ userName, passWord })
    return res.dataValues
}

async getUserInfo({ id, userName, passWord, isAdmin }) {
    const whereOpt = {}

    id && Object.assign(whereOpt, { id })
    userName && Object.assign(whereOpt, { userName })
    passWord && Object.assign(whereOpt, { passWord })
    isAdmin && Object.assign(whereOpt, { isAdmin })
    
    //findOne和js中的find一样,findAll则和filter一样
    const res = await User.findOne({
        // 返回哪些属性
        attributes: ['id', 'userName'],
        // 查询条件
        where: whereOpt
    })
        return res ? res.dataValues : null
    }
}
module.exports = new UserService()

controller/user.controller.js

const { createUser, getUserInfo } = require('../service/user.service')

class UserController {
    async register(ctx, next) {
        // 1、获取数据
        const { userName, passWord } = ctx.request.body
        // 合法性
        if (!userName || !passWord) {
            console.error('用户名或密码为空', ctx.request.body)
            ctx.status = 400
            ctx.body = {
            code: '10001',
                message: '用户名或密码为空',
                result: ''
            }
            return
        }

        console.log(111, await getUserInfo({ userName }));

        // 合理性(不能创建相同的用户)

        if (await getUserInfo({ userName })) {
            ctx.status = 409
            ctx.body = {
                code: '10002',
                message: '用户已经存在',
                result: '',
            }
            return
        }

        // 2、操作数据库
        const res = await createUser(userName, passWord)

        // 3、返回结果
        ctx.body = {
            code: 0,
            message: '用户注册成功',
            result: {
                id: res.id,
                userName: res.userName
             }
         }
     }
    async login(ctx, next) {
        ctx.body = '登录成功'
    }
}
module.exports = new UserController()

10、拆分中间件

文件目录

截屏2023-03-27 21.21.37.png

错误处理封装

constant/err.type.js

module.exports = {
    useFormateError: {
        code: '10001',
        message: '用户名或密码为空',
        result: ''
    },
    userAlreadyExited: {
        code: '10002',
        message: '用户名已经存在',
        result: ''
    },
    userRegisterError: {
        code: '10003',
        message: '用户注册失败',
        result: ''
    }
}

将一些请求的验证抽出来

middleware/user.model.js

const { getUserInfo } = require("../service/user.service")
const { useFormateError, userAlreadyExited, userRegisterError } = require('../constant/err.type')
const userValidator = async (ctx, next) => {
    const { userName, passWord } = ctx.request.body
    if (!userName || !passWord) {
        console.error('用户名或密码为空', ctx.request.body)
        ctx.app.emit("error", useFormateError, ctx)
        return
    }
    await next()
}

const verifyUser = async (ctx, next) => {
    const { userName } = ctx.request.body
    try {
        const res = await getUserInfo({ userName })
        if (res) {
            console.error('用户名已存在', { userName })
            ctx.app.emit("error", userAlreadyExited, ctx)
            return
        }
    } catch (e) {
        console.error('获取用户信息失败', e);
        ctx.app.emit('error', userRegisterError, ctx)
    }
    await next()
}

module.exports = {
    userValidator,
    verifyUser
}

将中间件引入放在register之前,这样在注册前会先执行中间件的代码

router/user.route.js

const Router = require('koa-router')

const router = new Router({ prefix: '/users' })

const { userValidator, verifyUser } = require('../middleware/user.middleware')
const { register, login } = require('../controller/user.controller')
// GET /users   /
router.post('/register', userValidator, verifyUser, register)
router.post('/login', login)

module.exports = router

code处理

app/errorHandler.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
}

通过“error”字符串进行监听,并进行错误统一处理

app/index.js

const Koa = require('koa')
const { koaBody } = require('koa-body')
const errHandle = require('./errorHandler')

const app = new Koa()
const userRouter = require('../router/user.route')

app.use(koaBody())
app.use(userRouter.routes())

// 统一的错误处理
app.on('error', errHandle)
module.exports = app

11、密码加密

下载依赖

npm i bcryptjs

middleware/user.middleware.js

const cryptPassword = async (ctx, next) => {
    const { passWord } = ctx.request.body
    const salt = bcrypt.genSaltSync(10)
    // hash保存的是密文
    const hash = bcrypt.hashSync(passWord, salt)
    ctx.request.body.passWord = hash
    await next()
}
module.exports = {
    userValidator,
    verifyUser,
    cryptPassword
}

router/user.route.js

const { userValidator, verifyUser, cryptPassword } = require('../middleware/user.middleware')
// GET /users   /
router.post('/register', userValidator, verifyUser, cryptPassword, register)

12、验证登录

constant/err.type.js

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

middleware/user.middleware.js

const verifyLogin = async (ctx, next) => {
const { userName, passWord } = ctx.request.body
try {
    const res = await getUserInfo({ userName })
    if (!res) {
        console.error('用户名不存在', { userName });
        ctx.app.emit('error', userDoesNotExist, ctx)
        return
    }
    if (!bcrypt.compareSync(passWord, res.passWord)) {
        ctx.app.emit('error', invalidPassword, ctx)
        return
    }
} catch (e) {
    console.error(e);
    return ctx.app.emit('error', userLoginError, ctx)
}

await next()
}

router/user.route.js

router.post('/login', userValidator, verifyLogin, login)



测试 截屏2023-03-27 22.43.48.png

13、颁发token

下载依赖

npm i jsonwebtoken

.env

JWT_SECRET = xzd

controller/user.controller.js

const jwt = require('jsonwebtoken')
const { JWT_SECRET } = require('../config/config.default')
async login(ctx, next) {
    const { userName } = ctx.request.body
    try {
        // 从返回结果对象中剔除password属性,将剩下的属性放到res对象
        const { passWord, ...res } = await getUserInfo({ userName })
        ctx.body = {
            code: 0,
            message: '用户登录成功',
            result: {
                // JWT_SECRET是密钥,expiresIn后面跟的属性是过期时间,1d表示1天
                token: jwt.sign(res, JWT_SECRET, { expiresIn: '1d' })
            }
        }
    } catch (e) {
        console.error('用户登录失败', e);
    }
}

测试结果

截屏2023-03-27 23.21.33.png

14、用户认证

目录结构

截屏2023-03-28 22.47.09.png

constant/err.type.js

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

middleware/auth.middleware.js

const jwt = require('jsonwebtoken')
const { JWT_SECRET } = require('../config/config.default')
const { tokenExpiredError, invalidToken } = require('../constant/err.type')

const auth = async (ctx, next) => {
    const { authorization } = ctx.request.header
    // 去除Bearer字段及空格
    const token = authorization.replace('Bearer', '').replace(/\s+/g, "");
    try {
        // 获取id、userName、
        const user = jwt.verify(token, JWT_SECRET)
        ctx.state.user = user
    } catch (e) {
        switch (e.name) {
            case 'TokenExpiredError':
                console.error('token已过期', e);
                return ctx.app.emit('error', tokenExpiredError, ctx)
            case 'JsonWebTokenError':
                console.error('无效的token', e);
                return ctx.app.emit('error', invalidToken, ctx)
        }
    }
    await next()
}
module.exports = {
    auth
}

router/user.service.js

const { auth } = require('../middleware/auth.middleware')

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

验证

  • 方法一

截屏2023-03-28 22.51.57.png

  • 方法二

截屏2023-03-28 22.54.09.png

截屏2023-03-28 22.55.26.png

15、修改密码

controller/user.controller.js

async changePassword(ctx, next) {
        const { id } = ctx.state.user
        const passWord = ctx.request.body.passWord
        console.log(6666, passWord);
        if (await updateById({ id, passWord })) {
            ctx.body = {
                code: 0,
                message: '修改密码成功',
                result: ''
            }
        } else {
            ctx.body = {
                code: '10007',
                message: '修改密码失败',
                result: ""
            }
        }
    }

service/user.service.js

async updateById({ id, userName, passWord, isAdmin }) {
    const whereOpt = { id }
    const newUser = {}

    userName && Object.assign(newUser, { userName }) 
    passWord && Object.assign(newUser, { passWord })
    isAdmin && Object.assign(newUser, { isAdmin })

    // 用newUser里的字段数据覆盖掉查询出来数据的字段数据(id、password)
    const res = await User.update(newUser, { where: whereOpt })

    return !!res
}

16、商品模块

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  └─ user.model.js
   ├─ router
   │  ├─ goods.route.js
   │  └─ user.route.js
   └─ service
      └─ user.service.js

controller/goods.controller.js

class GoodsController {
    async upload(ctx, next) {
        ctx.body = '商品图片上传成功'
    }
}

module.exports = new GoodsController()

router/goods.route.js

const Router = require('koa-router')

const { upload } = require('../controller/goods.controller')

const router = new Router({ prefix: '/goods' })

router.post('/upload', upload)

module.exports = router

app/index.js

const Koa = require('koa')
const { koaBody } = require('koa-body')
const errHandle = require('./errorHandler')

const app = new Koa()
const userRouter = require('../router/user.route')
const goodsRouter = require('../router/goods.route')

app.use(koaBody())
app.use(userRouter.routes())
app.use(goodsRouter.routes())

// 统一的错误处理
app.on('error', errHandle)
module.exports = app

17、路由自动加载

每次在创建新的路由时,都得在app/index.js里面注册新的路由,这样会很麻烦,所以可以采用以下方法来进行路由的自动注册

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  └─ user.model.js
   ├─ router
   │  ├─ goods.route.js
   │  ├─ index.js
   │  └─ user.route.js
   └─ service
      └─ user.service.js

router/index.js

const fs = require('fs')

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

// 参数file的值为当前目录里的所有文件夹:goods.route.js、user.route.js、index.js...
fs.readdirSync(__dirname).forEach(file => {
    // 过滤掉index.js
    if (file !== 'index.js') {
        let readRoute = require(`./${file}`)
        // router相当于一个容器,把每一个单独的路由注册到router容器里
        router.use(readRoute.routes())
    }
})

module.exports = router

app/index.js

const Koa = require('koa')
const { koaBody } = require('koa-body')
const errHandle = require('./errorHandler')

const app = new Koa()
const router = require('../router')

app.use(koaBody())
app.use(router.routes())
// 当使用没有注册过的接口请求时会报501错误,不加这行代码之前是报404错误
app.use(router.allowedMethods())

// 统一的错误处理
app.on('error', errHandle)
module.exports = app

18 、封装管理员权限

constant/err.type.js

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

middleware/auth.middleware.js

const hadAdminPermission = async (ctx, next) => {
    const { isAdmin } = ctx.state.user
    if (!isAdmin) {
        console.error('该用户没有管理员的权限', ctx.state.user);
        return ctx.app.emit('error', hasNotAdminPermission, ctx)
    }
    await next()
}

router/goods.route.js

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

19、添加图片上传

安装依赖

npm i koa-static

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
├─ src
│  ├─ app
│  │  ├─ errorHandler.js
│  │  └─ index.js
│  ├─ config
│  │  └─ config.default.js
│  ├─ constant
│  │  └─ err.type.js
│  ├─ controller
│  │  ├─ goods.controller.js
│  │  └─ user.controller.js
│  ├─ db
│  │  └─ seq.js
│  ├─ main.js
│  ├─ middleware
│  │  ├─ auth.middleware.js
│  │  ├─ goods.middleware.js
│  │  └─ user.middleware.js
│  ├─ model
│  │  └─ user.model.js
│  ├─ router
│  │  ├─ goods.route.js
│  │  ├─ index.js
│  │  └─ user.route.js
│  ├─ service
│  │  └─ user.service.js
│  └─ upload
│     ├─ 8a78e85c4f1f08c9b5c299200.jpg
│     ├─ 988c39d9bb4916c94e1d5d300.md
│     ├─ b6c681b6b9fe35601f8cde100.jpg
│     ├─ ca676cff8e47d1a39042e1d00.jpg
│     ├─ d8e0d200559cca0b33b4a3700.md
│     ├─ e8c2cc0f61dc651bc7a63cf00.md
│     ├─ fa3078c3b98dd2f4182f14201.jpg
│     ├─ fa3078c3b98dd2f4182f14202.jpg
│     └─ fe5d803c345288d2ade393000.jpg
└─ test
   └─ index.html

文件内容

constant/err.type.js

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

app/index.js

// 插件作用:能在服务器内输入 域名/端口号/图片名称 打开文件内容
const serve = require('koa-static')
app.use(koaBody({
    multipart: true,
    formidable: {
        /**
         * 在配置选项option里,不推荐使用相对路径
         * 在option里的相对路径,不是相对的当前文件,相对process.cwd(),可以将process.cwd()打印下试试    
         */
        uploadDir: path.join(__dirname, '../upload'),
        // 是否保持原来文件的后缀名
        keepExtensions: true
    }
}))

// 配置打开文件内容的路径
app.use(serve(path.join(__dirname, '../upload')))

controller/goods.controller.js

const path = require('path')
const { fileUploadError, unSupportedFileType } = require('../constant/err.type')

class GoodsController {
    async upload(ctx, next) {
        const { file } = ctx.request.files || []
        const fileTypes = ['image/jpeg', 'image/png']
        if (file) {
            if (!fileTypes.includes(file.mimetype)) {
                return ctx.app.emit('error', unSupportedFileType, ctx)
            }
            ctx.body = {
                code: 0,
                message: '商品图片上传成功',
                result: {
                    goodsImg: path.basename(file.filepath)
                }
            }
        } else {
            return ctx.app.emit('error', fileUploadError, ctx)
        }
    }
}

module.exports = new GoodsController()

测试

  • postman里进行配置

截屏2023-04-01 18.58.46.png

  • 可以写一个简单的前端页面

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <form
            action="http://localhost:8000/goods/upload"
            method="post"
            enctype="multipart/form-data"
        >
            <input type="file" name="file" />
            <input type="submit" value="上传" />
        </form>
    </body>
</html>

20、入参格式验证

安装依赖

npm i koa-parameter

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  ├─ goods.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  └─ user.model.js
   ├─ router
   │  ├─ goods.route.js
   │  ├─ index.js
   │  └─ user.route.js
   ├─ service
   │  └─ user.service.js
   └─ upload
      └─ fe5d803c345288d2ade393000.jpg

文件内容

constant/err.type.js

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

app/index.js

const parameter = require('koa-parameter')

// 放在router前面,将app当参数传进去,这样下面的route就可以用ctx里的verifyParams属性了
app.use(parameter(app))
app.use(router.routes())

middleware/goods.middleware.js

const { goodsFormatError } = require("../constant/err.type");

const validator = async (ctx, next) => {
    try {
        // verifyParams为插件里的属性
        ctx.verifyParams({
            // 入参时goodsName为必填,且为stirng类型
            goodsName: { type: 'string', require: true },
            goodsPrice: { type: 'number', require: true },
            goodsNum: { type: 'number', require: true },
            goodsImg: { type: 'string', require: true },
        })
    } catch (err) {
        console.error(err);
        goodsFormatError.result = err
        return ctx.app.emit('error', goodsFormatError, ctx)
    }
    await next()
}

module.exports = {
    validator
}

router/goods.route.js

// 发布商品接口
router.post('/', auth, hadAdminPermission, validator, (ctx, next) => {
    ctx.body = '发布商品成功'
})

测试

截屏2023-04-01 20.04.33.png

当故意传错一个参数时,会发现插件会自动进行验证并给出提示信息

21、发布商品写入数据库

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  ├─ goods.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  ├─ goods.model.js
   │  └─ user.model.js
   ├─ router
   │  ├─ goods.route.js
   │  ├─ index.js
   │  └─ user.route.js
   ├─ service
   │  ├─ goods.service.js
   │  └─ user.service.js
   └─ upload
      └─ fe5d803c345288d2ade393000.jpg

文件内容

model/goods.model.js

const { DataTypes } = require('sequelize')

const seq = require('../db/seq')

const User = seq.define('user', {

    // id会自动生成
    userName: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true,
        comment: '用户名, 唯一'
    },
    passWord: {
        type: DataTypes.CHAR(64),
        allowNull: false,
        comment: '‘密码'
    },
    isAdmin: {
        type: DataTypes.BOOLEAN,
        allowNull: false,
        defaultValue: 0,
        comment: '是否为管理员,0: 不是管理员(默认). 1: 是管理员'
    }
})

// 当force为true时表示如果表已经存在,则将其首先删除
User.sync({ force: false })
module.exports = User

执行node src/model/goods.model.js指令创建goods


service/goods.service.js

const Goods = require('../model/goods.model')

class GoodsService {
    async createGoods(goods) {
        const res = await Goods.create(goods)
        return res.dataValues
    }
}
module.exports = new GoodsService()

controller/goods.controller.js

async create(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.ap.emit('error', publishGoodsError, ctx)
    }
}

router/goods.route.js

router.post('/', auth, hadAdminPermission, validator, create)

22、修改商品接口

constant/err.type.js

invalidGoodsID: {
    code: '10205',
    message: '待修改的商品不存在',
    result: ''
}

service/goods.service.js

async updateGoods(id, goods) {
    const res = await Goods.update(goods, { where: { id } })
    return res[0] > 0
}

controller/goods.controller.js

 async update(ctx) {
    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);
    }
}

router/goods.route.js

router.put('/:id', auth, hadAdminPermission, validator, update)

23、硬删除接口

service/goods.service.js

async removeGoods(id) {
    const res = await Goods.destroy({ where: { id } })
    return res > 0
}

controller/goods.controller.js

async remove(ctx) {
    try {
        const res = await removeGoods(ctx.params.id)
        if (res) {
            ctx.body = {
                code: 0,
                message: '删除商品成功',
                result: ''
            }
        } else {
            return ctx.app.emit('error', invalidGoodsID, ctx)
        }
    } catch (err) {
        console.error(err);
    }
}

route/goods.route.js

router.delete('/:id', auth, hadAdminPermission, remove)

24、商品的上架与下架

直接硬删除商品这种方法不是很提倡,可以用商品的上架和下架来代替

需要在goods表中添加一个deleteAt字段

model/user.route.js

const Goods = seq.define('goods', {
    goodsName: {
        type: DataTypes.STRING,
        allowNull: false,
        comment: '商品名称'
    },
    goodsPrice: {
        type: DataTypes.DECIMAL(10, 2),
        allowNull: false,
        comment: '商品价格'
    },
    goodsNum: {
        type: DataTypes.INTEGER,
        allowNull: false,
        comment: '商品图片的url'
    },
    goodsImg: {
        type: DataTypes.STRING,
        allowNull: false,
        comment: '商品图片url地址'
    },
}, 
    // 添加这么个玩意儿,paranoid是固定字段
{
    paranoid: true
})

service/goods.service.js

async removeGoods(id) {
        const res = await Goods.destroy({ where: { id } })
        return res > 0
    }

    async restoreGoods(id) {
        const res = await Goods.restore({ where: { id } })
        return res > 0
    }

controller/goods.controller.js

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

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)
    }
}

router/goods.route.js

// 商品上架和下架来代替硬删除
router.post('/off/:id', auth, hadAdminPermission, remove)
router.post('/on/:id', auth, hadAdminPermission, restore)



测试

  • 下架商品

截屏2023-04-02 00.20.49.png




  • 上架商品

截屏2023-04-02 00.22.00.png

25、商品列表分页

route/goods.toute.js

// 商品列表分页数据
router.get('/', goodsList)

controller/goods.controller.js

async goodsList(ctx) {
    // 解析pageNum(第几页)和pageSize(一页有多少数据)
    const { pageNum = 1, pageSize = 10 } = ctx.request.query
    // 调用数据处理的相关方法
    const res = await getGoodsList(pageNum, pageSize)
    // 返回结果
    ctx.body = {
        code: 0,
        message: '获取商品列表成功',
        result: res
    }
}

service/goods.service

async getGoodsList(pageNum, pageSize) {
    // 获取表中的数据数量,用来显示有多少页数据
    const count = await Goods.count()
    // 获取分页当中的偏移量offset(从第几个数据开始)
    const offset = (pageNum - 1) * pageSize
    // 获取分页的具体数据
    const rows = await Goods.findAll({ offset, limit: Number(pageSize) })
    return {
        pageNum,
        pageSize,
        total: count,
        list: rows
    }
}

上述方法可以用另外一种方法findAndCountAll进行整合

async getGoodsList(pageNum, pageSize) {
    // 获取分页当中的偏移量offset(从第几个数据开始)
    const offset = (pageNum - 1) * pageSize
    // 获取分页的具体数据
    const { count, rows } = await Goods.findAndCountAll({ offset, limit: Number(pageSize) })
    return {
        pageNum,
        pageSize,
        total: count,
        list: rows
    }
}

查询时,当商品下架时,会将其自动过滤掉

26、添加购物车

具体功能:每请求一次接口,如果存在这个商品就在此的基础上加一,如若不存在就创建一个新的数据

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ cart.controller.js
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ auth.middleware.js
   │  ├─ cart.middleware.js
   │  ├─ goods.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  ├─ cart.model.js
   │  ├─ goods.model.js
   │  └─ user.model.js
   ├─ router
   │  ├─ cart.route.js
   │  ├─ goods.route.js
   │  ├─ index.js
   │  └─ user.route.js
   ├─ service
   │  ├─ cart.service.js
   │  ├─ goods.service.js
   │  └─ user.service.js
   └─ upload
      └─ fe5d803c345288d2ade393000.jpg

model/cat.model.js

const { DataTypes } = require('sequelize')

const seq = require('../db/seq')

const Cart = seq.define('cart', {
    goodsId: {
        type: DataTypes.INTEGER,
        allowNull: false,
        comment: '商品的id'
    },
    userId: {
        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: '是否选中'
    }
})
Cart.sync({ force: false })

module.exports = Cart 

service/cart.service.js

const { Op } = require('sequelize')
const Cart = require('../model/cart.model')

class CartService {
    async createOrUpdate(userId, goodsId) {
        let res = await Cart.findOne({
            where: {
                // 查询两个条件
                [Op.and]: {
                    userId,
                    goodsId
                }
            }
        })
        if (res) {
            // 已经存在一条记录就加1
            await res.increment('number')
            return await res.reload()
        } else {
            // 否则就重新创建一条记录
            return await Cart.create({
                userId,
                goodsId
            })
        }
    }
}
module.exports = new CartService()

controller/cart.controller.js

const { createOrUpdate } = require('../service/cart.service')

class CartController {
    async add(ctx) {

        const userId = ctx.state.user.id
        const goodsId = ctx.request.body.goodsId
        const res = await createOrUpdate(userId, goodsId)
        ctx.body = {
            code: 0,
            message: '添加到购物车成功',
            result: res
        }
    }
}
module.exports = new CartController()

middleware/cart.middleware.js

const { invalidGoodsID } = require('../constant/err.type')

const validator = async (ctx, next) => {
    try {
        ctx.verifyParams({
            // goodsId: { type: 'number', require: true }的简化写法
            goodsId: 'number'
        })
    } catch (err) {
        console.error(err);
        invalidGoodsID.result = err
        return ctx.app.emit('error', invalidGoodsID, ctx)
    }
    await next()
}

module.exports = {
    validator
}

router/cat.route.js

const Router = require('koa-router')

const { add } = require('../controller/cart.controller')
const { auth } = require('../middleware/auth.middleware')
const { validator } = require('../middleware/cart.middleware')

const router = new Router({ prefix: '/cart' })

router.post('/', auth, validator, add)

module.exports = router

27、获取购物车列表(添加表关联 一对一)

具体实现的功能为将

截屏2023-04-02 16.17.26.png

将这里的goodsId与goods表中id对应起来

router/cart.model.js

router.get('/', auth, cartList)

controller/cart.controler.js

async cartList(ctx) {
    const { pageNum = 1, pageSize = 10 } = ctx.request.body
    const res = await getCartList(pageNum, pageSize)

    ctx.body = {
        code: 0,
        message: '获取购物车列表成功',
        result: res
    }
}

添加表关联

model/cart.model.js

Cart.belongsTo(Goods, {
    foreignKey: 'goodsId',
    // 重命名
    as: 'goodsInfo'
})
module.exports = Cart 

service/cart.service.js

async getCartList(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: 'goodsInfo',
            // 只返回以下属性
            attributes: ['id', 'goodsName', 'goodsPrice', 'goodsImg']
        }
    })
    return {
        pageNum,
        pageSize,
        total: count,
        list: rows
    }
}

数据测试

cart表可参照开头那张图

goods

截屏2023-04-02 16.27.49.png

返回的数据为

{
    "code": 0,
    "message": "获取购物车列表成功",
    "result": {
        "pageNum": 1,
        "pageSize": 10,
        "total": 5,
        "list": [
            {
                "id": 1,
                "number": 1,
                "selected": true,
                "goodsInfo": {
                    "id": 2,
                    "goodsName": "蓝牙音响",
                    "goodsPrice": "100.00",
                    "goodsImg": "8a78e85c4f1f08c9b5c299200.jpg"
                }
            },
            {
                "id": 2,
                "number": 2,
                "selected": true,
                "goodsInfo": null
            },
            {
                "id": 3,
                "number": 1,
                "selected": true,
                "goodsInfo": {
                    "id": 1,
                    "goodsName": "蓝牙音响",
                    "goodsPrice": "100.00",
                    "goodsImg": "8a78e85c4f1f08c9b5c299200.jpg"
                }
            },
            {
                "id": 4,
                "number": 1,
                "selected": true,
                "goodsInfo": {
                    "id": 3,
                    "goodsName": "蓝牙音响",
                    "goodsPrice": "100.00",
                    "goodsImg": "8a78e85c4f1f08c9b5c299200.jpg"
                }
            },
            {
                "id": 5,
                "number": 1,
                "selected": true,
                "goodsInfo": {
                    "id": 3,
                    "goodsName": "蓝牙音响",
                    "goodsPrice": "100.00",
                    "goodsImg": "8a78e85c4f1f08c9b5c299200.jpg"
                }
            }
        ]
    }
}

28、更新购物车

更新购物车的number值和selected

改造验证

middleware/cart.middleware.js

const validator = (rules) => {
    return async (ctx, next) => {
        try {
            ctx.verifyParams(rules)
        } catch (err) {
            console.error(err);
            cartFormatError.result = err
            return ctx.app.emit('error', cartFormatError, ctx)
        }
        await next()
    }
}

router/cart.route.js

router.post('/', auth, validator({ goodsId: 'number' }), add)
router.patch('/:id', auth, validator({
    number: { type: 'number', required: false },
    selected: { type: 'bool', required: false }
}), update)

controller/cart.controller.js

async update(ctx) {
    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 updateCart({ id, number, selected })
    ctx.body = {
        code: 0,
        message: '更新购物车成功',
        result: res
    }
}

service/cart.service.js

async updateCart(params) {
    const { id, number, selected } = params
    // 根据id来进行数据查询
    const res = await Cart.findByPk(id)
    if (!res) return ''
    // 修改值并添加默认值
    number !== undefined ? (res.number = number) : ''
    selected !== undefined ? (res.selected = selected) : true
    // 修改完成后保存
    return await res.save()
}

27、删除购物车

批量删除,传递的参数是一个数组

router/cart.route.js

router.delete('/', auth, validator({ ids: 'array' }), remove)

app/index.js

app.use(koaBody({
    parsedMethods: ['POST', 'PUT', 'PATCH', 'DELETE']
}))

controller/cart.controller.js

async remove(ctx) {
    const { ids } = ctx.request.body
    const res = await removeCart(ids)
    ctx.body = {
        code: 0,
        message: '删除购物车成功',
        result: res
    }
}

servcie/cart.service.js

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

测试结果

截屏2023-04-02 18.38.20.png

28、全选和取消全选

  • 全选

router/cart.toute.js

router.post('/selectAll', auth, selectAll)

controller/cart.controller.js

async selectAll(ctx) {
    const userId = ctx.state.user.id

    const res = await selectAllCart(userId)

    ctx.body = {
        code: 0,
        message: '全部选中',
        result: res
    }
}

service/cart.service.js

async selectAllCart(userId) {
    console.log(111, userId);
    return await Cart.update(
        { selected: true },
        {
            where: {
                userId
            }
        }
    )
}
  • 取消全选与全选一样,并且可以进行整合成一个方法,我懒得整了

29、添加地址接口

目录结构

koa
├─ .env
├─ README.md
├─ package-lock.json
├─ package.json
└─ src
   ├─ app
   │  ├─ errorHandler.js
   │  └─ index.js
   ├─ config
   │  └─ config.default.js
   ├─ constant
   │  └─ err.type.js
   ├─ controller
   │  ├─ address.controller.js
   │  ├─ cart.controller.js
   │  ├─ goods.controller.js
   │  └─ user.controller.js
   ├─ db
   │  └─ seq.js
   ├─ main.js
   ├─ middleware
   │  ├─ address.middleware.js
   │  ├─ auth.middleware.js
   │  ├─ cart.middleware.js
   │  ├─ goods.middleware.js
   │  └─ user.middleware.js
   ├─ model
   │  ├─ address.model.js
   │  ├─ cart.model.js
   │  ├─ goods.model.js
   │  └─ user.model.js
   ├─ router
   │  ├─ address.route.js
   │  ├─ cart.route.js
   │  ├─ goods.route.js
   │  ├─ index.js
   │  └─ user.route.js
   ├─ service
   │  ├─ address.service.js
   │  ├─ cart.service.js
   │  ├─ goods.service.js
   │  └─ user.service.js
   └─ upload
      └─ fe5d803c345288d2ade393000.jpg

文件内容

router/address.router.js

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/address.controller.js

const { createAddress } = require('../service/address.service')

class AddController {
    async create(ctx) {
        const userId = ctx.state.user.id
        const { consignee, phone, address } = ctx.request.body

        const res = await createAddress({ userId, consignee, phone, address })
        ctx.body = {
            code: 0,
            message: '添加地址成功',
            result: res
        }
    }
}

module.exports = new AddController()

service/address.service.js

const Address = require('../model/address.model')

class AddressService {
    async createAddress(address) {
        return await Address.create(address)
    }
}

module.exports = new AddressService()

model/address.model.js

const { DataTypes } = require('sequelize')

const seq = require('../db/seq')

const Address = seq.define('addresses', {
    userId: {
        type: DataTypes.INTEGER,
        allowNull: false,
        comment: '用户id'
    },
    consignee: {
        type: DataTypes.STRING,
        allowNull: false,
        comment: '收货人的手机号',
    },
    address: {
        type: DataTypes.STRING,
        allowNull: false,
        comment: '收货人的地址',
    },
    isDefault: {
        type: DataTypes.BOOLEAN,
        allowNull: false,
        defaultValue: false,
        comment: '是否为默认地址,0:不是(默认值) 1:是'
    }
})

// 同步, sync
Address.sync({ force: false })

// 导出模型对象
module.exports = Address

除了这个正则,其余的都是老内容

30、获取、更新、删除地址和设置地址默认值

因为比较简单就写在一起

router/address.route.js

// 获取地址列表
router.get('/', auth, findAll)

// 修改地址数据
router.put('/:id', auth, validator({
    consignee: 'string',
    phone: { type: 'string', format: /^1\d{10}$/ },
    address: 'string'
}), update)

// 删除地址
router.delete('/:id', auth, remove)

// 设置默认
router.patch('/:id', auth, setDefault)

controller/address.controller.js

async findAll(ctx) {
        const userId = ctx.state.user.id

        const res = await findAllAddress(userId)

        ctx.body = {
            code: 0,
            message: '获取列表成功',
            result: res
        }
    }

async update(ctx) {
    const id = ctx.request.params.id

    const res = await updateAddress(id, ctx.request.body)

    ctx.body = {
        code: 0,
        message: '更新地址成功',
        result: res
    }
}

async remove(ctx) {
    const id = ctx.request.params.id
    const res = await removeAddress(id)

    ctx.body = {
        code: 0,
        message: '删除地址成功',
        result: res
    }
}

async setDefault(ctx) {
    const id = ctx.request.params.id
    const userId = ctx.state.user.id
    const res = await setDefaultAddress(userId, id)

    ctx.body = {
        code: 0,
        message: '设置默认成功',
        result: res
    }
}

service/address.service.js

async findAllAddress(userId) {
        return await Address.findAll({
            attributes: ['id', 'consignee', 'address', 'isDefault'],
            where: { userId }
        })
    }

async updateAddress(id, addressData) {
    return await Address.update(addressData, { where: { id } })
}

async removeAddress(id) {
    return await Address.destroy({ where: { id } })
}

async setDefaultAddress(userId, id) {

    console.log(111, userId, id);
    // 将其他的地址默认值都设为false,为传过来的id铺路
    await Address.update(
        { isDefault: false },
        {
            where: {
                userId
            }
        }
    )
    // 根据传过来的地址id,将地址设为默认值
    return await Address.update(
        { isDefault: true },
        {
            where: {
                id
            }
        }
    )
}