eggjs学习之路,基于egg+sequelize+mysql打造nodeJs后台服务(二)

俺滴创作格言: 俺写的不是文章,是心情,你看的不是文章,是文学,啥文学,废话文学,要记住,嗯!

说接上回

基于eggjs+sequelize+mysql搭建nodejs后台服务 - 掘金 (juejin.cn)

如果感兴趣的话可以进去瞅瞅。

废话文学: 可能会有人问,为啥不用nestjs写。啊,这这这,因为nestjs是外国人写的,文档没有中文的,有也是翻译过来的,有点不利于国人理解,而eggjs是阿里淘宝前端团队开发的,有官方中文文档,俺认为已经很详细了,生态也非常不错,更便于国人去理解。要不然就是刻在中国人骨子里的那份倔强吧。

话不多说,开干,我玩的就是真实(来自李炮儿的口头禅)😎

承上启下

停,别别别着急,俺先看看上篇俺写到哪了。好,俺又回来了。上篇俺把那个啥数据库迁移给讲完了,那接下来就继续跟进呗。

csrf校验

俺们在开发接口时,egg会默认提供csrf保护,限制服务无法访问自定义的接口,可以不需要egg提供的csrf保护了,需要关闭它。

js 复制代码
// config/config.default.js
// 关闭csrf
  config.security = {
    csrf: {
      enable: false
    },
    // 跨域白名单:网页端基地址
    domainWhiteList: []
  }

开启cors跨域

你可以通过在前端项目开启跨域,也可以在后端服务开启,没啥子要求的(俺想咋来就咋来)😎

废话文学: 至于为啥必须要开吗,因为同源策略,啥又是同源策略,要是会百度的话,你可以问问。

安装依赖

cmd 复制代码
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'
  }

路由分组

废话文学: 为啥路由分组,你想想,一个项目下来起码也有一两百个路由吧,要是都放在一个文件下,相似的结构,那要是有个路由写的不对或写的不满意,回来找的话,那眼睛不得瞎呀,晓得为啥子了吗

js 复制代码
// app/router.js
/**
 * @param {Egg.Application} app - egg application
 */
module.exports = (app) => {
  const { router, controller } = app
  // router.prefix('/fc/api') // 设置基础路径
  router.get('/', controller.home.index)
  require('./router/user')(app)
}
​
// app/router/user.js
module.exports = (app) => {
  app.router.post('/reg', app.controller.user.reg)
}

统一封装数据返回格式

创建context.js封装数据返回格式

js 复制代码
// app/extend/context.js
module.exports = {
  // 成功提示
  APISuccess(msg = 'success', data = '', code = 200) {
    this.body = {
      msg,
      data
    }
    this.status = code
  },
  // 失败提示
  APIError(msg = 'error', data = '', code = 400) {
    this.body = {
      msg,
      data
    }
    this.status = code
  }
}
​

来对比一下

  1. 这是不统一封装的响应数据格式
