一、数据库设计
可视化数据库设计MySQL Workbench

二、建立所有的表
1. 回滚迁移 sequelize db:migrate:undo
运行命令后,会回滚上一次运行的迁移
2. 创建文章表模型文件 sequelize model:generate --name Article --attributes title:string,content:text
js
// 文章迁移文件 migrations/***-create-article.js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Articles', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED // 无符号的,非负数
},
title: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.TEXT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
3. 创建分类表模型文件 sequelize model:generate --name Category --attributes name:string,rank:integer
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Categories', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
name: {
type: Sequelize.STRING,
allowNull: false
},
rank: {
type: Sequelize.INTEGER.UNSIGNED,
defaultValue: 1, // 默认值为1
allowNull: false // 不能为空
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
4. 创建用户表模型文件 sequelize model:generate --name User --attributes email:string,username:string,password:string,nickname:string,sex:tinyint,company:string,introduce:text,role:tinyint
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
email: {
type: Sequelize.STRING,
allowNull: false
},
username: {
type: Sequelize.STRING,
allowNull: false
},
password: {
type: Sequelize.STRING,
allowNull: false
},
nickname: {
type: Sequelize.STRING,
allowNull: false
},
sex: {
type: Sequelize.TINYINT.UNSIGNED,
defaultValue: 0,
allowNull: false
},
company: {
type: Sequelize.STRING,
},
introduce: {
type: Sequelize.TEXT
},
role: {
type: Sequelize.TINYINT.UNSIGNED,
defaultValue: 0,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('Users', ['email'], {
unique: true
});
await queryInterface.addIndex('Users', ['username'], {
unique: true
});
await queryInterface.addIndex('Users', ['role']);
},
5. 创建课程表模型文件 sequelize model:generate --name Course --attributes categoryId:integer,userId:integer,name:string,image:string,recommended:boolean,introductory:boolean,content:text,likesCount:integer,chaptersCount:integer
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Courses', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
categoryId: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false
},
userId: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
},
image: {
type: Sequelize.STRING
},
recommended: {
type: Sequelize.BOOLEAN
},
introductory: {
type: Sequelize.BOOLEAN
},
content: {
type: Sequelize.TEXT
},
likesCount: {
type: Sequelize.INTEGER.UNSIGNED,
defaultValue: 0,
allowNull: false
},
chaptersCount: {
type: Sequelize.INTEGER.UNSIGNED,
defaultValue: 0,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('Courses', ['categoryId']); // 建立普通索引
await queryInterface.addIndex('Courses', ['userId']);
},
6. 创建章节表模型文件 sequelize model:generate --name Chapter --attributes courseId:integer,title:string,content:text,video:string,rank:integer
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Chapters', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
courseId: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false
},
title: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.TEXT
},
video: {
type: Sequelize.STRING
},
rank: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('Chapters', ['courseId']);
},
7. 创建点赞表模型文件 sequelize model:generate --name Like --attributes courseId:integer,userId:integer
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Likes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
courseId: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false
},
userId: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('Likes', ['courseId']);
await queryInterface.addIndex('Likes', ['userId']);
},
7. 创建系统设置表 sequelize model:generate --name Setting --attributes name:string,icp:string,copyright:string
js
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Settings', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
name: {
type: Sequelize.STRING
},
icp: {
type: Sequelize.STRING
},
copyright: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
8. 建立所有的表 sequelize db:migrate
三、分类接口
1. 创建分类的种子文件 sequelize seed:generate --name category
js
// seeders/20250829083444-category.js
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Categories', [{
name: '前端开发',
rank: 1,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: '后端开发',
rank: 2,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: '移动端开发',
rank: 3,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: '数据库',
rank: 4,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: '服务器运维',
rank: 5,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: '公共',
rank: 6,
createdAt: new Date(),
updatedAt: new Date()
},
], {});
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Categories', null, {});
}
2. 执行种子文件命令 sequelize db:seed --seed 20250829083444-category

