您的专属全栈工程师,带您从零构建高性能博客系统
大家好!作为一名资深全栈工程师和Node.js专家,今天我将为您带来一个激动人心的实战项目:使用 MongoDB 作为数据库,Koa.js 构建后端API,从零开始搭建一个功能完善的多用户博客系统。我们将实现包括博客文章管理、标签、评论、点赞、排序和置顶等所有核心功能。
本教程将以后端API设计和实现为主,为确保可读性和实用性,前端部分将以思路和关键交互逻辑来呈现,您可以选择任何喜欢的前端框架(如Vue.js, React, Angular)进行对接。
我们将采用经典的MVC(Model-View-Controller)模式的变种,在后端API中更侧重于Model(数据模型)和Controller(路由逻辑)。
首先,确保您的开发环境中已安装 Node.js 和 MongoDB。
# 1. 初始化项目
mkdir my-blog-backend
cd my-blog-backend
npm init -y
# 2. 安装核心依赖
npm install koa koa-router mongoose jsonwebtoken bcryptjs koa-bodyparser @koa/cors
# 3. 创建文件结构
# my-blog-backend/
# ├── app.js # Koa 应用入口
# ├── config/ # 配置文件夹
# │ └── db.js # 数据库连接配置
# ├── models/ # MongoDB 数据模型
# │ ├── User.js
# │ ├── Article.js
# │ ├── Tag.js
# │ └── Comment.js
# ├── routes/ # 路由定义
# │ ├── auth.js
# │ ├── articles.js
# │ ├── tags.js
# │ └── comments.js
# ├── middleware/ # 中间件
# │ └── auth.js # 认证中间件
# └── utils/ # 工具函数
# └── jwt.js # JWT 相关工具
config/db.js)使用 Mongoose 连接 MongoDB 数据库。
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost:27017/my_blog_db', {
useNewUrlParser: true,
useUnifiedTopology: true,
// useCreateIndex: true, // Mongoose 6+ 默认true,不再需要
// useFindAndModify: false // Mongoose 6+ 默认false,不再需要
});
console.log('MongoDB 连接成功!');
} catch (err) {
console.error('MongoDB 连接失败:', err.message);
process.exit(1); // 退出进程
}
};
module.exports = connectDB;
定义博客系统的核心数据模型:用户、文章、标签、评论。
models/User.js)包含用户认证信息、角色等。
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
match: /^\S+@\S+\.\S+$/, // 简单邮箱验证
trim: true
},
role: { // 用户角色:admin, editor, user
type: String,
enum: ['admin', 'editor', 'user'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// 在保存用户前对密码进行哈希
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 验证密码方法
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
models/Tag.js)用于文章分类,可以简单只包含名称。
const mongoose = require('mongoose');
const TagSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Tag', TagSchema);
models/Comment.js)评论关联用户和文章。
const mongoose = require('mongoose');
const CommentSchema = new mongoose.Schema({
content: {
type: String,
required: true,
trim: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // 引用User模型
required: true
},
article: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Article', // 引用Article模型
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Comment', CommentSchema);
models/Article.js)这是核心模型,包含文章内容、作者、标签、点赞、评论计数、置顶状态等。
const mongoose = require('mongoose');
const ArticleSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // 引用User模型
required: true
},
tags: [{ // 关联多个标签
type: mongoose.Schema.Types.ObjectId,
ref: 'Tag'
}],
likes: { // 点赞用户ID列表
type: [mongoose.Schema.Types.ObjectId],
ref: 'User',
default: []
},
views: { // 阅读量
type: Number,
default: 0
},
commentsCount: { // 评论数量,用于排序
type: Number,
default: 0
},
isPublished: { // 是否发布
type: Boolean,
default: true
},
isTop: { // 是否置顶
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// 在每次更新时,自动更新 updatedAt 字段
ArticleSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Article', ArticleSchema);
我们将使用 JWT 进行用户认证。
utils/jwt.js)生成和验证 Token。
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; // 建议使用环境变量
const generateToken = (id) => {
return jwt.sign({ id }, JWT_SECRET, {
expiresIn: '7d', // Token有效期
});
};
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
return null; // Token无效或过期
}
};
module.exports = { generateToken, verifyToken };
middleware/auth.js)用于保护需要登录才能访问的路由。
const User = require('../models/User');
const { verifyToken } = require('../utils/jwt');
const protect = async (ctx, next) => {
let token;
if (ctx.request.headers.authorization && ctx.request.headers.authorization.startsWith('Bearer')) {
try {
token = ctx.request.headers.authorization.split(' ')[1];
const decoded = verifyToken(token);
if (!decoded) {
ctx.status = 401;
ctx.body = { message: '未授权,Token失效或过期' };
return;
}
ctx.state.user = await User.findById(decoded.id).select('-password');
if (!ctx.state.user) {
ctx.status = 401;
ctx.body = { message: '未授权,用户不存在' };
return;
}
await next();
} catch (error) {
console.error('认证错误:', error);
ctx.status = 401;
ctx.body = { message: '未授权,Token无效' };
}
} else {
ctx.status = 401;
ctx.body = { message: '未授权,没有Token' };
}
};
const authorize = (roles = []) => {
return async (ctx, next) => {
if (!ctx.state.user) {
ctx.status = 403;
ctx.body = { message: '未登录' };
return;
}
if (roles.length > 0 && !roles.includes(ctx.state.user.role)) {
ctx.status = 403;
ctx.body = { message: '无权限访问' };
return;
}
await next();
};
};
module.exports = { protect, authorize };
Koa.js 的路由通过 koa-router 实现。我们将把不同功能的路由分开。
routes/auth.js)用户注册、登录。
const Router = require('koa-router');
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
const router = new Router({ prefix: '/api/auth' });
// @route POST /api/auth/register
// @desc 注册用户
router.post('/register', async (ctx) => {
const { username, email, password } = ctx.request.body;
if (!username || !email || !password) {
ctx.status = 400;
ctx.body = { message: '请填写所有必填字段' };
return;
}
try {
let user = await User.findOne({ $or: [{ username }, { email }] });
if (user) {
ctx.status = 400;
ctx.body = { message: '用户名或邮箱已被注册' };
return;
}
user = new User({ username, email, password });
await user.save();
const token = generateToken(user._id);
ctx.status = 201;
ctx.body = {
_id: user._id,
username: user.username,
email: user.email,
role: user.role,
token
};
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: '服务器错误' };
}
});
// @route POST /api/auth/login
// @desc 用户登录
router.post('/login', async (ctx) => {
const { email, password } = ctx.request.body;
if (!email || !password) {
ctx.status = 400;
ctx.body = { message: '请填写邮箱和密码' };
return;
}
try {
const user = await User.findOne({ email });
if (!user) {
ctx.status = 400;
ctx.body = { message: '无效的凭证' };
return;
}
const isMatch = await user.matchPassword(password);
if (!isMatch) {
ctx.status = 400;
ctx.body = { message: '无效的凭证' };
return;
}
const token = generateToken(user._id);
ctx.status = 200;
ctx.body = {
_id: user._id,
username: user.username,
email: user.email,
role: user.role,
token
};
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: '服务器错误' };
}
});
module.exports = router;
routes/tags.js)标签的增删改查。
const Router = require('koa-router');
const Tag = require('../models/Tag');
const { protect, authorize } = require('../middleware/auth');
const router = new Router({ prefix: '/api/tags' });
// @route GET /api/tags
// @desc 获取所有标签
router.get('/', async (ctx) => {
try {
const tags = await Tag.find({}).sort({ name: 1 }); // 按名称升序
ctx.body = tags;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '获取标签失败', error: error.message };
}
});
// @route POST /api/tags
// @desc 创建新标签 (仅管理员和编辑)
router.post('/', protect, authorize(['admin', 'editor']), async (ctx) => {
const { name } = ctx.request.body;
if (!name) {
ctx.status = 400;
ctx.body = { message: '标签名称不能为空' };
return;
}
try {
const existingTag = await Tag.findOne({ name });
if (existingTag) {
ctx.status = 400;
ctx.body = { message: '标签已存在' };
return;
}
const tag = new Tag({ name });
await tag.save();
ctx.status = 201;
ctx.body = tag;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '创建标签失败', error: error.message };
}
});
// @route PUT /api/tags/:id
// @desc 更新标签 (仅管理员和编辑)
router.put('/:id', protect, authorize(['admin', 'editor']), async (ctx) => {
const { id } = ctx.params;
const { name } = ctx.request.body;
try {
const tag = await Tag.findByIdAndUpdate(id, { name }, { new: true, runValidators: true });
if (!tag) {
ctx.status = 404;
ctx.body = { message: '标签未找到' };
return;
}
ctx.body = tag;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '更新标签失败', error: error.message };
}
});
// @route DELETE /api/tags/:id
// @desc 删除标签 (仅管理员)
router.delete('/:id', protect, authorize(['admin']), async (ctx) => {
const { id } = ctx.params;
try {
const tag = await Tag.findByIdAndDelete(id);
if (!tag) {
ctx.status = 404;
ctx.body = { message: '标签未找到' };
return;
}
ctx.body = { message: '标签删除成功' };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '删除标签失败', error: error.message };
}
});
module.exports = router;
routes/articles.js)文章的增删改查、点赞、置顶、排序等功能。
const Router = require('koa-router');
const Article = require('../models/Article');
const Comment = require('../models/Comment');
const User = require('../models/User'); // 引入User模型用于populate
const { protect, authorize } = require('../middleware/auth');
const router = new Router({ prefix: '/api/articles' });
// @route POST /api/articles
// @desc 创建新文章 (需要登录)
router.post('/', protect, async (ctx) => {
const { title, content, tags } = ctx.request.body;
if (!title || !content) {
ctx.status = 400;
ctx.body = { message: '标题和内容不能为空' };
return;
}
try {
const article = new Article({
title,
content,
author: ctx.state.user._id, // 从认证中间件获取作者ID
tags: tags || [],
});
await article.save();
ctx.status = 201;
ctx.body = article;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '创建文章失败', error: error.message };
}
});
// @route GET /api/articles
// @desc 获取所有文章 (支持分页、排序、过滤)
router.get('/', async (ctx) => {
const { page = 1, limit = 10, sort = 'createdAt', order = -1, tag, search } = ctx.query;
const query = { isPublished: true }; // 默认只显示已发布的文章
const skip = (parseInt(page) - 1) * parseInt(limit);
const sortOrder = parseInt(order); // -1: 降序, 1: 升序
if (tag) {
// 根据标签名称查找标签ID
const tagObj = await Tag.findOne({ name: tag });
if (tagObj) {
query.tags = tagObj._id;
} else {
ctx.status = 200;
ctx.body = { articles: [], totalPages: 0, currentPage: 1, totalItems: 0 };
return;
}
}
if (search) {
query.$or = [
{ title: { $regex: search, $options: 'i' } }, // 标题模糊匹配 (不区分大小写)
{ content: { $regex: search, $options: 'i' } } // 内容模糊匹配
];
}
try {
// 置顶文章优先:先按 isTop 降序 (true在前),再按指定排序规则
const sortOptions = { isTop: -1 };
if (sort === 'hot') { // 热门排序:点赞数 + 评论数 * 权重
// MongoDB聚合框架更适合这种复杂排序,这里简化为 likes.length
// 实际项目中可以添加一个计算字段 likesCount + commentsCount * 2
sortOptions['likes.length'] = sortOrder; // 粗略按点赞数
} else if (sort === 'views') {
sortOptions.views = sortOrder;
} else {
sortOptions[sort] = sortOrder; // createdAt, updatedAt
}
const articles = await Article.find(query)
.populate('author', 'username email') // 关联作者信息
.populate('tags', 'name') // 关联标签信息
.sort(sortOptions)
.skip(skip)
.limit(parseInt(limit));
const totalItems = await Article.countDocuments(query);
const totalPages = Math.ceil(totalItems / parseInt(limit));
ctx.body = {
articles,
totalPages,
currentPage: parseInt(page),
totalItems
};
} catch (error) {
ctx.status = 500;
ctx.body = { message: '获取文章失败', error: error.message };
}
});
// @route GET /api/articles/:id
// @desc 获取单篇文章详情
router.get('/:id', async (ctx) => {
const { id } = ctx.params;
try {
const article = await Article.findById(id)
.populate('author', 'username email')
.populate('tags', 'name');
if (!article || !article.isPublished) {
ctx.status = 404;
ctx.body = { message: '文章未找到或未发布' };
return;
}
// 增加阅读量 (非阻塞)
article.views = (article.views || 0) + 1;
await article.save(); // 保存更新的views
// 获取文章评论
const comments = await Comment.find({ article: id })
.populate('author', 'username') // 关联评论作者信息
.sort({ createdAt: 1 }); // 评论按时间升序
ctx.body = { article, comments };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '获取文章详情失败', error: error.message };
}
});
// @route PUT /api/articles/:id
// @desc 更新文章 (只有作者或管理员/编辑可以)
router.put('/:id', protect, authorize(['admin', 'editor']), async (ctx) => {
const { id } = ctx.params;
const { title, content, tags, isPublished, isTop } = ctx.request.body;
try {
let article = await Article.findById(id);
if (!article) {
ctx.status = 404;
ctx.body = { message: '文章未找到' };
return;
}
// 检查权限:文章作者,或者管理员/编辑
if (article.author.toString() !== ctx.state.user._id.toString() && ctx.state.user.role !== 'admin' && ctx.state.user.role !== 'editor') {
ctx.status = 403;
ctx.body = { message: '无权限更新此文章' };
return;
}
article.title = title || article.title;
article.content = content || article.content;
article.tags = tags !== undefined ? tags : article.tags;
// 只有管理员或编辑可以设置isPublished和isTop
if (ctx.state.user.role === 'admin' || ctx.state.user.role === 'editor') {
if (isPublished !== undefined) article.isPublished = isPublished;
if (isTop !== undefined) article.isTop = isTop;
}
await article.save();
ctx.body = article;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '更新文章失败', error: error.message };
}
});
// @route DELETE /api/articles/:id
// @desc 删除文章 (只有作者或管理员)
router.delete('/:id', protect, authorize(['admin']), async (ctx) => {
const { id } = ctx.params;
try {
const article = await Article.findById(id);
if (!article) {
ctx.status = 404;
ctx.body = { message: '文章未找到' };
return;
}
// 检查权限:文章作者,或者管理员
if (article.author.toString() !== ctx.state.user._id.toString() && ctx.state.user.role !== 'admin') {
ctx.status = 403;
ctx.body = { message: '无权限删除此文章' };
return;
}
await article.remove(); // 删除文章
await Comment.deleteMany({ article: id }); // 同时删除该文章下的所有评论
ctx.body = { message: '文章及相关评论删除成功' };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '删除文章失败', error: error.message };
}
});
// @route POST /api/articles/:id/like
// @desc 点赞/取消点赞文章 (需要登录)
router.post('/:id/like', protect, async (ctx) => {
const { id } = ctx.params;
const userId = ctx.state.user._id;
try {
const article = await Article.findById(id);
if (!article) {
ctx.status = 404;
ctx.body = { message: '文章未找到' };
return;
}
const isLiked = article.likes.includes(userId);
if (isLiked) {
// 已点赞,则取消点赞
article.likes.pull(userId); // 从数组中移除
} else {
// 未点赞,则点赞
article.likes.push(userId); // 添加到数组
}
await article.save();
ctx.body = {
message: isLiked ? '取消点赞成功' : '点赞成功',
likesCount: article.likes.length
};
} catch (error) {
ctx.status = 500;
ctx.body = { message: '点赞操作失败', error: error.message };
}
});
// @route POST /api/articles/:id/top
// @desc 设置/取消置顶文章 (仅管理员)
router.post('/:id/top', protect, authorize(['admin']), async (ctx) => {
const { id } = ctx.params;
const { isTop } = ctx.request.body; // true/false
try {
const article = await Article.findById(id);
if (!article) {
ctx.status = 404;
ctx.body = { message: '文章未找到' };
return;
}
article.isTop = isTop;
await article.save();
ctx.body = { message: `文章已${isTop ? '置顶' : '取消置顶'}`, isTop: article.isTop };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '置顶操作失败', error: error.message };
}
});
module.exports = router;
routes/comments.js)评论的发布、删除。
const Router = require('koa-router');
const Comment = require('../models/Comment');
const Article = require('../models/Article'); // 用于更新评论计数
const { protect, authorize } = require('../middleware/auth');
const router = new Router({ prefix: '/api/comments' });
// @route POST /api/comments
// @desc 发布评论 (需要登录)
router.post('/', protect, async (ctx) => {
const { articleId, content } = ctx.request.body;
if (!articleId || !content) {
ctx.status = 400;
ctx.body = { message: '文章ID和评论内容不能为空' };
return;
}
try {
const article = await Article.findById(articleId);
if (!article || !article.isPublished) {
ctx.status = 404;
ctx.body = { message: '文章未找到或未发布,无法评论' };
return;
}
const comment = new Comment({
content,
author: ctx.state.user._id,
article: articleId
});
await comment.save();
// 更新文章的评论计数
article.commentsCount = (article.commentsCount || 0) + 1;
await article.save();
// 返回时带上作者信息
const populatedComment = await Comment.findById(comment._id).populate('author', 'username');
ctx.status = 201;
ctx.body = populatedComment;
} catch (error) {
ctx.status = 500;
ctx.body = { message: '发布评论失败', error: error.message };
}
});
// @route DELETE /api/comments/:id
// @desc 删除评论 (只有评论作者或文章作者或管理员可以)
router.delete('/:id', protect, async (ctx) => {
const { id } = ctx.params;
const userId = ctx.state.user._id;
const userRole = ctx.state.user.role;
try {
const comment = await Comment.findById(id);
if (!comment) {
ctx.status = 404;
ctx.body = { message: '评论未找到' };
return;
}
const article = await Article.findById(comment.article);
if (!article) {
ctx.status = 404; // 评论的对应文章不存在
ctx.body = { message: '关联文章不存在' };
return;
}
// 权限检查:评论作者本人 / 文章作者 / 管理员
const isCommentAuthor = comment.author.toString() === userId.toString();
const isArticleAuthor = article.author.toString() === userId.toString();
const isAdmin = userRole === 'admin';
if (!isCommentAuthor && !isArticleAuthor && !isAdmin) {
ctx.status = 403;
ctx.body = { message: '无权限删除此评论' };
return;
}
await comment.remove();
// 更新文章的评论计数 (减1)
article.commentsCount = Math.max(0, (article.commentsCount || 0) - 1);
await article.save();
ctx.body = { message: '评论删除成功' };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '删除评论失败', error: error.message };
}
});
module.exports = router;
app.js)整合所有配置和路由。
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const connectDB = require('./config/db');
// 导入路由
const authRoutes = require('./routes/auth');
const articleRoutes = require('./routes/articles');
const tagRoutes = require('./routes/tags');
const commentRoutes = require('./routes/comments');
const app = new Koa();
// 连接数据库
connectDB();
// 中间件
app.use(cors()); // 允许跨域请求
app.use(bodyParser()); // 解析请求体
// 路由
app.use(authRoutes.routes()).use(authRoutes.allowedMethods());
app.use(articleRoutes.routes()).use(articleRoutes.allowedMethods());
app.use(tagRoutes.routes()).use(tagRoutes.allowedMethods());
app.use(commentRoutes.routes()).use(commentRoutes.allowedMethods());
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
if (ctx.status === 404) {
ctx.body = { message: 'Not Found' };
}
} catch (err) {
console.error('全局错误捕获:', err);
ctx.status = err.status || 500;
ctx.body = {
message: err.message || '服务器内部错误',
// error: process.env.NODE_ENV === 'development' ? err.stack : undefined // 生产环境不暴露堆栈
};
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
前端应用(如Vue/React)通过HTTP请求与后端API进行交互。
// 登录请求
async login(email, password) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token); // 存储JWT
localStorage.setItem('user', JSON.stringify(data)); // 存储用户信息
return data;
} else {
throw new Error(data.message || '登录失败');
}
} catch (error) {
console.error('登录错误:', error);
throw error;
}
}
// 注册请求
async register(username, email, password) {
// ... 类似登录请求
}
// 获取文章列表,支持分页、排序、过滤
async getArticles(page = 1, limit = 10, sort = 'createdAt', order = -1, tag = '', search = '') {
const token = localStorage.getItem('token');
const query = new URLSearchParams({ page, limit, sort, order, tag, search }).toString();
const response = await fetch(`/api/articles?${query}`, {
headers: { 'Authorization': `Bearer ${token}` } // 需要认证的接口
});
const data = await response.json();
return data; // { articles: [...], totalPages, currentPage, totalItems }
}
// 获取单篇文章详情及评论
async getArticleDetail(articleId) {
const response = await fetch(`/api/articles/${articleId}`);
const data = await response.json();
return data; // { article: {...}, comments: [...] }
}
// 发表评论
async postComment(articleId, content) {
const token = localStorage.getItem('token');
const response = await fetch('/api/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ articleId, content })
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
return data;
}
// 点赞/取消点赞
async toggleLike(articleId) {
const token = localStorage.getItem('token');
const response = await fetch(`/api/articles/${articleId}/like`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
return data; // { message, likesCount }
}
这些操作通常只在后台管理界面提供给特定角色用户。
// 设置文章置顶 (管理员权限)
async setArticleTop(articleId, isTop) {
const token = localStorage.getItem('token');
const response = await fetch(`/api/articles/${articleId}/top`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ isTop })
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
return data;
}
通过本教程,您已经学会了如何使用 MongoDB 和 Koa.js 构建一个具备多用户、文章管理、标签、评论、点赞、排序和置顶等核心功能的博客系统后端API。
进一步的拓展:
koa-multer 或云存储服务(如七牛云、AWS S3)。希望这个详尽的案例能帮助您更好地理解和实践Node.js全栈开发。祝您编码愉快!