# koa **Repository Path**: yang-yongliang/koa ## Basic Information - **Project Name**: koa - **Description**: 用koa框架搭建的商城后台管理接口 - **Primary Language**: NodeJS - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-04-29 - **Last Updated**: 2024-09-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一.项目的初始化 ## 文件目录详情 ——main.js程序入口文件\ ——.env我们可以配置环境变量\ ——app 用框架(Koa)来搭建服务器的文件\ ——router路由文件\ ——controller操作数据的文件\ ——service操作数据库的文件\ ——middleware 中间件,当业务较复杂我们可以拆分多个中间件,而且可以实现\复用 ——config 获取配置文件\ ——constant用来配置出错信息或其它需要的文件\ ——db连接数据库\ ——model创建数据表\ ## 1. npm初始化 ``` npm init -y ``` 生成packge.json文件 ## 2. git初始化 生成.git文件夹,git的本地仓库 ``` git init ``` ## 3. 创建readme.md文件 # 二.搭建项目 ## 1安装koa框架 npm i koa ## 编写基础app ``` js // Koa本质是一个类 const Koa = require('koa') const app = new Koa() app.use((ctx,res) => { ctx.body = 'hello' }) app.listen(3000,() => { console.log('sever is running on http://localhost:3000'); }) ``` # 三.项目的基本优化 ## 自动重启服务 npm i nodemon -D 编写 package.json ```json "scripts": { "dev":"nodemon ./src/main.js" ``` npm run dev ## 读取配置文件 npm i dotenv 去根目录加载.env文件,把里面的建值对写到process.env环境变量中 创建.env文件 APP_PORT = 8000 创建src/config/config.default.js ```js const dotenv = require('dotenv') dotenv.config() module.exports = process.env ``` 改写main.js 导入config.default.js 把端口换${APP_PORT} # 四.添加路由 路由:根据不同的URL,调用不同的处理函数 ## 1.安装路由 ``` npm i koa-router ``` 步骤: ```js // 1.导入包 const Router = require('koa-router') // 2.实例化对象 const router = new Router({prefix:'/users'}) // 3.编写路由 // GET /users/ router.get('/',(ctx,next) => { ctx.body = 'users' }) module.exports = router // 4.注册中间件 app.use(userRouter.routes()) ``` # 五.项目结构优化 ## 1将http服务和app业务分开 ## 2将路由和控制器拆分 路由:解析URL,分页给控制器对应的方法 控制器,处理不同的业务 # 六.解析body # 安装koa-body npm i koa-body ## 注册中间件 ``` app.use(koaBody()) ``` ## 解析请求数据 ```js // 获取数据 const {user_name,password} = ctx.request.body // 操作数据库 const res = await createUser(user_name,password) console.log(res); // 返回结果 ctx.body = ctx.request.body ``` ## 拆分service层(数据库处理 ```js class UserServive{ async createUser(user_name,password){ // todo return '用户注册成功' } } module.exports = new UserServive() ``` # 七.数据库操作(集成 sequlize) sequellize ORM数据库工具 ORM:对象关系映射 * 数据表映射一个类 * 数据表中的数据行对应一个对象 * 数据表字段对应对象属性 * 数据表操作对应对象的方法 ## 1 安装 sequelize ```js npm i mysql2 sequelize ``` ## 2 连接数据库 `src/db/seq.js` ```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((err) => { console.log('数据库连接失败', err) }) module.exports = seq ``` ## 3 编写配置文件 ```js APP_PORT = 8000 MYSQL_HOST = localhost MYSQL_PORT = 3306 MYSQL_USER = root MYSQL_PWD = 123456 MYSQL_DB = zdsc ``` # 八. 创建 User 模型 ## 1 拆分 Model 层 sequelize 主要通过 Model 对应数据表 创建`src/model/user.model.js` ```js const { DataTypes } = require('sequelize') const seq = require('../db/seq') // 创建模型(Model zd_user -> 表 zd_users) const User = seq.define('zd_user', { // id 会被sequelize自动创建, 管理 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 }) module.exports = User ``` # 九. 添加用户入库 所有数据库的操作都在 Service 层完成, Service 调用 Model 完成数据库操作 改写`src/service/user.service.js` ```js const { createUser } = require('../service/user.service') class UserController { async register(ctx, next) { // 1. 获取数据 // console.log(ctx.request.body) const { user_name, password } = ctx.request.body // 2. 操作数据库 const res = await createUser(user_name, password) // console.log(res) // 3. 返回结果 ctx.body = { code: 0, message: '用户注册成功', result: { id: res.id, user_name: res.user_name, }, } } async login(ctx, next) { ctx.body = '登录成功' } } module.exports = new UserController() ``` # 十. 错误处理 在控制器中, 对不同的错误进行处理, 返回不同的提示错误提示, 提高代码质量 ## 合法性 用户名或密码为空时 status:400 bad request '10001':第1位代表后台处理 (也2前台处理,也可以说优先级),2-3:代表什么类型错误(用户),4-5代表具体的错误 ## 合理性 用户名已存在,不能注册 status:409 console.error()会记录错误日志 # 十一.拆分中间件 middleware ## 统一错误处理 定义错误处理文件 ```js module.exports = { userFormateError: { code: '10001', message: '用户名或密码为空', result: '', }, userAlreadyExited: { code: '10002', message: '用户已经存在', result: '', }, } ``` ## 错误处理函数 ```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 } ``` // 统一的错误处理(app处) ```js const errHandler = require('./errHandler') app.on('error', errHandler) ``` // 在出错的地方用 ```js ctx.app.emit('error',userFormateError,ctx) ``` # 十二.加密 在将密码保存到数据库之前, 要对密码进行加密处理 123123abc (加盐) 加盐加密 ```js const crpytPassword = 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() } ``` **我在这里出了问题,ileage arguments number,string,是由于我在postman测试数据时,password用的是数据,而bcrypt是要字符串类型** # 十三.登录验证 流程: - 验证格式 - 验证用户是否存在 - 验证密码是否匹配 # 十四.用户的认证 **token记录用户登录状态**,登录成功的用户我们要给他颁发令牌,以后的每一次请求中携带这个令牌 jwt:json web token 构成: header:头部 payload:载荷 signature:签名 ## 安装jsonwebtoken npm i jsonwebtoken ## 生成token ```js const jwt = require('jsonwebtoken') async login(ctx,next){ const {user_name} = ctx.request.body // ctx.body = `${user_name},用户登录成功` try { let res = await getUserInfo({user_name}) // rest(剩余)参数赋值 const {password,...userInfo} = res ctx.body = { code:'0', message:'用户登录成功', result:{ // header payload singure token:jwt.sign(userInfo,JWT_SECRET,{expiresIn:'1d'}) } } } catch (error) { console.error('用户登录失败',error); } } ``` ## 验证token ```js const jwt = require('jsonwebtoken') const {JWT_SECRET} = require('../config/config.default') const {TokenExpiredError,JsonWebTokenError} = require('../constant/err.type') const auth = async (ctx,next) => { const {authorization} = ctx.request.header const token = authorization?.replace('Bearer ','') try { // user中包含payload的信息 id user_name is_admin const user = jwt.verify(token,JWT_SECRET) ctx.state.user = user } catch (error) { switch(error.name){ case 'TokenExpiredError': console.error('token过期'); return ctx.app.emit('error',TokenExpiredError,ctx) case 'JsonWebTokenError': console.error('token无效'); return ctx.app.emit('error',JsonWebTokenError,ctx) } } await next() } module.exports = { auth } ``` # 十五.修改密码 # 十六.路由自动加载 router/index.js ```js 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 // 实现路由自动加载 避免每次导入 定义变量来接收\ ` const router = require('../router/index') ` # 十七.封装管理员权限 # 十八.文件上传 ## koa-body支持文件上传:它依赖formidable ```js async upload (ctx,next){ const file = ctx.request.files.file if(file){ ctx.body = { code:'10201', message:'文件上传成功', result:{ goods_img:path.basename(file.newFilename) } } }else{ console.error('文件上传失败'); return ctx.app.emit('error',fileUploadFailed,ctx) } } ``` ## 设置静态文件 `app.use(koaStatic(path.join(__dirname,'../upload')))` ## 上传优化 # 统一校验参数格式 # 发布商品到数据库 # 修改商品 # 删除商品 ## delete方式是硬删除 **实际中完全不推荐这样做,** ```js // controller async remove(ctx,next) { const {id} = ctx.params try { let res = await removeGoodById(id) if(res){ ctx.body = { code:0, message:'删除商品成功', result:{id} } }else{ ctx.body = { message:'删除商品失败' } } } catch (error) { console.error('删除商品失败',error); ctx.app.emit('error',deleteGoodFailed,ctx) } } // service async removeGoodById(id){ let res = await Goods.destroy({where:{id}}) return res > 0 ? true : false } ``` ## 上下架商品 --我们用上下架来改变商品\ **偏执表** Sequelize 支持 paranoid 表的概念. 一个 paranoid 表是一个被告知删除记录时不会真正删除它的表.反而一个名为 deletedAt 的特殊列会将其值设置为该删除请求的时间戳. 这意味着偏执表会执行记录的 软删除,而不是 硬删除. 当你调用 destroy 方法时,将发生软删除: ```js await Post.destroy({ where: { id: 1 } }); // UPDATE "posts" SET "deletedAt"=[timestamp] WHERE "deletedAt" IS NULL AND "id" = 1 ``` 如果你确实想要硬删除,并且模型是 paranoid,**则可以使用 force: true 参数强制执行:**\ 要恢复软删除的记录,可以使用 **restore**方法 # 获取商品列表 # 添加到购物车 # 获取购物加列表 ## pageNum,pageSize ## 去查询carts里面数据findAndCountAll ## 进行联表查询 **查询出来的list列表数据要用到goods表中数据,** # 修改购物车 ## 要进行格式校验 用之前add购物车的validotor封装函数,传递规则过去 ## 修改购物车的number或者selected(可只二传一) ### 判断number或者selected是否传递 ### 去数据库根据id修改 ### 返回数据 # 删除购物车数据 ## delete请求的数据 ctx.request.body是不能访问的 koa-body默认是严格模式,**parsedMethods来解决**\ strict {Boolean} DEPRECATED If enabled, don't parse GET, HEAD, DELETE requests, default true\ parsedMethods {String[]} Declares the HTTP methods where bodies will be parsed, default ['POST', 'PUT', 'PATCH']. Replaces strict option. # 全选与取消全选 用个参数来判断是全选还是取消全选 # 添加地址接口 # 获取地址列表 # 删除地址 # 设置默认地址 ## 先判断要修改的id在不在address中 **先把当前登录(user_id)的所有设置为0,在把当前(id)的设置为1** # 生成订单 ## 1.建orders表 ## 2.验证输入的数据 ## 3.验证成功添加到orders表中 # 获取订单列表 ## pageNum,pageSize ,status # 更新订单列表 **根据id来更改status的状态**