MongoDB: 综合项目:电商评论系统(6 模块)

最后更新:2026-08-26

综合项目是检验学习成果的最佳方式——本课程通过 6 模块实现完整电商评论系统,串联 Phase 1-5 全部知识点。

1. 项目概览

项目:ShopHub 评论系统 架构:Node.js + Express + mongoose + MongoDB + JWT 功能:用户认证、商品管理、嵌套评论、聚合分析、权限控制、Atlas 部署

系统架构设计思路:电商评论系统采用经典三层架构——路由层(URL 映射 + 中间件)、业务层(Controller 处理请求逻辑)、数据层(mongoose Model 定义数据结构与验证)。三层分离使得每一层可以独立测试和修改:改 API 路径不动业务逻辑,改查询方式不动路由定义。

架构选型决策过程

决策点 选项A 选项B 选择 原因
Web 框架 Express Koa / Fastify Express 生态最成熟,教程资源多
ODM mongoose 原生驱动 mongoose Schema 验证 + 中间件 + populate
认证方案 JWT Session JWT 无状态,易扩展,适合 API
评论结构 嵌套文档 引用模式 引用模式(parentId) 灵活深度,避免嵌套限制
部署平台 Atlas + Render 自建 Atlas + Render 托管省运维,适合中小项目

数据建模决策:评论系统选择引用模式(parentId 指向父评论)而非嵌套文档——嵌套文档有 BSON 16MB 限制,且深层回复难以查询和分页。引用模式虽然需要额外查询组装树形结构,但支持无限层级和灵活排序。

100%
graph TB
    Client[浏览器/移动 App] -->|HTTP| Express[Express 服务器]

    subgraph "Express 路由层"
        Express --> AuthMW[auth 路由<br/>注册/登录]
        Express --> ProductMW[products 路由<br/>CRUD]
        Express --> ReviewMW[reviews 路由<br/>评论/点赞]
    end

    subgraph "Controller 层"
        AuthMW --> AuthCtrl[authController]
        ProductMW --> ProdCtrl[productController]
        ReviewMW --> RevCtrl[reviewController]
    end

    subgraph "Model 层"
        AuthCtrl --> UserModel[User Model]
        ProdCtrl --> ProdModel[Product Model]
        RevCtrl --> RevModel[Review Model]
    end

    subgraph "MongoDB 集合"
        UserModel --> Users[(users)]
        ProdModel --> Products[(products)]
        RevModel --> Reviews[(reviews)]
    end

    style Express fill:#d4edda
    style Reviews fill:#cce5ff

数据模型关系

