一、回顾与本篇目标
前面九篇,我们一步步学习了后端开发的所有核心知识:
- 用 Node.js 和 Express 启动 HTTP 服务
- 定义路由处理不同的 URL 和请求方法
- 用 MySQL 数据库持久化存储数据
- 用 bcrypt 哈希加密保护用户密码
- 用 JWT 实现登录认证
这些知识点就像一堆零件。这一篇,我们要把它们组装成一个完整的项目——一个留言板后端。用户可以注册、登录、发表留言、查看留言列表、删除自己的留言。
这不是一个教学玩具。这是一个结构完整、可以直接部署的小型后端服务。跟下来,你就能说自己“独立完成过一个完整的后端项目”。
二、项目需求分析
动手写代码之前,先把需求列清楚。一个好的习惯是:先想清楚做什么,再动手。
功能列表:
- 用户注册:填写用户名、邮箱、密码,注册成功后自动登录。
- 用户登录:填写邮箱和密码,登录成功返回 Token。
- 获取当前用户信息:通过 Token 识别用户,返回其基本信息。
- 发表留言:登录用户可以发表留言,内容不能为空。
- 查看留言列表:任何人都可以查看所有留言,显示留言内容、作者、时间。
- 删除留言:只有留言的作者本人可以删除自己的留言。
技术选型:
- 运行环境:Node.js
- 框架:Express
- 数据库:MySQL
- 密码加密:bcrypt
- 用户认证:JWT
三、数据库设计
留言板需要两张表:用户表和留言表。
用户表 users
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT AUTO_INCREMENT PRIMARY KEY | 用户 ID,主键,自增 |
| name | VARCHAR(100) NOT NULL | 用户名 |
| VARCHAR(200) NOT NULL UNIQUE | 邮箱,唯一 | |
| password | VARCHAR(255) NOT NULL | 密码的 bcrypt 哈希值 |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 注册时间 |
留言表 messages
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT AUTO_INCREMENT PRIMARY KEY | 留言 ID,主键,自增 |
| content | TEXT NOT NULL | 留言内容 |
| user_id | INT NOT NULL | 留言作者的 ID,关联 users 表 |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 留言时间 |
在 MySQL 命令行中执行以下 SQL:
CREATE DATABASE IF NOT EXISTS message_board;
USE message_board;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE messages (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT NOT NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
新知识点:外键。最后一行 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 的意思是:user_id 的值必须在 users 表的 id 列中存在。如果某个用户被删除了,他所有的留言也会自动删除(ON DELETE CASCADE)。这保证了数据的一致性——不会出现一条留言的作者是一个已删除的用户。
四、项目结构
message-board/
├── .env ← 环境变量(数据库密码、JWT 密钥等)
├── package.json ← 项目配置
├── app.js ← 主入口文件
├── db.js ← 数据库连接
├── authMiddleware.js ← JWT 认证中间件
└── routes/
├── users.js ← 用户路由(注册、登录、获取当前用户)
└── messages.js ← 留言路由(发表、查看、删除)
五、环境准备
第 1 步:初始化项目并安装依赖
mkdir message-board
cd message-board
npm init -y
npm install express mysql2 bcrypt jsonwebtoken dotenv
第 2 步:创建 .env 文件
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=你的数据库密码
DB_NAME=message_board
JWT_SECRET=change-this-to-a-random-string-in-production
PORT=3000
第 3 步:创建数据库连接模块 db.js
// db.js
const mysql = require('mysql2');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
});
module.exports = pool.promise();
六、编写用户路由 routes/users.js
// routes/users.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const db = require('../db.js');
const router = express.Router();
// 注册
router.post('/register', async function (request, response) {
try {
let { name, email, password } = request.body;
if (!name || !email || !password) {
return response.status(400).json({ error: '缺少必填字段:name、email、password' });
}
// 检查邮箱是否已注册
let checkResult = await db.query('SELECT id FROM users WHERE email = ?', [email]);
if (checkResult[0].length > 0) {
return response.status(409).json({ error: '该邮箱已被注册' });
}
// 哈希加密密码
let hashedPassword = await bcrypt.hash(password, 10);
// 存入数据库
let result = await db.query(
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
[name, email, hashedPassword]
); let userId = result[0].insertId; // 注册成功后直接生成 Token(即“注册即登录”) let token = jwt.sign( { userId: userId, email: email }, process.env.JWT_SECRET, { expiresIn: ‘7d’ } ); response.status(201).json({ message: ‘注册成功’, token: token, user: { id: userId, name: name, email: email } }); } catch (error) { console.error(‘注册失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // 登录 router.post(‘/login’, async function (request, response) { try { let { email, password } = request.body; if (!email || !password) { return response.status(400).json({ error: ‘缺少邮箱或密码’ }); } let result = await db.query(‘SELECT * FROM users WHERE email = ?’, [email]); if (result[0].length === 0) { return response.status(401).json({ error: ‘邮箱或密码错误’ }); } let user = result[0][0]; let isPasswordCorrect = await bcrypt.compare(password, user.password); if (!isPasswordCorrect) { return response.status(401).json({ error: ‘邮箱或密码错误’ }); } let token = jwt.sign( { userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: ‘7d’ } ); response.json({ message: ‘登录成功’, token: token, user: { id: user.id, name: user.name, email: user.email } }); } catch (error) { console.error(‘登录失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // 获取当前用户信息(需要登录) router.get(‘/me’, require(‘../authMiddleware.js’), async function (request, response) { try { let result = await db.query( ‘SELECT id, name, email, created_at FROM users WHERE id = ?’,
[request.user.userId]
); if (result[0].length === 0) { return response.status(404).json({ error: ‘用户不存在’ }); } response.json(result[0][0]); } catch (error) { console.error(‘获取用户信息失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); module.exports = router;
七、编写认证中间件 authMiddleware.js
// authMiddleware.js
const jwt = require('jsonwebtoken');
function authMiddleware(request, response, next) {
let authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return response.status(401).json({ error: '未登录,请先登录' });
}
let token = authHeader.split(' ')[1];
try {
let decoded = jwt.verify(token, process.env.JWT_SECRET);
request.user = decoded;
next();
} catch (error) {
return response.status(401).json({ error: 'Token 无效或已过期,请重新登录' });
}
}
module.exports = authMiddleware;
八、编写留言路由 routes/messages.js
// routes/messages.js
const express = require('express');
const db = require('../db.js');
const authMiddleware = require('../authMiddleware.js');
const router = express.Router();
// 获取所有留言(不需要登录)
router.get('/', async function (request, response) {
try {
// JOIN 查询:把留言表和用户表关联,获取作者名字
let result = await db.query(
`SELECT messages.id, messages.content, messages.created_at,
users.id AS user_id, users.name AS user_name
FROM messages
JOIN users ON messages.user_id = users.id
ORDER BY messages.created_at DESC`
);
response.json(result[0]);
} catch (error) {
console.error('获取留言失败:', error.message);
response.status(500).json({ error: '服务器内部错误' });
}
});
// 发表留言(需要登录)
router.post('/', authMiddleware, async function (request, response) {
try {
let { content } = request.body;
if (!content || content.trim() === '') {
return response.status(400).json({ error: '留言内容不能为空' });
}
let result = await db.query(
'INSERT INTO messages (content, user_id) VALUES (?, ?)',
[content.trim(), request.user.userId]
); response.status(201).json({ message: ‘留言发表成功’, messageId: result[0].insertId }); } catch (error) { console.error(‘发表留言失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // 删除留言(只能删除自己的) router.delete(‘/:id’, authMiddleware, async function (request, response) { try { let messageId = Number(request.params.id); // 先查出这条留言的作者是谁 let checkResult = await db.query( ‘SELECT user_id FROM messages WHERE id = ?’, [messageId] ); if (checkResult[0].length === 0) { return response.status(404).json({ error: ‘留言不存在’ }); } let message = checkResult[0][0]; // 检查是不是留言作者本人 if (message.user_id !== request.user.userId) { return response.status(403).json({ error: ‘无权删除他人的留言’ }); } await db.query(‘DELETE FROM messages WHERE id = ?’, [messageId]); response.json({ message: ‘留言已删除’ }); } catch (error) { console.error(‘删除留言失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); module.exports = router;
这段代码中有两个新知识点:
- JOIN 查询:
JOIN users ON messages.user_id = users.id把留言表和用户表通过user_id关联起来。这样一条查询就能同时拿到留言内容和作者的用户名,不需要先查留言再逐个查作者。这是关系型数据库的核心能力。 - 权限检查:删除留言时,先查出留言的
user_id,然后和当前登录用户的 ID 对比。如果不一样,返回 403 Forbidden(有登录身份但没有操作权限)。这是后端安全的另一个重要概念——认证是知道你是谁,授权是确定你能做什么。
九、编写主入口 app.js
// app.js
require('dotenv').config();
const express = require('express');
const app = express();
// 中间件
app.use(express.json());
// 路由
app.use('/api/users', require('./routes/users.js'));
app.use('/api/messages', require('./routes/messages.js'));
// 404 兜底
app.use(function (request, response) {
response.status(404).json({ error: '接口不存在' });
});
// 启动服务
let port = process.env.PORT || 3000;
app.listen(port, function () {
console.log('留言板后端已启动,端口:' + port);
console.log('');
console.log('接口列表:');
console.log(' POST /api/users/register 注册');
console.log(' POST /api/users/login 登录');
console.log(' GET /api/users/me 获取当前用户');
console.log(' GET /api/messages 获取留言列表');
console.log(' POST /api/messages 发表留言(需登录)');
console.log(' DELETE /api/messages/:id 删除留言(需本人)');
});
十、完整测试流程
用 Postman 按以下顺序测试全部六个接口:
1. 注册用户 A
POST http://localhost:3000/api/users/register
{
"name": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}
复制返回的 Token,记为 TokenA。
2. 注册用户 B
{
"name": "李四",
"email": "lisi@example.com",
"password": "123456"
}
复制返回的 Token,记为 TokenB。
3. 用户 A 发表留言
POST http://localhost:3000/api/messages
Headers: Authorization: Bearer TokenA
{
"content": "大家好,我是张三,这是我第一条留言!"
}
再发一条:
{
"content": "今天天气真好,适合写代码。"
}
4. 用户 B 发表留言
POST http://localhost:3000/api/messages
Headers: Authorization: Bearer TokenB
{
"content": "我是李四,很高兴认识大家。"
}
5. 查看留言列表(无需登录)
GET http://localhost:3000/api/messages
返回三条留言,每条都包含作者名字、内容和时间,按时间倒序排列。
6. 用户 A 尝试删除用户 B 的留言(应该失败)
DELETE http://localhost:3000/api/messages/3(假设李四的留言 ID 是 3)
Headers: Authorization: Bearer TokenA
返回 403:“无权删除他人的留言”。
7. 用户 B 删除自己的留言(应该成功)
DELETE http://localhost:3000/api/messages/3
Headers: Authorization: Bearer TokenB
返回“留言已删除”。
8. 不带 Token 发表留言(应该失败)
POST http://localhost:3000/api/messages(不设置 Authorization 头)
返回 401:“未登录,请先登录”。
十一、本篇动手练习
练习 1:增加“编辑留言”功能
新增 PUT /api/messages/:id 接口,允许用户修改自己的留言内容。要求验证:只有作者本人才能编辑。修改后更新 created_at 为当前时间,或者新增一个 updated_at 字段。
练习 2:增加分页功能
修改 GET /api/messages,支持 ?page=1&limit=10 查询参数。使用 SQL 的 LIMIT 和 OFFSET 实现分页。返回数据的同时,也返回总页数或总留言数。
练习 3:增加“点赞”功能
新增一张 likes 表(字段:id、user_id、message_id),实现点赞和取消点赞。一个用户对同一条留言只能点赞一次。获取留言列表时同时返回每条留言的点赞数。
十二、本篇小结
这一篇我们完成了一个完整的留言板后端项目,综合运用了前面九篇学到的知识:
- 项目初始化:npm init、安装依赖、环境变量配置。
- 数据库设计:两张表 + 外键约束。
- 用户系统:注册(bcrypt 加密)、登录(JWT 签发)、Token 认证中间件。
- CRUD 操作:发表留言、查看留言列表(JOIN 查询)、删除留言。
- 权限控制:认证(是否登录)和授权(是否有权操作)的区别。403 表示已登录但无权。
- RESTful 设计:GET 获取、POST 创建、DELETE 删除,资源路径清晰。
这个留言板虽然功能不复杂,但它是你第一个从零搭建的完整后端项目。它具备了生产级后端服务的基本骨架——数据库、用户认证、权限控制、RESTful API。把这个项目吃透,你就可以自信地说自己能用 Node.js 做后端开发了。
下一篇预告
下一篇——《部署上线——让你的后端真正跑在云端》:代码写好了,但只有你电脑上能访问。我们会把它部署到云服务器上,让任何人都能通过公网访问。涉及:云服务器选购、环境搭建、代码上传、进程管理(PM2)、域名配置。这是从“学习阶段”到“上线交付”的最后一步。
后端零基础入门,每周更新。













暂无评论内容