后端 – NodeJS
后端用 Express 制作 API 。Sequelize 操作 数据库。
项目目录结构:
├── app.js # 整合文件
├── database # 数据库-文件夹
│ └── database.js # 数据库初始化
├── middlewares # 中间件-文件夹
│ └── auth.js # 认证中间件
├── models # 数据库模型-文件夹
│ ├── Article.js # 文章模型
│ ├── User.js # 用户模型
│ └── index.js # 合并索引
├── routes # 路由-文件夹
│ ├── admin.js # admin(后台)路由
│ └── login.js # login(登录)路由
├── uploads # 上传-文件夹
│ └── image # 图片-文件夹
│ └── "Title" # 图片-分夹
│ ├── 12.jpg # 图片
│ ├── 34.jpg # 图片
│ ├── 56.jpg # 图片
│ └── 78.jpg # 图片
└── utils # 工具-文件夹
└── jwt.js # JWT功能
使用上面的目录结构,使得项目更加清晰,利于维护。但是缺点也很明显,就是东西肉眼可见的多。
不喜欢这样,完全可以把 middlewares
和 utils
文件夹合并,database
和 models
文件夹合并。极端一点的话,可以把 route
写在一起,甚至是和 app.js
合并,把整个 models
和 database
合并为一。。。
缺点就是,不方便维护,你需要在一个文件里面找东西,改代码。会有一种牵一发而动全身的即视感。
不过也比所有东西写在一个 app.js
好。
代码&说明
1. Sequelize 操作数据库
1.1 安装 Sequelize 和相关依赖
在项目中安装 Sequelize 和数据库驱动(以 MySQL 为例):
npm install sequelize mysql2
1.2 初始化 Sequelize
创建一个 database.js
文件,用于配置和初始化 Sequelize。
const { Sequelize } = require('sequelize');
// 创建 Sequelize 实例
const sequelize = new Sequelize('database_name', 'username', 'password', {
host: 'localhost', // 数据库地址
dialect: 'mysql', // 数据库类型(mysql、postgres、sqlite 等)
logging: false // 禁用 SQL 查询日志(可选)
});
// 测试数据库连接
(async () => {
try {
await sequelize.authenticate();
console.log('Database connected successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();
// 下面代码是同步数据库的代码,如果你的数据库表没有创建的话,使用下面代码,那么ORM库会自动帮你“同步(创建)”出你定义模型的表。
(async () => {
try {
await sequelize.sync({ alter: true }); // 根据模型更新表结构
console.log('Database synchronized successfully.');
} catch (error) {
console.error('Error synchronizing database:', error);
}
})();
//////// 不过由于已经手动创建了表,并插入了测试数据,所以请忽略。 ////////
module.exports = sequelize;
1.3 定义模型
在 models/
文件夹中创建模型文件,例如 models/User.js
和 models/Article.js
。
先简单引用说明一下:
STRING
默认长度就是255
,所以不需要再写(255)
。INTEGER
亦不需要加(255)
,整数类型在 Sequelize 中不会受长度限制,MySQL 8.0 也已废弃显示宽度。
注意:DataTypes.INTEGER
接受的参数是 MySQL 的整数显示宽度(如 (12)
),但 MySQL 8.0 及以上版本已经废弃了显示宽度,它不再影响存储或行为。 所以下面,STRING等,之后不再需要(255).
在 Sequelize 中,使用 DataTypes.DATE
表示 DATETIME
类型。
布尔值的默认行为:
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true, // 默认值为 true
},
好了,我们切入正题
用户模型:User
值得注意的是,后台路由 /admin
没有加入判断管理员的逻辑 ,并且这个用户模型也没有明确区分出管理员和普通用户的列(字段),所以 /admin
可以直接被该表的用户登陆。可能会对系统造成不可估量的损失。
解决方案:
1. 数据库
– 把在该表后面增加多一个列(字段),来判断是否管理员。(推荐)
– 把管理员放在新的表。
2. 路由
– 在后台路由增加一个验证
(前端篇 后台路由 – 作进一步说明和解决)
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
}, // 如果填写不存在的列(因为没有使用sync同步),所以会报错,导致查不出数据
is_member: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
}
}, {
timestamps: true, // 自动创建 createdAt 和 updatedAt 字段 (由于没有书写 sync代码 所以需要手动在数据库里加入,之前加入的是created_at, 所以需要改名,否则不认,报错。再不然就false掉它(默认不填写 timestamps是true))
});
module.exports = User;
更改方式:
ALTER TABLE users
CHANGE COLUMN created_at createdAt datetime;
文章模型:Article
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const Article = sequelize.define('Article', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
publisher_id: {
type: DataTypes.INTEGER,
allowNull: false,
}
}, {
timestamps: true,
});
module.exports = Article;
图片模型:ArticleImage
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const ArticleImage = sequelize.define('ArticleImage', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
article_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
image_url: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
timestamps: true, // 自动创建 createdAt 和 updatedAt 字段
});
module.exports = ArticleImage;
建立数据库关联 (v1.0版本 – 倾向理论)
(v1.3说:非常建议读一下官方文档 – https://sequelize.org/docs/v6/core-concepts/assocs/ (或者中文文档))
在 models/index.js
文件中定义模型间的关联关系,并且将它们一起暴露出去,给 app.js
使用。
我们先来说说背景,可以看到上面有两个模型,分别是 User
& Article
它们分别对应了 用户模型
& 文章模型
。那么我们如何将文章绑定给发布者呢?
文章模型(Article) 处有一个 列 : 叫做 publisher_id
。而 用户模型(User) 处有一个 列 : 叫做 id(也是该表的主键)。用户名,email不可重复,每个用户都是独一无二的(这不废话吗),所以每个用户只有一个 主键ID ,这个ID也因为用户只可能在这个表出现一次,从而是唯一的。
那么这个 ID 就可以作为“快速识别码”,和文章模型(Article)的 publisher_id
相对应。比如说有 10 篇文章,publisher_id
都为 1,在 User
这个表里面,1 是 管理员的ID。那显而易见,这十篇文章都是管理员发的。
我们要做的,就是在 index.js
整合它们,把它们一次性暴露给 app.js
。虽然 app.js
也可以一个个导入 Models
文件夹里面的模型,但是这种面对大型项目情况下,就会显得力不从心。
const User = require('./User');
const Article = require('./Article');
const ArticleImage = require('./ArticleImage');
// 用户与文章:1对多关系
User.hasMany(Article, { foreignKey: 'publisher_id', onDelete: 'CASCADE' });
Article.belongsTo(User, { foreignKey: 'publisher_id' });
// 文章与图片:一对多关系
Article.hasMany(ArticleImage, {
// 因为每个文章都是唯一的,图片应该和文章的 id 相对应,而非作者id。所以直接填写 article_id,直接使其匹配到 id 上。
foreignKey: 'article_id',
onDelete: 'CASCADE',
});
ArticleImage.belongsTo(Article, {
foreignKey: 'article_id',
});
module.exports = { User, Article, ArticleImage };
或许你会有疑问:“users这个表里面没有publisher_id这一列啊,请问,上面代码的User的id是如何和publisher_id绑定在一起的呢?”
虽然
users
表中没有publisher_id
列,但这并不会影响外键绑定,因为外键绑定的核心不是字段是否存在于主表(users
表),而是从属表(articles
表)中的外键字段如何引用主表的主键。
外键绑定的核心原理
在 Sequelize 中,外键绑定是通过以下逻辑实现的:
- 主表(
users
)提供主键:users
表的主键是id
,这是默认的 Sequelize 主键字段。- 主表(
users
)的主键将作为从属表(articles
)中外键的目标。
- 从属表(
articles
)包含外键列:- 在
articles
表中有一个列publisher_id
,用于存储与users
表id
主键对应的值。 articles.publisher_id
是从属表中的外键。
- 在
- 关联定义:
- 在 Sequelize 中使用
foreignKey
指定从属表的外键字段名(publisher_id
),并将其绑定到主表的主键(默认是id
)。 - 例如:
- 在 Sequelize 中使用
User.hasMany(Article, { foreignKey: 'publisher_id' });
Article.belongsTo(User, { foreignKey: 'publisher_id' });
代码解释:
User.hasMany(Article, { foreignKey: 'publisher_id', onDelete: 'CASCADE' }); Article.belongsTo(User, { foreignKey: 'publisher_id' });
User.hasMany(Article)
:- 告诉 Sequelize,
User
表的主键(id
)将被关联到Article
表的外键publisher_id
。 - 每个
User
可以有多个Article
。
- 告诉 Sequelize,
Article.belongsTo(User)
:- 告诉 Sequelize,
Article
表中的publisher_id
字段引用了User
表的主键(id
)。 - 每个
Article
只能属于一个User
。
- 告诉 Sequelize,
- 这种叫做「一对多关系」。
如果
User
模型没有publisher_id
字段,而Article
模型( articles表 )中有一个publisher_id
外键列,Sequelize 会默认将User
模型( users表 )的主键id
与Article.publisher_id
进行关联。
CASCADE 是什么
这是个外键约束行为,onDelete: ‘CASCADE’ 表明了,如果这个用户删除(从数据库),那么另外一个表下面的,有关他的所有文章,都一并删除掉。
如果 users.userid
是随机生成的,如何与 articles.id
绑定?
在这种情况下,需要将 articles
表中的外键字段设置为 userid
,并通过 Sequelize 的 foreignKey
选项明确指定两者之间的关系。
我们来假设一下情况:
users
表:主键是 userid
,随机生成。
userid (主键) | username
------------------------
abcd1234 | Alice
efgh5678 | Bob
articles
表:外键是 userid
,引用 users
表中的 userid
。
id (主键) | title | content | userid (外键)
--------------------------------------------------------
1 | First Article | Hello World | abcd1234
2 | Second Post | More Content | abcd1234
Sequelize 模型定义:通过 foreignKey
明确指定外键字段名称。
const User = require('./User');
const Article = require('./Article');
// 用户与文章:一对多关系
User.hasMany(Article, {
foreignKey: 'userid', // 指定外键为 articles 表中的 userid
onDelete: 'CASCADE', // 如果用户被删除,相关文章也会被删除
});
Article.belongsTo(User, {
foreignKey: 'userid', // 指定外键为 articles 表中的 userid
});
发现什么问题了吗?外键必须设置和受到关联那个表的列的列名一样,使其保持一致
在数据库设计中,外键的列名应与其所引用的主表的主键保持一致,或者至少让它们的逻辑含义明确对应。
建立数据库关联 (v1.3版本 – 独立的, 方便理解)
代码放在一个新的文件夹 – Operational Database
(非常建议读一下官方文档 – https://sequelize.org/docs/v6/core-concepts/assocs/ (或者中文文档))
数据库表结构(SQL语句)与(目录结构):
我们有两个表,我们要对其进行关联。把表2(article_image) 关联给表1(article)
其中,表1(article)的表结构是:
+------------+------------------+------+-----+---------------------+---------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------------------+---------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| created_at | timestamp | NO | | current_timestamp() | |
| title | varchar(255) | NO | | NULL | |
| content | text | NO | | NULL | |
+------------+------------------+------+-----+---------------------+---------+
4 rows in set (0.006 sec)
--- SQL 语句 ---
CREATE TABLE article (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键,因为文章唯一,id也就肯定是唯一的,所以直接选用id作为主键。
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL
);
其次,表2(article_image)的表结构是:
+------------+------------------+------+-----+---------------------+---------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------------------+---------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| created_at | timestamp | NO | | current_timestamp() | |
| article_id | int(10) unsigned | NO | MUL | NULL | |
| image_url | varchar(255) | NO | | NULL | |
+------------+------------------+------+-----+---------------------+---------+
4 rows in set (0.006 sec)
--- SQL 语句 ---
CREATE TABLE article_image (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
article_id INT(10) UNSIGNED NOT NULL, -- 外键字段
image_url VARCHAR(255) NOT NULL,
FOREIGN KEY (article_id) REFERENCES article(id) -- 引用主键
ON DELETE CASCADE
ON UPDATE CASCADE
);
我们需要把 表1(article) 和 表2(article_image)关联在一起,方便我们查询。
目录结构:
project/
├── database/
│ └── database.js // 数据库连接配置
├── models/
│ ├── Article.js // Article 模型定义
│ ├── ArticleImage.js // ArticleImage 模型定义
│ └── index.js // 模型入口文件,统一导出
├── app.js // 主应用入口
把表1(article) – id(主键) 和 表2(article_image) – (article_id) 关联在一起.
代码编号为A1,存放在 Operational Database/A1
database/database.js:
负责配置并导出 Sequelize 实例。
const { Sequelize } = require('sequelize');
// 创建 Sequelize 实例
const sequelize = new Sequelize('数据库名', '数据库账号', '数据库密码', {
host: 'localhost',
dialect: 'mysql',
logging: false, // 禁用 SQL 日志输出
});
// 测试数据库连接
(async () => {
try {
await sequelize.authenticate();
console.log('Database connected successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();
module.exports = sequelize;
models/Article.js:
定义 Article
模型。
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const Article = sequelize.define('Article', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
}, {
tableName: 'article',
timestamps: false, // 禁用自动生成的 timestamps 字段
});
module.exports = Article;
models/ArticleImage.js:
定义 ArticleImage
模型。
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const ArticleImage = sequelize.define('ArticleImage', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
article_id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
image_url: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
tableName: 'article_image',
timestamps: false, // 禁用自动生成的 timestamps 字段
});
module.exports = ArticleImage;
models/index.js:
统一导出所有模型,并设置关联关系。
const Article = require('./Article');
const ArticleImage = require('./ArticleImage');
// 定义关联关系
Article.hasMany(ArticleImage, {
foreignKey: 'article_id', // 外键字段
sourceKey: 'id', // 主键字段
onDelete: 'CASCADE', // 设置级联删除
onUpdate: 'CASCADE', // 设置级联更新
});
ArticleImage.belongsTo(Article, {
foreignKey: 'article_id', // 外键字段
targetKey: 'id', // 目标主键字段
});
module.exports = { Article, ArticleImage };
app.js:
主应用入口文件,调用模型并同步数据库。
const sequelize = require('./database/database');
const { Article, ArticleImage } = require('./models');
(async () => {
try {
// 同步数据库
await sequelize.sync({ alter: true }); // `alter: true` 会更新表结构
console.log('数据库同步成功');
const newArticle = await Article.create({
title: 'Test Article',
content: 'This is a test article.',
});
await ArticleImage.create({
article_id: newArticle.id,
image_url: 'https://example.com/image1.jpg',
});
console.log('数据插入成功');
// 查询测试
const articles = await Article.findAll({
include: [
{
model: ArticleImage,
required: false, // 即使没有关联图片,也返回文章
},
],
});
// 打印查询到的内容
console.log(JSON.stringify(articles, null, 2));
} catch (error) {
console.error('数据库同步失败:', error);
}
})();
返回数据:
[
{
"id": 1,
"created_at": "2025-05-23T04:43:21.000Z",
"title": "Test Article",
"content": "This is a test article.",
"ArticleImages": [
{
"id": 1,
"created_at": "2025-05-23T04:43:21.000Z",
"article_id": 1,
"image_url": "https://example.com/image1.jpg"
}
]
}
]
获取文章及其图片
通过 include
选项查询 Article
及其关联的 ArticleImage
。
const { Article, ArticleImage } = require('./models');
(async () => {
try {
const articles = await Article.findAll({
include: [
{
model: ArticleImage,
required: false, // 即使没有关联图片,也返回文章
},
],
});
console.log(JSON.stringify(articles, null, 2));
} catch (error) {
console.error('Error fetching articles with images:', error);
}
})();
查询某个特定文章及其关联的图片
通过 where
条件查询某篇特定文章及其关联的图片。
(async () => {
const { Article, ArticleImage } = require('./models');
try {
const articleWithImages = await Article.findOne({
where: { id: 1 }, // 查询 ID 为 1 的文章
include: [
{
model: ArticleImage, // 包含其关联的图片
},
],
});
console.log('Article with Images:', JSON.stringify(articleWithImages, null, 2));
} catch (error) {
console.error('Error fetching article by ID:', error);
}
})();
查询某张图片及其对应的文章
通过 include
查询 ArticleImage
及其关联的 Article
。
(async () => {
const { Article, ArticleImage } = require('./models');
try {
const imageWithArticle = await ArticleImage.findOne({
where: { id: 1 }, // 查询 ID 为 1 的图片
include: [
{
model: Article, // 包含其关联的文章
},
],
});
console.log('Image with Article:', JSON.stringify(imageWithArticle, null, 2));
} catch (error) {
console.error('Error fetching image by ID:', error);
}
})();
核心步骤
- 定义模型:
Article
模型:- 表名为
article
,包含字段id
(主键)、created_at
(创建时间)、title
(文章标题)、content
(文章内容)。 - 禁用自动生成的时间戳字段(
timestamps: false
)。
- 表名为
ArticleImage
模型:- 表名为
article_image
,包含字段id
(主键)、created_at
(创建时间)、article_id
(外键,关联到Article
的id
)、image_url
(图片 URL)。 - 同样禁用自动生成时间戳字段。
- 表名为
- 定义模型关联:
Article
和ArticleImage
通过article_id
字段建立外键关联。- 关系定义:
Article.hasMany(ArticleImage)
:表示一个文章可以有多张图片。ArticleImage.belongsTo(Article)
:表示每张图片属于一个文章。
- 设置外键约束:
onDelete: 'CASCADE'
:当文章被删除时,自动删除关联的图片。onUpdate: 'CASCADE'
:当文章的主键被更新时,自动更新关联的外键。
- 同步数据库:
- 使用
sequelize.sync({ alter: true })
创建或更新数据库表结构。 - 确保模型定义和数据库结构保持一致。
- 使用
- 数据插入和查询:
- 插入一篇文章后,插入与该文章关联的图片。
- 使用
findAll
方法查询文章及其关联的图片,支持即使没有图片也返回文章的结果(required: false
)。
把表1(article) – release_level(主键) 和 表2(article_image) – (release_level) 关联在一起.
如果希望不是id,而在两个表都有一个“Release level”的列,通过里面的唯一值(随机6位数)来进行关联的话。
代码编号为A2,存放在 Operational Database/A2
SQL语句 – 不要一次性全部执行(会报错)
DROP TABLE IF EXISTS article;
-- 创建 Article 表
CREATE TABLE article (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
release_level CHAR(6) NOT NULL UNIQUE -- 唯一的 release_level 列
);
DROP TABLE IF EXISTS article_image;
-- 创建 ArticleImage 表
CREATE TABLE article_image (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
release_level CHAR(6) NOT NULL, -- 与 article.release_level 关联
image_url VARCHAR(255) NOT NULL,
FOREIGN KEY (release_level) REFERENCES article(release_level)
ON DELETE CASCADE
ON UPDATE CASCADE
);
database/database.js: – 不变
models/Article.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const Article = sequelize.define('Article', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
release_level: {
type: DataTypes.CHAR(6), // 固定6字符长度
allowNull: false,
unique: true, // 保证每个 release_level 唯一
},
}, {
tableName: 'article',
timestamps: false,
});
module.exports = Article;
models/ArticleImage.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const ArticleImage = sequelize.define('ArticleImage', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
release_level: {
type: DataTypes.CHAR(6), // 固定6字符长度
allowNull: false,
},
image_url: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
tableName: 'article_image',
timestamps: false,
});
module.exports = ArticleImage;
models/index.js
const Article = require('./Article');
const ArticleImage = require('./ArticleImage');
// 定义关联关系
Article.hasMany(ArticleImage, {
foreignKey: 'release_level', // 通过 release_level 进行关联
sourceKey: 'release_level', // article 的 release_level
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
ArticleImage.belongsTo(Article, {
foreignKey: 'release_level', // article_image 的 release_level
targetKey: 'release_level', // article 的 release_level
});
module.exports = { Article, ArticleImage };
app.js
const sequelize = require('./database/database');
const { Article, ArticleImage } = require('./models');
(async () => {
try {
await sequelize.sync({ alter: true }); // 自动同步表结构
console.log('数据库同步成功');
const releaseLevel = Math.random().toString(36).substring(2, 8).toUpperCase(); // 生成随机6位 release_level
// 插入文章
const newArticle = await Article.create({
title: 'Test Article',
content: 'This is a test article.',
release_level: releaseLevel, // 插入随机 release_level
});
// 插入关联图片
await ArticleImage.create({
release_level: releaseLevel, // 使用相同的 release_level 进行关联
image_url: 'https://example.com/image1.jpg',
});
console.log('数据插入成功');
} catch (error) {
console.error('数据库同步失败:', error);
}
})();
查询相关数据
const articlesWithImages = await Article.findAll({
include: [
{
model: ArticleImage,
required: false, // 即使没有关联图片也返回文章
},
],
});
console.log(JSON.stringify(articlesWithImages, null, 2));
查询返回:
[
{
"id": 1,
"created_at": "2025-05-23T04:32:56.000Z",
"title": "Test Article",
"content": "This is a test article.",
"release_level": "V5D9SU",
"ArticleImages": [
{
"id": 1,
"created_at": "2025-05-23T04:32:56.000Z",
"release_level": "V5D9SU",
"image_url": "https://example.com/image1.jpg"
}
]
}
]
总结:
release_level
是一个随机生成的 6 位唯一标识符,用于关联article
和article_image
表。- 在 Sequelize 中,通过
hasMany
和belongsTo
关联两个模型,使用release_level
作为外键字段。 - 外键的
ON DELETE CASCADE
和ON UPDATE CASCADE
确保关联数据一致性。
把表1(article) – release_level(主键) 和 表2(article_image) – (level) 关联在一起.
假如Article表里面叫“Release level”,ArticleImage表里面叫“level”又如何关联
代码编号为A3,存放在 Operational Database/A3
SQL语句 – 不要一次性全部执行(会报错)
DROP TABLE IF EXISTS article;
-- 创建 Article 表
CREATE TABLE article (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`Release level` CHAR(6) NOT NULL UNIQUE, -- 注意字段名带空格
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
DROP TABLE IF EXISTS article_image;
-- 创建 ArticleImage 表
CREATE TABLE article_image (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
level CHAR(6) NOT NULL, -- 关联到 article 的 `Release level`
image_url VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (level) REFERENCES article(`Release level`) -- 外键关联
ON DELETE CASCADE
ON UPDATE CASCADE
);
database/database.js: – 不变
models/Article.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const Article = sequelize.define('Article', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
releaseLevel: { // 映射到数据库中的 `Release level`
type: DataTypes.CHAR(6),
allowNull: false,
unique: true,
field: 'Release level', // 映射数据库字段名
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
}, {
tableName: 'article',
timestamps: false,
});
module.exports = Article;
models/ArticleImage.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const ArticleImage = sequelize.define('ArticleImage', {
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
level: { // 映射到数据库中的 `level`
type: DataTypes.CHAR(6),
allowNull: false,
},
imageUrl: {
type: DataTypes.STRING,
allowNull: false,
field: 'image_url', // 映射数据库字段名
},
}, {
tableName: 'article_image',
timestamps: false,
});
module.exports = ArticleImage;
models/index.js
const Article = require('./Article');
const ArticleImage = require('./ArticleImage');
// 定义关联关系
Article.hasMany(ArticleImage, {
foreignKey: 'level', // ArticleImage 的外键
sourceKey: 'releaseLevel', // Article 的目标键
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
ArticleImage.belongsTo(Article, {
foreignKey: 'level', // ArticleImage 的外键
targetKey: 'releaseLevel', // Article 的目标键
});
module.exports = { Article, ArticleImage };
app.js
const sequelize = require('./database/database');
const { Article, ArticleImage } = require('./models');
(async () => {
try {
await sequelize.sync({ alter: true }); // 更新数据库表结构
console.log('数据库同步成功');
const releaseLevel = 'ABC123';
// 插入 Article 数据
const newArticle = await Article.create({
releaseLevel, // 自动映射到数据库的 `Release level`
title: 'Test Article',
content: 'This is a test article.',
});
// 插入关联的 ArticleImage 数据
await ArticleImage.create({
level: releaseLevel, // 与 Article 的 releaseLevel 保持一致
imageUrl: 'https://example.com/image1.jpg',
});
console.log('数据插入成功');
} catch (error) {
console.error('数据库同步失败:', error);
}
})();
查询关联数据
const articlesWithImages = await Article.findAll({
include: [
{
model: ArticleImage,
required: false, // 即使没有关联图片也返回文章
},
],
});
console.log(JSON.stringify(articlesWithImages, null, 2));
返回数据:
[
{
"id": 1,
"releaseLevel": "ABC123",
"title": "Test Article",
"content": "This is a test article.",
"ArticleImages": [
{
"id": 1,
"level": "ABC123",
"imageUrl": "https://example.com/image1.jpg"
}
]
}
]
总结:
- 通过
field
属性,Sequelize 可以将模型字段与数据库中的字段名(即带空格的或不同的字段名)进行映射。 - 在模型关联中,通过设置
foreignKey
和sourceKey
或targetKey
,可以灵活实现不同字段名的关联逻辑。 - 数据库中的
Release level
对应模型中的releaseLevel
,level
直接对应模型中的level
。
同步数据库
在主入口文件(如 app.js
或 index.js
)中同步模型到数据库:
看是否需要,在上面已经说过了,这里只做(再次)展示
const sequelize = require('./database/database');
const { User, Article } = require('./models');
// 同步数据库
(async () => {
try {
await sequelize.sync({ alter: true }); // 根据模型更新表结构
console.log('Database synchronized successfully.');
} catch (error) {
console.error('Error synchronizing database:', error);
}
})();
2. 添加 JWT 认证
2.1 安装 JWT 相关依赖
使用 jsonwebtoken
包进行 JWT 生成和验证:
npm install jsonwebtoken
2.2 配置 JWT
请注意,无论如何都不要泄漏密钥,密钥泄漏会导致 身份伪造,绕过,数据篡改,会话挟持等等问题!
在项目中创建一个 utils/jwt.js
文件,封装 JWT 的生成和验证逻辑。
const jwt = require('jsonwebtoken');
// 定义密钥和过期时间
const JWT_SECRET = 'your_secret_key'; // 密钥 - 这个不应该被外人知道,否则会被伪造JWT
const JWT_EXPIRES_IN = '1h'; // Token 有效期
// 生成 JWT
const generateToken = (payload) => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
// 验证 JWT
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
};
module.exports = { generateToken, verifyToken };
2.3 创建认证中间件
在项目中创建一个 middlewares/auth.js
文件,定义 JWT 验证的中间件。
const { verifyToken } = require('../utils/jwt');
// 中间件:验证 JWT
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
// 检查是否存在 Bearer Token
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
try {
const decoded = verifyToken(token); // 验证 JWT
req.user = decoded; // 将用户信息附加到请求对象
next(); // 放行(反弹回去路由代码那里)|记住,后面要考
} catch (err) {
return res.status(401).json({ error: 'Unauthorized: Invalid token' });
}
} else {
return res.status(401).json({ error: 'Unauthorized: No token provided' });
}
};
module.exports = { authenticateJWT };
2.4 登录接口:生成 JWT
在路由文件(routes/login.js
)中添加登录接口,用于生成 JWT。
注意: 路由路径为 /
,因为在 app.js
中,你已经将 authRoutes
挂载到 /login
const express = require('express');
const bcrypt = require('bcrypt');
const { User } = require('../models');
const { generateToken } = require('../utils/jwt');
const router = express.Router();
// 用户登录接口
router.post('/', async (req, res) => {
// 注意!!!这里路径为 ‘/’,因为在 整合文件 app.js 里面已经挂载到了login!
const { username, password } = req.body;
try {
// 查找用户
const user = await User.findOne({ where: { username } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 生成 JWT
const token = generateToken({ id: user.id, username: user.username });
res.json({ token });
} catch (error) {
res.status(500).json({ error: 'Something went wrong' });
}
});
module.exports = router;
// 下面代码会发现,即便输入正确,但是都和数据库的密码匹配不上,因为每次的生成都不一样(加盐哈希),所以不要手动加密再去比较。直接使用 bcrypt.compare 作比较就行。
// const saltRounds = 10; // 密码加盐次数
// const hashedPassword = await bcrypt.hash(password, saltRounds);
// const isPasswordValid = await bcrypt.compare(hashedPassword, user.password);
// 上面是错误的
// console.log(`输入的密码{password}`)
// console.log(`输入的密码哈希{hashedPassword}`)
// console.log(`数据库里的密码${user.password}`)
//下面是正确的
//const isPasswordValid = await bcrypt.compare(password, user.password);
2.5 保护 /admin
接口
在 /admin
接口中使用 JWT 中间件,确保只有经过认证的用户可以访问。
下面范例是,直接挂载到 /admin
这将意味着,你必须在 整合文件 app.js
挂到 /
。(当然,挂载到其他地方也行,比如说 abc
,那么情况就是:abc/admin
)
const express = require('express');
const { authenticateJWT } = require('../middlewares/auth');
const router = express.Router();
// 受保护的 /admin 路由
router.get('/admin', authenticateJWT, (req, res) => {
res.json({ message: 'Welcome to the admin panel', user: req.user });
});
module.exports = router;
// 上面的 authenticateJWT 的意思是,调用中间件,对应了 JWT 认证(auth.js)里面的 next(); 放行,返回到路由。
// 如果验证没有通过呢?当然就是不返回后面的 JSON - Welcome to the admin panel。
// 而是直接返回 auth.js 里面的错误信息 - 如: 验证不通过/不合法
3. 主文件整合
在 app.js
或 index.js
中整合上述功能:
const express = require('express');
const bodyParser = require('body-parser');
const authRoutes = require('./routes/login');
const adminRoutes = require('./routes/admin');
const app = express();
// 中间件
app.use(bodyParser.json()); // JSON 化数据,和数据库返回数据做比较用
// 路由 | 这里的挂载仅作演示
app.use('/login', authRoutes); // 登录路由
app.use('/', adminRoutes); // 受保护路由
// 启动服务器
const PORT = 3000;
app.listen(PORT, async () => {
console.log(`Server running on http://localhost:${PORT}`);
});
登录认证
通过 PostMan 之类的程序进行测试,我们对 /login
路由进行登录测试看看。
这样做:POST – localhost:3000/login
之后在 Body 传入 raw – JSON
{
"username": "admin",
"password": "admin"
}
我已经在数据库手动插入了一个用户,账号密码都是 admin
。
之后我们将包发出,就会返回一个 Token。
{
"token": "eyJhbGciOiJIUxxxxxxxxxxxxCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hxxxxxxxxxxxxbiIsImlhdCI6MTc0NzM2NDg3MywiZXhwIjoxNzQ3Mzc5MjczfQ.NuAY7IN5OjDZWQxMeTxxxxxxxxxxxxorQrc7gu5GAcc"
}
但是当我们再去访问 /admin
的时候,会发现未被授权,这是为什么呢。
是因为我们虽然得到了下发的 Token,但是我们没有加进去 Authorization,所以报错。 平常的网站之所以不需要手动添加,是因为前端和浏览器帮你完成了这些工作。 而你自己写的后端API,测试阶段,都需要自己手动加上(为了测试API)。或者你可以直接搓一个前端出来测试,不过一般都是写完后端,再写前端的。
我们在 Authorization 添加一个 Bearer Token
,之后把 Token填进去就行: eyJhbGciOiJIUxxxxxxxxxxxxCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hxxxxxxxxxxxxbiIsImlhdCI6MTc0NzM2NDg3MywiZXhwIjoxNzQ3Mzc5MjczfQ.NuAY7IN5OjDZWQxMeTxxxxxxxxxxxxorQrc7gu5GAcc
这个时候再 GET /admin
就一切大吉了。
{
"message": "Welcome to the admin panel",
"user": {
"id": 1,
"username": "admin",
"iat": 174xxxx873,
"exp": 174xxxx273
}
}
暴露文章 – Tittle和Content可见,Images需登录
想要实现这个功能,游客可以看到文章 title
和 content
,但 images
需要注册登录才能看到,可以通过后端的权限控制和前端的条件渲染来实现。
我们必须先温习一下 – 什么是中间件?
在 Express.js 中,中间件是一个函数,用于处理请求(req
)和响应(res
)之间的逻辑。它还可以通过调用 next()
将控制权传递给下一个中间件。
中间件的用途
- 处理请求和响应:
- 修改请求对象(
req
)或响应对象(res
),例如解析请求体、添加用户信息等。
- 修改请求对象(
- 执行逻辑:
- 验证用户身份(如
verifyToken
)。 - 检查权限。
- 日志记录。
- 验证用户身份(如
- 控制请求流程:
- 决定请求是否继续(调用
next()
)或终止(返回响应)。
- 决定请求是否继续(调用
中间件的意义
- 逻辑复用:避免在每个路由中重复写相同的逻辑(如身份验证)。
- 代码清晰:将复杂的功能分成小的、可维护的模块。
- 请求处理链:通过多个中间件组合,形成灵活的请求处理流程。
总结
用于处理请求、添加逻辑、控制流程,从而提高代码的复用性和可维护性。
1. 后端实现权限控制
示例:基于 JWT 或 Session 验证
在登录完成时,会下发一个JWT,登录成功后,可以返回 username
之类的信息给浏览器(前端程序)去携带,通过判断是否有这个头,就可以判断用户是否登录,如果登录再展示这些信息。
我们在 middlewares/verify.js
下面尝试读取用户是否有登录(是否有JWT,有则分离)
// 复用 verifyToken 作为 Token 合法性校验和解密。
const { verifyToken } = require('../utils/jwt');
function verifyLogin(req, res, next) {
const authHeader = req.headers.authorization;
// 如果没有 Authorization 请求头,则视为未登录
if (!authHeader) {
console.log('No Authorization header found');
req.user = null;
return next();
}
// 提取 token(处理 Bearer 前缀)
const token = authHeader.startsWith('Bearer ') ? authHeader.split(' ')[1] : authHeader;
try {
// 验证并解码 token
const decoded = verifyToken(token);
console.log('Decoded Token:', decoded);
// 检查解码后的信息是否完整
if (!decoded || !decoded.username) {
console.error('Decoded token missing required fields:', decoded);
req.user = null;
return next();
}
req.user = decoded; // 将解码后的用户信息传递到后续中间件
next();
} catch (err) {
console.error('Token verification failed:', err.message); // 打印具体错误信息
req.user = null;
next();
}
}
module.exports = { verifyLogin };
routes/article.js
是无法复用之前的 verifyToken
中间件的,为什么?因为你会因为没有 Token
而被拦截,不放行。因为那个中间件是用来判断你是否登录,有无有效 Token ,否则不允许你访问被保护的 API 。如后台
const express = require('express');
const { verifyLogin } = require('../middlewares/verify'); // 验证登录中间件
const { Article } = require('../models')
const router = express.Router();
// 获取文章详情
router.get('/articles/:id', verifyLogin, async (req, res) => {
const { id } = req.params;
const user = req.user; // 登录信息通过 中间件 解析到 req.user
try {
const article = await Article.findByPk(id, {
attributes: user
? ['title', 'content', 'images'] // 登录用户可以看到所有字段|如果列(字段)不存在,会报500
: ['title', 'content'], // 游客只能看到 title 和 content
});
if (!article) {
return res.status(404).json({ message: 'Article not found' });
}
res.json(article);
} catch (err) {
res.status(500).json({ message: 'Internal server error' });
}
});
module.exports = router;
之后我们需要更新一下 app.js
const express = require('express');
const bodyParser = require('body-parser');
const authRoutes = require('./routes/login');
const adminRoutes = require('./routes/admin');
const articleRoutes = require('./routes/article') // 添加这行
const app = express();
// 中间件
app.use(bodyParser.json());
// 路由
app.use('/login', authRoutes); // 登录路由
app.use('/', adminRoutes); // 受保护路由
app.use('/', articleRoutes) // 添加这行
// 启动服务器
const PORT = 3000;
app.listen(PORT, async () => {
console.log(`Server running on http://localhost:${PORT}`);
});
假如 Article 模型里对应的 articles 库存在一条数据:
id: 1
title: hello
content: neo
images: /data/xxx/1.jpg
之后请求 GET /article/1
,如果没登录,那么它应该返回
{
"title": "hello",
"content": "neo"
}
带上 Token 去 GET,则返回
{
"title": "hello",
"content": "neo",
"images": "/data/xxx/1.jpg"
}
2. 前端实现条件渲染
示例:React
API请求函数
async function fetchArticle(id, token = null) {
const headers = token ? { Authorization: `Bearer {token}` } : {};
const response = await fetch(`/api/articles/{id}`, { headers });
return response.json();
}
React组件
import React, { useEffect, useState } from 'react';
function ArticlePage({ articleId, isLoggedIn, token }) {
const [article, setArticle] = useState(null);
useEffect(() => {
fetchArticle(articleId, isLoggedIn ? token : null).then(setArticle);
}, [articleId, isLoggedIn, token]);
if (!article) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{article.title}</h1>
<p>{article.content}</p>
{isLoggedIn && article.images && (
<div>
<h2>Images:</h2>
<img src={article.images} alt="Article" />
</div>
)}
{!isLoggedIn && <p>Login to see the images.</p>}
</div>
);
}
export default ArticlePage;
不过最好不要前端去做,因为抓包等一些原因,有可能会流出来。在一开始,也就是API,不要放出数据,更稳妥 —— 也就是通过中间件。
API 加入注册和注册码校验
我们先创建两个文件,一个是 models/ActivationCode.js
,一个是 routes/register.js
。一个数据库模型,一个注册路由。
以及需要在 models/index.js
追加内容,以暴露 ActivationCode
数据库模型。再在 app.js
挂载 routes/register.js
路由。
数据库模型 models/ActivationCode.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database/database');
const ActivationCode = sequelize.define('activation_code', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
code: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
valid_day: {
type: DataTypes.INTEGER,
allowNull: false,
},
is_permanent: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: false,
},
is_used: {
type: DataTypes.BOOLEAN,
allowNull: true,
},
user_by: {
type: DataTypes.INTEGER,
allowNull: true,
},
}, {
timestamps: true,
});
module.exports = ActivationCode;
关于 数据表模型:
– 邀请码数据模型 user_by
通过 用户ID
来标记。
– is_used
的 1/0 用来表示 注册码 使用与否
路由文件 routes/register.js
const express = require('express');
const { ActivationCode, User } = require('../models');
const bcrypt = require('bcrypt'); // 用于加密密码
const { Op } = require('sequelize'); // 引入 Sequelize 运算符
const router = express.Router();
router.post('/', async (req, res) => {
const { username, password, email, code } = req.body;
// 检查是否提供了激活码
if (!code) {
return res.status(400).json({ message: "The invitation code is empty." });
}
// 下面提供了两个思路
// 用户名 直接用递交过来的 username(用户名) 为条件查询,看看是否存在。
// 激活码 先查询所有的激活码,之后与递交过来的 code(激活码) 做匹配。
try {
// 检查用户名或邮箱是否已存在
const existingUser = await User.findOne({
where: {
[Op.or]: [ // Sequelize 运算符,用于 OR 条件
{ username: username },
{ email: email }
]
}
});
// 如果用户已存在
if (existingUser) {
return res.status(400).json({ message: 'Username or email already exists' });
};
// 查询所有未使用的激活码
const aCode = await ActivationCode.findAll({
attributes: ['code'],
where: { is_used: 0 },
});
// 提取激活码列表
const codes = aCode.map(item => item.code);
// 检查提供的激活码是否在未使用的激活码列表中
if (codes.includes(code)) {
// 匹配成功,返回成功响应
// return res.json({ message: 'DONE' });
// 匹配成功,激活码有效,保存用户信息
const hashedPassword = await bcrypt.hash(password, 10); // 加密密码
const newUser = await User.create({
username,
password: hashedPassword, // 存储加密后的密码
email,
});
// 更新激活码状态为已使用
await ActivationCode.update(
{ is_used: 1 }, // 设置为已使用
{ where: { code: code } }
);
return res.json({
message: 'Successful registration!',
user: { // 返回新用户的相关信息,强化客户端体验
id: newUser.id,
username: newUser.username,
email: newUser.email,
}
});
} else {
// 匹配失败,返回错误信息
return res.status(404).json({ message: "The invitation code is invalid or used." });
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server Error' });
}
});
module.exports = router;
关于代码的话,上面的注释已经够清楚,提供了两个判断思路,但是我认为前者更加合适。
需要注意的是,在逻辑完成后,如果没有状态码(或直接return),而直接 send json 回去的话,代码会继续执行下去的。所以一些 if 完毕后,要返回状态码。不要只是 send。
但是如果没有任何 send 返回,也会导致测试工具(页面) 一直处于加载状态。所以在代码正确地走下去(一路畅通 if )的情况下,也要记得 send / return 数据回去。
对于 非json对象 ,比如说一些普通的 “通告” ,要使用 message
。如:res.json({ message: 'Successful.' });
绑定事务:
我认为这已经不算是 注意 了,应该是 ⚠️警告⚠️
为什么需要警告?因为在创建用户和更新激活码的操作是独立的,如果在保存用户成功后,更新激活码状态失败,会导致激活码仍然可用,但用户已经注册成功。这种情况会产生数据不一致的问题。
使用 Sequelize 的事务(transaction
)确保两个操作要么同时成功,要么同时失败。
路由文件 routes/register.js
– 带绑定事务(这样才对)
// 追加内容
const transaction = await sequelize.transaction(); // 开启事务
try {
// 保存用户信息
const newUser = await User.create({
username,
password: hashedPassword,
email,
}, { transaction });
// 更新激活码状态
await ActivationCode.update(
{ is_used: 1 },
{ where: { code: code }, transaction }
);
await transaction.commit(); // 提交事务
return res.status(201).json({ message: 'User created successfully', user: newUser });
} catch (error) {
await transaction.rollback(); // 回滚事务
console.error(error);
return res.status(500).json({ error: 'Server Error' });
}
整体代码就是:
const express = require('express');
const { ActivationCode, User, sequelize } = require('../models');
const bcrypt = require('bcrypt');
const { Op } = require('sequelize');
const router = express.Router();
router.post('/', async (req, res) => {
const { username, password, email, code } = req.body;
if (!code) {
return res.status(400).json({ message: "The invitation code is empty." });
}
const transaction = await sequelize.transaction();
try {
// 检查用户名或邮箱是否已存在
const existingUser = await User.findOne({
where: {
[Op.or]: [
{ username: username },
{ email: email }
]
}
});
if (existingUser) {
return res.status(409).json({ message: 'Username or email already exists' });
}
// 检查激活码是否有效
const validCode = await ActivationCode.findOne({
where: { code: code, is_used: 0 }
});
if (!validCode) {
return res.status(400).json({ message: "The invitation code is invalid or used." });
}
// 验证密码强度
if (password.length < 8) {
return res.status(400).json({ message: 'Password must be at least 8 characters long' });
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
// 创建用户
const newUser = await User.create({
username,
password: hashedPassword,
email,
}, { transaction });
// 更新激活码状态
await ActivationCode.update(
{ is_used: 1 },
{ where: { code: code }, transaction }
);
await transaction.commit(); // 提交事务
res.status(201).json({
message: 'User created successfully',
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
}
});
} catch (error) {
await transaction.rollback(); // 回滚事务
console.error(error);
res.status(500).json({ error: 'Server Error' });
}
});
module.exports = router;
之后我们需要在 models/index.js
和 app.js
追加些东西
追加模型暴露 models/index.js
const User = require('./User');
const Article = require('./Article');
const ArticleImage = require('./ArticleImage');
const ActivationCode = require('./ActivationCode'); // 后添加
// 用户与文章:一对多关系
User.hasMany(Article, {
foreignKey: 'publisher_id',
onDelete: 'CASCADE',
});
Article.belongsTo(User, {
foreignKey: 'publisher_id',
});
// 文章与图片:一对多关系
Article.hasMany(ArticleImage, {
foreignKey: 'article_id', // 因为每个文章都是唯一的,图片应该和文章的 id 相对应,而非作者id。所以直接填写 article_id,直接使其匹配到 id 上。
onDelete: 'CASCADE',
});
ArticleImage.belongsTo(Article, {
foreignKey: 'article_id',
});
// 后添加 - 激活码功能 // user_by 与 主键ID 绑定
User.hasMany(ActivationCode, {
foreignKey: 'user_by',
onDelete: 'SET NULL',
});
ActivationCode.belongsTo(User, {
foreignKey: 'user_by',
});
module.exports = { User, Article, ArticleImage, ActivationCode }; // 追加暴露
追加路由挂载 app.js
const express = require('express');
const bodyParser = require('body-parser');
const authRoutes = require('./routes/login');
const adminRoutes = require('./routes/admin');
const articleRoutes = require('./routes/article') // 添加这行
const app = express();
// 中间件
app.use(bodyParser.json());
// 路由
app.use('/login', authRoutes); // 登录路由
app.use('/', adminRoutes); // 受保护路由
app.use('/', articleRoutes) // 添加这行
// 启动服务器
const PORT = 3000;
app.listen(PORT, async () => {
console.log(`Server running on http://localhost:${PORT}`);
});
indexRoutes – 主页路由
之后我们写一个主页路由(routes/index.js),显示最近 5 条最新的数据出来,供游客无偿浏览。只提供核心代码,来试试手:
......
.........
try {
const indexData = await Article.findAll({
order: [['id', 'DESC']], // 按 id 降序排列
limit: 5, // 限制为 10 条数据
});
if(!indexData) {
return res.status(404).json({ message: 'Article not found' });
};
res.json(indexData);
...
.........
......
网站基本信息 – 通过读取JSON返回
这里提供一个 读取 JSON 文件来返回 网站详细 信息的方案,如 “标题”,”副标题”,”公告” 等等..
1. 我们接着在 models
文件夹创建一个文件,如 models/siteintro.js
2. 之后创建一个文件夹用来存放 JSON ,如 config/SiteIntro.json
models/siteintro.js
文件内容
const fs = require('fs').promises; // 读取 JSON 文件(异步)
const path = require('path'); // 可有可无,只是方便管理 JSON 路径
const express = require('express');
const router = express.Router();
// 使用 path 管理 JSON 文件路径
const jsonFilePath = path.join(__dirname, '../config/SiteIntro.json');
router.get('/', async (req, res) => {
try {
const data = await fs.readFile(jsonFilePath, 'utf8'); // 读取文件内容
const readData = JSON.parse(data); // 解析 JSON
res.send({ readData }); // 返回解析后的数据
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal server error' });
}
});
module.exports = router;
config/SiteIntro.json
文件内容
{
"title": "xxxx",
"subtitle": "xxxxxx",
"announcement": "Welcome"
}
之后在 app.js
进行挂载
const express = require('express');
const bodyParser = require('body-parser');
const indexData = require('./routes/siteintro') // 追加
...
const cors = require("cors");
...
// 全局启用 CORS
app.use(cors());
// 路由
app.use('/', indexRoutes);
app.use('/data', indexData); // 追加
app.use('/article', articleRoutes);
...
之后访问接口 /data
就会返回数据
{
"readData": {
"title": "xxxx",
"subtitle": "xxxxxx",
"announcement": "Welcome"
}
}
之后前端 fetch 后就可以直接剥洋葱式使用了。
CORS – 无法获取 API 数据问题
或许你在对接前端的时候,会发现访问 API 不被授权/不安全。这是因为 CORS 跨域问题,浏览器把你的流量截断了,通过这个方式可以放行所有的接口。
安装 cors
npm install cors
往 App.js 内追加代码
const express = require('express');
const bodyParser = require('body-parser');
...
const cors = require("cors"); // 追加
const app = express();
...
// 全局启用 CORS
app.use(cors()); // 追加
// 路由
app.use('/', indexRoutes);
...
即可解决。也可以按照文档,自己选择允许哪些 IP/地址 访问。
发表回复