100%
erDiagram
    USER ||--o{ REVIEW : "writes"
    PRODUCT ||--o{ REVIEW : "has"
    REVIEW ||--o{ REVIEW : "parent reply"

    USER {
        ObjectId _id PK
        string email UK
        string username UK
        string passwordHash
        string role
        boolean isActive
    }
    PRODUCT {
        ObjectId _id PK
        string sku UK
        string title
        number price
        string category
        number rating
        number reviewCount
    }
    REVIEW {
        ObjectId _id PK
        ObjectId productId FK
        ObjectId userId FK
        string content
        number rating
        ObjectId parentId FK
        number likeCount
        boolean isApproved
    }

2. 模块 1:项目初始化

架构选型决策过程详解:电商评论系统的技术选型不是"哪个流行选哪个",而是基于约束条件的最优解——1. 选 Express 而非 NestJS:项目规模中小型,NestJS 的装饰器和 DI 增加复杂度但不增加收益;2. 选 mongoose 而非原生驱动:Schema 验证和中间件机制对评论系统至关重要(密码哈希、软删除过滤);3. 选 JWT 而非 Session:API 服务无状态需求,Session 需 Redis 共享存储增加运维成本;4. 选引用模式而非嵌套文档:评论数量不可预测,嵌套文档有 16MB 限制风险。

模块交互原理:六个模块的依赖关系形成清晰的层次——模块 1(初始化)提供基础设施,模块 2(模型)定义数据契约,模块 3(CRUD)实现业务逻辑,模块 4(聚合)添加分析能力,模块 5(安全)加固防护,模块 6(部署)完成上线。每个模块只依赖前序模块,不反向依赖,这使得开发可以迭代推进——先完成模块 1-3 获得可用 API,再逐步添加聚合、安全和部署。

项目初始化的设计思路:项目初始化不仅是"npm init",更是确定项目结构、依赖选择、配置管理策略的过程。良好的项目结构应该反映 MVC 分层:models/ 放数据定义、controllers/ 放业务逻辑、routes/ 放路由映射、middlewares/ 放横切关注点(认证、错误处理)。

目录结构设计原则

目录 职责 依赖方向 测试策略
models/ 数据定义 + 验证 无外部依赖 单元测试
controllers/ 请求处理 + 协调 依赖 models 集成测试
routes/ URL → Controller 映射 依赖 controllers + middlewares 路由测试
middlewares/ 认证/验证/错误处理 依赖 config 单元测试
validators/ joi Schema 定义 无外部依赖 单元测试
utils/ 工具函数 无外部依赖 单元测试

依赖选择理由

依赖 用途 为什么选它
express Web 框架 最成熟,中间件生态丰富
mongoose ODM Schema 验证 + populate + 中间件
jsonwebtoken JWT 认证 无状态认证,适合 API
bcrypt 密码哈希 行业标准,抗彩虹表
joi 输入验证 Schema 风格,可复用
dotenv 环境变量 12-factor app 规范
cors 跨域 API 必备

(1) 项目结构

BASH
shophub-reviews/
├── package.json
├── .env
├── .env.example
├── src/
│   ├── app.js              # Express 应用
│   ├── config/
│   │   └── db.js          # mongoose 连接
│   ├── models/            # 数据模型
│   │   ├── User.js
│   │   ├── Product.js
│   │   └── Review.js
│   ├── controllers/       # 业务逻辑
│   │   ├── authController.js
│   │   ├── productController.js
│   │   └── reviewController.js
│   ├── routes/            # 路由
│   │   ├── auth.js
│   │   ├── products.js
│   │   └── reviews.js
│   ├── middlewares/       # 中间件
│   │   ├── auth.js        # JWT 认证
│   │   ├── errorHandler.js
│   │   └── validate.js
│   ├── validators/        # 请求体验证
│   │   └── schemas.js
│   └── utils/             # 工具
│       ├── logger.js
│       └── jwt.js
└── README.md

(2) package.json

JSON
{
  "name": "shophub-reviews",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest --watch"
  },
  "dependencies": {
    "express": "^4.19.2",
    "mongoose": "^7.6.0",
    "jsonwebtoken": "^9.0.0",
    "bcrypt": "^5.1.0",
    "joi": "^17.13.0",
    "dotenv": "^16.4.0",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "jest": "^29.7.0"
  }
}

(3) .env 文件

BASH
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/shophub
JWT_SECRET=your-super-secret-key-change-in-prod
JWT_EXPIRES_IN=7d

3. 模块 2:用户与商品模型

数据模型设计原则:Schema 设计不是"把表单字段搬到 mongoose",而是要考虑:查询模式(最常用的查询是什么?)、数据关系(哪些实体需要关联?)、性能需求(是否需要索引?字段是否需要 select:false?)、安全需求(密码是否需要隐藏?是否需要软删除?)。

Schema 设计决策

设计决策 选择 原因
密码存储 passwordHash + select:false 默认查询不返回,防止泄露
密码哈希 pre-save 中间件 + bcrypt 自动哈希,业务代码无感知
角色设计 enum + RBAC 三角色:customer/admin/moderator
商品评分 冗余字段 rating + reviewCount 避免每次聚合计算
评论结构 parentId 引用 支持无限嵌套层级
软删除 isDeleted + pre-find 过滤 数据可恢复,合规需求
时间戳 timestamps: true createdAt/updatedAt 自动管理

模型关系与引用方向:Review 引用 User 和 Product(多对一),Review 引用 Review 自身(自引用实现嵌套)。引用方向是"多的一方引用一的一方"——不在 Product 中嵌入 Review 数组(会无限增长),而是 Review 中存 productId。

100%
graph LR
    User1[Alice] -->|userId| R1[Review: "Great!"]
    User2[Bob] -->|userId| R2[Review: "Good"]
    User3[Charlie] -->|userId| R3[Reply: "Thanks!"]
    
    Prod1[Product: Phone] -->|productId| R1
    Prod1 -->|productId| R2
    R1 -->|parentId| R3

    style User1 fill:#cce5ff
    style Prod1 fill:#d4edda
    style R1 fill:#fff3cd

(1) User 模型(含密码哈希中间件)

JAVASCRIPT
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
  },
  username: {
    type: String,
    required: true,
    unique: true,
    minlength: 3,
    maxlength: 30,
    match: [/^[a-zA-Z0-9_]+$/, 'Alphanumeric + underscore only']
  },
  passwordHash: {
    type: String,
    required: true,
    minlength: 60,  // bcrypt hash 长度
    select: false
  },
  role: {
    type: String,
    enum: ['customer', 'admin', 'moderator'],
    default: 'customer'
  },
  isActive: { type: Boolean, default: true }
}, { timestamps: true });

// 密码哈希中间件
UserSchema.pre('save', async function(next) {
  if (!this.isModified('passwordHash')) return next();
  this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
  next();
});

// 验证密码
UserSchema.methods.comparePassword = function(candidate) {
  return bcrypt.compare(candidate, this.passwordHash);
};

module.exports = mongoose.model('User', UserSchema);

(2) Product 模型

JAVASCRIPT
// models/Product.js
const ProductSchema = new mongoose.Schema({
  sku: { type: String, required: true, unique: true, index: true },
  title: { type: String, required: true, maxlength: 200 },
  description: { type: String, maxlength: 5000 },
  price: { type: mongoose.Schema.Types.Decimal128, required: true, min: 0 },
  category: { type: String, enum: ['Electronics', 'Books', 'Clothing', 'Home'], index: true },
  stock: { type: Number, default: 0, min: 0 },
  rating: { type: Number, default: 0, min: 0, max: 5 },
  reviewCount: { type: Number, default: 0 },
  isActive: { type: Boolean, default: true, index: true }
}, { timestamps: true });

ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ title: 'text', description: 'text' });

module.exports = mongoose.model('Product', ProductSchema);

(3) Review 模型(含嵌套回复)