js 复制代码
// 5.发送响应数据
    ctx.body = {
      code: 200,
      message: '登录成功',
      result: {
        user_code: user.user_code,
        user_name: user.user_name,
        user_phone: user.user_phone,
        user_password: user.user_password,
        user_gender: user.user_gender,
        user_age: user.user_age,
        user_avatar: user.user_avatar,
        department_id: user.department_id,
        role_id: user.role_id,
        user_status: user.user_status,
        token
      }
  1. 这是封装之后的响应数据格式
js 复制代码
ctx.apiSuccess('登录成功', user)

废话文学: 这么一对比,就能看出来那个好了吧,要用第一个的话,那得写多少重复代码呀,项目体积不就大了吗。

全局抛出异常处理

app/middleware 目录下新建一个 error_handler.js 的文件来新建一个 middleware

js 复制代码
module.exports = (option, app) => {
  return async function errorHandler(ctx, next) {
    try {
      await next()
      // 404 处理
      if (ctx.status === 404 && !ctx.body) {
        ctx.body = {
          msg: 'fail',
          data: '404 错误'
        }
      }
    } catch (err) {
      // 记录一条错误日志
      ctx.app.emit('error', err, ctx)
      const status = err.status || 500
      // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
      const error =
        status === 500 && app.config.env === 'prod' ? 'Internal Server Error' : err.message
​
      // 从 error 对象上读出各个属性,设置到响应中
      ctx.body = {
        msg: 'fail',
        data: error
      }
      // 参数验证异常
      if (status === 422 && err.message === 'Validation Failed') {
        if (err.errors && Array.isArray(err.errors)) {
          error = err.errors[0].err[0] ? err.errors[0].err[0] : err.errors[0].err[1]
        }
        ctx.body = {
          msg: 'fail',
          data: error
        }
      }
      ctx.status = status
    }
  }
}
​
js 复制代码
// config.default.js
config.middleware = ['errorHandler']

参数验证

相关依赖

cmd 复制代码
pnpm i egg-valparams --save

相关配置

js 复制代码
// config/plugin.js
exports.valparams = {
  enable: true,
  package: 'egg-valparams'
}
    
// config/config.default.js
config.valparams = {
    locale: 'zh-cn',
    throwError: true
  }

当然你还可以用egg-validate,配置跟上面的类似,但它好像没有将错误转中文的配置。

用户接口相关

大致思路:

  1. 先创建有关用户的表迁移文件
  2. 再创建相关的模型表
  3. 更新数据库
  4. 用户注册
  5. 用户登录
  6. 用户数据更新
  7. 删除用户

创建数据库迁移文件

cmd 复制代码
# 用户相关迁移文件
npx sequelize migration:generate --name=department # 科室相关
npx sequelize migration:generate --name=role # 用户角色相关
npx sequelize migration:generate --name=user # 用户信息相关

表迁移文件设置

创表必须先是被关联的表先,有关联的表后

js 复制代码
// 科室迁移
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    const { INTEGER, STRING, DATE, TEXT } = Sequelize
    // 科室
    await queryInterface.createTable(
      'department',
      {
        id: {
          type: INTEGER(20),
          primaryKey: true,
          autoIncrement: true,
          comment: '科室ID'
        },
        dep_code: {
          type: STRING(40),
          allowNull: false,
          defaultValue: '',
          comment: '科室编号'
        },
        dep_name: {
          type: STRING(30),
          allowNull: false,
          defaultValue: '',
          comment: '科室名'
        },
        dep_doctor: {
          type: STRING(30),
          allowNull: false,
          defaultValue: '',
          comment: '科室医生'
        },
        dep_avatar: {
          type: STRING(100),
          allowNull: false,
          defaultValue: '',
          comment: '医生头像'
        },
        dep_desc: {
          type: TEXT,
          allowNull: false,
          defaultValue: '',
          comment: '科室描述'
        },
        dep_status: {
          type: INTEGER,
          allowNull: false,
          defaultValue: 1,
          comment: '状态'
        },
        created_time: DATE,
        updated_time: DATE
      },
      {
        engine: 'InnoDB',
        autoIncrement: 10,
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci'
      }
    )
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('department')
  }
}
js 复制代码
// 角色迁移
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    const { INTEGER, STRING, DATE, TEXT } = Sequelize
    // 角色
    await queryInterface.createTable(
      'role',
      {
        id: {
          type: INTEGER(20),
          primaryKey: true,
          autoIncrement: true,
          comment: '角色ID'
        },
        rol_code: {
          type: STRING(40),
          allowNull: false,
          defaultValue: '',
          comment: '角色编号'
        },
        rol_name: {
          type: STRING(30),
          allowNull: false,
          defaultValue: '',
          comment: '角色名'
        },
        rol_desc: {
          type: TEXT,
          allowNull: false,
          defaultValue: '',
          comment: '角色描述'
        },
        rol_status: {
          type: INTEGER,
          allowNull: false,
          defaultValue: 1,
          comment: '状态'
        },
        created_time: DATE,
        updated_time: DATE
      },
      {
        engine: 'InnoDB',
        autoIncrement: 6,
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci'
      }
    )
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('role')
  }
}
js 复制代码
// 用户迁移
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    const { INTEGER, STRING, DATE, ENUM } = Sequelize
    // 用户
    await queryInterface.createTable(
      'user',
      {
        id: {
          type: INTEGER(20),
          primaryKey: true,
          autoIncrement: true,
          comment: '用户ID'
        },
        code: {
          type: STRING(40),
          allowNull: false,
          defaultValue: '',
          comment: '用户编号'
        },
        nickname: {
          type: STRING(30),
          allowNull: false,
          defaultValue: '',
          comment: '用户昵称',
          unique: true
        },
        username: {
          type: STRING(30),
          allowNull: false,
          defaultValue: '',
          comment: '用户名',
          unique: true
        },
        password: {
          type: STRING,
          allowNull: false,
          defaultValue: '',
          comment: '密码'
        },
        avatar: {
          type: STRING,
          allowNull: true,
          defaultValue: '',
          comment: '头像'
        },
        phone: {
          type: STRING(20),
          allowNull: false,
          defaultValue: '',
          comment: '手机'
        },
        sex: {
          type: ENUM,
          values: ['男', '女', '保密'],
          allowNull: false,
          defaultValue: '保密',
          comment: '性别'
        },
        age: {
          type: INTEGER(20),
          allowNull: true,
          comment: '年龄'
        },
        status: {
          type: INTEGER,
          allowNull: false,
          defaultValue: 1,
          comment: '状态'
        },
        departmentId: {
          type: INTEGER,
          allowNull: true,
          //  定义外键(重要)
          // references: {
          //   model: 'department', // 对应表名称(数据表名称)
          //   key: 'id' // 对应表的主键
          // },
          // onUpdate: 'restrict', // 更新时操作
          // onDelete: 'cascade',  // 删除时操作
          comment: '所属科室ID'
        },
        roleId: {
          type: INTEGER,
          allowNull: false,
          defaultValue: 1,
          //  定义外键(重要)
          // references: {
          //   model: 'role', // 对应表名称(数据表名称)
          //   key: 'id' // 对应表的主键
          // },
          // onUpdate: 'restrict', // 更新时操作
          // onDelete: 'cascade',  // 删除时操作
          comment: '角色ID'
        },
        created_time: DATE,
        updated_time: DATE
      },
      {
        engine: 'InnoDB',
        autoIncrement: 2,
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci'
      }
    )
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('user')
  }
}

