Skip to content

Feathers.js + MongoDB 实战 - 构建灵活的文档数据库应用

发布时间:2024-06-12
作者:一介布衣
标签:Feathers.js, MongoDB, 文档数据库, NoSQL

前言

上一篇文章我们学习了 Feathers.js 的适配器系统,今天咱们来深入实战 MongoDB 适配器。说实话,MongoDB 和 Feathers.js 真的是天作之合,一个提供灵活的文档存储,一个提供优雅的 API 抽象,两者结合起来开发效率特别高。

我记得第一次用 MongoDB 的时候,被它的灵活性震撼了:不需要预定义表结构,可以存储复杂的嵌套对象,查询功能强大。但是直接用 MongoDB 的原生驱动写代码还是挺繁琐的。后来用了 Feathers.js 的 MongoDB 适配器,发现原来可以这么简单优雅地操作 MongoDB。

今天我就带大家深入学习如何用 Feathers.js 构建一个功能完整的 MongoDB 应用。

MongoDB 环境搭建

1. 安装 MongoDB

bash
# 使用 Docker 快速启动 MongoDB
docker run -d \
  --name mongodb \
  -p 27017:27017 \
  -e MONGO_INITDB_ROOT_USERNAME=admin \
  -e MONGO_INITDB_ROOT_PASSWORD=password \
  -v mongodb_data:/data/db \
  mongo:latest

# 或者使用 Docker Compose
# docker-compose.yml
version: '3.8'
services:
  mongodb:
    image: mongo:latest
    container_name: mongodb
    restart: unless-stopped
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
    volumes:
      - mongodb_data:/data/db
      - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro

volumes:
  mongodb_data:

2. 安装依赖

bash
# 安装 Feathers.js MongoDB 适配器
npm install @feathersjs/mongodb mongodb

# 安装其他相关依赖
npm install mongoose  # 如果需要使用 Mongoose
npm install @faker-js/faker  # 生成测试数据

3. 连接配置

javascript
// config/default.json
{
  "mongodb": "mongodb://admin:password@localhost:27017/feathers_blog?authSource=admin",
  "paginate": {
    "default": 10,
    "max": 50
  }
}

// src/mongodb.js
const { MongoClient } = require('mongodb');

module.exports = function (app) {
  const connection = app.get('mongodb');
  const database = connection.substr(connection.lastIndexOf('/') + 1).split('?')[0];
  
  const mongoClient = MongoClient.connect(connection, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    maxPoolSize: 10,
    serverSelectionTimeoutMS: 5000,
    socketTimeoutMS: 45000,
  }).then(client => {
    console.log('MongoDB 连接成功');
    return client.db(database);
  }).catch(error => {
    console.error('MongoDB 连接失败:', error);
    throw error;
  });

  app.set('mongoClient', mongoClient);
};

文档模型设计

1. 用户模型

javascript
// src/models/users.model.js
module.exports = function (app) {
  const mongoClient = app.get('mongoClient');
  
  // 创建索引
  mongoClient.then(db => {
    const collection = db.collection('users');
    
    // 创建索引
    collection.createIndex({ email: 1 }, { unique: true });
    collection.createIndex({ username: 1 }, { unique: true });
    collection.createIndex({ 'profile.location': '2dsphere' });  // 地理位置索引
    collection.createIndex({ tags: 1 });  // 数组字段索引
    collection.createIndex({ 
      'profile.firstName': 'text', 
      'profile.lastName': 'text',
      'profile.bio': 'text'
    });  // 全文搜索索引
  });

  return mongoClient;
};