JAVASCRIPT
// models/Review.js
const ReviewSchema = new mongoose.Schema({
  productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true, index: true },
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  content: { type: String, required: true, maxlength: 1000 },
  rating: { type: Number, required: true, min: 1, max: 5 },
  parentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Review', default: null, index: true },
  likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  likeCount: { type: Number, default: 0 },
  isApproved: { type: Boolean, default: true },
  isDeleted: { type: Boolean, default: false }
}, { timestamps: true });

ReviewSchema.index({ productId: 1, createdAt: -1 });
ReviewSchema.pre(/^find/, function(next) {
  this.where({ isDeleted: { $ne: true } });
  next();
});

module.exports = mongoose.model('Review', ReviewSchema);

4. 模块 3:评论 CRUD API(嵌套评论)

嵌套评论设计原理补充:引用模式(parentId)的查询策略需要权衡"查询次数"和"数据完整性"——两次查询法(顶层评论 + 回复)只需 2 次数据库 I/O,但只支持两级嵌套;递归查询法($graphLookup)支持无限层级但性能差。本系统选择两次查询法 + 内存组装,因为:1. 大多数用户只看两层评论(顶层 + 回复);2. 更深层回复可通过"查看更多回复"按需加载;3. 两次查询的性能远优于递归查询。

评论安全策略设计:评论系统面临三类安全威胁——1. 内容安全:垃圾评论、敏感词、广告链接(防护:频率限制 + 敏感词过滤 + 人工审核);2. 权限安全:非作者编辑/删除他人评论(防护:authenticate + userId 匹配);3. 注入安全:$where 注入、$regex DoS(防护:参数化查询 + 正则转义)。安全措施应在中间件层统一实现,而非每个 Controller 重复编写。


**嵌套评论查询策略**:

```mermaid
sequenceDiagram
    participant Client
    participant API as /products/:id/reviews
    participant DB as MongoDB

    Client->>API: GET /products/123/reviews
    API->>DB: 查询顶层评论 (parentId=null)
    DB-->>API: 返回 [R1, R2, R3]
    API->>DB: 查询所有回复 (parentId in [R1,R2,R3]._id)
    DB-->>API: 返回 [R1.1, R1.2, R2.1]
    API->>API: 组装树形结构<br/>R1.replies=[R1.1, R1.2]<br/>R2.replies=[R2.1]
    API-->>Client: 返回树形数据

评论 CRUD API 设计

操作 端点 方法 认证 业务规则
查看评论 /products/:id/reviews GET 分页 + 嵌套回复
发表评论 /products/:id/reviews POST 验证商品存在 + 评分1-5
发表回复 /products/:id/reviews POST 验证父评论存在
编辑评论 /reviews/:id PUT 是(作者) 仅作者可编辑
删除评论 /reviews/:id DELETE 是(作者/admin) 软删除 isDeleted
点赞/取消 /reviews/:id/like POST $addToSet/$pull 切换

点赞设计:使用 $addToSet(幂等添加)+ $pull(移除)而非 push(可能重复)。同时维护 likeCount 冗余字段避免每次 count(likes)。

(1) 创建评论

JAVASCRIPT
// controllers/reviewController.js
exports.createReview = async (req, res) => {
  const { productId } = req.params;
  const { content, rating, parentId } = req.body;

  // 验证商品存在
  const product = await Product.findById(productId);
  if (!product) return res.status(404).json({ error: 'Product not found' });

  // 如果是回复,验证父评论存在
  if (parentId) {
    const parent = await Review.findById(parentId);
    if (!parent) return res.status(404).json({ error: 'Parent review not found' });
  }

  const review = await Review.create({
    productId,
    userId: req.user._id,
    content,
    rating,
    parentId: parentId || null
  });

  // 更新商品评分统计
  await updateProductRating(productId);

  await review.populate('userId', 'username avatar');
  res.status(201).json(review);
};

(2) 嵌套查询

JAVASCRIPT
exports.listReviews = async (req, res) => {
  const { productId } = req.params;
  const { sort = 'createdAt', order = 'desc', limit = 20, page = 1 } = req.query;

  // 查询顶层评论
  const reviews = await Review.find({
    productId,
    parentId: null
  })
    .populate('userId', 'username avatar')
    .sort({ [sort]: order === 'desc' ? -1 : 1 })
    .limit(limit * 1)
    .skip((page - 1) * limit)
    .lean();

  // 查询所有回复
  const reviewIds = reviews.map(r => r._id);
  const replies = await Review.find({
    parentId: { $in: reviewIds }
  })
    .populate('userId', 'username avatar')
    .sort({ createdAt: 1 })
    .lean();

  // 组装树形结构
  const tree = reviews.map(parent => ({
    ...parent,
    replies: replies.filter(r => r.parentId.toString() === parent._id.toString())
  }));

  res.json({ data: tree, page, limit });
};

(3) 点赞功能

JAVASCRIPT
exports.likeReview = async (req, res) => {
  const { reviewId } = req.params;
  const userId = req.user._id;

  const review = await Review.findById(reviewId);
  if (!review) return res.status(404).json({ error: 'Review not found' });

  const alreadyLiked = review.likes.some(id => id.toString() === userId.toString());

  if (alreadyLiked) {
    await Review.updateOne(
      { _id: reviewId },
      { $pull: { likes: userId }, $inc: { likeCount: -1 } }
    );
    return res.json({ liked: false });
  } else {
    await Review.updateOne(
      { _id: reviewId },
      { $addToSet: { likes: userId }, $inc: { likeCount: 1 } }
    );
    return res.json({ liked: true });
  }
};

5. 模块 4:聚合分析

聚合管道设计模式详解:评论系统的聚合需求分为三类——1. 单维度统计(评分分布、评论总数):单管道 $match + $group/$bucket 即可;2. 多维度并行统计(商品详情页的总览+分布+近期评论):必须用 $facet 一次查询返回;3. 跨集合关联统计(热门商品榜含商品信息):$group + $lookup + $unwind 组合。设计原则:先 $match 过滤减少数据量,再 $group 聚合,最后 $sort/$limit 排序限制。

$facet 子管道设计策略:$facet 的每个子管道应尽量精简——1. 总数子管道只需 $count,开销最小;2. 平均值子管道用 $group({ _id: null }),单条输出;3. 分布子管道用 $group({ _id: '$rating' }),输出条数等于值域大小;4. 列表子管道需要 $sort + $limit + $lookup,开销最大应放最后。子管道顺序不影响执行(并行),但影响代码可读性——建议按开销从小到大排列。


**聚合管道设计模式**:

| 统计需求 | 聚合阶段 | 输出 |
|:---------|:---------|:-----|
| 评分分布 | $match → $bucket | [{_id: 5, count: 120}, ...] |
| 商品统计 | $match → $facet | {total, avgRating, distribution, recent} |
| 热门商品 | $match → $group → $sort → $lookup → $limit | [{productId, avgRating, reviewCount}] |

**$facet 的价值**:$facet 允许在同一输入上并行执行多个聚合管道——一次查询返回 total、avgRating、ratingDistribution、recentReviews 四个维度。如果没有 $facet,需要 4 次独立查询。

```mermaid
graph TB
    Input[评论数据流] --> Facet["$facet<br/>多管道并行"]
    
    Facet --> P1["管道1: $count<br/>总数"]
    Facet --> P2["管道2: $group<br/>平均评分"]
    Facet --> P3["管道3: $group + $sort<br/>评分分布"]
    Facet --> P4["管道4: $sort + $limit + $lookup<br/>近期评论(含用户信息)"]
    
    P1 --> Output[合并输出<br/>{total, avg, distribution, recent}]
    P2 --> Output
    P3 --> Output
    P4 --> Output

    style Facet fill:#d4edda
    style Output fill:#cce5ff

