Node.js + MongoDB 搭建 RESTful API 实战教程

发布于:2025-08-29 ⋅ 阅读:(25) ⋅ 点赞:(0)

第一章:引言与概述

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,实现文章的增删改查、用户认证、文件上传、分页查询等核心功能。通过这个实践项目,您将掌握:

  1. Node.js 和 Express 框架的核心概念和用法
  2. MongoDB 数据库的设计和操作
  3. Mongoose ODM 库的高级用法
  4. RESTful API 的设计原则和最佳实践
  5. JWT 身份认证和授权机制
  6. 错误处理、日志记录和性能优化
  7. API 测试和文档编写
  8. 项目部署和运维考虑

第二章:环境搭建与项目初始化

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 系统安装:

  1. 访问 Node.js 官网(https://nodejs.org/)
  2. 下载 LTS 版本的安装程序
  3. 运行安装程序,按照向导完成安装
  4. 安装完成后,打开命令提示符或 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:

  1. 访问 MongoDB 官网(https://www.mongodb.com/try/download/community)
  2. 选择适合您操作系统的版本下载
  3. 按照官方文档完成安装和配置
    使用 MongoDB Atlas(云数据库):
  4. 访问 https://www.mongodb.com/atlas/database
  5. 注册账号并创建免费集群
  6. 配置网络访问和白名单
  7. 获取连接字符串
    使用 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, // 使用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:

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:

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);

网站公告

今日签到

点亮在社区的每一天
去签到