// 用户文档结构示例
const userDocument = {
  _id: ObjectId("..."),
  email: "john@example.com",
  username: "john_doe",
  password: "hashed_password",
  
  // 嵌套的个人资料
  profile: {
    firstName: "John",
    lastName: "Doe",
    avatar: "https://example.com/avatar.jpg",
    bio: "Full-stack developer passionate about JavaScript",
    birthDate: new Date("1990-01-01"),
    
    // 地理位置
    location: {
      type: "Point",
      coordinates: [-73.97, 40.77]  // [longitude, latitude]
    },
    
    // 社交媒体链接
    social: {
      twitter: "@johndoe",
      github: "johndoe",
      linkedin: "john-doe"
    }
  },
  
  // 用户偏好设置
  preferences: {
    theme: "dark",
    language: "en",
    timezone: "America/New_York",
    notifications: {
      email: true,
      push: true,
      sms: false
    }
  },
  
  // 标签数组
  tags: ["javascript", "nodejs", "react", "mongodb"],
  
  // 角色和权限
  roles: ["user"],
  permissions: ["read:posts", "write:own:posts"],
  
  // 统计信息
  stats: {
    postsCount: 0,
    followersCount: 0,
    followingCount: 0,
    lastLoginAt: new Date()
  },
  
  // 时间戳
  createdAt: new Date(),
  updatedAt: new Date()
};

2. 博客文章模型

javascript
// src/models/posts.model.js
module.exports = function (app) {
  const mongoClient = app.get('mongoClient');
  
  mongoClient.then(db => {
    const collection = db.collection('posts');
    
    // 创建索引
    collection.createIndex({ authorId: 1 });
    collection.createIndex({ status: 1 });
    collection.createIndex({ publishedAt: -1 });
    collection.createIndex({ 'category.slug': 1 });
    collection.createIndex({ tags: 1 });
    collection.createIndex({ 
      title: 'text', 
      content: 'text',
      excerpt: 'text'
    });  // 全文搜索
  });

  return mongoClient;
};

// 文章文档结构
const postDocument = {
  _id: ObjectId("..."),
  title: "深入理解 MongoDB 聚合管道",
  slug: "understanding-mongodb-aggregation-pipeline",
  excerpt: "MongoDB 聚合管道是一个强大的数据处理工具...",
  content: "完整的文章内容...",
  
  // 作者信息(引用)
  authorId: ObjectId("..."),
  
  // 嵌套的分类信息
  category: {
    _id: ObjectId("..."),
    name: "数据库技术",
    slug: "database"
  },
  
  // 标签数组
  tags: ["mongodb", "database", "aggregation", "nosql"],
  
  // 文章状态
  status: "published",  // draft, published, archived
  
  // 特色图片
  featuredImage: {
    url: "https://example.com/image.jpg",
    alt: "MongoDB 聚合管道示意图",
    caption: "聚合管道的工作流程"
  },
  
  // SEO 信息
  seo: {
    metaTitle: "深入理解 MongoDB 聚合管道 - 完整指南",
    metaDescription: "学习 MongoDB 聚合管道的核心概念和实战应用",
    keywords: ["mongodb", "aggregation", "pipeline", "database"]
  },
  
  // 统计信息
  stats: {
    viewCount: 1250,
    likeCount: 89,
    commentCount: 23,
    shareCount: 15
  },
  
  // 评论(嵌套文档数组)
  comments: [
    {
      _id: ObjectId("..."),
      authorId: ObjectId("..."),
      content: "很棒的文章!",
      createdAt: new Date(),
      replies: [
        {
          _id: ObjectId("..."),
          authorId: ObjectId("..."),
          content: "同意!",
          createdAt: new Date()
        }
      ]
    }
  ],
  
  // 版本控制
  version: 1,
  revisions: [
    {
      version: 1,
      content: "初始版本内容",
      updatedAt: new Date(),
      updatedBy: ObjectId("...")
    }
  ],
  
  // 时间戳
  createdAt: new Date(),
  updatedAt: new Date(),
  publishedAt: new Date()
};

MongoDB 服务实现

1. 用户服务

javascript
// src/services/users/users.class.js
const { MongoDBService } = require('@feathersjs/mongodb');
const { ObjectId } = require('mongodb');

