# egg **Repository Path**: tease-not-bald/egg ## Basic Information - **Project Name**: egg - **Description**: egg+sequelize学习同步笔记。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 2 - **Created**: 2023-10-09 - **Last Updated**: 2025-06-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # egg ## 介绍 egg+sequelize学习同步笔记。 ## NVM 下载:[nvm文档手册 - nvm是一个nodejs版本管理工具 - nvm中文网 (uihtm.com)](https://nvm.uihtm.com/) ~~~cmd # 查看是否安装成功 nvm -v # 已经安装的版本 nvm list # 可以安装的版本 nvm list available # 安装自己想要的版本 nvm install 18.16.0 # 切换版本 nvm use 18.16.0 ~~~ ## 相关文档 + [egg - 为企业级框架和应用而生 - Egg (eggjs.org)](https://www.eggjs.org/zh-CN) **约定优于配置** + [Sequelize 简介 | Sequelize中文文档 | Sequelize中文网](https://www.sequelize.cn/) ## 安装脚手架 + 切换镜像 ~~~shell nrm use taobao ~~~ + 全局安装脚手架 ~~~shell npm install create-egg -g ~~~ ## 安装运行初始化项目 我这里用的是egg脚手架`create-egg`创建的 ~~~shell F:\菜鸟前端\EggJs\egg>create-egg server [egg-init] use registry: https://registry.npmjs.org [egg-init] target dir is F:\菜鸟前端\EggJs\egg\server [egg-init] fetching npm info of egg-init-config ? Please select a boilerplate type simple - Simple egg app boilerplate [egg-init] use boilerplate: simple(egg-boilerplate-simple) [egg-init] fetching npm info of egg-boilerplate-simple [egg-init] downloading https://registry.npmjs.org/egg-boilerplate-simple/-/egg-boilerplate-simple-3.5.0.tgz [egg-init] extract to C:\Users\doudo\AppData\Local\Temp\egg-init-boilerplate [egg-init] collecting boilerplate config... ? project name server ? project description egg+sequelize learn to sync notes. ? project author tease-not-bald ? cookie security keys 1696813045841_6489 [egg-init] write to F:\菜鸟前端\EggJs\egg\server\.eslintignore [egg-init] write to F:\菜鸟前端\EggJs\egg\server\.eslintrc [egg-init] write to F:\菜鸟前端\EggJs\egg\server\README.md [egg-init] write to F:\菜鸟前端\EggJs\egg\server\.gitignore [egg-init] write to F:\菜鸟前端\EggJs\egg\server\package.json [egg-init] write to F:\菜鸟前端\EggJs\egg\server\app\router.js [egg-init] write to F:\菜鸟前端\EggJs\egg\server\config\config.default.js [egg-init] write to F:\菜鸟前端\EggJs\egg\server\config\plugin.js [egg-init] write to F:\菜鸟前端\EggJs\egg\server\app\controller\home.js [egg-init] write to F:\菜鸟前端\EggJs\egg\server\test\app\controller\home.test.js [egg-init] usage: - cd F:\菜鸟前端\EggJs\egg\server - npm install - npm start / npm run dev / npm test F:\菜鸟前端\EggJs\egg>cd server F:\菜鸟前端\EggJs\egg\server>pnpm install Packages: +812 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Progress: resolved 812, reused 811, downloaded 0, added 812, done dependencies: + egg 3.17.4 + egg-scripts 2.17.0 devDependencies: + egg-bin 5.14.2 (6.5.2 is available) + egg-ci 2.2.0 + egg-mock 5.10.8 + eslint 8.51.0 + eslint-config-egg 12.3.1 (13.0.0 is available) Done in 13.1s F:\菜鸟前端\EggJs\egg\server>pnpm dev > server@1.0.0 dev F:\菜鸟前端\EggJs\egg\server > egg-bin dev [egg-ts-helper] create typings\app\controller\index.d.ts (7ms) [egg-ts-helper] create typings\config\index.d.ts (21ms) [egg-ts-helper] create typings\config\plugin.d.ts (2ms) [egg-ts-helper] create typings\app\index.d.ts (1ms) 2023-10-09 09:07:57,998 INFO 15380 [master] node version v16.20.0 2023-10-09 09:07:57,999 INFO 15380 [master] egg version 3.17.4 [egg-ts-helper] create typings\app\controller\index.d.ts (6ms) [egg-ts-helper] create typings\config\index.d.ts (16ms) [egg-ts-helper] create typings\config\plugin.d.ts (3ms) [egg-ts-helper] create typings\app\index.d.ts (2ms) 2023-10-09 09:08:01,897 INFO 15380 [master] agent_worker#1:1776 started (3894ms) 2023-10-09 09:08:04,330 INFO 15380 [master] egg started on http://127.0.0.1:7001 (6331ms) ~~~ ## 写第一个api接口 ### 安装vscode拓展 + `Egg.js dev tools` + `Egg助手` + `eggjs` ### 用户数据列表展示 1. 创建控制器`controller/user.js` 2. 取数据库数据`result` 3. 响应数据`ctx.body` 4. 编写路由`router.js` **代码展示:** ~~~js const { Controller } = require('egg'); const demo = [ { id: 1, user_name: '王浩', nick_name: '逗逗不秃头', sex: '男' }, { id: 2, user_name: '王浩', nick_name: '逗逗不秃头', sex: '男' } ] class UserController extends Controller { // 列表展示 async index() { const { ctx } = this; const result = demo // 响应数据 ctx.body = { msg: 'success', data: result } } } module.exports = UserController; ~~~ ~~~js // router.js module.exports = app => { const { router, controller } = app; router.prefix('/man/api') // 设置基础路径 router.get('/user/list', controller.user.index) }; ~~~ **效果展示:** + 浏览器插件拓展:`JSON-handle` ![](./static/Snipaste_2023-10-09_10-03-42.png) ### 读取指定用户信息 1. 获取路由get传值参数`ctx.params` 2. 查找数据库中对应id`find()` 3. 响应数据`ctx.body` 4. 编写路由`router.js` **代码展示:** ~~~js // user.js const { Controller } = require('egg'); const demo = [ { id: 1, user_name: '王浩', nick_name: '逗逗不秃头', sex: '男' }, { id: 2, user_name: '海燕', nick_name: '不秃头', sex: '女' } ] class UserController extends Controller { // 获取路由get传值参数(路由:id) // ctx.params; // 获取url的问号get传值参数 // ctx.query; // 响应 // ctx.body = '响应'; // 状态码 // ctx.status = 201; // 读取某个用户 async read() { const { ctx } = this let id = ctx.params.id let detail = demo.find(item => item.id == id) ctx.body = { msg: 'success', data: detail } } } module.exports = UserController; ~~~ ~~~js // router.js /** * @param {Egg.Application} app - egg application */ module.exports = app => { router.get('/user/read/:id', controller.user.read) }; ~~~ **效果展示:** ![](./static/Snipaste_2023-10-09_10-18-46.png) > 还有一种形式当地址时如下形式时该咋办 ![](./static/Snipaste_2023-10-09_10-21-22.png) + 可以用`ctx.query`去获取url的问号get传值参数 + 还可以修改状态码`ctx.status` **代码展示:** ~~~js // user.js const { Controller } = require('egg'); const demo = [ { id: 1, user_name: '王浩', nick_name: '逗逗不秃头', sex: '男' }, { id: 2, user_name: '海燕', nick_name: '不秃头', sex: '女' } ] class UserController extends Controller { // 列表展示 async index() { const { ctx } = this; const result = demo // 获取url的问号get传值参数 ctx.query.page // 响应数据 ctx.body = { msg: 'success', data: result } // 修改状态码 ctx.status = 201 } } module.exports = UserController; ~~~ **效果展示:** ![](./static/Snipaste_2023-10-09_10-30-06.png) ## postman基础使用 1. 创建一个项目接口集合(接口根目录) 2. 创建一个开发环境(将基地址设置一个变量并在接口中通过模板语法`{{ }}`,便于后期接口维护) 3. 创建子目录,将同一模块的接口放一块 4. 在子目录下写接口 ![](./static/Snipaste_2023-10-09_10-37-28.png) ![](./static/Snipaste_2023-10-09_10-41-14.png) ![](./static/Snipaste_2023-10-09_10-46-41.png) ![](./static/Snipaste_2023-10-09_10-47-43.png) ![](./static/Snipaste_2023-10-09_10-50-42.png) ## csrf校验 **在开发接口时,就不需要egg提供的csrf保护了,需要关闭** + 相关文档:[安全 - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/core/security#防范方式-2) ~~~js // config/config.default.js // add your middleware config here config.middleware = []; // 关闭csrf config.security = { csrf: { enable: false }, // 跨域白名单:网页端基地址 domainWhiteList: [] } ~~~ ## 开启cors跨域 + 相关文档:https://www.npmjs.com/package/egg-cors + 安装依赖:`pnpm i egg-cors --save` + 配置依赖 **代码展示:** ~~~js // config/plugin.js exports.cors = { enable: true, package: 'egg-cors' } ~~~ ~~~js // config/config.default.js // 跨域 config.cors = { origin: '*', allowMethods: 'GET, PUT, POST, DELETE, PATCH' } ~~~ ## 创建用户 1. 编写控制器 2. 编写路由 **代码展示:** ~~~js // user.js // 创建用户 async create() { console.log(this.ctx.request); this.ctx.body = { msg: 'success', data: { id: 3, user_name: '石昊', nick_name: '不秃头', sex: '男' } } } ~~~ ~~~js // router.js router.post('/user/create', controller.user.create) ~~~ **效果展示:** ![](./static/Snipaste_2023-10-09_11-23-33.png) ## RESTful 风格定义路由 + 相关文档:[路由(Router) - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/basics/router#restful-风格的-url-定义) ~~~js // app/router.js module.exports = app => { const { router, controller } = app; router.resources('posts', '/api/posts', controller.posts); // app/controller/v1/users.js router.resources('users', '/api/v1/users', controller.v1.users); }; ~~~ 上面代码就在 `/posts` 路径上部署了一组 CRUD 路径结构,对应的 Controller 为 `app/controller/posts.js` 接下来, 你只需要在 `posts.js` 里面实现对应的函数就可以了。 | Method | Path | Route Name | Controller.Action | | ------ | --------------- | ---------- | ----------------------------- | | GET | /posts | posts | app.controllers.posts.index | | GET | /posts/new | new_post | app.controllers.posts.new | | GET | /posts/:id | post | app.controllers.posts.show | | GET | /posts/:id/edit | edit_post | app.controllers.posts.edit | | POST | /posts | posts | app.controllers.posts.create | | PUT | /posts/:id | post | app.controllers.posts.update | | DELETE | /posts/:id | post | app.controllers.posts.destroy | ~~~js // app/controller/posts.js // 列表页 exports.index = async () => {}; // 新增表单页 exports.new = async () => {}; // 新增逻辑 exports.create = async () => {}; // 详情页 exports.show = async () => {}; // 编辑表单页 exports.edit = async () => {}; // 更新逻辑 exports.update = async () => {}; // 删除逻辑 exports.destroy = async () => {}; ~~~ ## 路由分组 + 相关文档:[路由(Router) - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/basics/router#太多路由映射) ~~~js // app/router.js module.exports = app => { require('./router/news')(app); require('./router/admin')(app); }; // app/router/news.js module.exports = app => { app.router.get('/news/list', app.controller.news.list); app.router.get('/news/detail', app.controller.news.detail); }; // app/router/admin.js module.exports = app => { app.router.get('/admin/user', app.controller.admin.user); app.router.get('/admin/log', app.controller.admin.log); }; ~~~ ## 使用sequelize实现数据库迁移 > **数据库迁移就是通过命令行的方式,实现对数据库和数据表的创建、管理、维护** 1. 安装相关依赖 ~~~shell pnpm install --save egg-sequelize mysql2 ~~~ 2. 在`plugin.js`中引入`egg-sequelize` ~~~js exports.sequelize = { enable: true, package: 'egg-sequelize', }; ~~~ 3. 在`config.default.js`配置相关 ~~~js // 数据库相关 config.sequelize = { dialect: 'mysql', // support: mysql, mariadb, postgres, mssql database: 'man', host: 'localhost', port: 3306, username: 'root', password: 'mysql526', // 配置数据库时间为东八区北京时间 timezone: '+08:00', define: { // model的全局配置 timestamps: false, // 不添加create,update,delete时间戳 // 字段生成软删除时间戳 deleted_at paranoid: false, createdAt: 'created_at', updatedAt: 'updated_at', // deletedAt: 'deleted_at', freezeTableName: true, // 防止修改表名为复数 underscored: false // 防止驼峰式字段被默认转为下划线 }, // 打印日志 logging: true, // 时间格式化 dialectOptions: { // 让读取date类型数据时返回时间戳而不是UTC时间 dateStrings: true, typeCast: true }, // 连接池 pool: { max: 5, min: 0, idle: 10000 } } ~~~ 4. 创建数据库`man` ~~~cmd C:\Users\doudo>mysql -u root -p Enter password: ******** Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 24 Server version: 8.0.19 MySQL Community Server - GPL Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show databases; +--------------------+ | Database | +--------------------+ | atguigudb | | carbon_butler | | dandelion | | db_user | | information_schema | | mysql | | news_management | | performance_schema | | pig_pig_doctor | | qy | | restaurant | | sakila | | smart_hospital | | sys | | text | | travel | | world | +--------------------+ 17 rows in set (0.04 sec) mysql> create database man; Query OK, 1 row affected (0.01 sec) mysql> show databases; +--------------------+ | Database | +--------------------+ | atguigudb | | carbon_butler | | dandelion | | db_user | | information_schema | | man | | mysql | | news_management | | performance_schema | | pig_pig_doctor | | qy | | restaurant | | sakila | | smart_hospital | | sys | | text | | travel | | world | +--------------------+ 18 rows in set (0.00 sec) mysql> use man; Database changed ~~~ 5. sequelize 提供了[sequelize-cli](https://github.com/sequelize/cli)工具来实现[Migrations](http://docs.sequelizejs.com/manual/tutorial/migrations.html),我们也可以在 egg 项目中引入 sequelize-cli ~~~cmd pnpm install --save-dev sequelize-cli ~~~ + 相关文档:[Sequelize - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/tutorials/sequelize#初始化数据库和-migrations) 6. egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在`database`目录下,所以我们在项目根目录下新建一个`.sequelizerc`配置文件 ~~~diff const path = require('path'); module.exports = { config: path.join(__dirname, 'database/config.json'), 'migrations-path': path.join(__dirname, 'database/migrations'), 'seeders-path': path.join(__dirname, 'database/seeders'), 'models-path': path.join(__dirname, 'app/model'), }; ~~~ 7. 初始化 Migrations 配置文件和目录 ~~~cmd npx sequelize init:config npx sequelize init:migrations // npx sequelize init:models ~~~ 8. 行完后会生成`database/config.json`文件和`database/migrations`目录,我们修改一下`database/config.json`中的内容,将其改成我们项目中使用的数据库配置 ~~~json { "development": { "username": "root", "password": "mysql526", "database": "man", "host": "127.0.0.1", "dialect": "mysql", "timezone": "+08:00" } } ~~~ 9. 创建数据库 ~~~cmd npx sequelize db:create ~~~ 10. 创建数据库迁移表 ~~~cmd npx sequelize migration:generate --name=init-user ~~~ 11. 执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义 ~~~js 'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { const { INTEGER, STRING, DATE, ENUM } = Sequelize; // 创建表 await queryInterface.createTable('user', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true }, username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true }, password: { type: STRING(200), allowNull: false, defaultValue: '' }, avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' }, sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '男', comment: '用户性别' }, created_at: DATE, updated_at: DATE }); }, down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('user') } }; ~~~ 12. 执行 migrate 进行数据库变更 ~~~cmd # 升级数据库 npx sequelize db:migrate # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 # npx sequelize db:migrate:undo # 可以通过 `db:migrate:undo:all` 回退到初始状态 # npx sequelize db:migrate:undo:all ~~~ **效果展示:** ![](./static/Snipaste_2023-10-09_12-38-05.png) ## 数据表模型 > 模型是用来操作数据库的,对数据表经行增删改查的 ### 插入单个或批量 1. 创建`app/model/user.js`用户表模型 2. 通过控制器实现用户数据的创建 **代码展示:** ~~~js // model/user.js module.exports = app => { const { STRING, INTEGER, DATE, ENUM } = app.Sequelize // 配置(重要:一定要配置详细,一定要!!!) const User = app.model.define('user', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true }, username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true }, password: { type: STRING(200), allowNull: false, defaultValue: '' }, avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' }, sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '男', comment: '用户性别' }, created_at: DATE, updated_at: DATE },{ // timestamps: true, // 是否自动写入时间戳 // tableName: 'user' // 自定义数据表名称 }) return User } ~~~ ~~~js // controller/user.js // 创建用户 async create() { // console.log(this.ctx.request); // 新增单个 // const res = await this.app.model.User.create({ // username: '小石头', // password: '123456', // avatar_url: 'https://gitee.com/tease-not-bald/imgs/raw/master/qy/avatar8.png', // sex: '男' // }) // 批量新增 const res = await this.app.model.User.bulkCreate([ { username: '小石头1', password: '123456', avatar_url: 'https://gitee.com/tease-not-bald/imgs/raw/master/qy/avatar8.png', sex: '男' }, { username: '小石头2', password: '123456', avatar_url: 'https://gitee.com/tease-not-bald/imgs/raw/master/qy/avatar8.png', sex: '男' }, { username: '小石头3', password: '123456', avatar_url: 'https://gitee.com/tease-not-bald/imgs/raw/master/qy/avatar8.png', sex: '男' } ]) this.ctx.body = res } ~~~ ### 查询单个或批量 1. 可通过主键去查询`findByPk()` 2. 可通过`where`查询单个数据 3. 通过`findAll()`查询多个数据 4. 通过`findAndCountAll()`查询多个数据并计数 ~~~js // controller/user.js // 读取某个用户 async read() { const { ctx, app } = this // 通过主键查询单个数据 let id = parseInt(ctx.params.id) // let detail = demo.find(item => item.id == id) // let detail = await app.model.User.findByPk(id) // if (!detail) { // return ctx.body = { // msg: 'fail', // data: '用户不存在' // } // } // 查询单个数据 const detail = await app.model.User.findOne({ where: { id: 6, sex: '女' } }) ctx.body = { msg: 'success', data: detail } } // 列表展示 async index() { const { ctx } = this; // const result = demo // 获取url的问号get传值参数 // ctx.query.page let result = [] // result = await this.app.model.User.findAll() // 查询多个并计数 result = await this.app.model.User.findAndCountAll() // 响应数据 ctx.body = { msg: 'success', data: result } ctx.status = 201 } ~~~ ## 修改器 > 通过在模型表中为字段如密码进行加密,需要用到修改器 ~~~js // model/user.js password: { type: STRING(200), allowNull: false, defaultValue: '', set(val) { const hash = val + '123' this.setDataValue('password', hash) } }, ~~~ ![](./static/Snipaste_2023-10-09_15-19-49.png) ## 获取器 > 查询的时候都会调取获取器,如将时间返回为时间搓的形式 **代码展示:** ~~~js // model/user.js created_at: { type: DATE, get() { const val = this.getDataValue('created_at') return (new Date(val)).getTime() } }, ~~~ **效果展示:** ![](./static/Snipaste_2023-10-09_16-28-41.png) ## where操作符 无论你是通过 findAll/find 或批量 updates/destroys 进行查询,都可以传递一个 `where` 对象来过滤查询. `where` 通常用 attribute:value 键值对获取一个对象,其中 value 可以是匹配等式的数据或其他运算符的键值对象. 也可以通过嵌套 `or` 和 `and` `运算符` 的集合来生成复杂的 AND/OR 条件. ~~~js const Op = Sequelize.Op [Op.and]: {a: 5} // 且 (a = 5) [Op.or]: [{a: 5}, {a: 6}] // (a = 5 或 a = 6) [Op.gt]: 6, // id > 6 [Op.gte]: 6, // id >= 6 [Op.lt]: 10, // id < 10 [Op.lte]: 10, // id <= 10 [Op.ne]: 20, // id != 20 [Op.eq]: 3, // = 3 [Op.not]: true, // 不是 TRUE [Op.between]: [6, 10], // 在 6 和 10 之间 [Op.notBetween]: [11, 15], // 不在 11 和 15 之间 [Op.in]: [1, 2], // 在 [1, 2] 之中 [Op.notIn]: [1, 2], // 不在 [1, 2] 之中 [Op.like]: '%hat', // 包含 '%hat' [Op.notLike]: '%hat' // 不包含 '%hat' [Op.iLike]: '%hat' // 包含 '%hat' (不区分大小写) (仅限 PG) [Op.notILike]: '%hat' // 不包含 '%hat' (仅限 PG) [Op.startsWith]: 'hat' // 类似 'hat%' [Op.endsWith]: 'hat' // 类似 '%hat' [Op.substring]: 'hat' // 类似 '%hat%' [Op.regexp]: '^[h|a|t]' // 匹配正则表达式/~ '^[h|a|t]' (仅限 MySQL/PG) [Op.notRegexp]: '^[h|a|t]' // 不匹配正则表达式/!~ '^[h|a|t]' (仅限 MySQL/PG) [Op.iRegexp]: '^[h|a|t]' // ~* '^[h|a|t]' (仅限 PG) [Op.notIRegexp]: '^[h|a|t]' // !~* '^[h|a|t]' (仅限 PG) [Op.like]: { [Op.any]: ['cat', 'hat']} // 包含任何数组['cat', 'hat'] - 同样适用于 iLike 和 notLike [Op.overlap]: [1, 2] // && [1, 2] (PG数组重叠运算符) [Op.contains]: [1, 2] // @> [1, 2] (PG数组包含运算符) [Op.contained]: [1, 2] // <@ [1, 2] (PG数组包含于运算符) [Op.any]: [2,3] // 任何数组[2, 3]::INTEGER (仅限PG) [Op.col]: 'user.organization_id' // = 'user'.'organization_id', 使用数据库语言特定的列标识符, 本例使用 PG ~~~ ## 错误和异常统一处理 + 文档参考:[实现 RESTful API - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/tutorials/restful#统一错误处理) ~~~js // app/middleware/error_handler.js module.exports = () => { return async function errorHandler(ctx, next) { try { await next(); } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit('error', err, ctx); const status = err.status || 500; // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 const error = status === 500 && ctx.app.config.env === 'prod' ? 'Internal Server Error' : err.message; // 从 error 对象上读出各个属性,设置到响应中 ctx.body = { error }; if (status === 422) { ctx.body.detail = err.errors; } ctx.status = status; } }; }; ~~~ + 设置errorHandler中间件的配置 ~~~js // config/config.default.js // add your middleware config here config.middleware = ['errorHandler']; // config.errorHandler = { // ceshi: 123, // // 通用配置(以下是重点) // enable:true, // 控制中间件是否开启。 // match:'/man', // 设置只有符合某些规则的请求才会经过这个中间件(匹配路由) // ignore:'/shop', // 设置符合某些规则的请求不经过这个中间件。 // /** // 注意: // 1. match 和 ignore 不允许同时配置 // 2. 例如:match:'/news',只要包含/news的任何页面都生效 // **/ // // match 和 ignore 支持多种类型的配置方式:字符串、正则、函数(推荐) // match(ctx) { // // 只有 ios 设备才开启 // const reg = /iphone|ipad|ipod/i; // return reg.test(ctx.get('user-agent')); // }, // } ~~~ ## 参数验证 + 参考文档:https://www.npmjs.com/package/egg-valparams ~~~shell pnpm i egg-valparams --save ~~~ ~~~js // config/plugin.js valparams : { enable : true, package: 'egg-valparams' }, // config/config.default.js config.valparams = { locale : 'zh-cn', throwError: true }; ~~~ ~~~js // controller/user.js // 新增单个 const params = this.ctx.request.body // 参数验证 this.ctx.validate({ username: { type: 'string', required: true, desc: '用户名' }, password: { type: 'string', required: true, desc: '密码' }, sex: { type: 'string', required: false, defValue: '保密', desc: '性别' } }) const res = await this.app.model.User.create(params) ~~~ ![](./static/Snipaste_2023-10-09_20-59-06.png) # docker > **快速构建、运行、管理应用的工具** ## 准备Linux + [vm16破解版-vm16虚拟机中文破解版下载 附激活密钥 - 多多软件站 (ddooo.com)](http://www.ddooo.com/softdown/177981.htm) + 配置`Linux+CentOS7`:[2、VMware16安装Linux系统_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1XD4y1k7kE/?p=2&spm_id_from=pageDriver&vd_source=133ec2d20ab75cd55a5b84e3d0e80962) + `CentOS7`下载:[centos-7-isos-x86_64安装包下载_开源镜像站-阿里云 (aliyun.com)](https://mirrors.aliyun.com/centos/7/isos/x86_64/?spm=a2c6h.25603864.0.0.41e64511mHmRvg) ## SSH终端 + 轻量终端推荐:[MobaXterm Xserver with SSH, telnet, RDP, VNC and X11 - Home Edition (mobatek.net)](https://mobaxterm.mobatek.net/download-home-edition.html) + 第一次连接时可能需输入密码登录Linux系统输入啥密码就输入啥 ![](./static/Snipaste_2023-10-10_22-34-16.png) + 设置默认编辑器我这里设置的是VSCode ![](./static/Snipaste_2023-10-10_22-40-02.png) + 复制粘贴是很常用的配置,MobarXterm默认左键选中即**复制**,但是需要配置右键点击为**粘贴** ![](./static/Snipaste_2023-10-10_22-41-39.png) + SSH配置 ![](./static/Snipaste_2023-10-10_22-43-37.png) + 大多数情况下,我们没有x-server的需求,因此可以选择不要自启动 ![](./static/Snipaste_2023-10-10_22-44-57.png) ## 部署docker + 相关文档:[Install Docker Engine on CentOS | Docker Docs](https://docs.docker.com/engine/install/centos/) 1. 卸载旧版 ~~~shell yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine ~~~ > 如果你遇到了**您需要 root 权限执行此命令**这个问题 ![](./static/Snipaste_2023-10-11_08-38-58.png) + 那你需要切换到root账户 ~~~shell su ~~~ + 输入root账户的密码,就是你刚安装Linux系统时设置的密码 ![](./static/Snipaste_2023-10-11_08-41-23.png) + 接下来输入以下命令 ~~~shell export PATH=/bin:/usr/bin:$PATH ~~~ ![](./static/Snipaste_2023-10-11_08-45-53.png) 2. 首先要安装一个yum工具,当出现完毕字眼时,则安装成功 ~~~shell yum install -y yum-utils ~~~ 3. 安装成功后,执行命令,配置Docker的yum源 ~~~shell yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo ~~~ 4. 安装docker,当出现完毕字眼时,则安装成功 ~~~shell yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ~~~ ![](./static/Snipaste_2023-10-11_08-50-47.png) 5. 查看版本 ~~~shell docker -v ~~~ ![](./static/Snipaste_2023-10-11_08-53-58.png) ~~~shell docker images ~~~ ![](./static/Snipaste_2023-10-11_08-58-10.png) **无法连接到docker的守护进程** 6. 启动docker ~~~shell # 启动Docker systemctl start docker # 停止Docker systemctl stop docker # 重启 systemctl restart docker # 设置开机自启 systemctl enable docker # 执行docker ps命令,如果不报错,说明安装启动成功 docker ps ~~~ ![](./static/Snipaste_2023-10-11_09-03-35.png) 7. 配置镜像加速,这里以阿里云镜像加速为例 > 你先要在阿里云注册一个账号,https://www.aliyun.com/ ![](./static/Snipaste_2023-10-11_09-07-00.png) [容器镜像服务 (aliyun.com)](https://cr.console.aliyun.com/cn-shanghai/instances/mirrors) ![](./static/Snipaste_2023-10-11_09-10-07.png) 找当镜像加速器 ![](./static/Snipaste_2023-10-11_09-11-19.png) 输入以下命令 ~~~shell # 创建目录 mkdir -p /etc/docker # 复制内容,注意把其中的镜像加速地址改成你自己的 tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://yp0vmvz0.mirror.aliyuncs.com"] } EOF # 重新加载配置 systemctl daemon-reload # 重启Docker systemctl restart docker ~~~ ![](./static/Snipaste_2023-10-11_09-26-46.png) ![](./static/Snipaste_2023-10-11_09-27-34.png) ## docker部署redis 1. 拉去`redis:bullseye`的镜像 ~~~shell docker pull redis:bullseye # 查看镜像 docker images ~~~ ![](./static/Snipaste_2023-10-11_09-57-45.png) 2. 创建相应的目录和文件 ~~~shell mkdir -p /redis/conf mkdir -p /redis/data cd /redis/conf/ ~~~ ![](./static/Snipaste_2023-10-11_10-08-02.png) 3. 带上参数运行redis容器 ~~~shell docker run \ --name redis \ -p 6379:6379 \ --restart unless-stopped \ -v /data/redis/data:/data \ -v /data/redis/conf/redis.conf:/etc/redis/redis.conf \ -d redis:bullseye \ redis-server /etc/redis/redis.conf ~~~ ![](./static/Snipaste_2023-10-11_10-28-33.png) 4. 用命令连接docker中的redis ~~~shell docker exec -it redis redis-cli ~~~ 当输入info回车出现以下等信息,则表示连接成功 ![](./static/Snipaste_2023-10-11_10-31-58.png) ![](./static/Snipaste_2023-10-11_10-49-13.png) 5. 停止容器 ~~~shell docker stop redis ~~~ > 如果打不开dockerhub仓库,可以看看它[Docker Hub网站仓库国内进不去了?手把手教你通过GitHub项目构建自己的镜像仓库站!_dockerhub国内仓库-CSDN博客](https://blog.csdn.net/m0_61323675/article/details/130756529) 你也可以部署以下MySQL试试 ![](./static/Snipaste_2023-10-11_11-16-46.png) ![](./static/Snipaste_2023-10-11_11-18-49.png) # 项目实战 > 创建项目和一些相关的配置,跟上面差不多,我就不重复了 1. 项目初始化 2. csrf校验 3. cors跨域 4. sequelize初始化 5. 数据库迁移 ## 统一封装数据返回格式 1. 新建`extend`目录(扩展统一设置) 2. 创建`context.js`封装数据返回格式 **代码展示:** ~~~js' module.exports = { // 成功提示 apiSuccess(data = "", msg = "ok", code = 200) { this.body = { msg, data } this.status = code }, // 失败提示 apiFail(data = "", msg = "fail", code = 400) { this.body = { msg, data } this.status = code } } ~~~ ## 全局抛出异常处理 ~~~js module.exports = () => { return async function errorHandler(ctx, next) { try { await next(); // 404 处理 if (ctx.status === 404 && !ctx.body) { ctx.body = { msg: "fail", data: "404 错误" }; } } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit("error", err, ctx); const status = err.status || 500; // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 const error = status === 500 && ctx.app.config.env === "prod" ? "Internal Server Error" : err.message; // 从 error 对象上读出各个属性,设置到响应中 ctx.body = { error }; if (status === 422 && err.message === "Validation Failed") { ctx.body.detail = err.errors; } ctx.status = status; } }; }; ~~~ ## 数据表设计与迁移 1. 表迁移 ~~~shell npx sequelize migration:generate --name=user ~~~ ~~~js module.exports = { up: (queryInterface, Sequelize) => { const { INTEGER, STRING, DATE, ENUM, TEXT } = Sequelize return queryInterface.createTable("user", { id: { type: INTEGER(20), primaryKey: true, autoIncrement: true, }, username: { type: STRING(30), allowNull: false, defaultValue: "", comment: "用户名", unique: true, }, nickname: { type: STRING(30), allowNull: false, defaultValue: "", comment: "昵称", }, email: { type: STRING(160), allowNull: false, defaultValue: "", comment: "邮箱", }, password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码", }, avatar: { type: STRING, allowNull: true, defaultValue: "", comment: "头像", }, phone: { type: STRING(11), allowNull: false, defaultValue: "", comment: "手机", }, sex: { type: ENUM, values: ["男", "女", "保密"], allowNull: false, defaultValue: "男", comment: "性别", }, desc: { type: TEXT, allowNull: false, defaultValue: "", comment: "个性签名", }, created_time: DATE, updated_time: DATE }) }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable("user") } } ~~~ 1. 表模型创建 ~~~js // model/user.js module.exports = (app) => { const { STRING, INTEGER, DATE, ENUM, TEXT } = app.Sequelize // 配置(重要:一定要配置详细,一定要!!!) const User = app.model.define("user", { id: { type: INTEGER(20), primaryKey: true, autoIncrement: true, }, username: { type: STRING(30), allowNull: false, defaultValue: "", comment: "用户名", unique: true, }, nickname: { type: STRING(30), allowNull: false, defaultValue: "", comment: "昵称", }, email: { type: STRING(160), allowNull: false, defaultValue: "", comment: "邮箱", }, password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码", }, avatar: { type: STRING, allowNull: true, defaultValue: "", comment: "头像", }, phone: { type: STRING(11), allowNull: false, defaultValue: "", comment: "手机", }, sex: { type: ENUM, values: ["男", "女", "保密"], allowNull: false, defaultValue: "男", comment: "性别", }, desc: { type: TEXT, allowNull: false, defaultValue: "", comment: "个性签名", }, created_time: DATE, updated_time: DATE, }) return User } ~~~ 1. 数据库变更 ~~~shell npx sequelize db:migrate ~~~ **其他表同上操作,数据表模型可看源码** ## 用户注册 ### 路由分组 ~~~js // app/router.js module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); // 用户 require('./router/user')(app) }; // app/router/user.js module.exports = app => { // 注册 app.router.get('/user/register', app.controller.user.register); }; ~~~ ### 设置基础路径 ~~~js // app/router.js module.exports = app => { const { router, controller } = app; router.prefix('/man/api') // 设置基础路径 }; ~~~ ### 验证用户是否已经存在 ~~~js const { username, password } = ctx.request.body; // 验证用户是否已经存在 if (await app.model.User.findOne({ where: { username, } })) { ctx.throw(400, '用户名已存在'); } ~~~ ### 创建用户 ~~~js // 创建用户 const user = await app.model.User.create({ username, password }); if (!user) { ctx.throw(400, '创建用户失败'); } ctx.apiSuccess(user); ~~~ ### 参数校验 ~~~shell pnpm i egg-valparams --save ~~~ ~~~js // config/plugin.js valparams : { enable : true, package: 'egg-valparams' }, // config/config.default.js // 数据校验 config.valparams = { locale: 'zh-cn', throwError: true } ~~~ ~~~js // controller/user.js // 参数验证 ctx.validate({ username: { type: 'string', required: true, range: { min: 5, max: 20 }, desc: '用户名' }, password: { type: 'string', required: true, desc: '密码' }, repassword: { type: 'string', required: true, desc: '确认密码' } }, { equals: [ ['password', 'repassword'] ] // 判断密码和确认密码是否一致 }); ~~~ ### crypto 数据加密 1. 安装依赖 ~~~shell pnpm install crypto --save ~~~ 2. 配置文件配置 config / config.default.js ~~~js config.crypto = { secret: "qhdgw@45ncashdaksh2!#@3nxjdas*_672", }; ~~~ 3. 使用`UUID Generator`拓展制动生成`secret`(Ctrl+Shift+P) 4. 在`user`模型中使用修改器对密码进行加密 ~~~js password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码", set(val) { const hmac = crypto.createHash("md5", app.config.crypto.secret) hmac.update(val) this.setDataValue('password', hmac.digest('hex')) } }, ~~~ ## 用户登录 1. 获取页面数据 2. 参数验证 3. 验证用户是否存在 4. 验证密码 5. 生成token ~~~js // controller/user.js // 登录 async login() { const { ctx, app } = this; // 参数验证 ctx.validate({ username: { type: 'string', required: true, desc: '用户名' }, password: { type: 'string', required: true, desc: '密码' }, }); let { username, password } = ctx.request.body; let user = await app.model.User.findOne({ where: { username } }) if (!user) { ctx.throw(400, '用户不存在或被禁用') } // 验证密码 await this.checkPassword(password, user.password) user = JSON.parse(JSON.stringify(user)) // 生成token let token = ctx.getToken(user) user.token = token // delete user.password // 返回数据 return ctx.apiSuccess('登录成功', user) } // 验证密码 async checkPassword(password, hash_password) { // 先对需要验证的密码进行加密 const hmac = crypto.createHash("md5", this.app.config.crypto.secret); hmac.update(password); if(hmac.digest('hex') !== hash_password) { this.ctx.throw(400, '密码错误') } return true; } ~~~ ### jwt 加密鉴权 1. 安装依赖 ~~~shell pnpm i egg-jwt --save ~~~ 2. 配置相关 ~~~js // {app_root}/config/plugin.js exports.jwt = { enable: true, package: "egg-jwt", }; // {app_root}/config/config.default.js config.jwt = { secret: "355c667d-8043-49ff-84b4-753e46580244", expiresIn: '1d' // 有效(过期)时间 } ~~~ 3. 在context.js中生成token函数 ~~~js // extend/context.js // 生成token getToken(value) { return this.app.jwt.sign(value, this.app.config.jwt.secret); } ~~~ ### redis 缓存插件和封装 1. 安装依赖 ~~~shell pnpm i egg-redis --save ~~~ 2. 配置相关 ~~~js // config/plugin.js exports.redis = { enable: true, package: "egg-redis", }; // redis存储 config.redis = { client: { port: 6379, // Redis port host: "127.0.0.1", // Redis host password: "", db: 2, }, }; ~~~ 3. 缓存库封装 ~~~js // app/service/cache.js "use strict"; const Service = require("egg").Service; class CacheService extends Service { /** * 获取列表 * @param {string} key 键 * @param {boolean} isChildObject 元素是否为对象 * @return { array } 返回数组 */ async getList(key, isChildObject = false) { const { redis } = this.app; let data = await redis.lrange(key, 0, -1); if (isChildObject) { data = data.map((item) => { return JSON.parse(item); }); } return data; } /** * 设置列表 * @param {string} key 键 * @param {object|string} value 值 * @param {string} type 类型:push和unshift * @param {Number} expir 过期时间 单位秒 * @return { Number } 返回索引 */ async setList(key, value, type = "push", expir = 0) { const { redis } = this.app; if (expir > 0) { await redis.expire(key, expir); } if (typeof value === "object") { value = JSON.stringify(value); } if (type === "push") { return await redis.rpush(key, value); } return await redis.lpush(key, value); } /** * 设置 redis 缓存 * @param { String } key 键 * @param {String | Object | array} value 值 * @param { Number } expir 过期时间 单位秒 * @return { String } 返回成功字符串OK */ async set(key, value, expir = 0) { const { redis } = this.app; if (expir === 0) { return await redis.set(key, JSON.stringify(value)); } else { return await redis.set(key, JSON.stringify(value), "EX", expir); } } /** * 获取 redis 缓存 * @param { String } key 键 * @return { String | array | Object } 返回获取的数据 */ async get(key) { const { redis } = this.app; const result = await redis.get(key); return JSON.parse(result); } /** * redis 自增 * @param { String } key 键 * @param { Number } value 自增的值 * @return { Number } 返回递增值 */ async incr(key, number = 1) { const { redis } = this.app; if (number === 1) { return await redis.incr(key); } else { return await redis.incrby(key, number); } } /** * 查询长度 * @param { String } key * @return { Number } 返回数据长度 */ async strlen(key) { const { redis } = this.app; return await redis.strlen(key); } /** * 删除指定key * @param {String} key */ async remove(key) { const { redis } = this.app; return await redis.del(key); } /** * 清空缓存 */ async clear() { return await this.app.redis.flushall(); } } module.exports = CacheService; ~~~ 4. 缓存库使用 ~~~js // 控制器 await this.service.cache.set("key", "value"); ~~~ ## 全局权限验证中间件实现 1. 定义中间件`auth.js` 2. 配置中间件并限制访问 ~~~js // auth.js module.exports = (option, app) => { return async (ctx, next) => { //1. 获取 header 头token const { token } = ctx.header; if (!token) { ctx.throw(400, '您没有权限访问该接口!'); } //2. 根据token解密,换取用户信息 let user = {}; try { user = app.jwt.verify(token, app.config.jwt.secret) } catch (err) { let fail = err.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!'; ctx.throw(400, fail); } //3. 判断当前用户是否登录 // let t = await ctx.service.cache.get('user_' + user.id); // if (!t || t !== token) { // ctx.throw(400, 'Token 令牌不合法!'); // } // 4. 获取当前用户,验证当前用户是否存在 user = await app.model.User.findOne({ where: { id: user.id } }); if (!user) { ctx.throw(400, '当前用户不存在!'); } // 5. 把 user 信息挂载到全局ctx上 ctx.authUser = user; await next(); } }; ~~~ ~~~js // config.default.js config.middleware = [ //... "auth", // ... ]; // 配置那些路由需要验证 config.auth = { //ignore: ['/reg', '/login'] match: [ "/logout", "/video", "/video_detail", "/vod/sign", "/comment", "/fava", "/user/follow", "/user/unfollow", "/user/follows", "/user/fens", "/user/statistics", ], }; ~~~ ### 登录相关 ~~~js // controller/user.js // 登录 async login() { const { ctx, app } = this; // 参数验证 ctx.validate({ username: { type: 'string', required: true, desc: '用户名' }, password: { type: 'string', required: true, desc: '密码' }, }); let { username, password } = ctx.request.body; let user = await app.model.User.findOne({ where: { username } }) if (!user) { ctx.throw(400, '用户不存在或被禁用') } // 验证密码 await this.checkPassword(password, user.password) user = JSON.parse(JSON.stringify(user)) // 生成token let token = ctx.getToken(user) user.token = token delete user.password // 加入缓存中 if (!await this.service.cache.set('user_' + user.id, user.token)) { ctx.throw(400, '登录失败'); } // 返回数据 return ctx.apiSuccess('登录成功', user) } // 验证密码 async checkPassword(password, hash_password) { // 先对需要验证的密码进行加密 const hmac = crypto.createHash("md5", this.app.config.crypto.secret); hmac.update(password); if(hmac.digest('hex') !== hash_password) { this.ctx.throw(400, '密码错误') } return true; } ~~~ ### 统计用户相关数据 控制器:app/controller.user.js ```js // 统计相关数据 async statistics() { const { ctx, service, app } = this; let user_id = ctx.authUser.id; let followCount = await service.user.getFollowCount(user_id); let videoCount = await service.user.getVideoCount(user_id); ctx.apiSuccess({ followCount, videoCount }); } ``` 服务:app/service/user.js ```js const Service = require('egg').Service class UserService extends Service { // 指定用户关注人数 async getFollowCount(user_id) { return await this.app.model.Follow.count({ where: { user_id } }); } // 指定用户的作品量 async getVideoCount(user_id) { return await this.app.model.Video.count({ where: { user_id } }); } } module.exports = UserService ``` 路由:router/user.js ```js // 统计用户相关数据 app.router.get("/user/statistics", authHandler, app.controller.user.statistics); ``` ### 获取用户信息 控制器:app/controller/user.js ```js // 获取用户相关信息 async user_info() { const { ctx, service, app } = this; let currentUser = ctx.authUser; ctx.validate({ user_id: { required: true, desc: "用户id", type: "int" } }); let user_id = ctx.query.user_id; let res = await service.user.getUserInfo(user_id); let fensCount = 0; let followCount = 0; if (res) { fensCount = await service.user.getFensCount(user_id); followCount = await service.user.getFollowCount(user_id); } let follow = false; if (currentUser) { follow = await service.user.isFollow(currentUser.id, user_id); } ctx.apiSuccess({ user: res, fensCount, followCount, follow }); } ``` 服务:app/service/user.js ```js // 用户相关信息 async getUserInfo(user_id) { return await this.app.model.User.findOne({ where: { id: user_id }, attributes: { exclude: ['password'] } }); } ``` 路由:app/router.js ```js // 获取用户相关信息 router.get("/user/user_info", controller.user.user_info); ``` # DES ~~~cmd npm i create-egg -g create-egg des pnpm install pnpm dev ~~~ ![](./static/Snipaste_2023-11-30_15-03-27.png) ## sequelize 相关依赖: ~~~cmd pnpm install egg-sequelize mysql2 --save ~~~ ### 相关配置 ~~~js // plugin.js sequelize: { enable: true, package: "egg-sequelize" } ~~~ ~~~js // config.default.js // 数据库相关 config.sequelize = { dialect: "mysql", // support: mysql, mariadb, postgres, mssql database: "twhc", host: "localhost", port: 3306, username: "root", password: "123456", // 配置数据库时间为东八区北京时间 timezone: "+08:00", define: { // model的全局配置 timestamps: true, // 不添加create,update,delete时间戳 // 字段生成软删除时间戳 deleted_at paranoid: false, createdAt: "created_time", updatedAt: "updated_time", // deletedAt: 'deleted_at', freezeTableName: true, // 防止修改表名为复数 underscored: false // 防止驼峰式字段被默认转为下划线 }, // 打印日志 logging: true, // 时间格式化 dialectOptions: { // 让读取date类型数据时返回时间戳而不是UTC时间 dateStrings: true, typeCast: true }, // 连接池 pool: { max: 5, min: 0, idle: 10000 } }; ~~~ ### 创建一个数据库 ~~~cmd mysql -u root -p password: ****** create database des; ~~~ ## 数据库迁移 ~~~cmd pnpm install sequelize-cli --save-dev ~~~ ### 迁移配置文件 > 官方讲把涉及到迁移的文件都放在 `database` 目录下比较好,那咱们就听他的,在项目根目录下新建一个 `.sequelizerc` 配置文件 ,这里面的东西有些是啥俺也不晓得,管他的,不必知道这有啥用,跟着来就中: ~~~idff // .sequelizerc const path = require('path') module.exports = { config: path.join(__dirname, 'database/config.json'), 'migrations-path': path.join(__dirname, 'database/migrations'), 'seeders-path': path.join(__dirname, 'database/seeders'), 'models-path': path.join(__dirname, 'app/model') } ~~~ + 运行以下命令,会在 `database` 目录下生成一些与数据库迁移相关的文件或目录: ~~~cmd # 添加配置环境,如开发、测试、运维环境 npx sequelize init:config # 添加迁移文件目录 npx sequelize init:migrations # npx sequelize init:models ~~~ + 运行后,会在根目录下生成下图的玩意: ![](./static/Snipaste_2023-11-30_15-16-41.png) + 在 `config.json` 文件内将 `password` 和 `database` 改为自己的密码和数据库名: ~~~cmd # 创建数据库 npx sequelize db:create ~~~ ![](./static/Snipaste_2023-11-30_15-17-54.png) + 这样子就算是创建成功了 ## 用户数据相关 > 俺这只是举了个简单的例子,后面如果有啥更牛逼的玩意俺会再回来改它的,别太在意这玩意。 ~~~cmd # 创建一个用户迁移文件 npx sequelize migration:generate --name=user ~~~ ![](./static/Snipaste_2023-11-30_15-20-50.png) ### 用户迁移文件 + 然后就生成个这玩意,接着在这个文件里写一些东西来初始化那个 `user` 表: ~~~js /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { const { INTEGER, STRING, DATE, ENUM, TEXT } = Sequelize; await queryInterface.createTable( "user", { id: { type: INTEGER, primaryKey: true, autoIncrement: true, comment: "主键ID" }, username: { type: STRING(30), allowNull: false, defaultValue: "", comment: "用户名" // unique: true }, avatar: { type: STRING(100), allowNull: false, defaultValue: "", comment: "头像" }, email: { type: STRING(50), allowNull: false, defaultValue: "", comment: "邮箱" }, phone: { type: STRING(11), allowNull: false, defaultValue: "", unique: true, comment: "手机号" }, password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码" }, desc: { type: TEXT, allowNull: true, comment: "描述" }, status: { type: ENUM("0", "1"), allowNull: false, defaultValue: "1", comment: "状态 0 禁用 1 启用" }, gender: { type: ENUM("0", "1", "2"), allowNull: false, defaultValue: "0", comment: "性别 0 未知 1 男 2 女" }, ruleId: { type: INTEGER, allowNull: false, defaultValue: 1, comment: "角色ID" }, created_time: DATE, updated_time: DATE }, { engine: "InnoDB", // autoIncrement: 10, // 锁定从哪开始递增 charset: "utf8mb4", collate: "utf8mb4_general_ci" } ); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable("user"); } }; ~~~ ### 执行数据库变更 ~~~cmd # 数据库变更 npx sequelize db:migrate # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo # 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all ~~~ + 然后,你就能在 `datagrip` 上看到你的数据库和表了 ![](./static/Snipaste_2023-11-30_15-37-52.png) ## csrf校验 > 在开发接口时,就不需要egg提供的csrf保护了,需要关闭,虽然哈,俺不知道官方限制这东西这到底有啥用,反正俺觉得这就是多此一举。 + 相关文档:[安全 - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/core/security#防范方式-2) ~~~js // config/config.default.js // 关闭csrf config.security = { csrf: { enable: false }, // 跨域白名单:网页端基地址 domainWhiteList: [] } ~~~ ## cors跨域 > 同源策略问题,俺没时间解释,自个上网搜搜,讲的都比俺好滴很。 + 相关文档:https://www.npmjs.com/package/egg-cors ~~~cmd pnpm i egg-cors --save ~~~ + 配置依赖: ~~~js // config/plugin.js cors: { enable: true, package: "egg-cors" } ~~~ ~~~js // config/config.default.js // 跨域 config.cors = { origin: '*', allowMethods: 'GET, PUT, POST, DELETE, PATCH' } ~~~ ## 统一封装数据响应格式 > 你可以将统一封装数据响应格式的函数放在你觉得舒服的位置,在不会的前提下,咱建议哈,还是跟着官方来吧。`app/extend/**` 用于框架的扩展,可选,具体参见[框架扩展](https://www.eggjs.org/zh-CN/basics/extend)。 + 创建`context.js`封装数据返回格式: ~~~js module.exports = { // 成功提示 apiSuc(msg = "success", data = "", code = 200) { this.body = { msg, data, code }; this.status = code; }, // 失败提示 apiErr(msg = "error", data = "", code = 400) { this.body = { msg, data, code }; this.status = code; } }; ~~~ ## 全局抛出异常处理 > 框架虽然提供了默认的异常处理,但是可能和我们在前面的接口约定不一致,因此我们需要自己实现一个统一错误处理的中间件来对错误进行处理。 + 文档参考:[实现 RESTful API - Egg (eggjs.org)](https://www.eggjs.org/zh-CN/tutorials/restful#统一错误处理) 在 `app/middleware` 目录下新建一个 `error_handler.js` 的文件来新建一个 [middleware](https://www.eggjs.org/zh-CN/basics/middleware): ~~~js module.exports = () => { return async function errorHandler(ctx, next) { try { await next(); // 404 处理 if (ctx.status === 404 && !ctx.body) { ctx.body = { msg: "fail", data: "404 错误" }; } } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit("error", err, ctx); const status = err.status || 500; // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 const error = status === 500 && ctx.app.config.env === "prod" ? "Internal Server Error" : err.message; // 从 error 对象上读出各个属性,设置到响应中 ctx.body = { error }; if (status === 422 && err.message === "Validation Failed") { ctx.body.detail = err.errors; } ctx.status = status; } }; }; ~~~ ~~~js // config.default.js config.middleware = ['errorHandler'] ~~~ ## 参数验证 + 参考文档:https://www.npmjs.com/package/egg-valparams ~~~cmd pnpm i egg-valparams --save ~~~ ~~~js // config/plugin.js valparams: { enable: true, package: "egg-valparams" } // config/config.default.js // 参数验证 config.valparams = { locale: "zh-cn", throwError: true }; ~~~ ## 用户模型 + 采用 `moment` 插件在模型中使用构造器 ·`get()` 对时间格式化处理: ~~~cmd pnpm install moment --save ~~~ ~~~js // model/user.js const moment = require("moment"); created_time: { type: DATE, get() { const v = this.getDataValue("created_time"); return v ? moment(v).format("YYYY-MM-DD HH:mm:ss") : v; } }, ~~~ + 通过使用`crypto`,在用户模型中使用修改器`set()`对密码进行加密,通过使用`UUID Generator`拓展自动生成`secret`参数值 ~~~cmd pnpm install crypto --save ~~~ ~~~js // config/config.default.js // 数据加密 config.crypto = { secret: 'aef0a5c3-3daf-4ac5-b689-7b8b8f116f3e' } ~~~ ~~~js // model/user.js const crypto = require("crypto"); password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码", set(val) { const hash = crypto.createHash("md5", app.config.crypto.secret); hash.update(val); this.setDataValue("password", hash.digest("hex")); } }, ~~~ ### 用户模型完整 ~~~js module.exports = (app) => { const { INTEGER, STRING, DATE, ENUM, TEXT } = app.Sequelize; const moment = require("moment"); const crypto = require("crypto"); // 配置(重要:一定要配置详细,一定要!!!) const User = app.model.define("user", { id: { type: INTEGER, primaryKey: true, autoIncrement: true, comment: "主键ID" }, avatar: { type: STRING(100), allowNull: false, defaultValue: "", comment: "头像" }, username: { type: STRING(30), allowNull: false, defaultValue: "", comment: "用户名" // unique: true }, email: { type: STRING(50), allowNull: false, defaultValue: "", comment: "邮箱" }, phone: { type: STRING(11), allowNull: false, defaultValue: "", unique: true, comment: "手机号" }, password: { type: STRING, allowNull: false, defaultValue: "", comment: "密码", set(val) { const hash = crypto.createHash("md5", app.config.crypto.secret); hash.update(val); this.setDataValue("password", hash.digest("hex")); } }, desc: { type: TEXT, allowNull: true, comment: "描述" }, status: { type: ENUM("0", "1"), allowNull: false, defaultValue: "1", comment: "状态 0 禁用 1 启用" }, gender: { type: ENUM("0", "1", "2"), allowNull: false, defaultValue: "0", comment: "性别 0 未知 1 男 2 女" }, createrId: { type: INTEGER, allowNull: false, defaultValue: 1, comment: "创建人ID" }, ruleId: { type: INTEGER, allowNull: false, defaultValue: 1, comment: "角色ID" }, created_time: { type: DATE, get() { const v = this.getDataValue("created_time"); return v ? moment(v).format("YYYY-MM-DD HH:mm:ss") : v; } }, updated_time: { type: DATE, get() { const v = this.getDataValue("created_time"); return v ? moment(v).format("YYYY-MM-DD HH:mm:ss") : v; } } }); return User; }; ~~~ ## 路由分组 ~~~js // app/router.js /** * @param {Egg.Application} app - egg application */ module.exports = (app) => { const { router, controller } = app; router.prefix("/api"); // 设置基础路径 router.get("/", controller.home.index); // 用户 require("./router/user")(app); }; ~~~ ~~~js // router/user.js module.exports = (app) => { const { router, controller } = app; router.post("/user/reg", controller.user.reg); }; ~~~ ## 用户 > **大致思路:** > > + 设置路由 > + 创建控制器 `controller` 和服务器 `service` > + 获取页面参数 > + 可差数验证 > + 判断用户是否存在 > + 创建用户 > + 响应数据 ### 注册 ~~~js // controller/user.js const Controller = require("egg").Controller; class UserController extends Controller { async reg() { const { ctx, app } = this; const { phone, password } = ctx.request.body; ctx.validate( { phone: { type: "string", required: true, desc: "手机号" }, password: { type: "string", required: true, desc: "密码" }, repassword: { type: "string", required: true, desc: "确认密码" } }, { // 判断密码和确认密码是否一致 equals: [["password", "repassword"]] } ); if (await app.model.User.findOne({ where: { phone } })) { ctx.throw(409, "用户已存在"); } const user = await app.model.User.create({ phone, password }); if (!user) { ctx.throw(500, "注册失败"); } ctx.apiSuc("注册成功", user); } } module.exports = UserController; ~~~ 注册成功: ![](./static/Snipaste_2023-11-30_16-54-02.png) 用户已存在: ![](./static/Snipaste_2023-11-30_16-54-46.png) 确认密码不能为空: ![](./static/Snipaste_2023-11-30_17-10-06.png) 密码不能空: ![](./static/Snipaste_2023-11-30_17-11-01.png) 手机号不能为空: ![](./static/Snipaste_2023-11-30_17-11-58.png) + 接下来咱们来简化一下user控制层的代码,将其封装到user服务层去只要是关于数据库操作的,俺都放在service层内: ~~~js // service/user.js const Service = require("egg").Service; class UserService extends Service { // 查找用户 async findByPhone(phone) { const { app } = this; const user = await app.model.User.findOne({ where: { phone } }); return user; } // 创建用户 async createUser(phone, password) { const { app } = this; const user = await app.model.User.create({ phone, password }); return user; } } module.exports = UserService; ~~~ 修改 `controller/user.js` : ~~~js const Controller = require("egg").Controller; class UserController extends Controller { async reg() { const { ctx } = this; const { phone, password } = ctx.request.body; const userService = ctx.service.user; ctx.validate( { phone: { type: "string", required: true, desc: "手机号" }, password: { type: "string", required: true, desc: "密码" }, repassword: { type: "string", required: true, desc: "确认密码" } }, { // 判断密码和确认密码是否一致 equals: [["password", "repassword"]] } ); if (await userService.findByPhone(phone)) { ctx.throw(409, "用户已存在"); } const user = await userService.createUser(phone, password); if (!user) { ctx.throw(500, "注册失败"); } ctx.apiSuc("注册成功", user); } } module.exports = UserController; ~~~ ### token > **token获取流程** > > + 浏览器向服务器请求token口令 > > + 服务器创建token,并返回给浏览器 > > + 浏览器接受到服务器返回的token,并保存在本地(cookie、localStorage、sessionStorage均可) > > + 浏览器向服务器发送的所有请求在headers中都带上token,供服务器校验服务器接收到请求,校验附带的tokent口令,校验通过则做后续工作,否则直接拒绝请求; ~~~cmd pnpm i egg-jwt --save ~~~ ~~~js // plugin.js jwt: { // 实现登录 token enable: true, package: "egg-jwt" } // config.default.js config.jwt = { // token 密码 secret: "5dcbe1bb-b470-4d4d-9fb1-1b8379e75e0d", // 可以自定义 sign: { //jwt.sign(***,***,[options,***])方法中,options的默认设置可以在这里配置; // 过期时间8小时 expiresIn: 8 * (60 * 60) //多少s后过期。actionToken.js中,jwt.sing(plyload,secret,{expiresIn:number})会被合并,调用时设置优先级更高; } }; ~~~ 添加解析token ~~~js // service/user.js // 生成token async generateToken(val) { const { app } = this; return app.jwt.sign(val, app.config.jwt.secret, { expiresIn: app.config.jwt.sign.expiresIn }); } // 验证token async verifyToken(token) { const { app } = this; return app.jwt.verify(token, app.config.jwt.secret); } ~~~ ### token中间件处理 > 因为后续的接口都需要通过token处理,使用中间件处理简化代码 ~~~js // middleware/token_handler.js module.exports = (options, app) => { return async (ctx, next) => { let { token } = ctx.header; console.log(token); await next(); }; }; ~~~ 在`config.middleware` 内使用,并通过 `match` 限制访问权限 ~~~js // config.default.js // add your middleware config here config.middleware = ["errorHandler", "tokenHandler"]; config.tokenHandler = { match: ["/api/user/info"] }; ~~~ 重写获取用户信息接口 ~~~js async info() { const { ctx, app } = this; ctx.APISuccess("123"); } ~~~ 点击发送请求后,此时不给token值控制台打印的是undefined,这说明经过了中间件 `token_handler.js` ![](./static/Snipaste_2023-11-30_19-17-38.png) ![](./static/Snipaste_2023-11-30_19-16-30.png) 给token随便写个值: ![](./static/Snipaste_2023-11-30_19-18-32.png) ![](./static/Snipaste_2023-11-30_19-18-56.png) > 中间件模块无法引入控制层controller和服务层service,所以无法使用service层中的验证token函数verifyToken ~~~js // middleware/token_handler.js module.exports = (options, app) => { return async (ctx, next) => { // 获取请求头token let { token } = ctx.header; console.log(token); if (!token) { ctx.throw(400, "您没有权限访问该接口!"); } // 根据token解密,换取用户信息 let user = {}; try { user = app.jwt.verify(token, app.config.jwt.secret); } catch (err) { let fail = err.name === "TokenExpiredError" ? "token 已过期! 请重新获取令牌" : "Token 令牌不合法!"; ctx.throw(400, fail); } // 获取当前用户,验证当前用户是否存在 user = await app.model.User.findOne({ where: { id: user.id } }); if (!user) { ctx.throw(400, "当前用户不存在!"); } // 把 user 信息挂载到全局ctx上 ctx.tokenUser = user; await next(); }; }; ~~~ ~~~js // controller/user.js async info() { const { ctx, app } = this; console.log(ctx.tokenUser); // ... ctx.APISuccess("123"); } ~~~ ![](./static/Snipaste_2023-11-30_19-23-26.png) ![](./static/Snipaste_2023-11-30_19-22-44.png) ### 登录 > 大致思路与注册一致,但是多了密码验证、生成token,可以将这两个函数封装到service层中处理 ~~~js // service/user.js const Service = require("egg").Service; const crypto = require("crypto"); class UserService extends Service { // ... // 验证密码 async validatePassword(password, hash_password) { // 先对需要验证的密码进行加密 const hash = crypto.createHash("md5", this.app.config.crypto.secret); hash.update(password); password = hash.digest("hex"); let res = password === hash_password; if (!res) { this.ctx.throw(400, "密码错误"); } return true; } } module.exports = UserService; // controller/user.js const Controller = require("egg").Controller; class UserController extends Controller { // ... async login() { const { ctx } = this; const { phone, password } = ctx.request.body; const userService = ctx.service.user; // 验证该用户是否存在|验证该用户状态是否启用 let user = await userService.findByPhone(phone); if (!user || user.status === "0") { ctx.throw(404, "用户不存在或已被禁用"); } // 验证密码 await userService.validatePassword(password, user.password); // 转json user = JSON.parse(JSON.stringify(user)); // 生成token const token = await userService.generateToken({ id: user.id, username: user.username, phone: user.phone }); // 赋值给user对象 user.token = token; // 取消密码展示 delete user.password; ctx.apiSuc("登录成功", user); } } module.exports = UserController; ~~~ ![](./static/Snipaste_2023-11-30_18-56-48.png) 看看能不能解析token: ~~~js // controller/user.js const userInfo = await userService.verifyToken(token); console.log(userInfo); ~~~ ![](./static/Snipaste_2023-11-30_18-59-53.png) ### 获取用户信息 > 这是没使用中间件处理的了解下就行,别太当真了 ~~~js // router.user.js router.get("/user/info", controller.user.info); ~~~ ~~~js // controller/user.js async info() { const { ctx } = this; // console.log(ctx.tokenUser); // const userService = ctx.service.user; // let token = ctx.headers["authorization"]; // token = token ? token.split("Bearer ")[1] : null; // const { token } = ctx.header; // const user = await userService.verifyToken(token); // const userInfo = await app.model.User.findOne({ // where: { // id: user.id // } // }); // ctx.APISuccess("获取成功", userInfo); const user = ctx.tokenUser; ctx.apiSuc("获取成功", user); } ~~~ ![](./static/Snipaste_2023-11-30_19-26-25.png) ### 模糊查询遍历用户信息 ~~~js // 所有用户信息列表 async list() { const { ctx, app } = this; const { Op } = app.Sequelize; let pageBean = ctx.query.pageBean; let username = ctx.query.username; let phone = ctx.query.phone; // 数据查询 const where = {}; if (username !== undefined) { where.username = { [Op.like]: `%${username}%` }; } if (phone !== undefined) { where.phone = { [Op.like]: `%${phone}%` }; } const userList = await app.model.User.findAndCountAll({ where, offset: pageBean ? (pageBean.pageIndex - 1) * pageBean.pageSize : undefined, limit: pageBean ? parseInt(pageBean.pageSize) : undefined }); ctx.apiSuc("查询成功", userList); } ~~~ 拆分到 `service/user.js` 层: ~~~js // service/user.js // 模糊查询遍历用户信息 async findByUsernameAndPhone(username, phone) { const { ctx, app } = this; const { Op } = app.Sequelize; let pageBean = ctx.query.pageBean; // 数据查询 const where = {}; if (username !== undefined) { where.username = { [Op.like]: `%${username}%` }; } if (phone !== undefined) { where.phone = { [Op.like]: `%${phone}%` }; } const userList = await app.model.User.findAndCountAll({ where, offset: pageBean ? (pageBean.pageIndex - 1) * pageBean.pageSize : undefined, limit: pageBean ? parseInt(pageBean.pageSize) : undefined }); return userList; } ~~~ 修改 `controller/user.js` 层: ~~~js // controller/user.js async list() { const { ctx } = this; const userService = ctx.service.user; let username = ctx.query.username; let phone = ctx.query.phone; const user = await userService.findByUsernameAndPhone(username, phone); ctx.apiSuc("获取成功", user); } ~~~ 手机号查询: ![](./static/Snipaste_2023-11-30_20-10-42.png) 姓名查询: ![](./static/Snipaste_2023-11-30_20-11-21.png) 遍历所有数据: ![](./static/Snipaste_2023-11-30_20-12-46.png) ### 更新当前用户信息 ~~~js // router/user.js router.put("/user/edit", controller.user.edit); ~~~ + 在 `config.default.js` 中配置中间件处理: ~~~js config.tokenHandler = { match: ["/api/user/info", "/api/user/edit"] }; ~~~ + 在 `service/user.js` 中添加数据库操作相关: ~~~js // 编辑当前用户信息 async editCurrentUser(data) { const { app, ctx } = this; const id = ctx.tokenUser.id; const user = await app.model.User.findByPk(id); const editUser = await user.update(data); return editUser; } ~~~ + 在 `controller/user.js` 添加控制相关: ~~~js async edit() { const { ctx, service } = this; // const id = +ctx.params.id; const body = ctx.request.body; const userService = service.user; const user = await userService.editCurrentUser(body); ctx.apiSuc("更新成功", user); } ~~~ 编辑当前登录用户的信息: ![](./static/Snipaste_2023-11-30_20-48-14.png) ![](./static/Snipaste_2023-11-30_20-48-40.png) ### 注销当前登录用户 ~~~js // router/user.js router.del("/user/logout", controller.user.logout); ~~~ ~~~js // config.default.js config.tokenHandler = { match: ["/api/user/logout"] }; ~~~ ~~~js // service/user.js // 注销当前登录用户 async logoutCurrentUser() { const { app, ctx } = this; const id = ctx.tokenUser.id; const user = await app.model.User.findByPk(id); const res = await user.destroy(); return res; } ~~~ ~~~js // controller/user.js // 注销当前登录用户 async logout() { const { ctx, app, service } = this; const userService = service.user; const user = await userService.logoutUser(); ctx.APISuccess("注销成功", user); } ~~~ ![](./static/Snipaste_2023-11-30_21-08-48.png) ![](./static/Snipaste_2023-11-30_21-09-24.png) ## 角色 需要与用户表进行关联 ### 数据库迁移 ~~~cmd # 创建一个角色迁移文件 npx sequelize migration:generate --name=role ~~~ ~~~js /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { const { INTEGER, STRING, DATE, TEXT } = Sequelize; await queryInterface.createTable( "role", { id: { type: INTEGER, primaryKey: true, autoIncrement: true, comment: "主键ID" }, rolename: { type: STRING(30), allowNull: false, defaultValue: "", unique: true, comment: "角色名称" }, memberId: { type: INTEGER, allowNull: false, defaultValue: 1, comment: "会员ID" }, desc: { type: TEXT, allowNull: false, defaultValue: "", comment: "角色描述" }, creator: { type: STRING(30), allowNull: false, defaultValue: "", comment: "创建人" }, created_time: DATE, updated_time: DATE }, { engine: "InnoDB", // autoIncrement: 10, // 锁定从哪开始递增 charset: "utf8mb4", collate: "utf8mb4_general_ci" } ); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable("role"); } }; ~~~ ~~~cmd # 数据库变更 npx sequelize db:migrate # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo # 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all ~~~ **一定要记得在 `router.js` 下引入 `router/role.js` 路由模块哈,俺就是忘了引入,导致不知道那里出了错误**😭 ~~~js // router.js // 角色 require("./router/role")(app); ~~~ ~~~js // router/role.js module.exports = (app) => { const { router, controller } = app; router.post("/role/add", controller.role.add); router.del("/role/del/:id", controller.role.del); router.put("/role/edit/:id", controller.role.edit); router.get("/role/list", controller.role.list); }; ~~~ ~~~js // controller/role.js "use strict"; const Controller = require("egg").Controller; class RoleController extends Controller { // 增 async add() {} // 删 async del() {} // 改 async edit() {} // 查 async list() {} } module.exports = RoleController; ~~~ **同时在添加、删除、编辑角色时需要用到当前用户权限** ~~~js // config.default.js config.tokenHandler = { // 路由为/api/role才使用中间件 match: [ "/api/user/info", "/api/user/edit", "/api/user/logout", "/api/role/add", "/api/role/del/:id", "/api/role/edit/:id" ] // 路由为/api/role不使用中间件 // ignore: ["/api/role"] }; ~~~ ~~~js // model/role.js module.exports = (app) => { const { INTEGER, STRING, DATE, TEXT } = app.Sequelize; const moment = require("moment"); // 配置(重要:一定要配置详细,一定要!!!) const Role = app.model.define("role", { id: { type: INTEGER, primaryKey: true, autoIncrement: true, comment: "主键ID" }, rolename: { type: STRING(30), allowNull: false, defaultValue: "", unique: true, comment: "角色名称" }, memberId: { type: INTEGER, allowNull: false, defaultValue: 1, comment: "会员ID" }, desc: { type: TEXT, allowNull: false, defaultValue: "", comment: "角色描述" }, creator: { type: STRING(30), allowNull: false, defaultValue: "", comment: "创建人" }, created_time: { type: DATE, get() { const v = this.getDataValue("created_time"); return v ? moment(v).format("YYYY-MM-DD HH:mm:ss") : v; } }, updated_time: { type: DATE, get() { const v = this.getDataValue("created_time"); return v ? moment(v).format("YYYY-MM-DD HH:mm:ss") : v; } } }); return Role; }; ~~~ ### 增 > 需要把创建人名也存到数据库中 试下能不能得到当前用户名: ~~~js // controller/role.js // 添加 async add() { const creator = ctx.tokenUser.username; console.log(creator); } ~~~ ![](./static/Snipaste_2023-12-01_16-03-26.png) 封装角色service层: ~~~js // service/role.js const Service = require("egg").Service; class RoleService extends Service { // 查找角色名称 async findByRoleName(rolename) { const { app } = this; const res = await app.model.Role.findOne({ where: { rolename } }); return res; } // 添加角色 async addRole(rolename, desc = "") { const { app, ctx } = this; // 当前用户名 const creator = ctx.tokenUser.username; const res = await app.model.Role.create({ rolename, desc, creator }); return res; } } module.exports = RoleService; ~~~ 在角色controller层使用: ~~~js // controller/role.js const Controller = require("egg").Controller; class RoleController extends Controller { // 增 async add() { const { ctx, service } = this; const roleService = service.role; const { rolename, desc } = ctx.request.body; // const creator = ctx.tokenUser.username; // console.log(creator); // 查看角色是否存在 const name = await roleService.findByRoleName(rolename); if (name) { ctx.throw(409, "角色名已存在"); } const role = await roleService.addRole(rolename, desc); ctx.apiSuc("新增成功", role); } } module.exports = RoleController; ~~~ 新增成功: ![](./static/Snipaste_2023-12-01_16-40-34.png) 角色名已存在: ![](./static/Snipaste_2023-12-01_16-41-30.png) 当只添加角色名时: ![](./static/Snipaste_2023-12-01_16-44-01.png) ### 删 ~~~js // service/role.js // 删除角色 async delRoleInfo(id) { const { app, ctx } = this; const currentUser = ctx.tokenUser; ctx.validate({ id: { type: "number", required: true, desc: "角色id" } }); const role = await app.model.Role.findOne({ where: { id, creator: currentUser.username } }); if (!role) { ctx.throw(401, "角色不存在或不是当前创建用户"); } const res = await role.destroy(); return res; } ~~~ ~~~js // controller/role.js // 删 async del() { const { ctx, service } = this; const roleService = service.role; const id = +ctx.params.id; await roleService.delRoleInfo(id); ctx.apiSuc("删除成功", null); } ~~~ ![](./static/Snipaste_2023-12-01_16-55-16.png) 无权限: ![](./static/Snipaste_2023-12-01_16-57-54.png) token 已过期: ![](./static/Snipaste_2023-12-02_16-49-04.png) 用户不存在: ![](./static/Snipaste_2023-12-01_16-59-40.png) 不是当前创建用户: ![](./static/Snipaste_2023-12-01_17-05-03.png) ### 改 ~~~js // service/role.js // 编辑角色 async editRoleInfo(rolename, desc, memberId) { const { app, ctx } = this; const id = +ctx.params.id; const currentUser = ctx.tokenUser; ctx.validate({ id: { type: "number", required: true, desc: "角色id" }, ...rules }); const role = await app.model.Role.findOne({ where: { id, creator: currentUser.username } }); if (!role) { ctx.throw(401, "角色不存在或不是当前创建用户"); } const res = await role.update({ rolename, desc, memberId }); return res; } ~~~ ~~~js // controller/role.js // 改 async edit() { const { ctx, service } = this; const roleService = service.role; const { rolename, desc, memberId } = ctx.request.body; const res = await roleService.editRoleInfo(rolename, desc, memberId); ctx.apiSuc("修改成功", res); } ~~~ 角色不存在: ![](./static/Snipaste_2023-12-01_17-15-15.png) 创建几个角色: ![](./static/Snipaste_2023-12-01_17-21-52.png) 修改成功: ![](./static/Snipaste_2023-12-01_17-24-22.png) ### 查 ~~~js // service/role.js // 模糊查询列表角色 async findNameOrCreatorList(rolename, creator) { const { ctx, app } = this; const { Op } = app.Sequelize; let pageBean = ctx.query.pageBean; // 数据查询 const where = {}; if (rolename !== undefined) { where.rolename = { [Op.like]: `%${rolename}%` }; } if (creator !== undefined) { where.creator = { [Op.like]: `%${creator}%` }; } const res = await app.model.Role.findAndCountAll({ where, offset: pageBean ? (pageBean.pageIndex - 1) * pageBean.pageSize : undefined, limit: pageBean ? parseInt(pageBean.pageSize) : undefined }); return res; } ~~~ ~~~js // controller/role.js // 查 async list() { const { ctx, service } = this; const roleService = service.role; let rolename = ctx.query.rolename; let creator = ctx.query.creator; const role = await roleService.findNameOrCreatorList(rolename, creator); ctx.apiSuc("获取成功", role); } ~~~ 列出所有数据: ![](./static/Snipaste_2023-12-01_17-56-03.png) 通过角色名查询: ![](./static/Snipaste_2023-12-01_17-57-50.png) 通过创建人查询: ![](./static/Snipaste_2023-12-01_17-59-28.png) ## node脚本 ## 会员 > 与角色的逻辑一样,只是对应的字段改一下就行 ### 数据库迁移 ~~~cmd # 创建一个会员迁移文件 npx sequelize migration:generate --name=member ~~~ ~~~js /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { const { INTEGER, STRING, DATE, TEXT, FLOAT } = Sequelize; await queryInterface.createTable( "member", { id: { type: INTEGER, primaryKey: true, autoIncrement: true, comment: "主键ID" }, memname: { type: STRING(30), allowNull: false, defaultValue: "", unique: true, comment: "会员名称" }, memlevel: { type: STRING(30), allowNull: false, defaultValue: "", comment: "会员等级" }, discount: { type: FLOAT(5, 2), allowNull: false, defaultValue: 0, comment: "会员折扣" }, lowpoint: { type: INTEGER, allowNull: false, defaultValue: 200, comment: "最低积分" }, highpoint: { type: INTEGER, allowNull: false, defaultValue: 1000, comment: "最高积分" }, desc: { type: TEXT, allowNull: false, defaultValue: "", comment: "会员相关" }, creator: { type: STRING(30), allowNull: false, defaultValue: "", comment: "创建人" }, created_time: DATE, updated_time: DATE }, { engine: "InnoDB", // autoIncrement: 10, // 锁定从哪开始递增 charset: "utf8mb4", collate: "utf8mb4_general_ci" } ); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable("member"); } }; ~~~ ~~~cmd # 数据库变更 npx sequelize db:migrate # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo # 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all ~~~ 接下来的事,就跟角色差不多了。 ## 表关联 角色和用户是一对多关系,会员与用户是一对多关系。 ~~~js // model/user.js // 定义关联 User.associate = () => { app.model.User.belongsTo(app.model.Role, { foreignKey: "roleId", targetKey: "id" // as: "role" }); app.model.User.belongsTo(app.model.Member, { foreignKey: "memberId", targetKey: "id" }); }; // model/role.js // 关联 Role.associate = function () { app.model.Role.hasMany(app.model.User, { foreignKey: "roleId" }); }; // model/member.js // 关联 Member.associate = function () { app.model.Member.hasMany(app.model.User, { foreignKey: "memberId" }); }; // service/user.js const userList = await app.model.User.findAndCountAll({ // 略 include: [ { model: app.model.Role, required: true, attributes: ["rolename"] }, { model: app.model.Member } ], // 略 }); ~~~ ![](./static/Snipaste_2023-12-03_18-53-48.png) ## swagger ~~~cmd pnpm i egg-swagger-doc --save ~~~ ~~~js // config/plugin.js swaggerdoc: { enable: true, package: "egg-swagger-doc" } ~~~ ~~~js // config/config.default.ts // swagger // config.swaggerdoc = { // dirScanner: "./app/controller", // apiInfo: { // title: "egg-swagger", // description: "swagger-ui for egg", // version: "1.0.0" // }, // schemes: ["http", "https"], // consumes: ["application/json"], // produces: ["application/json"], // securityDefinitions: { // // apikey: { // // type: 'apiKey', // // name: 'clientkey', // // in: 'header', // // }, // // oauth2: { // // type: 'oauth2', // // tokenUrl: 'http://petstore.swagger.io/oauth/dialog', // // flow: 'password', // // scopes: { // // 'write:access_token': 'write access_token', // // 'read:access_token': 'read access_token', // // }, // // }, // }, // enableSecurity: false, // // enableValidate: true, // routerMap: false, // enable: true // }; ~~~ 路由重定向(非必操项): > 这一步操作的目的是为了进到我们的IP地址直接不需要输入复杂的后缀直接重定向到接口文档地址。 找到文件 app/router.ts 添加代码: ~~~js router.prefix("/api"); // 设置基础路径 router.redirect("/api", "/swagger-ui.html", 302); ~~~ 在app目录下创建contract目录: 然后在contract目录下分别创建request目录和response目录 然后你就可以看到这个样子了: ![](./static/Snipaste_2023-11-30_21-21-55.png) ## 文件上传 先把基本的东西都写好: ~~~js // app/router.js require("./router/file")(app); // router/file.js module.exports = (app) => { const { router, controller } = app; router.post("/file/upload", controller.file.upload); }; // app/controller/file.js const Controller = require("egg").Controller; const fs = require("fs"); const path = require("path"); class FileController extends Controller { async upload() { const { ctx } = this; // 需要前往 config/config.default.js 设置 config.multipart 的 mode 属性为 file const file = ctx.request.files[0]; // 声明存放资源的路径 let uploadDir = ""; try { // ctx.request.files[0] 表示获取第一个文件,若前端上传多个文件则可以遍历这个数组对象 const f = fs.readFileSync(file.filepath); const date = Date.now(); // 毫秒数 // 生成文件名 uploadDir = this.config.uploadDir + `file_${date}${path.extname(file.filename)}`; const api = "http://127.0.0.1:7001/"; const file_url = api + uploadDir; // 写入文件夹 fs.writeFileSync(uploadDir, f); // 返回上传成功的信息 ctx.apiSuc("上传成功", file_url); } catch (err) { console.log("error", err); // 清除临时文件 ctx.cleanupRequestFiles(); } } } module.exports = FileController; ~~~ ~~~cmd pnpm i egg-multipart --save ~~~ ~~~js // config/plugin.js multipart: { enable: true, package: "egg-multipart" } ~~~ 在 `config.default.js` 配置: ~~~js config.multipart = { mode: "file", fileSize: 1048576000, whitelist: [ ".txt", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".wbmp", ".tif", ".psd", ".raw", ".aac", ".mp" ] }; // 上传路径 config.uploadDir = "app/public/uploads/"; // 静态路由访问, config.static = { prefix: "/app/public", //访问前缀 dir: path.join(appInfo.baseDir, "app/public"), dynamic: true, preload: false, maxAge: 31536000, buffer: true }; ~~~ ![](./static/Snipaste_2023-12-12_15-34-24.png) ![](./static/Snipaste_2023-12-12_15-35-08.png) ## git同时使用gitee和github > 新安装 git 请跳过 若之前对 git 设置过全局的 `user.name` 和 `user.email`。 类似 (用 `git config --global --list` 进行查看你是否设置) ```verilog $ git config --global user.name "你的名字" $ git config --global user.email "你的邮箱" ``` 必须删除该设置 ```verilog $ git config --global --unset user.name "你的名字" $ git config --global --unset user.email "你的邮箱" ``` > 生成新的密钥 SSH Keys `GitHub` 的钥匙: 指定文件路径,方便后面操作:`~/.ssh/id_rsa.gitlab` ```verilog $ ssh-keygen -t rsa -f ~/.ssh/id_rsa.github -C "xx@qq.com" ``` 直接回车3下,什么也不要输入,就是默认没有密码。 注意 github 和 gitee 的文件名是不同的。 `Gitee` 的钥匙 : ```verilog $ ssh-keygen -t rsa -f ~/.ssh/id_rsa.gitee -C "lx@qq.com" ``` > 添加识别SSH keys新的私钥,并添加到ssh-agent 默认只读取 `id_rsa` ,为了让 SSH 识别新的私钥,需要将新的私钥加入到 ssh-agent 中: ```ruby $ ssh-agent bash $ ssh-add ~/.ssh/id_rsa.github $ ssh-add ~/.ssh/id_rsa.gitee ``` > 多个账号必须配置 config 文件(**重点**) 若无 `config` 文件,则需创建 config 文件(注意config文件无后缀名) > 创建config文件 在 `.ssh/` 目录下新建一个名为 config 的文件。 用文本编辑器打开,并进行编辑,或执行下面命令。 ```shell $ touch ~/.ssh/config ``` > config 里需要填的内容 可以不缩进,但是为了方便看,建议缩进。 最简配置 ```bash #github Host github.com HostName github.com IdentityFile ~/.ssh/id_rsa.github ``` 完整配置 ```sql #Default gitHub user Self Host github.com HostName github.com User your@email PreferredAuthentications publickey IdentityFile ~/.ssh/id_rsa.github AddKeysToAgent yes #Add gitee user Host gitee.com HostName gitee.com User your@email PreferredAuthentications publickey IdentityFile ~/.ssh/id_rsa.gitee AddKeysToAgent yes ``` 下面对上述配置文件中使用到的配置字段信息进行简单解释: ```bash Host 它涵盖了下面一个段的配置,我们可以通过他来替代将要连接的服务器地址。 这里可以使用任意字段或通配符。 当ssh的时候如果服务器地址能匹配上这里Host指定的值,则Host下面指定的HostName将被作为最终的服务器地址使用,并且将使用该Host字段下面配置的所有自定义配置来覆盖默认的/etc/ssh/ssh_config配置信息。 Port 自定义的端口。默认为22,可不配置 User 自定义的用户名,默认为git,设置为你的注册邮箱,也可不配置,不写邮箱,每次都报please tell who you are! HostName 真正连接的服务器地址 PreferredAuthentications 指定优先使用哪种方式验证,支持密码和秘钥验证方式 IdentityFile 指定本次连接使用的密钥文件 AddKeysToAgent yes 加载到 ssh-agent,等同于$ ssh-add ~/.ssh/id_rsa.github ``` > 在 github 和 gitee 网站添加 ssh `Github`: Github:添加SSH公钥直达地址:https://github.com/settings/keys 具体过程如下: 1. 登录 Github 2. 点击右上方的头像,点击 `settings` 3. 选择 `SSH key` 4. 点击 `Add SSH key` 在出现的界面中填写 SSH key 的名称,填一个你自己喜欢的名称(邮箱就行,好记)即可。 将上面拷贝的 `~/.ssh/id_rsa.xxx.pub` 文件内容粘帖到 key 一栏,在点击 “add key” 按钮就可以了。 添加过程 github 会提示你输入一次你的 github 密码 ,确认后即添加完毕。 `Gitee` : 码云:添加SSH公钥直达地址:https://gitee.com/profile/sshkeys 具体过程如下: 1. 登录 Gitee 2. 点击右上方的头像,点击 `设置` 3. 后续步骤如 Github 添加过程 码云 会提示你输入一次你的 Gitee 密码 ,确认后即添加完毕。 > 测试是否连接成功 由于每个托管商的仓库都有唯一的后缀,比如 Github 的是 git@github.com。 所以可以这样测试: ```css ssh -T git@github.com ``` 而 gitlab 的可以这样测试: ```css ssh -T git@gitee.com ``` 如果能看到一些 Welcome 信息,说明就是 OK 的了。 ```vhdl $ ssh -T git@github.com Warning: Permanently added the RSA host key for IP address '13.250.177.223' to the list of known hosts. Hi dragon! You've successfully authenticated, but GitHub does not provide shell access. $ ssh -T git@gitee.com The authenticity of host 'gitee.com (116.211.167.14)' can't be established. ECDSA key fingerprint is SHA256:FQGC9Kn/eye1W8icdBgrp+KkGYoFgbVr17bmjeyc. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added 'gitee.com,116.211.167.14' (ECDSA) to the list of known hosts. Hi 我是x! You've successfully authenticated, but GITEE.COM does not provide shell access. ``` 结果如果出现这个就代表成功: ```markdown * GitHub -> successfully * GitLab -> Welcome to GitLab * Gitee -> successfully ``` 测试 clone 项目 ```scss $ git clone git@gitlab.com:d-d-u/java-xxx.git Cloning into 'java-basic'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0) Receiving objects: 100% (3/3), done. ``` > 操作过程出现的问题或报错 ``` tilde\_expand\_filename: No such user ``` 检查是否成功的时候,报错: ``` tilde_expand_filename: No such user . ``` 解决方法: 此问题是因为 `写错文件路径` 或者 `大小写有误` ,删除重新配置,或者直接复制我的改好粘贴进去。 > GIT设置代理 如果使用 socks5,本地 ip 和端口是 127.0.0.1:1080 > 仅设置github代理: 这个只设置了github的git服务走代理通道,不会对国内仓库gitee使用代理。 ```verilog git config --global http.https://github.com.proxy socks5://127.0.0.1:1080 git config --global https.https://github.com.proxy socks5://127.0.0.1:1080 #取消设置的代理 git config --global --unset http.https://github.com.proxy git config --global --unset https.https://github.com.proxy ``` > 设置git全面代理: 包括github、gitee等git服务全部走代理通道。 ```verilog git config --global http.proxy socks5://127.0.0.1:1080 git config --global https.proxy socks5://127.0.0.1:1080 取消设置的代理 git config --global --unset http.proxy git config --global --unset https.proxy ``` > git其他参数配置 git 的配置文件在 `~/.gitconfig` ,仓库的配置文件是仓库内的 `.git/config` 。 可运行 `git help git config` 和 `man git` 查看更多帮助信息。 官方文档 `git-config Manual Pagee` 部分设置命令: 加上 `--global` 参数,则设置内容对当前用户生效,不加 `--global` 则对当前仓库生效。 检查配置情况:`git config --list` 设置默认编辑器,如 nano: `git config --global core.editor nano` 设置默认对比工具,如 meld:`git config --global merge.tool meld` 彩色输出:`git config --global color.ui true` 中文文件名显示:`git config --global core.quotepath false`(避免中文显示成数字 ) 显示历史记录时每个提交的信息显示一行: `git --global config format.pretty oneline` > 设置全局用户名和电子邮箱(不建议) ```verilog git config --global user.name "your name" git config --global user.email "email@example.com ``` > 协议更换 如 https 替代 git 协议 ```php git config --global url."https://".insteadof "git://" git config --global url."https://github.com/".insteadof "git@github.com:" ``` 4)设置命令别名: ```verilog git config --global alias. status ``` ## 好