博客系统 #
一、项目概述 #
1.1 功能需求 #
- 文章管理(CRUD)
- 分类和标签
- 评论系统
- 用户管理
- 搜索功能
1.2 项目结构 #
text
blog-system/
├── controllers/
│ ├── articleController.js
│ ├── categoryController.js
│ ├── commentController.js
│ └── tagController.js
├── models/
│ ├── Article.js
│ ├── Category.js
│ ├── Comment.js
│ └── Tag.js
├── routes/
│ ├── articleRoutes.js
│ ├── categoryRoutes.js
│ └── commentRoutes.js
├── services/
│ └── searchService.js
├── views/
│ ├── layouts/
│ ├── partials/
│ └── pages/
└── public/
├── css/
└── js/
二、数据模型 #
2.1 models/Article.js #
javascript
const mongoose = require('mongoose');
const articleSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '请输入标题'],
trim: true,
maxlength: [200, '标题不能超过200个字符']
},
slug: {
type: String,
unique: true,
lowercase: true
},
content: {
type: String,
required: [true, '请输入内容']
},
excerpt: {
type: String,
maxlength: [500, '摘要不能超过500个字符']
},
cover: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Tag'
}],
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
viewCount: {
type: Number,
default: 0
},
likeCount: {
type: Number,
default: 0
},
commentCount: {
type: Number,
default: 0
},
isTop: {
type: Boolean,
default: false
},
publishedAt: Date
}, {
timestamps: true
});
articleSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = this.title
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.substring(0, 100);
}
if (this.status === 'published' && !this.publishedAt) {
this.publishedAt = Date.now();
}
if (!this.excerpt && this.content) {
this.excerpt = this.content
.replace(/<[^>]*>/g, '')
.substring(0, 200);
}
next();
});
articleSchema.index({ title: 'text', content: 'text' });
articleSchema.index({ slug: 1 });
articleSchema.index({ status: 1, publishedAt: -1 });
articleSchema.methods.incrementViewCount = function() {
return this.updateOne({ $inc: { viewCount: 1 } });
};
module.exports = mongoose.model('Article', articleSchema);
2.2 models/Category.js #
javascript
const mongoose = require('mongoose');
const categorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
trim: true
},
slug: {
type: String,
unique: true,
lowercase: true
},
description: String,
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
order: {
type: Number,
default: 0
},
articleCount: {
type: Number,
default: 0
}
}, {
timestamps: true
});
categorySchema.pre('save', function(next) {
if (this.isModified('name')) {
this.slug = this.name
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
}
next();
});
module.exports = mongoose.model('Category', categorySchema);
2.3 models/Comment.js #
javascript
const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({
content: {
type: String,
required: [true, '请输入评论内容'],
maxlength: [1000, '评论不能超过1000个字符']
},
article: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Article',
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment'
},
replies: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment'
}],
likeCount: {
type: Number,
default: 0
},
isApproved: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
commentSchema.post('save', async function() {
await this.model('Article').findByIdAndUpdate(
this.article,
{ $inc: { commentCount: 1 } }
);
});
commentSchema.post('remove', async function() {
await this.model('Article').findByIdAndUpdate(
this.article,
{ $inc: { commentCount: -1 } }
);
});
module.exports = mongoose.model('Comment', commentSchema);
三、文章控制器 #
3.1 controllers/articleController.js #
javascript
const Article = require('../models/Article');
const Category = require('../models/Category');
const Tag = require('../models/Tag');
const asyncHandler = require('../utils/asyncHandler');
exports.getAll = asyncHandler(async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const query = { status: 'published' };
if (req.query.category) {
query.category = req.query.category;
}
if (req.query.tag) {
query.tags = req.query.tag;
}
if (req.query.search) {
query.$text = { $search: req.query.search };
}
const [articles, total] = await Promise.all([
Article.find(query)
.populate('author', 'name avatar')
.populate('category', 'name slug')
.populate('tags', 'name slug')
.sort({ isTop: -1, publishedAt: -1 })
.skip(skip)
.limit(limit),
Article.countDocuments(query)
]);
res.json({
success: true,
data: articles,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
});
exports.getBySlug = asyncHandler(async (req, res) => {
const article = await Article.findOne({ slug: req.params.slug })
.populate('author', 'name avatar bio')
.populate('category', 'name slug')
.populate('tags', 'name slug');
if (!article) {
return res.status(404).json({ error: '文章不存在' });
}
await article.incrementViewCount();
const relatedArticles = await Article.find({
_id: { $ne: article._id },
category: article.category,
status: 'published'
})
.limit(5)
.select('title slug cover publishedAt');
res.json({
success: true,
data: { article, relatedArticles }
});
});
exports.create = asyncHandler(async (req, res) => {
const article = await Article.create({
...req.body,
author: req.user.id
});
if (article.category) {
await Category.findByIdAndUpdate(article.category, {
$inc: { articleCount: 1 }
});
}
res.status(201).json({
success: true,
data: article,
message: '文章创建成功'
});
});
exports.update = asyncHandler(async (req, res) => {
let article = await Article.findById(req.params.id);
if (!article) {
return res.status(404).json({ error: '文章不存在' });
}
if (article.author.toString() !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: '无权修改此文章' });
}
article = await Article.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true }
);
res.json({
success: true,
data: article,
message: '文章更新成功'
});
});
exports.delete = asyncHandler(async (req, res) => {
const article = await Article.findById(req.params.id);
if (!article) {
return res.status(404).json({ error: '文章不存在' });
}
if (article.author.toString() !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: '无权删除此文章' });
}
await article.deleteOne();
res.json({
success: true,
message: '文章删除成功'
});
});
exports.getMyArticles = asyncHandler(async (req, res) => {
const articles = await Article.find({ author: req.user.id })
.populate('category', 'name')
.sort({ createdAt: -1 });
res.json({
success: true,
data: articles
});
});
四、评论控制器 #
4.1 controllers/commentController.js #
javascript
const Comment = require('../models/Comment');
const Article = require('../models/Article');
const asyncHandler = require('../utils/asyncHandler');
exports.getByArticle = asyncHandler(async (req, res) => {
const comments = await Comment.find({
article: req.params.articleId,
isApproved: true,
parent: null
})
.populate('author', 'name avatar')
.populate({
path: 'replies',
populate: { path: 'author', select: 'name avatar' }
})
.sort({ createdAt: -1 });
res.json({
success: true,
data: comments
});
});
exports.create = asyncHandler(async (req, res) => {
const article = await Article.findById(req.params.articleId);
if (!article) {
return res.status(404).json({ error: '文章不存在' });
}
const comment = await Comment.create({
content: req.body.content,
article: req.params.articleId,
author: req.user.id,
parent: req.body.parentId || null
});
if (req.body.parentId) {
await Comment.findByIdAndUpdate(req.body.parentId, {
$push: { replies: comment._id }
});
}
const populatedComment = await Comment.findById(comment._id)
.populate('author', 'name avatar');
res.status(201).json({
success: true,
data: populatedComment,
message: '评论发表成功'
});
});
exports.delete = asyncHandler(async (req, res) => {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({ error: '评论不存在' });
}
if (comment.author.toString() !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: '无权删除此评论' });
}
await comment.deleteOne();
res.json({
success: true,
message: '评论删除成功'
});
});
五、搜索服务 #
5.1 services/searchService.js #
javascript
const Article = require('../models/Article');
const searchService = {
async search(keyword, options = {}) {
const page = parseInt(options.page) || 1;
const limit = parseInt(options.limit) || 10;
const skip = (page - 1) * limit;
const query = {
status: 'published',
$text: { $search: keyword }
};
const [articles, total] = await Promise.all([
Article.find(query, { score: { $meta: 'textScore' } })
.populate('author', 'name')
.populate('category', 'name slug')
.sort({ score: { $meta: 'textScore' } })
.skip(skip)
.limit(limit),
Article.countDocuments(query)
]);
return {
articles,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
},
async getHotKeywords(limit = 10) {
const articles = await Article.aggregate([
{ $match: { status: 'published' } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return articles;
},
async getArchive() {
const articles = await Article.aggregate([
{ $match: { status: 'published' } },
{
$group: {
_id: {
year: { $year: '$publishedAt' },
month: { $month: '$publishedAt' }
},
count: { $sum: 1 },
articles: {
$push: {
title: '$title',
slug: '$slug',
publishedAt: '$publishedAt'
}
}
}
},
{ $sort: { '_id.year': -1, '_id.month': -1 } }
]);
return articles;
}
};
module.exports = searchService;
六、路由定义 #
6.1 routes/articleRoutes.js #
javascript
const express = require('express');
const router = express.Router();
const articleController = require('../controllers/articleController');
const { auth, authorize } = require('../middlewares/auth');
router.get('/', articleController.getAll);
router.get('/my', auth, articleController.getMyArticles);
router.get('/:slug', articleController.getBySlug);
router.post('/', auth, articleController.create);
router.put('/:id', auth, articleController.update);
router.delete('/:id', auth, articleController.delete);
module.exports = router;
6.2 routes/commentRoutes.js #
javascript
const express = require('express');
const router = express.Router();
const commentController = require('../controllers/commentController');
const { auth } = require('../middlewares/auth');
router.get('/article/:articleId', commentController.getByArticle);
router.post('/article/:articleId', auth, commentController.create);
router.delete('/:id', auth, commentController.delete);
module.exports = router;
七、总结 #
博客系统要点:
| 模块 | 功能 |
|---|---|
| 文章 | CRUD、分类、标签 |
| 评论 | 发表、回复、删除 |
| 搜索 | 全文搜索、归档 |
| 用户 | 认证、权限 |
下一步,让我们学习部署上线!
最后更新:2026-03-28