执行 migrate 进行数据库变更

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/department.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, TEXT } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const Department = app.model.define('department', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true,
      comment: '科室ID'
    },
    dep_code: {
      type: STRING(40),
      allowNull: false,
      defaultValue: '',
      comment: '科室编号'
    },
    dep_name: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '科室名'
    },
    dep_doctor: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '科室医生'
    },
    dep_avatar: {
      type: STRING(100),
      allowNull: false,
      defaultValue: '',
      comment: '医生头像'
    },
    dep_desc: {
      type: TEXT,
      allowNull: false,
      defaultValue: '',
      comment: '科室描述'
    },
    dep_status: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '状态'
    },
    created_time: DATE,
    updated_time: DATE
  })

  return Department
}


// model/role.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, TEXT } = app.Sequelize
  const moment = require('moment')
  // 配置(重要:一定要配置详细,一定要!!!)
  const Role = app.model.define('role', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true,
      comment: '角色ID'
    },
    rol_code: {
      type: STRING(40),
      allowNull: false,
      defaultValue: '',
      comment: '角色编号'
    },
    rol_name: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '角色名'
    },
    rol_desc: {
      type: TEXT,
      allowNull: false,
      defaultValue: '',
      comment: '角色描述'
    },
    rol_status: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '状态'
    },
    created_time: DATE,
    updated_time: DATE
  })

  return Role
}