class UsersService extends MongoDBService {
  constructor(options, app) {
    super(options, app);
    this.app = app;
  }

  async find(params) {
    const { query = {} } = params;
    
    // 构建聚合管道
    const pipeline = [];
    
    // 匹配阶段
    const matchStage = {};
    
    // 处理基础查询
    Object.keys(query).forEach(key => {
      if (!key.startsWith('$') && key !== 'search' && key !== 'near') {
        matchStage[key] = query[key];
      }
    });
    
    if (Object.keys(matchStage).length > 0) {
      pipeline.push({ $match: matchStage });
    }
    
    // 全文搜索
    if (query.search) {
      pipeline.unshift({
        $match: {
          $text: { $search: query.search }
        }
      });
      
      // 添加搜索分数
      pipeline.push({
        $addFields: {
          searchScore: { $meta: 'textScore' }
        }
      });
    }
    
    // 地理位置查询
    if (query.near) {
      const { coordinates, maxDistance = 10000 } = query.near;
      pipeline.unshift({
        $geoNear: {
          near: {
            type: 'Point',
            coordinates: coordinates
          },
          distanceField: 'distance',
          maxDistance: maxDistance,
          spherical: true
        }
      });
    }
    
    // 添加计算字段
    pipeline.push({
      $addFields: {
        fullName: {
          $concat: ['$profile.firstName', ' ', '$profile.lastName']
        },
        isActive: {
          $gt: ['$stats.lastLoginAt', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)]
        }
      }
    });
    
    // 排序
    if (query.$sort) {
      pipeline.push({ $sort: query.$sort });
    } else if (query.search) {
      pipeline.push({ $sort: { searchScore: { $meta: 'textScore' } } });
    } else {
      pipeline.push({ $sort: { createdAt: -1 } });
    }
    
    // 分页
    if (query.$skip) {
      pipeline.push({ $skip: parseInt(query.$skip) });
    }
    
    if (query.$limit) {
      pipeline.push({ $limit: parseInt(query.$limit) });
    }
    
    // 字段选择
    if (query.$select) {
      const projection = {};
      query.$select.forEach(field => {
        projection[field] = 1;
      });
      pipeline.push({ $project: projection });
    } else {
      // 默认排除敏感字段
      pipeline.push({
        $project: {
          password: 0,
          'preferences.notifications': 0
        }
      });
    }
    
    // 执行聚合查询
    const [data, totalResult] = await Promise.all([
      this.Model.aggregate(pipeline).toArray(),
      this.getAggregationCount(pipeline.slice(0, -3))  // 排除分页和投影阶段
    ]);
    
    const total = totalResult[0]?.count || 0;
    
