You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+MongoDB实现用户积分计算与排行榜排名功能

MERN栈用户积分排行榜实现方案

1. 现有数据模型修正

你提供的Schema存在语法缺失、类型定义错误的问题,先补全并加必要索引提升查询性能:

import mongoose from 'mongoose'
const { Schema } = mongoose

const userSchema = new Schema(
  {
    name: {
      type: String,
      trim: true,
      required: true,
    },
    email: {
      type: String,
      trim: true,
      required: true,
      unique: true,
    },
  },
  { timestamps: true }
)
const User = mongoose.model('User', userSchema)

const postSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
      required: true,
      index: true // 外键加索引,大幅提升聚合速度
    },
    title: {
      type: String,
      required: true,
    },
    comments: [
      {
        content: String, // 原代码此处多余引号,已修正
        user: {
          type: mongoose.Schema.Types.ObjectId,
          ref: 'User',
          index: true
        },
      },
    ],
    likes: [
      {
        user: {
          type: mongoose.Schema.Types.ObjectId,
          ref: 'User',
          index: true
        },
      },
    ],
  },
  { timestamps: true }
)
const Post = mongoose.model('Post', postSchema)

export { User, Post }

2. 后端聚合统计实现(核心逻辑)

禁止循环遍历用户逐个查询计数,数据量超过千级就会出现严重性能问题,直接用MongoDB聚合管道一次性完成全量数据统计,示例为Express路由代码:

import express from 'express'
import { Post, User } from './models.js'
const router = express.Router()

router.get('/api/leaderboard', async (req, res) => {
  try {
    const leaderboard = await Post.aggregate([
      {
        $facet: {
          // 分维度统计每个用户的行为计数
          postCounts: [
            { $group: { _id: '$user', count: { $sum: 1 } } }
          ],
          commentCounts: [
            { $unwind: '$comments' },
            { $group: { _id: '$comments.user', count: { $sum: 1 } } }
          ],
          likeCounts: [
            { $unwind: '$likes' },
            { $group: { _id: '$likes.user', count: { $sum: 1 } } }
          ]
        }
      },
      {
        $project: {
          // 将三类统计结果转换为ID映射字典,方便后续合并
          postMap: { $arrayToObject: { $map: { input: '$postCounts', in: { k: { $toString: '$$this._id' }, v: '$$this.count' } } } },
          commentMap: { $arrayToObject: { $map: { input: '$commentCounts', in: { k: { $toString: '$$this._id' }, v: '$$this.count' } } } },
          likeMap: { $arrayToObject: { $map: { input: '$likeCounts', in: { k: { $toString: '$$this._id' }, v: '$$this.count' } } } },
          allUserIds: {
            $setUnion: [
              { $map: { input: '$postCounts', in: '$$this._id' } },
              { $map: { input: '$commentCounts', in: '$$this._id' } },
              { $map: { input: '$likeCounts', in: '$$this._id' } }
            ]
          }
        }
      },
      { $unwind: '$allUserIds' },
      {
        $project: {
          _id: '$allUserIds',
          posts: { $ifNull: [{ $getField: { input: '$postMap', field: { $toString: '$allUserIds' } } }, 0] },
          comments: { $ifNull: [{ $getField: { input: '$commentMap', field: { $toString: '$allUserIds' } } }, 0] },
          likes: { $ifNull: [{ $getField: { input: '$likeMap', field: { $toString: '$allUserIds' } } }, 0] }
        }
      },
      // 关联查询用户基础信息
      {
        $lookup: {
          from: 'users',
          localField: '_id',
          foreignField: '_id',
          as: 'userInfo'
        }
      },
      { $unwind: '$userInfo' },
      // 计算总积分,可自行调整不同行为的权重
      {
        $addFields: {
          totalPoints: {
            $sum: [
              { $multiply: ['$posts', 3] }, // 单帖3分
              { $multiply: ['$comments', 2] }, // 单条评论2分
              { $multiply: ['$likes', 1] } // 单次点赞1分
            ]
          },
          userName: '$userInfo.name',
          userEmail: '$userInfo.email'
        }
      },
      // 移除冗余字段
      { $project: { userInfo: 0 } },
      // 按总积分倒序排序,同分按发帖数、评论数优先级排序
      { $sort: { totalPoints: -1, posts: -1, comments: -1 } },
      // 生成排名序号
      {
        $setWindowFields: {
          sortBy: { totalPoints: -1 },
          output: {
            rank: { $rank: {} }
          }
        }
      }
    ])

    // 可选:将无任何行为的0分用户补充到榜单末尾
    const zeroPointUsers = await User.find({
      _id: { $nin: leaderboard.map(item => item._id) }
    }).select('name email')
    const zeroPointList = zeroPointUsers.map((user, idx) => ({
      _id: user._id,
      posts: 0,
      comments: 0,
      likes: 0,
      totalPoints: 0,
      userName: user.name,
      userEmail: user.email,
      rank: leaderboard.length + idx + 1
    }))

    res.json({
      code: 200,
      data: [...leaderboard, ...zeroPointList]
    })
  } catch (err) {
    res.status(500).json({ code: 500, msg: err.message })
  }
})

export default router

性能优化建议

  • 数据量超过10w条时,不要每次接口请求都执行聚合,用node-cron写定时任务每5-10分钟跑一次聚合,结果存入Redis或单独的排行榜集合,接口直接读缓存即可。
  • 后续如果积分规则调整,只需修改聚合管道中$addFields阶段的权重计算逻辑,无需改动表结构。

3. 前端React渲染示例

import { useEffect, useState } from 'react'
import axios from 'axios'

export default function Leaderboard() {
  const [list, setList] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true)
      const res = await axios.get('/api/leaderboard')
      if (res.data.code === 200) {
        setList(res.data.data)
      }
      setLoading(false)
    }
    fetchData()
  }, [])

  if (loading) return <div>加载中...</div>

  return (
    <div className="leaderboard-wrap">
      <h2>用户贡献排行榜</h2>
      <table>
        <thead>
          <tr>
            <th>排名</th>
            <th>用户名</th>
            <th>发帖数</th>
            <th>评论数</th>
            <th>点赞数</th>
            <th>总积分</th>
          </tr>
        </thead>
        <tbody>
          {list.map(item => (
            <tr key={item._id}>
              <td>
                {item.rank === 1 && '🥇'}
                {item.rank === 2 && '🥈'}
                {item.rank === 3 && '🥉'}
                {item.rank > 3 && item.rank}
              </td>
              <td>{item.userName}</td>
              <td>{item.posts}</td>
              <td>{item.comments}</td>
              <td>{item.likes}</td>
              <td>*{item.totalPoints}*</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

内容的提问来源于stack exchange,提问作者ohhhBro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 06:31:27