// model/user.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, ENUM } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const User = app.model.define('user', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true,
      comment: '用户ID'
    },
    code: {
      type: STRING(40),
      allowNull: false,
      defaultValue: '',
      comment: '用户编号'
    },
    nickname: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '用户昵称',
      unique: true
    },
    username: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '用户名',
      unique: true
    },
    password: {
      type: STRING,
      allowNull: false,
      defaultValue: '',
      comment: '密码'
    },
    avatar: {
      type: STRING,
      allowNull: true,
      defaultValue: '',
      comment: '头像'
    },
    phone: {
      type: STRING(20),
      allowNull: false,
      defaultValue: '',
      comment: '手机'
    },
    sex: {
      type: ENUM,
      values: ['男', '女', '保密'],
      allowNull: false,
      defaultValue: '保密',
      comment: '性别'
    },
    age: {
      type: INTEGER(20),
      allowNull: true,
      comment: '年龄'
    },
    status: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 1,
      comment: '状态'
    },
    departmentId: {
      type: INTEGER,
      allowNull: true,
      //  定义外键(重要)
      // references: {
      //   model: 'department', // 对应表名称(数据表名称)
      //   key: 'id' // 对应表的主键
      // },
      // onUpdate: 'restrict', // 更新时操作
      // onDelete: 'cascade',  // 删除时操作
      comment: '所属科室ID'
    },
    roleId: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 1,
      comment: '角色ID'
    },
    created_time: DATE,
    updated_time: DATE
  })

  return User
}

数据表模型关联

通过关联查询,可以查到用户对应的科室和角色

js 复制代码
// model/department.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, TEXT } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const Department = app.model.define('department', {
    略
  })

  Department.associate = () => {
    app.model.Department.hasOne(app.model.User, { foreignKey: 'departmentId' })
  }

  return Department
}

// model/role.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, TEXT } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const Role = app.model.define('role', {
    略
  })

  Role.associate = () => {
    app.model.Role.hasOne(app.model.User, { foreignKey: 'roleId' })
  }

  return Role
}

// model/user.js
module.exports = (app) => {
  const { STRING, INTEGER, DATE, ENUM } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const User = app.model.define('user', {
    略
  })

  // 关联关系
  User.associate = () => {
    // 关联角色
    app.model.User.belongsTo(app.model.Role, {
      foreignKey: 'roleId', // 关联外键
      targetKey: 'id', // 关联的目标键
      // constraints: false // 关闭外键约束
    })
    app.model.User.belongsTo(app.model.Department, {
      foreignKey: 'departmentId',
      targetKey: 'id'
    })
    app.model.User.hasOne(app.model.Examine, {
      foreignKey: 'userId'
    })
  }

  return User
}

获取器get()格式化时间

查询的时候都会调取获取器,如将时间返回为时间搓的形式

  1. 相关依赖
cmd 复制代码
pnpm i moment
  1. 每个表模型文件中格式化时间
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
      }
    },
    updated_time: {
      type: DATE,
      get() {
        const v = this.getDataValue('created_time')
        return v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : v
      }
    }

注册用户相关

  1. 设置注册路由

路由分组后,在router/user.js写入注册路由,同时新建一个user.js的controller用来控制操作用户数据

js 复制代码
// router/user.js
module.exports = (app) => {
  const { router, controller } = app
  // 注册
  router.post('/user/reg', controller.user.reg)
}
​
js 复制代码
// controller/user.js
'use strict'
​
const Controller = require('egg').Controller
​
class UserController extends Controller {
  // 注册用户
  async reg() {
   
  }
}
​
module.exports = UserController
​
  1. 参数校验

可以将需要校验的参数统一封装,添加一个确认密码参数,判断密码和确认密码是否一致,俺这就直接写了