$bucket 评分分布原理:$bucket 按 boundaries 将评分分到桶中——boundaries=[1,2,3,4,5,6] 表示 5 个桶:[1,2)、[2,3)、[3,4)、[4,5)、[5,6)。每个桶统计数量和平均值。

(1) 评分分布($bucket)

JAVASCRIPT
exports.getRatingDistribution = async (req, res) => {
  const { productId } = req.params;

  const distribution = await Review.aggregate([
    { $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
    {
      $bucket: {
        groupBy: '$rating',
        boundaries: [1, 2, 3, 4, 5, 6],
        default: 'Other',
        output: {
          count: { $sum: 1 },
          avgHelpful: { $avg: '$likeCount' }
        }
      }
    }
  ]);

  res.json({ data: distribution });
};

(2) 商品统计($facet)

JAVASCRIPT
exports.getProductStats = async (req, res) => {
  const { productId } = req.params;

  const stats = await Review.aggregate([
    { $match: { productId: new mongoose.Types.ObjectId(productId), parentId: null } },
    {
      $facet: {
        total: [{ $count: 'count' }],
        avgRating: [{ $group: { _id: null, avg: { $avg: '$rating' } } }],
        ratingDistribution: [
          { $group: { _id: '$rating', count: { $sum: 1 } } },
          { $sort: { _id: 1 } }
        ],
        recentReviews: [
          { $sort: { createdAt: -1 } },
          { $limit: 5 },
          {
            $lookup: {
              from: 'users',
              localField: 'userId',
              foreignField: '_id',
              as: 'userInfo'
            }
          },
          { $unwind: '$userInfo' },
          {
            $project: {
              content: 1,
              rating: 1,
              createdAt: 1,
              username: '$userInfo.username',
              avatar: '$userInfo.avatar'
            }
          }
        ]
      }
    }
  ]);

  res.json(stats[0]);
};

(3) 热门商品榜

JAVASCRIPT
exports.getTopProducts = async (req, res) => {
  const { limit = 10 } = req.query;

  const topProducts = await Review.aggregate([
    { $match: { parentId: null, isApproved: true } },
    {
      $group: {
        _id: '$productId',
        avgRating: { $avg: '$rating' },
        reviewCount: { $sum: 1 },
        totalLikes: { $sum: '$likeCount' }
      }
    },
    { $sort: { avgRating: -1, reviewCount: -1 } },
    { $limit: limit * 1 },
    {
      $lookup: {
        from: 'products',
        localField: '_id',
        foreignField: '_id',
        as: 'product'
      }
    },
    { $unwind: '$product' },
    {
      $project: {
        sku: '$product.sku',
        title: '$product.title',
        thumbnail: '$product.thumbnail',
        avgRating: 1,
        reviewCount: 1
      }
    }
  ]);

  res.json({ data: topProducts });
};

6. 模块 5:权限与安全

纵深防御原则详解:API 安全不是单点防护而是多层防线——网络层(HTTPS + CORS)防止窃听和跨域滥用;应用层(JWT 认证 + RBAC 授权 + 输入验证)拦截未授权和恶意请求;数据层(参数化查询 + 最小权限数据库用户)防止注入和越权。每层独立防御的意义:任一层被突破不影响其他层的保护——即使 CORS 配置错误,JWT 认证仍能拦截未登录用户;即使 JWT 泄露,RBAC 仍能限制低权限用户的操作范围。

JWT Token 安全最佳实践:1. 密钥强度:用 openssl rand -hex 32 生成 256 位随机密钥;2. 过期时间:7 天(平衡安全与体验,敏感操作可缩短);3. Token 刷新:access token 短过期 + refresh token 长过期(双 Token 机制);4. Token 存储:前端用 httpOnly cookie(防 XSS)而非 localStorage;5. Token 撤销:维护黑名单(Redis SET)或使用短过期时间减少撤销需求。

MERMAIDAPI

**JWT 认证 vs Session 认证**:

| 维度 | JWT | Session |
|:-----|:----|:--------|
| 存储位置 | 客户端(Token) | 服务端(内存/Redis) |
| 扩展性 | 无状态,天然支持分布式 | 需共享 Session 存储 |
| 安全性 | Token 泄露无法即时撤销 | 可即时销毁 Session |
| 性能 | 无服务端查询开销 | 每次查 Session 存储 |
| 过期管理 | exp 声明,无法主动续期 | 可滑动续期 |
| 适用场景 | API / 微服务 | 传统 Web 应用 |

**RBAC 权限模型**:Role-Based Access Control 基于角色分配权限——用户拥有角色,角色拥有权限。本系统三个角色:

| 角色 | 权限 | 操作范围 |
|:-----|:-----|:---------|
| customer | 评论、点赞、编辑自己的评论 | /reviews(自己的) |
| moderator | 审核/删除任意评论 | /reviews(所有) + 审核后台 |
| admin | 管理商品、用户、所有评论 | /products + /users + /reviews(所有) |

**注入防护要点**:MongoDB 注入风险主要来自 $where(执行任意 JS)和无锚定 $regex(DoS 攻击)。防护原则:① 永远用参数化查询而非字符串拼接;② mongoose 自动转义查询值;③ $regex 需转义特殊字符。

```mermaid
sequenceDiagram
    participant Client
    participant Auth as authenticate 中间件
    participant Authorize as authorize 中间件
    participant Controller
    participant DB

    Client->>Auth: 请求 + Bearer Token
    Auth->>Auth: jwt.verify(token, secret)
    alt Token 无效
        Auth-->>Client: 401 Unauthorized
    else Token 有效
        Auth->>Authorize: req.user = {id, role}
        Authorize->>Authorize: roles.includes(req.user.role)?
        alt 无权限
            Authorize-->>Client: 403 Forbidden
        else 有权限
            Authorize->>Controller: 执行业务逻辑
            Controller->>DB: 参数化查询
            DB-->>Client: 200 OK
        end
    end

(1) JWT 中间件

JAVASCRIPT
// middlewares/auth.js
const jwt = require('jsonwebtoken');

exports.authenticate = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

exports.authorize = (...roles) => (req, res, next) => {
  if (!req.user || !roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

(2) 认证 API

JAVASCRIPT
// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');

exports.register = async (req, res) => {
  const { email, username, password } = req.body;

  const existing = await User.findOne({ $or: [{ email }, { username }] });
  if (existing) return res.status(409).json({ error: 'Email or username already exists' });

  const user = await User.create({
    email,
    username,
    passwordHash: password  // pre-save 中间件会哈希
  });

  const token = jwt.sign(
    { id: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  res.status(201).json({ user: { id: user._id, email: user.email, username: user.username }, token });
};

exports.login = async (req, res) => {
  const { email, password } = req.body;

  const user = await User.findOne({ email }).select('+passwordHash');
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const valid = await user.comparePassword(password);
  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });

  const token = jwt.sign(
    { id: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  res.json({ user: { id: user._id, email: user.email, username: user.username, role: user.role }, token });
};

(3) 注入防护

JAVASCRIPT
// ❌ 危险:$where 执行任意 JS
db.reviews.find({ $where: 'this.userId == "' + userId + '"' });

// ✅ 安全:使用参数化查询
db.reviews.find({ userId: new ObjectId(userId) });

// ✅ mongoose 自动转义
const reviews = await Review.find({ userId: userId });

// ❌ 危险:$regex DoS
db.reviews.find({ content: { $regex: req.query.q } });

// ✅ 安全:转义正则特殊字符
function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
db.reviews.find({ content: { $regex: escapeRegex(req.query.q), $options: 'i' } });

7. 模块 6:部署上线

部署架构选型决策:部署方案的选择基于项目规模和团队能力——1. Atlas + Render(本项目选择):零运维启动,适合中小项目和 MVP 阶段,月成本 < $50;2. 自建 MongoDB + Docker:完全可控但需运维能力,适合有 DevOps 团队的中大型项目;3. Kubernetes + Atlas:弹性伸缩 + 托管数据库,适合流量波动大的生产系统。本项目选 Atlas + Render 的核心理由:把有限精力投入业务开发而非运维。

健康检查与自动恢复:生产部署的健康检查不仅是 /healthz 返回 200——它需要检测三层健康:1. 应用层(Express 是否响应);2. 数据库层(MongoDB 是否可达);3. 依赖层(Redis/ES 等是否正常)。健康检查端点被负载均衡器定期调用,连续失败 N 次后自动摘除实例并重启。数据库断连时应返回 503(Service Unavailable)而非 200,避免流量打到不健康实例。


**部署架构**:

```mermaid
graph LR
    Client[用户浏览器] -->|HTTPS| CDN[CDN / 静态资源]
    Client -->|HTTPS| LB[Load Balancer]
    LB -->|HTTP| App1[Render Instance 1<br/>Express App]
    LB -->|HTTP| App2[Render Instance 2<br/>Express App]
    App1 -->|TLS + SRV| Atlas[MongoDB Atlas<br/>3 节点副本集]
    App2 -->|TLS + SRV| Atlas
    Atlas -->| PITR | Backup[自动备份<br/>7天保留]

    style Atlas fill:#d4edda
    style App1 fill:#cce5ff

环境配置策略

环境变量 开发值 生产值 管理方式
NODE_ENV development production .env / 平台设置
MONGODB_URI mongodb://localhost:27017 mongodb+srv://... 平台密钥管理
JWT_SECRET test-secret 256bit 随机串 openssl rand -hex 32
CORS_ORIGIN * https://shophub.example.com .env / 平台设置
PORT 3000 平台分配 默认即可

健康检查设计:/healthz 端点不仅检查 Express 是否在运行,还要检查 MongoDB 是否可达——如果数据库断连,应用应返回 503(Service Unavailable)而非 200,让负载均衡器自动摘除不健康实例。

(1) MongoDB Atlas 配置

BASH
# 1. 创建 Atlas 集群(参考课程 #01)
# 2. 配置 IP 白名单:0.0.0.0/0(开发)或应用服务器 IP
# 3. 创建数据库用户:app_user / <password>
# 4. 获取连接字符串:
mongodb+srv://app_user:<password>@cluster0.mongodb.net/shophub?retryWrites=true&w=majority

(2) 环境变量(生产)

BASH
# .env.production
NODE_ENV=production
PORT=3000
MONGODB_URI=mongodb+srv://app_user:StrongPass@cluster0.mongodb.net/shophub?retryWrites=true&w=majority
JWT_SECRET=<generated-strong-secret-256-bit>
JWT_EXPIRES_IN=7d
CORS_ORIGIN=https://shophub.example.com

(3) 健康检查

JAVASCRIPT
app.get('/healthz', async (req, res) => {
  try {
    const db = mongoose.connection.db;
    await db.admin().command({ ping: 1 });
    res.json({
      status: 'ok',
      uptime: process.uptime(),
      mongo: 'connected',
      timestamp: new Date().toISOString()
    });
  } catch (err) {
    res.status(503).json({ status: 'error', error: err.message });
  }
});

(4) Render / Railway 部署

BASH
# === Render 部署 ===
# 1. 连接 GitHub 仓库
# 2. 设置环境变量(MONGODB_URI, JWT_SECRET 等)
# 3. 设置构建命令:npm install
# 4. 设置启动命令:npm start
# 5. 自动 HTTPS + 部署

# === Railway 部署 ===
railway login
railway init
railway add mongodb  # 一键添加 MongoDB
railway up

(5) 性能优化 checklist

JAVASCRIPT
// === src/app.js 优化版 ===
const mongoose = require('mongoose');

mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 50,
  minPoolSize: 5,
  serverSelectionTimeoutMS: 5000
});

