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

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

相关推荐
LUwantAC7 分钟前
CSS(四)display和float
前端·css
cwtlw11 分钟前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥16 分钟前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
米奇妙妙wuu30 分钟前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖35 分钟前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
m0_548514772 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect2 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5502 小时前
Vue3:uv-upload图片上传
前端·vue.js