    return {
      total,
      limit: query.$limit || total,
      skip: query.$skip || 0,
      data
    };
  }

  async getAggregationCount(pipeline) {
    const countPipeline = [
      ...pipeline,
      { $count: 'count' }
    ];
    
    return this.Model.aggregate(countPipeline).toArray();
  }

  async create(data, params) {
    // 数据预处理
    const userData = {
      ...data,
      createdAt: new Date(),
      updatedAt: new Date(),
      stats: {
        postsCount: 0,
        followersCount: 0,
        followingCount: 0,
        lastLoginAt: null,
        ...data.stats
      }
    };
    
    // 生成用户名(如果没有提供)
    if (!userData.username && userData.email) {
      userData.username = await this.generateUsername(userData.email);
    }
    
    const result = await super.create(userData, params);
    
    // 创建用户后的后续操作
    await this.initializeUserData(result._id);
    
    return result;
  }

  async patch(id, data, params) {
    const updateData = {
      ...data,
      updatedAt: new Date()
    };
    
    // 如果更新了个人资料,重新计算全名
    if (data.profile) {
      updateData.$set = updateData.$set || {};
      if (data.profile.firstName || data.profile.lastName) {
        const user = await this.get(id, params);
        const firstName = data.profile.firstName || user.profile?.firstName || '';
        const lastName = data.profile.lastName || user.profile?.lastName || '';
        updateData.fullName = `\${firstName} \${lastName}`.trim();
      }
    }
    
    return super.patch(id, updateData, params);
  }

  // 生成唯一用户名
  async generateUsername(email) {
    const baseUsername = email.split('@')[0].toLowerCase();
    let username = baseUsername;
    let counter = 1;
    
    while (await this.isUsernameExists(username)) {
      username = `\${baseUsername}\${counter}`;
      counter++;
    }
    
    return username;
  }

  async isUsernameExists(username) {
    const result = await this.find({
      query: { username, $limit: 1 }
    });
    return result.total > 0;
  }

  // 初始化用户数据
  async initializeUserData(userId) {
    // 创建默认分类
    await this.app.service('categories').create({
      name: '默认分类',
      slug: 'default',
      userId: userId,
      isDefault: true
    });
    
    // 发送欢迎邮件
    await this.app.service('notifications').create({
      type: 'welcome',
      userId: userId,
      title: '欢迎加入我们!',
      content: '感谢您注册我们的平台...'
    });
  }

  // 用户统计信息
  async getUserStats(userId) {
    const pipeline = [
      { $match: { _id: new ObjectId(userId) } },
      {
        $lookup: {
          from: 'posts',
          localField: '_id',
          foreignField: 'authorId',
          as: 'posts'
        }
      },
      {
        $lookup: {
          from: 'comments',
          localField: '_id',
          foreignField: 'authorId',
          as: 'comments'
        }
      },
      {
        $addFields: {
          'stats.postsCount': { $size: '$posts' },
          'stats.commentsCount': { $size: '$comments' },
          'stats.publishedPostsCount': {
            $size: {
              $filter: {
                input: '$posts',
                cond: { $eq: ['$$this.status', 'published'] }
              }
            }
          }
        }
      },
      {
        $project: {
          posts: 0,
          comments: 0
        }
      }
    ];
    
    const result = await this.Model.aggregate(pipeline).toArray();
    return result[0] || null;
  }

  // 附近的用户
  async findNearbyUsers(coordinates, maxDistance = 10000, params = {}) {
    const pipeline = [
      {
        $geoNear: {
          near: {
            type: 'Point',
            coordinates: coordinates
          },
          distanceField: 'distance',
          maxDistance: maxDistance,
          spherical: true,
          query: { 'profile.location': { $exists: true } }
        }
      },
      {
        $project: {
          username: 1,
          'profile.firstName': 1,
          'profile.lastName': 1,
          'profile.avatar': 1,
          'profile.bio': 1,
          distance: 1
        }
      },
      { $limit: params.limit || 20 }
    ];
    
    const data = await this.Model.aggregate(pipeline).toArray();
    
    return {
      total: data.length,
      data
    };
  }

  // 用户活动时间线
  async getUserTimeline(userId, params = {}) {
    const { limit = 20, skip = 0 } = params;
    
    const pipeline = [
      {
        $facet: {
          posts: [
            { $match: { authorId: new ObjectId(userId), status: 'published' } },
            { $addFields: { type: 'post', date: '$publishedAt' } },
            { $project: { title: 1, slug: 1, type: 1, date: 1 } }
          ],
          comments: [
            { $match: { authorId: new ObjectId(userId) } },
            { $addFields: { type: 'comment', date: '$createdAt' } },
            { $project: { content: 1, postId: 1, type: 1, date: 1 } }
          ]
        }
      },
      {
        $project: {
          timeline: { $concatArrays: ['$posts', '$comments'] }
        }
      },
      { $unwind: '$timeline' },
      { $replaceRoot: { newRoot: '$timeline' } },
      { $sort: { date: -1 } },
      { $skip: skip },
      { $limit: limit }
    ];
    
    const data = await this.app.get('mongoClient')
      .then(db => db.collection('posts'))
      .then(collection => collection.aggregate(pipeline).toArray());
    
    return {
      total: data.length,
      data
    };
  }
}