js 复制代码
// controller/user.js
const { ctx, app } = this
    // 接收页面返回过来的参数
    const { nickname, password } = ctx.request.body
    // 参数验证
    ctx.validate(
      {
        nickname: {
          type: 'string',
          required: true,
          range: {
            min: 6,
            max: 20
          },
          desc: '用户昵称'
        },
        password: {
          type: 'string',
          required: true,
          desc: '密码'
        },
        repassword: {
          type: 'string',
          required: true,
          desc: '确认密码'
        }
      },
      {
        equals: [['password', 'repassword']]
      }
    )
  1. 验证用户是否存在
csharp 复制代码
// controller/user.js
    // 验证用户是否存在
    if (await app.model.User.findOne({ where: { nickname } })) ctx.throw(409, '用户已存在')
​
  1. 创建用户并响应数据
csharp 复制代码
  // controller/user.js
// 创建用户
    const user = await app.model.User.create({ nickname, password })
    if (!user) ctx.throw(409, '注册失败')
    ctx.apiSuccess('注册成功', user)

数据加密

通过使用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')
module.exports = (app) => {
  const { STRING, INTEGER, DATE, ENUM } = app.Sequelize
  // 配置(重要:一定要配置详细,一定要!!!)
  const User = app.model.define('user', {
    ...略,
    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'))
      }
    },
...略
  })
  return User
}
​

登录用户相关

与注册用户的步骤大差不差,只是多了一个添加token和密码验证

js 复制代码
// router/user.js
// 登录用户
  router.post('/user/login', controller.user.login)
​
// controller/user.js
// 登录用户
  async login() {
    const { ctx, app } = this
    // 参数验证
    ctx.validate({
      nickname: {
        type: 'string',
        required: true,
        desc: '用户名'
      },
      password: {
        type: 'string',
        required: true,
        desc: '密码'
      }
    })
    let { nickname, password } = ctx.request.body
    // 验证该用户是否存在|验证该用户状态是否启用
    let user = await app.model.User.findOne({
      where: {
        nickname
      }
    })
    if (!user) {
      ctx.throw(409, '用户不存在或已被禁用')
    }
    // 验证密码
    await this.checkPassword(password, user.password)
    // 转json
    user = JSON.parse(JSON.stringify(user))
    // 生成token
    let token = ctx.getToken({userId: user.id})
    // 赋值给user对象
    user.token = token
    // 取消密码展示
    delete user.password
    ctx.apiSuccess('登录成功', user)
  }
  1. 验证密码函数

将页面返回的密码转换称加密形式与数据库的密码对比

js 复制代码
// controller/user.js
const crypto = require('crypto')
​
// 验证密码
  async checkPassword(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
  }
  1. 生成token
  • 安装依赖
css 复制代码
pnpm i egg-jwt --save
  • 配置相关
java 复制代码
// config/plugin.js
exports.jwt = {
  enable: true,
  package: 'egg-jwt'
}
​
// config/config.default.js
  // jwt加密鉴权
  exports.jwt = {
    secret: 'b3b6dc8a-26ed-4cb1-bd73-854378ae1f4e',
    expiresIn: '1d' // 有效(过期)时间
  }
  • 在context.js中生成token函数
kotlin 复制代码
// 生成token
  getToken(value) {
    return this.app.jwt.sign(value, this.app.config.jwt.secret)
  }
  • 转json字符串并赋值新对象
ini 复制代码
user = JSON.parse(JSON.stringify(user))
  • 生成token并赋值给user对象
ini 复制代码
let token = ctx.getToken(user)
user.token = token
  • 取消密码展示
arduino 复制代码
// 取消密码展示
    delete user.password
kotlin 复制代码
// extend/context.js
// 生成token
  getToken(value) {
    return this.app.jwt.sign(value, this.app.config.jwt.secret, {
      expiresIn: this.app.config.jwt.expiresIn
    })
  },
      
// controller/user.js
      // 生成token
    let token = ctx.getToken({userId: user.id})
    // 赋值给user对象
    user.token = token

好了好了,俺有点小疲倦了,下回继续说。

相关推荐
y先森32 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy32 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891135 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐4 小时前
前端图像处理(一)
前端