3. 修改分类的模型文件,添加验证
js
// models/category.js
Category.init({
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '名称必须存在'
},
notEmpty: {
msg: '名称不能为空'
},
len: {
args:[2,45],
msg: '名称长度需要在2~45个字符之间'
},
async isUnique(value) {
const category = await Category.findOne({
where: {
name: value
}
})
if (category) {
throw new Error('名称已存在,请重新输入')
}
}
}
},
rank: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: {
msg: '排序必须存在'
},
notEmpty: {
msg: '排序不能为空'
},
isInt: {
msg: '排序必须是整数'
},
isPositive(value) {
if (value <= 0) {
throw new Error('排序必须是正数');
}
}
}
}
}, {
sequelize,
modelName: 'Category',
});
4. 添加分类的路由文件
js
// routes/admin/categories.js
const express = require('express');
const router = express.Router();
const {Category} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError, success, failure} = require('../../utils/response')
router.get('/', async function(req, res, next) {
try {
const query = req.query
const currentPage = Math.abs(Number(query.currentPage)) || 1
const pageSize = Math.abs(Number(query.pageSize)) || 10
const offset = (currentPage - 1) * pageSize
const condition = {
order: [['rank', 'ASC'],['id', 'ASC']],
limit: pageSize,
offset
}
if(query.name) {
condition.where = {
name: {
[Op.like]: `%${query.name}%`
}
}
}
const {count, rows} = await Category.findAndCountAll(condition)
success(res, '查询分类列表成功', {
categories: rows,
pagination: {
total: count,
currentPage,
pageSize
}
})
}catch(error) {
failure(res, error)
}
});
router.get('/:id', async function(req, res, next) {
try {
const category = await getCategory(req)
success(res, '查询分类成功', {category})
} catch(error) {
failure(res, error)
}
})
router.post('/', async function(req, res, next) {
try {
const body = filterBody(req)
const category = await Category.create(body)
success(res, '创建分类成功', {category}, 201)
}catch(error) {
failure(res, error)
}
});
router.delete('/:id', async function(req, res, next) {
try {
const category = await getCategory(req)
await category.destroy()
success(res, '删除分类成功')
} catch(error) {
failure(res, error)
}
})
router.put('/:id', async function(req, res, next) {
try {
const body = filterBody(req)
const category = await getCategory(req)
await category.update(body)
success(res, '更新分类成功')
} catch(error) {
failure(res, error)
}
})
async function getCategory(req) {
const {id} = req.params
const category = await Category.findByPk(id)
if(!category) {
throw new NotFoundError(`ID: ${id}的分类未找到。`)
}
return category
}
function filterBody(req) {
return {
name: req.body.name,
rank: req.body.rank
}
}
module.exports = router;
5. 引入分类的路由文件
js
// app.js
const adminCategoriesRouter = require('./routes/admin/categories');
app.use('/admin/categories', adminCategoriesRouter);
四、系统设置接口
1. 创建系统设置种子文件 sequelize seed:generate --name setting
js
// seeders/20250829101031-setting.js
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Settings', [{
name: '长乐未央',
icp: 'ICP备123456789号',
copyright: 'Copyright © 2025 长乐未央',
createdAt: new Date(),
updatedAt: new Date()
}], {});
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Settings', null, {});
}
2. 执行种子命令 sequelize db:seed --seed 20250829101031-setting
3. 添加系统设置的路由文件
js
// routes/admin/settings.js
const express = require('express');
const router = express.Router();
const {Setting} = require('../../models')
const {NotFoundError, success, failure} = require('../../utils/response')
router.get('/', async function(req, res, next) {
try {
const setting = await getSetting()
success(res, '查询系统设置成功', {setting})
} catch(error) {
failure(res, error)
}
})
router.put('/', async function(req, res, next) {
try {
const body = filterBody(req)
const setting = await getSetting()
await setting.update(body)
success(res, '更新系统设置成功')
} catch(error) {
failure(res, error)
}
})
async function getSetting() {
const setting = await Setting.findOne()
if(!setting) {
throw new NotFoundError(`ID: ${id}的系统设置未找到。`)
}
return setting
}
function filterBody(req) {
return {
name: req.body.name,
icp: req.body.icp,
copyright: req.body.copyright
}
}
module.exports = router;
4. 引入系统设置的路由文件
js
// app.js
const adminSettingsRouter = require('./routes/admin/settings');
app.use('/admin/settings', adminSettingsRouter);
五、用户管理接口
1. 发现用户头像字段没建,新增另一个迁移,给用户表增加头像字段
sequelize migration:create --name add-avatar-to-user
2. 修改新增迁移文件
js
// 用户表的另一个迁移文件 migrations/**-add-avatar-to-user.js
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.addColumn('Users', 'avatar', {
type: Sequelize.STRING
})
},
async down (queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'avatar')
}
};
3. 执行迁移命令 sequelize db:migrate
4. 在 user.js
手动增加 avatar
字段
js
// models/user.js
User.init({
avatar: DataTypes.STRING
})
5. 创建用户的种子文件 sequelize seed:generate --name user
js
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Users', [
{
email: 'admin@example.com',
username: 'admin',
password: '123456',
nickname: '管理员',
sex: 2,
role: 100,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user@example.com',
username: 'user',
password: '123456',
nickname: '用户',
sex: 0,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user2@example.com',
username: 'user2',
password: '123456',
nickname: '用户2',
sex: 0,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user3@example.com',
username: 'user3',
password: '123456',
nickname: '用户3',
sex: 1,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
])
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Users', null, {});
}
6. 执行种子命令 sequelize db:seed --seed 20250901025916-user
7. 修改用户的模型文件,添加验证
js
User.init({
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '邮箱必须存在'
},
notEmpty: {
msg: '邮箱不能为空'
},
isEmail: {
msg: '邮箱格式错误'
},
async isUnique(value) {
const user = await User.findOne({
where: {
email: value
}
})
if (user) {
throw new Error('邮箱已存在')
}
}
}
},
username: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '用户名必须存在'
},
notEmpty: {
msg: '用户名不能为空'
},
len: {
args: [2,45],
msg: '用户名长度需要在2~45个字符之间'
},
async isUnique(value) {
const user = await User.findOne({
where: {
username: value
}
})
if (user) {
throw new Error('用户名已存在')
}
}
}
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '密码必须存在'
},
notEmpty: {
msg: '密码不能为空'
},
len: {
args: [6,16],
msg: '密码长度需要在6~16个字符之间'
}
}
},
nickname: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '昵称必须存在'
},
notEmpty: {
msg: '昵称不能为空'
},
len: {
args: [2,45],
msg: '昵称长度需要在2~45个字符之间'
}
}
},
sex: {
type: DataTypes.TINYINT.UNSIGNED,
allowNull: false,
validate: {
notNull: {
msg: '性别必须存在'
},
notEmpty: {
msg: '性别不能为空'
},
isIn: {
args: [[0,1,2]],
msg: '性别必须是男性:0、女性:1、未选择:2'
}
}
},
company: DataTypes.STRING,
introduce: DataTypes.TEXT,
role: {
type: DataTypes.TINYINT,
allowNull: false,
validate: {
notNull: {
msg: '角色必须存在'
},
notEmpty: {
msg: '角色不能为空'
},
isIn: {
args: [[0,100]],
msg: '角色必须是普通用户:0、管理员:100'
}
}
},
avatar: {
type: DataTypes.STRING,
validate: {
isUrlOrEmpty(value) {
if (value && typeof value === 'string' && value.trim() !== '') {
// 使用正则表达式验证URL格式
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
if (!urlRegex.test(value)) {
throw new Error('头像必须是URL');
}
}
}
}
}
}, {
sequelize,
modelName: 'User',
});
8. 添加用户的路由文件
js
// routes/admin/users.js
const express = require('express');
const router = express.Router();
const {User} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError, success, failure} = require('../../utils/response')
router.get('/', async function(req, res, next) {
try {
const query = req.query
const currentPage = Math.abs(Number(query.currentPage)) || 1
const pageSize = Math.abs(Number(query.pageSize)) || 10
const offset = (currentPage - 1) * pageSize
const condition = {
order: [['id', 'DESC']],
limit: pageSize,
offset
}
if(query.email) {
condition.where = {
email: {
[Op.eq]: query.email
}
}
}
if(query.username) {
condition.where = {
username: {
[Op.eq]: query.username
}
}
}
if(query.nickname) {
condition.where = {
nickname: {
[Op.like]: `%${query.nickname}%`
}
}
}
if(query.role) {
condition.where = {
role: {
[Op.eq]: query.role
}
}
}
const {count, rows} = await User.findAndCountAll(condition)
success(res, '查询用户列表成功', {
users: rows,
pagination: {
total: count,
currentPage,
pageSize
}
})
}catch(error) {
failure(res, error)
}
});
router.get('/:id', async function(req, res, next) {
try {
const user = await getUser(req)
success(res, '查询用户成功', {user})
} catch(error) {
failure(res, error)
}
})
router.post('/', async function(req, res, next) {
try {
const body = filterBody(req)
const user = await User.create(body)
success(res, '创建用户成功', {user}, 201)
}catch(error) {
failure(res, error)
}
});
router.put('/:id', async function(req, res, next) {
try {
const body = filterBody(req)
const user = await getUser(req)
await user.update(body)
success(res, '更新用户成功')
} catch(error) {
failure(res, error)
}
})
async function getUser(req) {
const {id} = req.params
const user = await User.findByPk(id)
if(!user) {
throw new NotFoundError(`ID: ${id}的用户未找到。`)
}
return user
}
function filterBody(req) {
return {
email: req.body.email,
username: req.body.username,
password: req.body.password,
nickname: req.body.nickname,
role: req.body.role,
avatar: req.body.avatar,
sex: req.body.sex,
company: req.body.company,
introduce: req.body.introduce,
}
}
module.exports = router;
9. 引入用户的路由文件
js
// app.js
const adminUsersRouter = require('./routes/admin/users');
app.use('/admin/users', adminUsersRouter);
六、使用 bcryptjs
加密数据
1. 安装 npm i bcryptjs
2. 在用户的模型文件中对用户密码的加密
js
// models/user.js
const bcrypt = require('bcryptjs')
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '密码必须存在'
},
notEmpty: {
msg: '密码不能为空'
},
},
set(value) {
if(value.length >= 6 && value.length <= 45) {
const hash = bcrypt.hashSync(value, 10)
this.setDataValue('password', hash)
} else {
throw new Error('密码长度需要在6个字符以上45个字符以下')
}
}
},
3. 在用户的种子文件中对用户密码进行加密
js
const bcrypt = require('bcryptjs')
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Users', [
{
email: 'admin@example.com',
username: 'admin',
password: bcrypt.hashSync('123456', 10),
nickname: '管理员',
sex: 2,
role: 100,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user@example.com',
username: 'user',
password: bcrypt.hashSync('123456', 10),
nickname: '用户',
sex: 0,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user2@example.com',
username: 'user2',
password: bcrypt.hashSync('123456', 10),
nickname: '用户2',
sex: 0,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
{
email: 'user3@example.com',
username: 'user3',
password: bcrypt.hashSync('123456', 10),
nickname: '用户3',
sex: 1,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
])
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Users', null, {});
}
};
七、课程管理接口(关联模型)
1. 创建种子文件 sequelize seed:generate --name course
js
// seeders/20250901064412-course.js
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Courses', [
{
categoryId: 1,
userId: 1,
name: '前端课程',
recommended: true,
introductory: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
categoryId: 2,
userId: 1,
name: 'nodeJs-课程',
recommended: true,
introductory: false,
createdAt: new Date(),
updatedAt: new Date(),
},
])
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Courses', null, {})
}
2. 执行种子命令 sequelize db:seed --seed 20250901064412-course
3. 添加模型文件中的验证
js
Course.init({
categoryId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: {
msg: '分类ID必须存在'
},
notEmpty: {
msg: '分类ID不能为空'
},
async isPresent(value) {
const category = await sequelize.models.Category.findByPk(value)
if(!category) {
throw new Error('ID为' + value + '的分类不存在')
}
}
}
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: {
msg: '用户ID必须存在'
},
notEmpty: {
msg: '用户ID不能为空'
},
async isPresent(value) {
const user = await sequelize.models.User.findByPk(value)
if(!user) {
throw new Error('ID为' + value + '的用户不存在')
}
}
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '名称必须存在'
},
notEmpty: {
msg: '名称不能为空'
},
len: {
args:[2,45],
msg: '名称长度需要在2~45个字符之间'
},
async isUnique(value) {
const course = await Course.findOne({
where: {
name: value
}
})
if (course) {
throw new Error('名称已存在')
}
}
}
},
image: DataTypes.STRING,
recommended: DataTypes.BOOLEAN,
introductory: DataTypes.BOOLEAN,
content: DataTypes.TEXT,
likesCount: DataTypes.INTEGER,
chaptersCount: DataTypes.INTEGER
}, {
sequelize,
modelName: 'Course',
});
4. 添加课程的路由文件
js
// routes/admin/courses.js
const express = require('express');
const router = express.Router();
const {Course, Category, User} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError, success, failure} = require('../../utils/response')
router.get('/', async function(req, res, next) {
try {
const query = req.query
const currentPage = Math.abs(Number(query.currentPage)) || 1
const pageSize = Math.abs(Number(query.pageSize)) || 10
const offset = (currentPage - 1) * pageSize
const condition = {
...getCondition(),
order: [['id', 'DESC']],
limit: pageSize,
offset
}
if(query.categoryId) {
condition.where = {
categoryId: {
[Op.eq]: query.categoryId
}
}
}
if(query.userId) {
condition.where = {
userId: {
[Op.eq]: query.userId
}
}
}
if(query.name) {
condition.where = {
name: {
[Op.like]: `%${query.name}%`
}
}
}
if(query.recommended) {
condition.where = {
[Op.eq]: query.recommended === 'true'
}
}
if(query.introductory) {
condition.where = {
[Op.eq]: query.introductory === 'true'
}
}
const {count, rows} = await Course.findAndCountAll(condition)
success(res, '查询课程列表成功', {
courses: rows,
pagination: {
total: count,
currentPage,
pageSize
}
})
}catch(error) {
failure(res, error)
}
});
router.get('/:id', async function(req, res, next) {
try {
const course = await getCourse(req)
success(res, '查询课程成功', {course})
} catch(error) {
failure(res, error)
}
})
router.post('/', async function(req, res, next) {
try {
const body = filterBody(req)
const course = await Course.create(body)
success(res, '创建课程成功', {course}, 201)
}catch(error) {
failure(res, error)
}
});
router.delete('/:id', async function(req, res, next) {
try {
const course = await getCourse(req)
await course.destroy()
success(res, '删除课程成功')
} catch(error) {
failure(res, error)
}
})
router.put('/:id', async function(req, res, next) {
try {
const body = filterBody(req)
const course = await getCourse(req)
await course.update(body)
success(res, '更新课程成功')
} catch(error) {
failure(res, error)
}
})
function getCondition() {
return {
attributes: {
exclude: ['CategoryId', 'UserId']
},
include: [
{
model: Category,
as: 'category',
attributes: ['id', 'name']
},
{
model: User,
as: 'user',
attributes: ['id', 'username', 'avatar']
}
]
}
}
async function getCourse(req) {
const {id} = req.params
const condition = getCondition()
const course = await Course.findByPk(id, condition)
if(!course) {
throw new NotFoundError(`ID: ${id}的课程未找到。`)
}
return course
}
function filterBody(req) {
return {
categoryId: req.body.categoryId,
userId: req.body.userId,
name: req.body.name,
image: req.body.image,
recommended: req.body.recommended,
introductory: req.body.introductory,
content: req.body.content,
}
}
module.exports = router;
5. 引入课程的路由文件
js
// app.js
const adminCoursesRouter = require('./routes/admin/courses');
app.use('/admin/courses', adminCoursesRouter);
6. 关联模型
js
// models/course.js
static associate(models) {
models.Course.belongsTo(models.Category, {
as: 'category'
})
models.Course.belongsTo(models.User, {
as: 'user'
})
}
js
// models/category.js
static associate(models) {
models.Category.hasMany(models.Course, {
as: 'courses' // 因为分类会有很多课程,这里是复数
})
}
js
// models/user.js
static associate(models) {
models.User.hasMany(models.Course, {
as: 'courses' // 同上,复数
})
}
js
// routes/admin/courses.js
const {Course, Category, User} = require('../../models')
// 获取分类表和用户表的数据
function getCondition() {
return {
attributes: {
exclude: ['CategoryId', 'UserId']
},
include: [
{
model: Category,
as: 'category',
attributes: ['id', 'name']
},
{
model: User,
as: 'user',
attributes: ['id', 'username', 'avatar']
}
]
}
}
7. 孤儿问题
如果删除分类表中的一条数据,课程表中就会有些数据找不到对应的分类了,这种没有对应表记录的数据,我们称为孤儿数据
解决方法:
- 在数据库里,设置
外键约束
,删除时就会提示错误。注意:一般企业是不会让使用外键约束,因为使用外键约束后,数据库会产生额外的性能开销。在高并发、数据量大的情况,可能造成性能瓶颈 - 删除分类的同时,删除所有关联课程
- 只有没有关联课程的分类,才能被删除
我们使用 方法3
解决问题
js
// routes/admin/categories.js
const {Category, Course} = require('../../models')
router.delete('/:id', async function(req, res, next) {
try {
const category = await getCategory(req)
const count = await Course.count({
where: {
categoryId: req.params.id
}
})
if(count > 0) {
throw new Error('该分类下有课程,不能删除。')
}
await category.destroy()
success(res, '删除分类成功')
} catch(error) {
failure(res, error)
}
})
8. 表之间的关联关系
有关联字段的表,一定是属于(belongsTo)其他表
例如课程表里有 categoryId
,那它就一定是 belongsTo
属于分类。反过来,分类模型里就是 hasMany
有很多课程
八、章节接口(关联模型)
1. 创建种子文件
js
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Chapters', [
{
courseId: 1,
title: 'css 第1章',
content: '这是第1章的内容',
video:'',
rank: 1,
createdAt: new Date(),
updatedAt: new Date()
},
{
courseId: 2,
title: 'node 第1章',
content: '这是第1章的内容',
video:'',
rank: 1,
createdAt: new Date(),
updatedAt: new Date()
},
{
courseId: 2,
title: 'node 第2章',
content: '这是第2章的内容',
video:'',
rank: 2,
createdAt: new Date(),
updatedAt: new Date()
},
], {})
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Chapters', null, {})
}
2. 执行种子命令 sequelize db:seed --seed 20250901085407-chapter
3.添加模型文件中的验证
js
// models/course.js
static associate(models) {
models.Course.hasMany(models.Chapter, {
as: 'chapters'
})
}
js
// models/chapter.js
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Chapter extends Model {
static associate(models) {
Chapter.belongsTo(models.Course, {
as: 'course'
})
}
}
Chapter.init({
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notEmpty: {
msg: '课程ID必须存在'
},
notNull: {
msg: '课程ID不能为空'
},
async isPresent(value) {
const course = await sequelize.models.Course.findByPk(value)
if(!course) {
throw new Error('课程不存在')
}
}
}
},
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: {
msg: '标题必须存在'
},
notNull: {
msg: '标题不能为空'
},
len: {
args:[2,45],
msg: '标题长度需要在2~45个字符之间'
}
}
},
content: DataTypes.TEXT,
video: DataTypes.STRING,
rank: DataTypes.INTEGER
}, {
sequelize,
modelName: 'Chapter',
});
return Chapter;
};
4. 添加路由文件
js
const express = require('express');
const router = express.Router();
const {Chapter, Course} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError, success, failure} = require('../../utils/response')
router.get('/', async function(req, res, next) {
try {
const query = req.query
const currentPage = Math.abs(Number(query.currentPage)) || 1
const pageSize = Math.abs(Number(query.pageSize)) || 10
const offset = (currentPage - 1) * pageSize
if(!query.courseId) {
throw new Error('获取章节列表失败,课程ID不能为空')
}
const condition = {
...getCondition(),
order: [['rank', 'ASC'],['id', 'ASC']],
limit: pageSize,
offset
}
condition.where = {
courseId: {
[Op.eq]: query.courseId
}
}
if(query.title) {
condition.where = {
title: {
[Op.like]: `%${query.title}%`
}
}
}
const {count, rows} = await Chapter.findAndCountAll(condition)
success(res, '查询章节列表成功', {
chapters: rows,
pagination: {
total: count,
currentPage,
pageSize
}
})
}catch(error) {
failure(res, error)
}
});
router.get('/:id', async function(req, res, next) {
try {
const chapter = await getChapter(req)
success(res, '查询章节成功', {chapter})
} catch(error) {
failure(res, error)
}
})
router.post('/', async function(req, res, next) {
try {
const body = filterBody(req)
const chapter = await Chapter.create(body)
success(res, '创建章节成功', {chapter}, 201)
}catch(error) {
failure(res, error)
}
});
router.delete('/:id', async function(req, res, next) {
try {
const chapter = await getChapter(req)
await chapter.destroy()
success(res, '删除章节成功')
} catch(error) {
failure(res, error)
}
})
router.put('/:id', async function(req, res, next) {
try {
const body = filterBody(req)
const chapter = await getChapter(req)
await chapter.update(body)
success(res, '更新章节成功')
} catch(error) {
failure(res, error)
}
})
function getCondition() {
return {
attributes: {
exclude: ['CourseId']
},
include: [
{
model: Course,
as: 'course',
attributes: ['id', 'name']
}
]
}
}
async function getChapter(req) {
const {id} = req.params
const chapter = await Chapter.findByPk(id, getCondition())
if(!chapter) {
throw new NotFoundError(`ID: ${id}的章节未找到。`)
}
return chapter
}
function filterBody(req) {
return {
courseId: req.body.courseId,
title: req.body.title,
content: req.body.content,
video: req.body.video,
rank: req.body.rank
}
}
module.exports = router;
5. 在课程路由文件中增加删除判断
js
// routes/admin/courses.js
const {Chapter} = require('../../models')
router.delete('/:id', async function(req, res, next) {
try {
const course = await getCourse(req)
const count = await Chapter.count({
where: {
courseId: course.id
}
})
if(count > 0) {
throw new Error('删除课程失败,该课程下存在章节')
}
await course.destroy()
success(res, '删除课程成功')
} catch(error) {
failure(res, error)
}
})
6. 引入路由文件
js
// app.js
const adminChaptersRouter = require('./routes/admin/chapters');
app.use('/admin/chapters', adminChaptersRouter);
九、Echarts 数据统计接口
1. 路由文件
js
// routes/admin/chart.js
const express = require('express');
const router = express.Router();
const {sequelize, User} = require('../../models');
const {success, failure} = require('../../utils/response');
/**
* 统计用户性别
* Get /admin/chart/sex
*/
router.get('/', async function(req, res, next) {
try {
const male = await User.count({
where: {
sex: 0
}
})
const female = await User.count({
where: {
sex: 1
}
})
const unknown = await User.count({
where: {
sex: 2
}
})
const data = [
{value: male, name: '男'},
{value: female, name: '女'},
{value: unknown, name: '未选择'},
]
success(res, '查询用户性别成功', data)
} catch(error) {
failure(res, error)
}
})
/**
* 统计用户每月注册人数
* Get /admin/chart/user
*/
router.get('/user', async function(req, res, next) {
try {
const [results] = await sequelize.query('SELECT DATE_FORMAT(createdAt, "%Y-%m") AS months, COUNT(*) AS value FROM users GROUP BY months ORDER BY months ASC')
const data = results.map(item => ({
months: item.months,
value: item.value
}))
success(res, '查询用户每月注册人数成功', data)
} catch(error) {
failure(res, error)
}
})
module.exports = router;
2. 引入路由文件
js
// app.js
const adminChartRouter = require('./routes/admin/chart');
app.use('/admin/chart', adminChartRouter);
十、jwt实现管理员登陆
1. 新增错误类型判断
js
// utils/errors.js
class BadRequestError extends Error {
constructor(message) {
super(message)
this.name = 'BadRequestError'
}
}
/**
* 401 错误
*/
class UnauthorizedError extends Error {
constructor(message) {
super(message)
this.name = 'UnauthorizedError'
}
}
/**
* 404 错误
*/
class NotFoundError extends Error {
constructor(message) {
super(message)
this.name = 'NotFoundError'
}
}
module.exports = {
BadRequestError,
UnauthorizedError,
NotFoundError,
}
2. 修改原先的 response
文件
js
// utils/response.js -> utils/responses.js
function success(res, message, data = {}, code = 200) {
res.status(code).json({
status:true,
message,
data
})
}
function failure(res, error) {
if(error.name == 'SequelizeValidationError') {
const errors = error.errors.map(e => e.message)
return res.status(400).json({
status:false,
message: '请求参数错误',
errors
})
}
if(error.name === 'BadRequestError') {
return res.status(400).json({
status:false,
message: '请求参数错误',
errors: [error.message]
})
}
if(error.name === 'UnauthorizedError') {
return res.status(401).json({
status:false,
message: '未授权',
errors: [error.message]
})
}
if(error.name === 'NotFoundError') {
return res.status(404).json({
status:false,
message: '资源不存在',
errors: [error.message]
})
}
res.status(500).json({
status:false,
message: '服务器错误',
errors: [error.message]
})
}
module.exports = {
success,
failure
}
3. 修改路由文件中的引用
js
// const {NotFoundError, success, failure} = require('../../utils/response');
const {NotFoundError} = require('../../utils/errors')
const {success, failure} = require('../../utils/responses');
4. 新增登陆判断的路由文件
js
// routes/admin/auth.js
const express = require('express');
const router = express.Router();
const {User} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError} = require('../../utils/errors')
const {success, failure} = require('../../utils/responses');
/**
* 管理员登陆
* POST /admin/auth/sign_in
*/
router.post('/sign_in', async function(req, res, next) {
try {
const {login, password} = req.body
if(!login) {
throw new Error('邮箱/用户名不能为空')
}
if(!password) {
throw new Error('密码不能为空')
}
const condition = {
where: {
[Op.or]: [
{
email: login
},
{
username: login
}
]
}
}
const user = await User.findOne(condition)
if(!user) {
throw new NotFoundError('用户不存在,无法登陆。')
}
success(res, '登陆成功', {})
} catch(error) {
failure(res, error)
}
})
module.exports = router
5. 引用路由文件
js
// app.js
app.use(express.static(path.join(__dirname, 'public')));
app.use('/admin/auth', adminAuthRouter);
6. 判断密码是否相等
js
const bcrypt = require('bcryptjs')
// 验证密码错误
const isPasswordValid = await bcrypt.compareSync(password, user.password)
if(!isPasswordValid) {
throw new Error('密码错误')
}
7. 新建环境变量
npm install dotenv
js
// .env
SECRET=94cb853f7b3a9c3adb5a59903962d1125fe56efb860f331ca669d13c0a979bb7
8. 生成token
npm install jsonwebtoken
js
// 可以生成32位的随机字符串作为密钥(SECRET)
// const crypto = require('crypto')
// console.log(crypto.randomBytes(32).toString('hex'))
const token = jwt.sign({
userId: user.id,
}, process.env.SECRET, {
expiresIn: '30d'
})
success(res, '登陆成功', {token})
十一、使用中间件认证接口
1. 声明中间件
js
// middlewares/admin-auth.js
const jwt = require('jsonwebtoken')
const {User} = require('../models')
const {UnauthorizedError} = require('../utils/errors')
const {success, failure} = require('../utils/responses')
module.exports = async function(req, res, next) {
try {
const {token} = req.headers
if(!token) {
throw new Error('当前接口需要认证才能访问')
}
// 验证token是否正确
const decoded = jwt.verify(token, process.env.SECRET)
// 从jwt中 解析出之前存入的userId
const {userId} = decoded
// 根据userId查询用户信息
const user = await User.findByPk(userId)
if(!user) {
throw new Error('用户不存在')
}
// 验证当前用户是否是管理员
if(user.role !== 100) {
throw new Error('当前用户没有权限访问')
}
// 验证通过,将用户信息挂载到req对象上
req.user = user
next()
} catch(error) {
failure(res, error)
}
}
2. 使用中间件
js
// app.js
const adminAuth = require('./middlewares/admin-auth');
// 后台路由配置
app.use('/admin/articles', adminAuth, adminArticlesRouter);
app.use('/admin/categories', adminAuth, adminCategoriesRouter);
app.use('/admin/settings', adminAuth, adminSettingsRouter);
app.use('/admin/users', adminAuth, adminUsersRouter);
app.use('/admin/courses', adminAuth, adminCoursesRouter);
app.use('/admin/chapters', adminAuth, adminChaptersRouter);
app.use('/admin/chart', adminAuth, adminChartRouter);
app.use('/admin/auth', adminAuthRouter);
学习视频地址:bilibili