app.use(express.json({ limit: '1mb' }));
app.use(cors({
  origin: process.env.CORS_ORIGIN || '*',
  credentials: true
}));

8. 完整项目演示

项目启动与测试流程:完整项目演示遵循"环境准备→启动服务→测试API→验证功能→部署上线"的流程。先启动 MongoDB 副本集(事务和 Change Streams 需要副本集),再启动 Express 应用,最后用 curl 测试核心 API 端点。

API 测试清单

# 功能 端点 预期状态码
1 注册 POST /api/auth/register 201
2 登录 POST /api/auth/login 200
3 创建商品 POST /api/products 201 (admin)
4 商品列表 GET /api/products 200
5 发表评论 POST /api/products/:id/reviews 201
6 评论列表 GET /api/products/:id/reviews 200
7 点赞 POST /api/reviews/:id/like 200
8 商品统计 GET /api/products/:id/stats 200
9 健康检查 GET /healthz 200
BASH
# === 启动项目 ===
npm install
npm start

# === API 调用示例 ===

# 1. 注册用户
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'

# 2. 登录
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"Pass123!"}'

# 3. 创建评论
curl -X POST http://localhost:3000/api/products/<productId>/reviews \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"content":"Great product!","rating":5}'

# 4. 查看商品统计
curl http://localhost:3000/api/products/<productId>/stats