module.exports = UsersService;

2. 文章服务

javascript
// src/services/posts/posts.class.js
const { MongoDBService } = require('@feathersjs/mongodb');
const { ObjectId } = require('mongodb');

class PostsService extends MongoDBService {
  constructor(options, app) {
    super(options, app);
    this.app = app;
  }

  async find(params) {
    const { query = {} } = params;
    
    const pipeline = [];
    const matchStage = {};
    
    // 基础查询条件
    Object.keys(query).forEach(key => {
      if (!key.startsWith('$') && !['search', 'author', 'category', 'dateRange'].includes(key)) {
        matchStage[key] = query[key];
      }
    });
    
    // 默认只显示已发布的文章(除非明确查询其他状态)
    if (!matchStage.status && !params.user?.role === 'admin') {
      matchStage.status = 'published';
    }
    
    if (Object.keys(matchStage).length > 0) {
      pipeline.push({ $match: matchStage });
    }
    
    // 全文搜索
    if (query.search) {
      pipeline.unshift({
        $match: {
          $text: { $search: query.search }
        }
      });
    }
    
    // 作者筛选
    if (query.author) {
      pipeline.push({
        $match: {
          authorId: new ObjectId(query.author)
        }
      });
    }
    
    // 分类筛选
    if (query.category) {
      pipeline.push({
        $match: {
          'category.slug': query.category
        }
      });
    }
    
    // 日期范围筛选
    if (query.dateRange) {
      const { start, end } = query.dateRange;
      const dateMatch = {};
      
      if (start) dateMatch.$gte = new Date(start);
      if (end) dateMatch.$lte = new Date(end);
      
      if (Object.keys(dateMatch).length > 0) {
        pipeline.push({
          $match: {
            publishedAt: dateMatch
          }
        });
      }
    }
    
    // 关联作者信息
    pipeline.push({
      $lookup: {
        from: 'users',
        localField: 'authorId',
        foreignField: '_id',
        as: 'author',
        pipeline: [
          {
            $project: {
              username: 1,
              'profile.firstName': 1,
              'profile.lastName': 1,
              'profile.avatar': 1
            }
          }
        ]
      }
    });
    
    // 展开作者信息
    pipeline.push({
      $addFields: {
        author: { $arrayElemAt: ['$author', 0] }
      }
    });
    
    // 添加计算字段
    pipeline.push({
      $addFields: {
        readingTime: {
          $ceil: {
            $divide: [
              { $size: { $split: ['$content', ' '] } },
              200  // 假设每分钟阅读200个单词
            ]
          }
        },
        isPopular: {
          $gt: ['$stats.viewCount', 1000]
        }
      }
    });
    
    // 排序
    if (query.$sort) {
      pipeline.push({ $sort: query.$sort });
    } else if (query.search) {
      pipeline.push({ $sort: { score: { $meta: 'textScore' } } });
    } else {
      pipeline.push({ $sort: { publishedAt: -1 } });
    }
    
    // 分页
    const skip = parseInt(query.$skip) || 0;
    const limit = parseInt(query.$limit) || 10;
    
    // 获取总数
    const countPipeline = [...pipeline, { $count: 'total' }];
    
    // 添加分页
    pipeline.push({ $skip: skip });
    pipeline.push({ $limit: limit });
    
    // 字段投影
    if (query.$select) {
      const projection = {};
      query.$select.forEach(field => {
        projection[field] = 1;
      });
      pipeline.push({ $project: projection });
    }
    
    // 执行查询
    const [data, totalResult] = await Promise.all([
      this.Model.aggregate(pipeline).toArray(),
      this.Model.aggregate(countPipeline).toArray()
    ]);
    
    const total = totalResult[0]?.total || 0;
    
    return {
      total,
      limit,
      skip,
      data
    };
  }

