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
相关产品推荐
相关产品推荐