# 5. 健康检查
curl http://localhost:3000/healthz

▶ 示例 1:嵌套评论 + 点赞功能演示

JAVASCRIPT
// === 场景:Alice 评论商品,Bob 回复 Alice,Charlie 点赞 ===

// 1. Alice 发表 5 星评论
const aliceReview = await Review.create({
  productId: '647f1f77bcf86cd799439011',
  userId: aliceId,
  content: 'Excellent smartphone! The camera quality is outstanding.',
  rating: 5,
  parentId: null  // 顶层评论
});
// 返回: { _id: 'review001', content: '...', rating: 5, likeCount: 0 }

// 2. Bob 回复 Alice
const bobReply = await Review.create({
  productId: '647f1f77bcf86cd799439011',
  userId: bobId,
  content: 'I agree, the camera is amazing!',
  rating: 5,
  parentId: aliceReview._id  // 引用父评论
});
// 返回: { _id: 'review002', parentId: 'review001', content: '...' }

// 3. Charlie 点赞 Alice 的评论
await Review.updateOne(
  { _id: aliceReview._id },
  { $addToSet: { likes: charlieId }, $inc: { likeCount: 1 } }
);
// Alice 的评论: { likeCount: 1, likes: [charlieId] }

// 4. 再次点赞 → 取消
await Review.updateOne(
  { _id: aliceReview._id },
  { $pull: { likes: charlieId }, $inc: { likeCount: -1 } }
);
// Alice 的评论: { likeCount: 0, likes: [] }

