Node.js + MongoDB 搭建 RESTful API 实战教程
-
- 第一章:引言与概述
-
- [1.1 为什么选择 Node.js 和 MongoDB](#1.1 为什么选择 Node.js 和 MongoDB)
- [1.2 RESTful API 设计原则](#1.2 RESTful API 设计原则)
- [1.3 教程目标与内容概述](#1.3 教程目标与内容概述)
- 第二章:环境搭建与项目初始化
-
- [2.1 开发环境要求](#2.1 开发环境要求)
- [2.2 安装和配置 Node.js](#2.2 安装和配置 Node.js)
- [2.3 安装和配置 MongoDB](#2.3 安装和配置 MongoDB)
- [2.4 项目初始化与结构设计](#2.4 项目初始化与结构设计)
- [第三章:Express 服务器基础搭建](#第三章:Express 服务器基础搭建)
-
- [3.1 创建基本的 Express 服务器](#3.1 创建基本的 Express 服务器)
- [3.2 环境变量配置](#3.2 环境变量配置)
- [第四章:MongoDB 数据库连接与配置](#第四章:MongoDB 数据库连接与配置)
-
- [4.1 数据库连接配置](#4.1 数据库连接配置)
- [4.2 数据库连接优化](#4.2 数据库连接优化)
- [4.3 数据库健康检查中间件](#4.3 数据库健康检查中间件)
- [第五章:数据模型设计与 Mongoose 进阶](#第五章:数据模型设计与 Mongoose 进阶)
-
- [5.1 用户模型设计](#5.1 用户模型设计)
- [5.2 文章模型设计](#5.2 文章模型设计)
- [5.3 评论和点赞模型](#5.3 评论和点赞模型)
第一章:引言与概述
1.1 为什么选择 Node.js 和 MongoDB
在当今的 Web 开发领域,Node.js 和 MongoDB 已经成为构建现代应用程序的首选技术组合。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它采用事件驱动、非阻塞 I/O 模型,使其轻量且高效。MongoDB 是一个基于分布式文件存储的 NoSQL 数据库,由 C++ 语言编写,旨在为 Web 应用提供可扩展的高性能数据存储解决方案。
这个技术栈的优势体现在多个方面。首先,JavaScript 全栈开发使得前后端开发人员能够使用同一种语言,降低了学习成本和上下文切换的开销。其次,JSON 数据格式在两者之间的无缝流转------Node.js 使用 JSON 作为数据交换格式,MongoDB 使用 BSON(Binary JSON)存储数据------这种一致性大大简化了开发流程。第三,非阻塞异步特性让 Node.js 特别适合处理高并发的 I/O 密集型应用,而 MongoDB 的横向扩展能力能够很好地支持这种应用场景。
1.2 RESTful API 设计原则
REST(Representational State Transfer)是一种软件架构风格,而不是标准或协议。它由 Roy Fielding 在 2000 年的博士论文中提出,定义了一组约束和原则,用于创建可扩展、可靠和高效的 Web 服务。
RESTful API 的核心原则包括:
- 无状态性(Stateless):每个请求都包含处理该请求所需的所有信息,服务器不存储客户端的状态信息
- 统一接口(Uniform Interface):使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)和状态码
- 资源导向(Resource-Based):所有内容都被抽象为资源,每个资源有唯一的标识符(URI)
- 表述性(Representation):客户端与服务器交换的是资源的表述,而不是资源本身
- 可缓存性(Cacheable):响应应该被标记为可缓存或不可缓存,以提高性能
- 分层系统(Layered System):客户端不需要知道是否直接连接到最后端的服务器
1.3 教程目标与内容概述
本教程将带领您从零开始构建一个完整的博客平台 API,实现文章的增删改查、用户认证、文件上传、分页查询等核心功能。通过这个实践项目,您将掌握:
- Node.js 和 Express 框架的核心概念和用法
- MongoDB 数据库的设计和操作
- Mongoose ODM 库的高级用法
- RESTful API 的设计原则和最佳实践
- JWT 身份认证和授权机制
- 错误处理、日志记录和性能优化
- API 测试和文档编写
- 项目部署和运维考虑
第二章:环境搭建与项目初始化
2.1 开发环境要求
在开始之前,请确保您的系统满足以下要求:
操作系统要求:
- Windows 7 或更高版本
- macOS 10.10 或更高版本
- Ubuntu 16.04 或更高版本(推荐 LTS 版本)
软件依赖: - Node.js 版本 14.0.0 或更高版本(推荐 LTS 版本)
- MongoDB 版本 4.0 或更高版本
- npm 版本 6.0.0 或更高版本
- Git 版本控制工具
开发工具推荐: - 代码编辑器:Visual Studio Code(推荐)、WebStorm、Sublime Text
- API 测试工具:Postman、Insomnia、Thunder Client(VSCode 扩展)
- 数据库管理工具:MongoDB Compass、Studio 3T
- 命令行工具:Windows Terminal、iTerm2(macOS)、Git Bash
2.2 安装和配置 Node.js
Windows 系统安装:
-
访问 Node.js 官网(https://nodejs.org/)
-
下载 LTS 版本的安装程序
-
运行安装程序,按照向导完成安装
-
安装完成后,打开命令提示符或 PowerShell,验证安装:
bash
node --version
npm --version
macOS 系统安装:
推荐使用 Homebrew 包管理器:
```bash
# 安装 Homebrew(如果尚未安装)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 使用 Homebrew 安装 Node.js
brew install node
Linux(Ubuntu)系统安装:
bash
# 使用 NodeSource 安装脚本
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
2.3 安装和配置 MongoDB
本地安装 MongoDB:
- 访问 MongoDB 官网(https://www.mongodb.com/try/download/community)
- 选择适合您操作系统的版本下载
- 按照官方文档完成安装和配置
使用 MongoDB Atlas(云数据库): - 访问 https://www.mongodb.com/atlas/database
- 注册账号并创建免费集群
- 配置网络访问和白名单
- 获取连接字符串
使用 Docker 运行 MongoDB:
bash
# 拉取 MongoDB 镜像
docker pull mongo:latest
# 运行 MongoDB 容器
docker run --name mongodb -d -p 27017:27017 -v ~/mongo/data:/data/db mongo:latest
# 带认证的启动方式
docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -v ~/mongo/data:/data/db mongo:latest
2.4 项目初始化与结构设计
创建项目目录并初始化:
bash
# 创建项目目录
mkdir blog-api
cd blog-api
# 初始化 npm 项目
npm init -y
# 创建项目目录结构
mkdir -p src/
mkdir -p src/controllers
mkdir -p src/models
mkdir -p src/routes
mkdir -p src/middleware
mkdir -p src/utils
mkdir -p src/config
mkdir -p tests
mkdir -p docs
# 创建基础文件
touch src/app.js
touch src/server.js
touch .env
touch .gitignore
touch README.md
安装项目依赖:
bash
# 生产依赖
npm install express mongoose dotenv bcryptjs jsonwebtoken cors helmet morgan multer express-rate-limit express-validator
# 开发依赖
npm install --save-dev nodemon eslint prettier eslint-config-prettier eslint-plugin-prettier jest supertest mongodb-memory-server
配置 package.json 脚本:
json
{
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write src/**/*.js"
}
}
配置 .gitignore 文件:
# 依赖目录
node_modules/
# 环境变量文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# 日志文件
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 运行时数据
pids/
*.pid
*.seed
*.pid.lock
# 覆盖率目录
coverage/
.nyc_output
# 系统文件
.DS_Store
Thumbs.db
# IDE文件
.vscode/
.idea/
*.swp
*.swo
# 操作系统文件
*.DS_Store
Thumbs.db
第三章:Express 服务器基础搭建
3.1 创建基本的 Express 服务器
首先创建主要的应用文件 src/app.js:
javascript
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
// 导入路由
const postRoutes = require('./routes/posts');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
// 导入中间件
const errorHandler = require('./middleware/errorHandler');
const notFound = require('./middleware/notFound');
const app = express();
// 安全中间件
app.use(helmet());
// CORS 配置
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? process.env.FRONTEND_URL
: 'http://localhost:3000',
credentials: true
}));
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP每15分钟最多100个请求
message: {
error: '请求过于频繁,请稍后再试。',
status: 429
}
});
app.use(limiter);
// 日志记录
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
// 解析请求体
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 静态文件服务
app.use('/uploads', express.static('uploads'));
// 路由配置
app.use('/api/posts', postRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// 健康检查端点
app.get('/api/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development'
});
});
// 404处理
app.use(notFound);
// 错误处理
app.use(errorHandler);
module.exports = app;
创建服务器启动文件 src/server.js:
javascript
const app = require('./app');
const connectDB = require('./config/database');
// 环境变量配置
const PORT = process.env.PORT || 5000;
const NODE_ENV = process.env.NODE_ENV || 'development';
// 优雅关闭处理
const gracefulShutdown = (signal) => {
console.log(`收到 ${signal},开始优雅关闭服务器...`);
process.exit(0);
};
// 启动服务器
const startServer = async () => {
try {
// 连接数据库
await connectDB();
// 启动Express服务器
const server = app.listen(PORT, () => {
console.log(`
🚀 服务器已启动!
📍 环境: ${NODE_ENV}
📍 端口: ${PORT}
📍 时间: ${new Date().toLocaleString()}
📍 健康检查: http://localhost:${PORT}/api/health
`);
});
// 优雅关闭处理
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
gracefulShutdown('unhandledRejection');
});
} catch (error) {
console.error('服务器启动失败:', error);
process.exit(1);
}
};
// 启动应用
startServer();
3.2 环境变量配置
创建 .env 文件:
env
# 服务器配置
NODE_ENV=development
PORT=5000
FRONTEND_URL=http://localhost:3000
# 数据库配置
MONGODB_URI=mongodb://localhost:27017/blog_api
MONGODB_URI_TEST=mongodb://localhost:27017/blog_api_test
# JWT配置
JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
JWT_EXPIRE=7d
JWT_COOKIE_EXPIRE=7
# 文件上传配置
MAX_FILE_UPLOAD=5
FILE_UPLOAD_PATH=./uploads
# 速率限制配置
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
# 邮件配置(可选)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_EMAIL=your_email@gmail.com
SMTP_PASSWORD=your_app_password
FROM_EMAIL=noreply@blogapi.com
FROM_NAME=Blog API
创建环境配置工具 src/config/env.js:
javascript
const Joi = require('joi');
// 环境变量验证规则
const envVarsSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(5000),
MONGODB_URI: Joi.string().required().description('MongoDB连接字符串'),
JWT_SECRET: Joi.string().required().description('JWT密钥'),
JWT_EXPIRE: Joi.string().default('7d').description('JWT过期时间'),
}).unknown().required();
// 验证环境变量
const { value: envVars, error } = envVarsSchema.validate(process.env);
if (error) {
throw new Error(`环境变量配置错误: ${error.message}`);
}
// 导出配置对象
module.exports = {
env: envVars.NODE_ENV,
port: envVars.PORT,
mongoose: {
url: envVars.MONGODB_URI + (envVars.NODE_ENV === 'test' ? '_test' : ''),
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
},
},
jwt: {
secret: envVars.JWT_SECRET,
expire: envVars.JWT_EXPIRE,
},
};
第四章:MongoDB 数据库连接与配置
4.1 数据库连接配置
创建数据库连接文件 src/config/database.js:
javascript
const mongoose = require('mongoose');
const config = require('./env');
const connectDB = async () => {
try {
const conn = await mongoose.connect(config.mongoose.url, config.mongoose.options);
console.log(`
✅ MongoDB连接成功!
📍 主机: ${conn.connection.host}
📍 数据库: ${conn.connection.name}
📍 状态: ${conn.connection.readyState === 1 ? '已连接' : '断开'}
📍 时间: ${new Date().toLocaleString()}
`);
// 监听连接事件
mongoose.connection.on('connected', () => {
console.log('Mongoose已连接到数据库');
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose连接错误:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('Mongoose已断开数据库连接');
});
// 进程关闭时关闭数据库连接
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('Mongoose连接已通过应用终止关闭');
process.exit(0);
});
} catch (error) {
console.error('❌ MongoDB连接失败:', error.message);
process.exit(1);
}
};
module.exports = connectDB;
4.2 数据库连接优化
创建高级数据库配置 src/config/databaseAdvanced.js:
javascript
const mongoose = require('mongoose');
const config = require('./env');
class DatabaseManager {
constructor() {
this.isConnected = false;
this.connection = null;
this.retryAttempts = 0;
this.maxRetryAttempts = 5;
this.retryDelay = 5000; // 5秒
}
async connect() {
try {
// 连接选项配置
const options = {
...config.mongoose.options,
poolSize: 10, // 连接池大小
bufferMaxEntries: 0, // 禁用缓冲
connectTimeoutMS: 10000, // 10秒连接超时
socketTimeoutMS: 45000, // 45秒套接字超时
family: 4, // 使用IPv4
useCreateIndex: true,
useFindAndModify: false
};
this.connection = await mongoose.connect(config.mongoose.url, options);
this.isConnected = true;
this.retryAttempts = 0;
this.setupEventListeners();
return this.connection;
} catch (error) {
console.error('数据库连接失败:', error.message);
if (this.retryAttempts < this.maxRetryAttempts) {
this.retryAttempts++;
console.log(`尝试重新连接 (${this.retryAttempts}/${this.maxRetryAttempts})...`);
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
return this.connect();
} else {
throw new Error(`数据库连接失败,已达到最大重试次数: ${this.maxRetryAttempts}`);
}
}
}
setupEventListeners() {
mongoose.connection.on('connected', () => {
console.log('Mongoose已连接到数据库');
this.isConnected = true;
});
mongoose.connection.on('error', (error) => {
console.error('Mongoose连接错误:', error);
this.isConnected = false;
});
mongoose.connection.on('disconnected', () => {
console.log('Mongoose已断开数据库连接');
this.isConnected = false;
});
mongoose.connection.on('reconnected', () => {
console.log('Mongoose已重新连接到数据库');
this.isConnected = true;
});
}
async disconnect() {
if (this.isConnected) {
await mongoose.disconnect();
this.isConnected = false;
console.log('Mongoose连接已关闭');
}
}
getConnectionStatus() {
return {
isConnected: this.isConnected,
readyState: mongoose.connection.readyState,
host: mongoose.connection.host,
name: mongoose.connection.name,
retryAttempts: this.retryAttempts
};
}
}
// 创建单例实例
const databaseManager = new DatabaseManager();
module.exports = databaseManager;
4.3 数据库健康检查中间件
创建数据库健康检查中间件 src/middleware/dbHealthCheck.js:
javascript
const mongoose = require('mongoose');
const dbHealthCheck = async (req, res, next) => {
try {
const dbState = mongoose.connection.readyState;
// readyState 值说明:
// 0 = disconnected
// 1 = connected
// 2 = connecting
// 3 = disconnecting
if (dbState !== 1) {
return res.status(503).json({
success: false,
error: '数据库连接异常',
details: {
status: dbState,
statusText: ['断开连接', '已连接', '连接中', '断开中'][dbState],
timestamp: new Date().toISOString()
}
});
}
// 执行简单的查询来验证数据库响应
await mongoose.connection.db.admin().ping();
next();
} catch (error) {
res.status(503).json({
success: false,
error: '数据库健康检查失败',
details: {
message: error.message,
timestamp: new Date().toISOString()
}
});
}
};
module.exports = dbHealthCheck;
第五章:数据模型设计与 Mongoose 进阶
5.1 用户模型设计
创建用户模型 src/models/User.js:
javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('../config/env');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, '用户名不能为空'],
unique: true,
trim: true,
minlength: [3, '用户名至少3个字符'],
maxlength: [20, '用户名不能超过20个字符'],
match: [/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线']
},
email: {
type: String,
required: [true, '邮箱不能为空'],
unique: true,
lowercase: true,
trim: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '请输入有效的邮箱地址']
},
password: {
type: String,
required: [true, '密码不能为空'],
minlength: [6, '密码至少6个字符'],
select: false // 默认不返回密码字段
},
role: {
type: String,
enum: ['user', 'author', 'admin'],
default: 'user'
},
avatar: {
type: String,
default: 'default-avatar.png'
},
bio: {
type: String,
maxlength: [500, '个人简介不能超过500个字符'],
default: ''
},
website: {
type: String,
match: [/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, '请输入有效的网址']
},
isVerified: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
lastLogin: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// 虚拟字段:用户的文章
userSchema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'author',
justOne: false
});
// 索引优化
userSchema.index({ email: 1 });
userSchema.index({ username: 1 });
userSchema.index({ createdAt: 1 });
// 密码加密中间件
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// 比较密码方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// 生成JWT令牌方法
userSchema.methods.generateAuthToken = function() {
return jwt.sign(
{
userId: this._id,
role: this.role
},
config.jwt.secret,
{
expiresIn: config.jwt.expire,
issuer: 'blog-api',
audience: 'blog-api-users'
}
);
};
// 获取用户基本信息(不包含敏感信息)
userSchema.methods.getPublicProfile = function() {
const userObject = this.toObject();
delete userObject.password;
delete userObject.__v;
return userObject;
};
// 静态方法:通过邮箱查找用户
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
// 静态方法:通过用户名查找用户
userSchema.statics.findByUsername = function(username) {
return this.findOne({ username: new RegExp('^' + username + '$', 'i') });
};
// 查询中间件:自动过滤已删除的用户
userSchema.pre(/^find/, function(next) {
this.find({ isActive: { $ne: false } });
next();
});
module.exports = mongoose.model('User', userSchema);
5.2 文章模型设计
创建文章模型 src/models/Post.js:
javascript
const mongoose = require('mongoose');
const slugify = require('slugify');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '文章标题不能为空'],
trim: true,
minlength: [5, '文章标题至少5个字符'],
maxlength: [200, '文章标题不能超过200个字符']
},
slug: {
type: String,
unique: true,
lowercase: true
},
content: {
type: String,
required: [true, '文章内容不能为空'],
minlength: [50, '文章内容至少50个字符'],
maxlength: [20000, '文章内容不能超过20000个字符']
},
excerpt: {
type: String,
maxlength: [300, '文章摘要不能超过300个字符']
},
coverImage: {
type: String,
default: 'default-cover.jpg'
},
author: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
tags: [{
type: String,
trim: true,
lowercase: true
}],
category: {
type: String,
required: [true, '文章分类不能为空'],
trim: true,
enum: [
'technology', 'programming', 'design', 'business',
'lifestyle', 'travel', 'food', 'health', 'education'
]
},
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
isFeatured: {
type: Boolean,
default: false
},
viewCount: {
type: Number,
default: 0
},
likeCount: {
type: Number,
default: 0
},
commentCount: {
type: Number,
default: 0
},
readingTime: {
type: Number, // 阅读时间(分钟)
default: 0
},
meta: {
title: String,
description: String,
keywords: [String]
},
publishedAt: Date
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// 虚拟字段:评论
postSchema.virtual('comments', {
ref: 'Comment',
localField: '_id',
foreignField: 'post',
justOne: false
});
// 虚拟字段:点赞用户
postSchema.virtual('likes', {
ref: 'Like',
localField: '_id',
foreignField: 'post',
justOne: false
});
// 索引优化
postSchema.index({ title: 'text', content: 'text' });
postSchema.index({ author: 1, createdAt: -1 });
postSchema.index({ category: 1, status: 1 });
postSchema.index({ tags: 1 });
postSchema.index({ status: 1, publishedAt: -1 });
postSchema.index({ slug: 1 });
// 生成slug中间件
postSchema.pre('save', function(next) {
if (this.isModified('title') && this.title) {
this.slug = slugify(this.title, {
lower: true,
strict: true,
remove: /[*+~.()'"!:@]/g
});
}
next();
});
// 计算阅读时间和摘要中间件
postSchema.pre('save', function(next) {
if (this.isModified('content')) {
// 计算阅读时间(按每分钟200字计算)
const wordCount = this.content.trim().split(/\s+/).length;
this.readingTime = Math.ceil(wordCount / 200);
// 自动生成摘要
if (!this.excerpt) {
this.excerpt = this.content
.replace(/[#*`~>]/g, '') // 移除Markdown标记
.substring(0, 200)
.trim() + '...';
}
}
next();
});
// 发布文章时设置发布时间
postSchema.pre('save', function(next) {
if (this.isModified('status') && this.status === 'published' && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
// 静态方法:获取已发布文章
postSchema.statics.getPublishedPosts = function() {
return this.find({ status: 'published' });
};
// 静态方法:按分类获取文章
postSchema.statics.getPostsByCategory = function(category) {
return this.find({
category: category.toLowerCase(),
status: 'published'
});
};
// 静态方法:搜索文章
postSchema.statics.searchPosts = function(query) {
return this.find({
status: 'published',
$text: { $search: query }
}, { score: { $meta: 'textScore' } })
.sort({ score: { $meta: 'textScore' } });
};
// 实例方法:增加浏览量
postSchema.methods.incrementViews = function() {
this.viewCount += 1;
return this.save();
};
// 查询中间件:自动填充作者信息
postSchema.pre(/^find/, function(next) {
this.populate({
path: 'author',
select: 'username avatar bio'
});
next();
});
module.exports = mongoose.model('Post', postSchema);
5.3 评论和点赞模型
创建评论模型 src/models/Comment.js:
javascript
const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({
content: {
type: String,
required: [true, '评论内容不能为空'],
trim: true,
minlength: [1, '评论内容至少1个字符'],
maxlength: [1000, '评论内容不能超过1000个字符']
},
author: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
post: {
type: mongoose.Schema.ObjectId,
ref: 'Post',
required: true
},
parentComment: {
type: mongoose.Schema.ObjectId,
ref: 'Comment',
default: null
},
likes: {
type: Number,
default: 0
},
isEdited: {
type: Boolean,
default: false
},
isApproved: {
type: Boolean,
default: true
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// 虚拟字段:回复评论
commentSchema.virtual('replies', {
ref: 'Comment',
localField: '_id',
foreignField: 'parentComment',
justOne: false
});
// 索引优化
commentSchema.index({ post: 1, createdAt: -1 });
commentSchema.index({ author: 1 });
commentSchema.index({ parentComment: 1 });
// 保存后更新文章的评论计数
commentSchema.post('save', async function() {
const Post = mongoose.model('Post');
await Post.findByIdAndUpdate(this.post, {
$inc: { commentCount: 1 }
});
});
// 删除后更新文章的评论计数
commentSchema.post('findOneAndDelete', async function(doc) {
if (doc) {
const Post = mongoose.model('Post');
await Post.findByIdAndUpdate(doc.post, {
$inc: { commentCount: -1 }
});
}
});
// 查询中间件:自动填充作者信息
commentSchema.pre(/^find/, function(next) {
this.populate({
path: 'author',
select: 'username avatar'
}).populate({
path: 'replies',
populate: {
path: 'author',
select: 'username avatar'
}
});
next();
});
module.exports = mongoose.model('Comment', commentSchema);
创建点赞模型 src/models/Like.js:
javascript
const mongoose = require('mongoose');
const likeSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
post: {
type: mongoose.Schema.ObjectId,
ref: 'Post',
required: true
},
type: {
type: String,
enum: ['like', 'love', 'laugh', 'wow', 'sad', 'angry'],
default: 'like'
}
}, {
timestamps: true
});
// 复合唯一索引,确保一个用户只能对一篇文章点一次赞
likeSchema.index({ user: 1, post: 1 }, { unique: true });
// 索引优化
likeSchema.index({ post: 1 });
likeSchema.index({ user: 1 });
// 保存后更新文章的点赞计数
likeSchema.post('save', async function() {
const Post = mongoose.model('Post');
await Post.findByIdAndUpdate(this.post, {
$inc: { likeCount: 1 }
});
});
// 删除后更新文章的点赞计数
likeSchema.post('findOneAndDelete', async function(doc) {
if (doc) {
const Post = mongoose.model('Post');
await Post.findByIdAndUpdate(doc.post, {
$inc: { likeCount: -1 }
});
}
});
module.exports = mongoose.model('Like', likeSchema);