  async get(id, params) {
    const pipeline = [
      { $match: { _id: new ObjectId(id) } },
      
      // 关联作者信息
      {
        $lookup: {
          from: 'users',
          localField: 'authorId',
          foreignField: '_id',
          as: 'author',
          pipeline: [
            {
              $project: {
                username: 1,
                'profile.firstName': 1,
                'profile.lastName': 1,
                'profile.avatar': 1,
                'profile.bio': 1
              }
            }
          ]
        }
      },
      
      // 关联评论信息
      {
        $lookup: {
          from: 'users',
          localField: 'comments.authorId',
          foreignField: '_id',
          as: 'commentAuthors'
        }
      },
      
      // 处理评论作者信息
      {
        $addFields: {
          author: { $arrayElemAt: ['$author', 0] },
          comments: {
            $map: {
              input: '$comments',
              as: 'comment',
              in: {
                $mergeObjects: [
                  '$$comment',
                  {
                    author: {
                      $arrayElemAt: [
                        {
                          $filter: {
                            input: '$commentAuthors',
                            cond: { $eq: ['$$this._id', '$$comment.authorId'] }
                          }
                        },
                        0
                      ]
                    }
                  }
                ]
              }
            }
          }
        }
      },
      
      // 清理临时字段
      {
        $project: {
          commentAuthors: 0
        }
      }
    ];
    
    const result = await this.Model.aggregate(pipeline).toArray();
    
    if (result.length === 0) {
      throw new Error(`文章 \${id} 不存在`);
    }
    
    const post = result[0];
    
    // 增加浏览量(异步执行,不影响响应速度)
    this.incrementViewCount(id).catch(console.error);
    
    return post;
  }

  async create(data, params) {
    const { user } = params;
    
    const postData = {
      ...data,
      authorId: new ObjectId(user._id),
      slug: await this.generateSlug(data.title),
      stats: {
        viewCount: 0,
        likeCount: 0,
        commentCount: 0,
        shareCount: 0
      },
      createdAt: new Date(),
      updatedAt: new Date()
    };
    
    // 如果是发布状态,设置发布时间
    if (postData.status === 'published') {
      postData.publishedAt = new Date();
    }
    
    const result = await super.create(postData, params);
    
    // 更新用户文章计数
    await this.updateUserPostCount(user._id);
    
    return result;
  }

  async patch(id, data, params) {
    const updateData = {
      ...data,
      updatedAt: new Date()
    };
    
    // 如果状态变为发布,设置发布时间
    if (data.status === 'published') {
      const existing = await super.get(id, params);
      if (existing.status !== 'published') {
        updateData.publishedAt = new Date();
      }
    }
    
    // 如果更新了标题,重新生成 slug
    if (data.title) {
      updateData.slug = await this.generateSlug(data.title, id);
    }
    
    return super.patch(id, updateData, params);
  }

  // 生成唯一 slug
  async generateSlug(title, excludeId = null) {
    const baseSlug = title
      .toLowerCase()
      .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
      .replace(/^-+|-+$/g, '');
    
    let slug = baseSlug;
    let counter = 1;
    
    while (await this.isSlugExists(slug, excludeId)) {
      slug = `\${baseSlug}-\${counter}`;
      counter++;
    }
    
    return slug;
  }

  async isSlugExists(slug, excludeId = null) {
    const query = { slug };
    if (excludeId) {
      query._id = { $ne: new ObjectId(excludeId) };
    }
    
    const result = await this.find({ query: { ...query, $limit: 1 } });
    return result.total > 0;
  }

  // 增加浏览量
  async incrementViewCount(id) {
    await this.Model.updateOne(
      { _id: new ObjectId(id) },
      { $inc: { 'stats.viewCount': 1 } }
    );
  }