// 5. 查询嵌套评论树
const reviews = await Review.find({ productId: '647f...', parentId: null })
  .populate('userId', 'username avatar')
  .lean();
const replies = await Review.find({ parentId: { $in: reviews.map(r => r._id) } })
  .populate('userId', 'username avatar')
  .lean();
const tree = reviews.map(r => ({
  ...r,
  replies: replies.filter(rep => rep.parentId.toString() === r._id.toString())
}));

console.log(JSON.stringify(tree, null, 2));
// [{
//   content: 'Excellent smartphone!...',
//   userId: { username: 'alice', avatar: '...' },
//   replies: [{ content: 'I agree...', userId: { username: 'bob' } }]
// }]

输出:嵌套评论树形结构,Alice 评论 → Bob 回复,Charlie 点赞/取消点赞。

▶ 示例 2:完整 ShopHub 评论系统运行演示

BASH
# === 1. 启动 MongoDB 副本集 ===
docker run -d --name mongo -p 27017:27017 mongo:7.0 --replSet rs0
docker exec mongo mongosh --eval 'rs.initiate()'

# === 2. 启动 Node.js 应用 ===
npm install
npm start

# 输出:
# ✅ MongoDB connected: localhost
# 🚀 Server running on port 3000

# === 3. 测试核心 API ===

# 3.1 用户注册
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","username":"alice","password":"Pass123!"}'

# 返回:
# {
#   "success": true,
#   "user": { "id": "...", "email": "alice@example.com", "username": "alice" },
#   "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }

# 3.2 用户登录
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"Pass123!"}'

# 3.3 创建商品
curl -X POST http://localhost:3000/api/products \
  -H "Authorization: Bearer <admin_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "PHONE-001",
    "title": "Smartphone X",
    "price": 599,
    "category": "Electronics",
    "stock": 50
  }'

# 3.4 商品列表(带分页 + 投影)
curl 'http://localhost:3000/api/products?page=1&limit=20&category=Electronics'

# 返回:
# {
#   "success": true,
#   "data": [{ "sku": "PHONE-001", "title": "Smartphone X", "price": 599, "thumbnail": "...", "rating": 0 }],
#   "meta": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
# }

# 3.5 添加评论
curl -X POST http://localhost:3000/api/products/507f1f77bcf86cd799439021/reviews \
  -H "Authorization: Bearer <user_token>" \
  -H "Content-Type: application/json" \
  -d '{"content":"Great phone!","rating":5}'

# 3.6 点赞评论
curl -X POST http://localhost:3000/api/reviews/507f1f77bcf86cd799439031/like \
  -H "Authorization: Bearer <user_token>"

# 3.7 查看商品统计
curl http://localhost:3000/api/products/507f1f77bcf86cd799439021/stats

# 返回:
# {
#   "total": [{ "count": 1 }],
#   "avgRating": [{ "avg": 5 }],
#   "ratingDistribution": [{ "_id": 5, "count": 1 }],
#   "recentReviews": [{ "content": "Great phone!", "rating": 5, "username": "alice" }]
# }

# 3.8 健康检查
curl http://localhost:3000/healthz

# 返回:
# {
#   "status": "ok",
#   "uptime": 1234,
#   "mongo": "connected",
#   "timestamp": "2026-07-06T10:00:00.000Z"
# }

# === 4. 部署到 Render/Railway ===
git push heroku main
# 自动部署,环境变量 MONGODB_URI 指向 Atlas

# === 5. 性能监控(生产)===
# Datadog APM 自动追踪:
# - mongoose 查询耗时
# - API 响应时间
# - 数据库连接数
# - 慢查询告警

输出:完整电商评论系统从注册、登录、商品管理、评论 CRUD、点赞、统计到健康检查全流程演示,可直接部署到生产环境。

▶ 示例 3:生产部署检查清单 + 监控告警配置

项目开发完成只是第一步——上线前需要逐项检查安全、性能、可靠性,上线后需要监控关键指标并配置告警。本示例提供完整的生产部署检查清单和监控告警配置,确保评论系统稳定运行。

