俺滴创作格言: 俺写的不是文章,是心情,你看的不是文章,是文学,啥文学,废话文学,要记住,嗯!
说接上回
基于eggjs+sequelize+mysql搭建nodejs后台服务 - 掘金 (juejin.cn)
如果感兴趣的话可以进去瞅瞅。
废话文学: 可能会有人问,为啥不用nestjs写。啊,这这这,因为nestjs是外国人写的,文档没有中文的,有也是翻译过来的,有点不利于国人理解,而eggjs是阿里淘宝前端团队开发的,有官方中文文档,俺认为已经很详细了,生态也非常不错,更便于国人去理解。要不然就是刻在中国人骨子里的那份倔强吧。
话不多说,开干,我玩的就是真实(来自李炮儿的口头禅)😎
承上启下
停,别别别着急,俺先看看上篇俺写到哪了。好,俺又回来了。上篇俺把那个啥数据库迁移给讲完了,那接下来就继续跟进呗。
csrf校验
俺们在开发接口时,egg会默认提供csrf保护,限制服务无法访问自定义的接口,可以不需要egg提供的csrf保护了,需要关闭它。
- 相关文档:安全 - Egg (eggjs.org)
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)
}
统一封装数据返回格式
-
新建
app/extend
目录
创建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
}
}
来对比一下
- 这是不统一封装的响应数据格式
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
}
- 这是封装之后的响应数据格式
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
,配置跟上面的类似,但它好像没有将错误转中文的配置。
用户接口相关
大致思路:
- 先创建有关用户的表迁移文件
- 再创建相关的模型表
- 更新数据库
- 用户注册
- 用户登录
- 用户数据更新
- 删除用户
创建数据库迁移文件
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()格式化时间
查询的时候都会调取获取器,如将时间返回为时间搓的形式
- 相关依赖
cmd
pnpm i moment
- 每个表模型文件中格式化时间
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
}
}
注册用户相关
- 设置注册路由
路由分组后,在
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
- 参数校验
可以将需要校验的参数统一封装,添加一个确认密码参数,判断密码和确认密码是否一致,俺这就直接写了
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']]
}
)
- 验证用户是否存在
csharp
// controller/user.js
// 验证用户是否存在
if (await app.model.User.findOne({ where: { nickname } })) ctx.throw(409, '用户已存在')
- 创建用户并响应数据
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)
}
- 验证密码函数
将页面返回的密码转换称加密形式与数据库的密码对比
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
}
- 生成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
好了好了,俺有点小疲倦了,下回继续说。