现代网页开发入门指南书 – Express(后端)

后端 – 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功能

使用上面的目录结构,使得项目更加清晰,利于维护。但是缺点也很明显,就是东西肉眼可见的多。

不喜欢这样,完全可以把 middlewaresutils 文件夹合并,databasemodels 文件夹合并。极端一点的话,可以把 route 写在一起,甚至是和 app.js 合并,把整个 modelsdatabase 合并为一。。。

缺点就是,不方便维护,你需要在一个文件里面找东西,改代码。会有一种牵一发而动全身的即视感。

不过也比所有东西写在一个 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.jsmodels/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 中,外键绑定是通过以下逻辑实现的:

  1. 主表(users)提供主键:
    • users 表的主键是 id,这是默认的 Sequelize 主键字段。
    • 主表(users)的主键将作为从属表(articles)中外键的目标。
  2. 从属表(articles)包含外键列:
    • articles 表中有一个列 publisher_id,用于存储与 usersid 主键对应的值。
    • articles.publisher_id 是从属表中的外键。
  3. 关联定义:
    • 在 Sequelize 中使用 foreignKey 指定从属表的外键字段名(publisher_id),并将其绑定到主表的主键(默认是 id)。
    • 例如:
    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
  • Article.belongsTo(User)
    • 告诉 Sequelize,Article 表中的 publisher_id 字段引用了 User 表的主键(id)。
    • 每个 Article 只能属于一个 User
  • 这种叫做「一对多关系」。

如果 User 模型没有 publisher_id 字段,而 Article 模型( articles表 )中有一个 publisher_id 外键列,Sequelize 会默认将 User 模型( users表 )的主键 idArticle.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);
    }
})();
核心步骤
  1. 定义模型
    • Article 模型
      • 表名为 article,包含字段 id(主键)、created_at(创建时间)、title(文章标题)、content(文章内容)。
      • 禁用自动生成的时间戳字段(timestamps: false)。
    • ArticleImage 模型
      • 表名为 article_image,包含字段 id(主键)、created_at(创建时间)、article_id(外键,关联到 Articleid)、image_url(图片 URL)。
      • 同样禁用自动生成时间戳字段。
  2. 定义模型关联
    • ArticleArticleImage 通过 article_id 字段建立外键关联。
    • 关系定义
      • Article.hasMany(ArticleImage):表示一个文章可以有多张图片。
      • ArticleImage.belongsTo(Article):表示每张图片属于一个文章。
    • 设置外键约束:
      • onDelete: 'CASCADE':当文章被删除时,自动删除关联的图片。
      • onUpdate: 'CASCADE':当文章的主键被更新时,自动更新关联的外键。
  3. 同步数据库
    • 使用 sequelize.sync({ alter: true }) 创建或更新数据库表结构。
    • 确保模型定义和数据库结构保持一致。
  4. 数据插入和查询
    • 插入一篇文章后,插入与该文章关联的图片。
    • 使用 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 位唯一标识符,用于关联 articlearticle_image 表。
  • 在 Sequelize 中,通过 hasManybelongsTo 关联两个模型,使用 release_level 作为外键字段。
  • 外键的 ON DELETE CASCADEON 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 可以将模型字段与数据库中的字段名(即带空格的或不同的字段名)进行映射。
  • 在模型关联中,通过设置 foreignKeysourceKeytargetKey,可以灵活实现不同字段名的关联逻辑。
  • 数据库中的 Release level 对应模型中的 releaseLevellevel 直接对应模型中的 level

同步数据库

在主入口文件(如 app.jsindex.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.jsindex.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需登录

想要实现这个功能,游客可以看到文章 titlecontent,但 images 需要注册登录才能看到,可以通过后端的权限控制和前端的条件渲染来实现。


我们必须先温习一下 – 什么是中间件?

Express.js 中,中间件是一个函数,用于处理请求(req)和响应(res)之间的逻辑。它还可以通过调用 next() 将控制权传递给下一个中间件。

中间件的用途

  1. 处理请求和响应
    • 修改请求对象(req)或响应对象(res),例如解析请求体、添加用户信息等。
  2. 执行逻辑
    • 验证用户身份(如 verifyToken)。
    • 检查权限。
    • 日志记录。
  3. 控制请求流程
    • 决定请求是否继续(调用 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.jsapp.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/地址 访问。



发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注