JAVASCRIPT
// === 1. 生产部署检查清单(启动时自动验证)===
async function productionReadinessCheck() {
  const checks = [];

  // 环境变量
  const requiredEnvVars = ['MONGODB_URI', 'JWT_SECRET', 'NODE_ENV', 'PORT'];
  requiredEnvVars.forEach(v => {
    checks.push({
      item: `环境变量 ${v}`,
      status: process.env[v] ? 'PASS' : 'FAIL',
      detail: process.env[v] ? '已设置' : '未设置'
    });
  });
  checks.push({
    item: 'NODE_ENV=production',
    status: process.env.NODE_ENV === 'production' ? 'PASS' : 'WARN',
    detail: `当前值: ${process.env.NODE_ENV || '未设置'}`
  });

  // 数据库连接
  try {
    const adminStatus = await mongoose.connection.db.adminCommand({ ping: 1 });
    checks.push({ item: '数据库连接', status: 'PASS', detail: '连接正常' });

    const serverStatus = await mongoose.connection.db.adminCommand({ serverStatus: 1 });
    checks.push({
      item: '连接池使用率',
      status: serverStatus.connections.current < serverStatus.connections.available * 0.8 ? 'PASS' : 'WARN',
      detail: `${serverStatus.connections.current}/${serverStatus.connections.available}`
    });
  } catch (err) {
    checks.push({ item: '数据库连接', status: 'FAIL', detail: err.message });
  }

  // 索引检查
  const collections = ['products', 'reviews', 'users'];
  for (const coll of collections) {
    const indexes = await mongoose.connection.db.collection(coll).indexes();
    const hasTextIndex = indexes.some(i => Object.values(i.key).includes('text'));
    checks.push({
      item: `${coll} 文本索引`,
      status: hasTextIndex ? 'PASS' : 'WARN',
      detail: hasTextIndex ? '已创建' : '未创建,全文搜索不可用'
    });
  }

  return checks;
}

// === 2. 健康检查端点(供负载均衡器探测)===
app.get('/health', async (req, res) => {
  const start = Date.now();
  const health = {
    status: 'ok',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    checks: {}
  };

  // MongoDB 连接检查
  try {
    await mongoose.connection.db.adminCommand({ ping: 1 });
    health.checks.mongodb = { status: 'up', responseTime: `${Date.now() - start}ms` };
  } catch {
    health.checks.mongodb = { status: 'down' };
    health.status = 'degraded';
  }

  // 内存检查
  const mem = process.memoryUsage();
  health.checks.memory = {
    rss: `${Math.round(mem.rss / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(mem.heapTotal / 1024 / 1024)}MB`,
    status: mem.heapUsed / mem.heapTotal < 0.9 ? 'ok' : 'warn'
  };

  const statusCode = health.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(health);
});

// === 3. Prometheus 指标采集 ===
const client = require('prom-client');
const register = new client.Registry();

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
});
register.registerMetric(httpRequestDuration);

const dbQueryDuration = new client.Histogram({
  name: 'mongodb_query_duration_seconds',
  help: 'MongoDB query duration',
  labelNames: ['collection', 'operation'],
  buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1]
});
register.registerMetric(dbQueryDuration);

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode)
      .observe((Date.now() - start) / 1000);
  });
  next();
});

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

// === 4. 告警规则配置(Prometheus 规则文件)===
const alertRules = `
groups:
  - name: review_system
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
        for: 5m
        labels: { severity: critical }
        annotations: { summary: "5xx 错误率超过 1%" }

      - alert: SlowAPIResponse
        expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
        for: 10m
        labels: { severity: warning }
        annotations: { summary: "API P95 响应时间超过 1 秒" }

      - alert: MongoDBConnectionPoolExhausted
        expr: mongodb_connections{state="current"} / mongodb_connections{state="available"} > 0.8
        for: 5m
        labels: { severity: warning }
        annotations: { summary: "MongoDB 连接池使用率超过 80%" }

      - alert: HighMemoryUsage
        expr: process_heap_bytes / process_max_heap_bytes > 0.9
        for: 5m
        labels: { severity: critical }
        annotations: { summary: "Node.js 内存使用率超过 90%" }
`;

输出:生产部署前自动检查环境变量、数据库连接、索引完整性;/health 端点实时报告服务状态;Prometheus 指标采集 HTTP 延迟和数据库查询耗时;告警规则覆盖错误率、响应时间、连接池、内存四个维度。

部署检查清单的优先级:1. P0(必须通过才能上线)——环境变量齐全、数据库连接正常、JWT 密钥安全、HTTPS 已配置;2. P1(上线后一周内修复)——文本索引已创建、连接池参数合理、限流已开启、日志写入文件;3. P2(持续优化)——未使用索引清理、慢查询优化、缓存命中率提升、监控仪表盘完善;4. 核心原则——"先上线再优化"但不跳过 P0 检查,P0 不过不上线。

❓ 常见问题

Q 项目完成后如何进一步优化?
A Redis 缓存热门商品、Elasticsearch 全文搜索、CDN 静态资源、K8s 容器化部署。
Q 评论系统的反垃圾策略?
A 评论频率限制、敏感词过滤、用户举报机制、人工审核后台。
Q 如何实现评论通知?
A 用 Change Streams 监听评论变化,触发邮件/推送通知。

📖 小节


📝 作业

  1. 基础题(⭐):搭建项目骨架(含 package.json / .env / app.js)。
  2. 基础题(⭐):实现 User + Product + Review 三个 Model。
  3. 进阶题(⭐⭐):实现评论 CRUD(创建、列表、点赞、软删除)。
  4. 进阶题(⭐⭐):实现聚合统计(评分分布 + 商品统计 + 热门榜)。
  5. 挑战题(⭐⭐⭐):完整电商评论系统部署到 Atlas 或 Render,包含 6 个模块全部功能。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