  // 更新用户文章计数
  async updateUserPostCount(userId) {
    const postCount = await this.Model.countDocuments({
      authorId: new ObjectId(userId),
      status: 'published'
    });
    
    await this.app.service('users').patch(userId, {
      'stats.postsCount': postCount
    });
  }

  // 热门文章
  async getPopularPosts(params = {}) {
    const { limit = 10, days = 7 } = params;
    const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
    
    const pipeline = [
      {
        $match: {
          status: 'published',
          publishedAt: { $gte: startDate }
        }
      },
      {
        $addFields: {
          popularityScore: {
            $add: [
              { $multiply: ['$stats.viewCount', 1] },
              { $multiply: ['$stats.likeCount', 5] },
              { $multiply: ['$stats.commentCount', 10] },
              { $multiply: ['$stats.shareCount', 15] }
            ]
          }
        }
      },
      { $sort: { popularityScore: -1 } },
      { $limit: limit },
      {
        $lookup: {
          from: 'users',
          localField: 'authorId',
          foreignField: '_id',
          as: 'author',
          pipeline: [
            {
              $project: {
                username: 1,
                'profile.firstName': 1,
                'profile.lastName': 1,
                'profile.avatar': 1
              }
            }
          ]
        }
      },
      {
        $addFields: {
          author: { $arrayElemAt: ['$author', 0] }
        }
      },
      {
        $project: {
          title: 1,
          slug: 1,
          excerpt: 1,
          featuredImage: 1,
          author: 1,
          stats: 1,
          publishedAt: 1,
          popularityScore: 1
        }
      }
    ];
    
    const data = await this.Model.aggregate(pipeline).toArray();
    
    return {
      total: data.length,
      data
    };
  }

  // 相关文章推荐
  async getRelatedPosts(postId, params = {}) {
    const { limit = 5 } = params;
    
    // 获取当前文章的标签
    const currentPost = await super.get(postId, {});
    const tags = currentPost.tags || [];
    
    if (tags.length === 0) {
      return { total: 0, data: [] };
    }
    
    const pipeline = [
      {
        $match: {
          _id: { $ne: new ObjectId(postId) },
          status: 'published',
          tags: { $in: tags }
        }
      },
      {
        $addFields: {
          matchingTags: {
            $size: {
              $setIntersection: ['$tags', tags]
            }
          }
        }
      },
      { $sort: { matchingTags: -1, publishedAt: -1 } },
      { $limit: limit },
      {
        $lookup: {
          from: 'users',
          localField: 'authorId',
          foreignField: '_id',
          as: 'author',
          pipeline: [
            {
              $project: {
                username: 1,
                'profile.firstName': 1,
                'profile.lastName': 1,
                'profile.avatar': 1
              }
            }
          ]
        }
      },
      {
        $addFields: {
          author: { $arrayElemAt: ['$author', 0] }
        }
      },
      {
        $project: {
          title: 1,
          slug: 1,
          excerpt: 1,
          featuredImage: 1,
          author: 1,
          publishedAt: 1,
          matchingTags: 1
        }
      }
    ];
    
    const data = await this.Model.aggregate(pipeline).toArray();
    
    return {
      total: data.length,
      data
    };
  }
}

module.exports = PostsService;

总结

通过这篇文章,我们深入学习了 Feathers.js 与 MongoDB 的结合使用:

MongoDB 环境搭建

  • Docker 快速部署
  • 连接配置和优化
  • 索引策略设计

文档模型设计

  • 灵活的嵌套结构
  • 地理位置数据
  • 全文搜索索引

高级查询功能

  • 聚合管道应用
  • 复杂关联查询
  • 地理位置查询

实战服务实现

  • 用户服务完整实现
  • 文章服务高级功能
  • 性能优化技巧

掌握了这些知识,你就能够充分发挥 MongoDB 的优势,构建高性能的文档数据库应用。

下一篇文章,我们将学习 SQL 数据库的集成和使用。


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!