Node.js + MongoDB 搭建 RESTful API 实战教程
Node.js + MongoDB 搭建 RESTful API 实战教程
- 第一章:引言与概述
- 1.1 为什么选择 Node.js 和 MongoDB
- 1.2 RESTful API 设计原则
- 1.3 教程目标与内容概述
- 第二章:环境搭建与项目初始化
- 2.1 开发环境要求
- 2.2 安装和配置 Node.js
- 2.3 安装和配置 MongoDB
- 2.4 项目初始化与结构设计
- 第三章:Express 服务器基础搭建
- 3.1 创建基本的 Express 服务器
- 3.2 环境变量配置
- 第四章:MongoDB 数据库连接与配置
- 4.1 数据库连接配置
- 4.2 数据库连接优化
- 4.3 数据库健康检查中间件
- 第五章:数据模型设计与 Mongoose 进阶
- 5.1 用户模型设计
- 5.2 文章模型设计
- 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,验证安装:
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)系统安装:
# 使用 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:
# 拉取 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 项目初始化与结构设计
创建项目目录并初始化:
# 创建项目目录
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
安装项目依赖:
# 生产依赖
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 脚本:
{"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:
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:
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 文件:
# 服务器配置
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:
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:
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:
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, // 使用IPv4useCreateIndex: 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:
const mongoose = require('mongoose');const dbHealthCheck = async (req, res, next) => {try {const dbState = mongoose.connection.readyState;// readyState 值说明:// 0 = disconnected// 1 = connected// 2 = connecting// 3 = disconnectingif (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:
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:
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:
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:
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);