后端小白的我,是如何成功搭建 express+mongodb 的简洁博客网站后端的

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: 后端小白的我,是如何成功搭建 express+mongodb 的简洁博客网站后端的

前言


blog-node 是采用了主流的前后端分离思想的,主里只讲 后端。

blog-node 项目是 node + express + mongodb 的进行开发的,项目已经开源,项目地址在 github 上。


效果请看 http://biaochenxuying.cn/main.html


1. 后端


1.1 已经实现功能


  • [x] 登录
  • [x] 文章管理
  • [x] 标签管理
  • [x] 评论
  • [x] 留言管理
  • [x] 用户管理
  • [x] 友情链接管理
  • [x] 时间轴管理
  • [x] 身份验证


1.2 待实现功能


  • [ ] 点赞、留言和评论 的通知管理
  • [ ] 个人中心(用来设置博主的各种信息)
  • [ ] 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )


2. 技术


  • node
  • cookie-parser : "~1.4.3"
  • crypto : "^1.0.1"
  • express: "~4.16.0"
  • express-session : "^1.15.6",
  • http-errors : "~1.6.2",
  • mongodb : "^3.1.8",
  • mongoose : "^5.3.7",
  • mongoose-auto-increment : "^5.0.1",
  • yargs : "^12.0.2"


3. 主文件 app.js


// modules
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const session = require('express-session');
// import 等语法要用到 babel 支持
require('babel-register');
const app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(cookieParser('blog_node_cookie'));
app.use(
    session({
        secret: 'blog_node_cookie',
        name: 'session_id', //# 在浏览器中生成cookie的名称key,默认是connect.sid
        resave: true,
        saveUninitialized: true,
        cookie: { maxAge: 60 * 1000 * 30, httpOnly: true }, //过期时间
    }),
);
const mongodb = require('./core/mongodb');
// data server
mongodb.connect();
//将路由文件引入
const route = require('./routes/index');
//初始化所有路由
route(app);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};
    // render the error page
    res.status(err.status || 500);
    res.render('error');
});
module.exports = app;


4. 数据库 core/mongodb.js


/**
 * Mongoose module.
 * @file 数据库模块
 * @module core/mongoose
 * @author  biaochenxuying <https://github.com/biaochenxuying>
 */
const consola = require('consola')
const CONFIG = require('../app.config.js')
const mongoose = require('mongoose')
const autoIncrement = require('mongoose-auto-increment')
// remove DeprecationWarning
mongoose.set('useFindAndModify', false)
// mongoose Promise
mongoose.Promise = global.Promise
// mongoose
exports.mongoose = mongoose
// connect
exports.connect = () => {
    // 连接数据库
    mongoose.connect(CONFIG.MONGODB.uri, {
        useCreateIndex: true,
        useNewUrlParser: true,
        promiseLibrary: global.Promise
    })
    // 连接错误
    mongoose.connection.on('error', error => {
        consola.warn('数据库连接失败!', error)
    })
    // 连接成功
    mongoose.connection.once('open', () => {
        consola.ready('数据库连接成功!')
    })
    // 自增 ID 初始化
    autoIncrement.initialize(mongoose.connection)
    // 返回实例
    return mongoose
}


5. 数据模型 Model


这里只介绍 用户、文章和评论 的模型。


5.1 用户


用户的字段都有设置类型 type,大多都设置了默认值 default ,邮箱设置了验证规则 validate,密码保存用了 crypto 来加密。


用了中间件自增 ID 插件 mongoose-auto-increment。


/**
 * User model module.
 * @file 权限和用户数据模型
 * @module model/user
 * @author biaochenxuying <https://github.com/biaochenxuying>
 */
const crypto = require('crypto');
const { argv } = require('yargs');
const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');
const adminSchema = new mongoose.Schema({
    // 名字
    name: { type: String, required: true, default: '' },
    // 用户类型 0:博主 1:其他用户
    type: { type: Number, default: 1 },
    // 手机
    phone: { type: String, default: '' },
    //封面
    img_url: { type: String, default: '' },
    // 邮箱
    email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ },
    // 个人介绍
    introduce: { type: String, default: '' },
    // 头像
    avatar: { type: String, default: 'user' },
    // 密码
    password: {
        type: String,
        required: true,
        default: crypto
            .createHash('md5')
            .update(argv.auth_default_password || 'root')
            .digest('hex'),
    },
    // 创建日期
    create_time: { type: Date, default: Date.now },
    // 最后修改日期
    update_time: { type: Date, default: Date.now },
});
// 自增 ID 插件配置
adminSchema.plugin(autoIncrement.plugin, {
    model: 'User',
    field: 'id',
    startAt: 1,
    incrementBy: 1,
});
module.exports = mongoose.model('User', adminSchema);


5.2 文章


文章是分类型的:文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍

而且简历和管理员介绍的文章只能是各自一篇(因为前台展示那里有个导航 关于我 ,就是请求管理员介绍这篇文章的,简历也是打算这样子用的),普通文章可以是无数篇。

点赞的用户 like_users 那里应该只保存用户 id 的,这个后面修改一下。


/**
* Article model module.
* @file 文章数据模型
* @module model/article
* @author biaochenxuying <https://github.com/biaochenxuying>
*/
const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');
// 文章模型
const articleSchema = new mongoose.Schema({
    // 文章标题
    title: { type: String, required: true, validate: /\S+/ },
    // 文章关键字(SEO)
    keyword: [{ type: String, default: '' }],
    // 作者
    author: { type: String, required: true, validate: /\S+/ },
    // 文章描述
    desc: { type: String, default: '' },
    // 文章内容
    content: { type: String, required: true, validate: /\S+/ },
    // 字数
    numbers: { type: String, default: 0 },
    // 封面图
    img_url: { type: String, default: 'https://upload-images.jianshu.io/upload_images/12890819-80fa7517ab3f2783.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240' },
    // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
    type: { type: Number, default: 1 },
    // 文章发布状态 => 0 草稿,1 已发布
    state: { type: Number, default: 1 },
    // 文章转载状态 => 0 原创,1 转载,2 混合
    origin: { type: Number, default: 0 },
    // 文章标签
    tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag', required: true }],
    comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment', required: true }],
    // 文章分类
    category: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: true }],
    // 点赞的用户
    like_users: [
        {
            // 用户id
            id: { type: mongoose.Schema.Types.ObjectId },
            // 名字
            name: { type: String, required: true, default: '' },
            // 用户类型 0:博主 1:其他用户
            type: { type: Number, default: 1 },
            // 个人介绍
            introduce: { type: String, default: '' },
            // 头像
            avatar: { type: String, default: 'user' },
            // 创建日期
            create_time: { type: Date, default: Date.now },
        },
    ],
    // 其他元信息
    meta: {
        views: { type: Number, default: 0 },
        likes: { type: Number, default: 0 },
        comments: { type: Number, default: 0 },
    },
    // 创建日期
    create_time: { type: Date, default: Date.now },
    // 最后修改日期
    update_time: { type: Date, default: Date.now },
});
// 自增 ID 插件配置
articleSchema.plugin(autoIncrement.plugin, {
    model: 'Article',
    field: 'id',
    startAt: 1,
    incrementBy: 1,
});
// 文章模型
module.exports = mongoose.model('Article', articleSchema);


5.3 评论


评论功能是实现了简单的三级评论的,第三者的评论(就是别人对一级评论进行再评论)放在 other_comments 里面。


/**
* Comment model module.
* @file 评论数据模型
* @module model/comment
* @author biaochenxuying <https://github.com/biaochenxuying>
*/
const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');
// 评论模型
const commentSchema = new mongoose.Schema({
    // 评论所在的文章 id
    article_id: { type: mongoose.Schema.Types.ObjectId, required: true },
    // content
    content: { type: String, required: true, validate: /\S+/ },
    // 是否置顶
    is_top: { type: Boolean, default: false },
    // 被赞数
    likes: { type: Number, default: 0 },
    user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
    // 父评论的用户信息
    user: {
        // 用户id
        user_id: { type: mongoose.Schema.Types.ObjectId },
        // 名字
        name: { type: String, required: true, default: '' },
        // 用户类型 0:博主 1:其他用户
        type: { type: Number, default: 1 },
        // 头像
        avatar: { type: String, default: 'user' },
    },
    // 第三者评论
    other_comments: [
        {
            user: {
                id: { type: mongoose.Schema.Types.ObjectId },
                // 名字
                name: { type: String, required: true, default: '' },
                // 用户类型 0:博主 1:其他用户
                type: { type: Number, default: 1 },
            },
            // content
            content: { type: String, required: true, validate: /\S+/ },
            // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论
            state: { type: Number, default: 1 },
            // 创建日期
            create_time: { type: Date, default: Date.now },
        },
    ],
    // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论
    state: { type: Number, default: 1 },
    // 创建日期
    create_time: { type: Date, default: Date.now },
    // 最后修改日期
    update_time: { type: Date, default: Date.now },
});
// 自增 ID 插件配置
commentSchema.plugin(autoIncrement.plugin, {
    model: 'Comment',
    field: 'id',
    startAt: 1,
    incrementBy: 1,
});
// 标签模型
module.exports = mongoose.model('Comment', commentSchema);


其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。


6. 路由接口 routes


6.1 主文件


/*
*所有的路由接口
*/
const user = require('./user');
const article = require('./article');
const comment = require('./comment');
const message = require('./message');
const tag = require('./tag');
const link = require('./link');
const category = require('./category');
const timeAxis = require('./timeAxis');
module.exports = app => {
    app.post('/login', user.login);
    app.post('/logout', user.logout);
    app.post('/loginAdmin', user.loginAdmin);
    app.post('/register', user.register);
    app.post('/delUser', user.delUser);
    app.get('/currentUser', user.currentUser);
    app.get('/getUserList', user.getUserList);
    app.post('/addComment', comment.addComment);
    app.post('/addThirdComment', comment.addThirdComment);
    app.post('/changeComment', comment.changeComment);
    app.post('/changeThirdComment', comment.changeThirdComment);
    app.get('/getCommentList', comment.getCommentList);
    app.post('/addArticle', article.addArticle);
    app.post('/updateArticle', article.updateArticle);
    app.post('/delArticle', article.delArticle);
    app.get('/getArticleList', article.getArticleList);
    app.get('/getArticleListAdmin', article.getArticleListAdmin);
    app.post('/getArticleDetail', article.getArticleDetail);
    app.post('/likeArticle', article.likeArticle);
    app.post('/addTag', tag.addTag);
    app.post('/delTag', tag.delTag);
    app.get('/getTagList', tag.getTagList);
    app.post('/addMessage', message.addMessage);
    app.post('/addReplyMessage', message.addReplyMessage);
    app.post('/delMessage', message.delMessage);
    app.post('/getMessageDetail', message.getMessageDetail);
    app.get('/getMessageList', message.getMessageList);
    app.post('/addLink', link.addLink);
    app.post('/updateLink', link.updateLink);
    app.post('/delLink', link.delLink);
    app.get('/getLinkList', link.getLinkList);
    app.post('/addCategory', category.addCategory);
    app.post('/delCategory', category.delCategory);
    app.get('/getCategoryList', category.getCategoryList);
    app.post('/addTimeAxis', timeAxis.addTimeAxis);
    app.post('/updateTimeAxis', timeAxis.updateTimeAxis);
    app.post('/delTimeAxis', timeAxis.delTimeAxis);
    app.get('/getTimeAxisList', timeAxis.getTimeAxisList);
    app.post('/getTimeAxisDetail', timeAxis.getTimeAxisDetail);
};


6.2 文章


各模块的列表都是用了分页的形式的。


import Article from '../models/article';
import User from '../models/user';
import { responseClient, timestampToTime } from '../util/util';
exports.addArticle = (req, res) => {
    // if (!req.session.userInfo) {
    //     responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
    //     return;
    // }
    const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin } = req.body;
    let tempArticle = null
    if(img_url){
        tempArticle = new Article({
            title,
            author,
            keyword: keyword ? keyword.split(',') : [],
            content,
            numbers: content.length,
            desc,
            img_url,
            tags: tags ? tags.split(',') : [],
            category: category ? category.split(',') : [],
            state,
            type,
            origin,
        });
    }else{
        tempArticle = new Article({
            title,
            author,
            keyword: keyword ? keyword.split(',') : [],
            content,
            numbers: content.length,
            desc,
            tags: tags ? tags.split(',') : [],
            category: category ? category.split(',') : [],
            state,
            type,
            origin,
        });
    }
    tempArticle
        .save()
        .then(data => {
            responseClient(res, 200, 0, '保存成功', data);
        })
        .catch(err => {
            console.log(err);
            responseClient(res);
        });
};
exports.updateArticle = (req, res) => {
    // if (!req.session.userInfo) {
    //     responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
    //     return;
    // }
    const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin, id } = req.body;
    Article.update(
        { _id: id },
        {
            title,
            author,
            keyword: keyword ? keyword.split(','): [],
            content,
            desc,
            img_url,
            tags: tags ? tags.split(',') : [],
            category:category ? category.split(',') : [],
            state,
            type,
            origin,
        },
    )
        .then(result => {
            responseClient(res, 200, 0, '操作成功', result);
        })
        .catch(err => {
            console.error(err);
            responseClient(res);
        });
};
exports.delArticle = (req, res) => {
    let { id } = req.body;
    Article.deleteMany({ _id: id })
        .then(result => {
            if (result.n === 1) {
                responseClient(res, 200, 0, '删除成功!');
            } else {
                responseClient(res, 200, 1, '文章不存在');
            }
        })
        .catch(err => {
            console.error('err :', err);
            responseClient(res);
        });
};
// 前台文章列表
exports.getArticleList = (req, res) => {
    let keyword = req.query.keyword || null;
    let state = req.query.state || '';
    let likes = req.query.likes || '';
    let tag_id = req.query.tag_id || '';
    let category_id = req.query.category_id || '';
    let pageNum = parseInt(req.query.pageNum) || 1;
    let pageSize = parseInt(req.query.pageSize) || 10;
    let conditions = {};
    if (!state) {
        if (keyword) {
            const reg = new RegExp(keyword, 'i'); //不区分大小写
            conditions = {
                $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }],
            };
        }
    } else if (state) {
        state = parseInt(state);
        if (keyword) {
            const reg = new RegExp(keyword, 'i');
            conditions = {
                $and: [
                    { $or: [{ state: state }] },
                    { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] },
                ],
            };
        } else {
            conditions = { state };
        }
    }
    let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
    let responseData = {
        count: 0,
        list: [],
    };
    Article.countDocuments(conditions, (err, count) => {
        if (err) {
            console.log('Error:' + err);
        } else {
            responseData.count = count;
            // 待返回的字段
            let fields = {
                title: 1,
                author: 1,
                keyword: 1,
                content: 1,
                desc: 1,
                img_url: 1,
                tags: 1,
                category: 1,
                state: 1,
                type: 1,
                origin: 1,
                comments: 1,
                like_User_id: 1,
                meta: 1,
                create_time: 1,
                update_time: 1,
            };
            let options = {
                skip: skip,
                limit: pageSize,
                sort: { create_time: -1 },
            };
            Article.find(conditions, fields, options, (error, result) => {
                if (err) {
                    console.error('Error:' + error);
                    // throw error;
                } else {
                    let newList = [];
                    if (likes) {
                        // 根据热度 likes 返回数据
                        result.sort((a, b) => {
                            return b.meta.likes - a.meta.likes;
                        });
                        responseData.list = result;
                    } else if (category_id) {
                        // 根据 分类 id 返回数据
                        result.forEach(item => {
                            if (item.category.indexOf(category_id) > -1) {
                                newList.push(item);
                            }
                        });
                        let len = newList.length;
                        responseData.count = len;
                        responseData.list = newList;
                    } else if (tag_id) {
                        // 根据标签 id 返回数据
                        result.forEach(item => {
                            if (item.tags.indexOf(tag_id) > -1) {
                                newList.push(item);
                            }
                        });
                        let len = newList.length;
                        responseData.count = len;
                        responseData.list = newList;
                    } else {
                        responseData.list = result;
                    }
                    responseClient(res, 200, 0, '操作成功!', responseData);
                }
            });
        }
    });
};
// 后台文章列表
exports.getArticleListAdmin = (req, res) => {
    let keyword = req.query.keyword || null;
    let state = req.query.state || '';
    let likes = req.query.likes || '';
    let pageNum = parseInt(req.query.pageNum) || 1;
    let pageSize = parseInt(req.query.pageSize) || 10;
    let conditions = {};
    if (!state) {
        if (keyword) {
            const reg = new RegExp(keyword, 'i'); //不区分大小写
            conditions = {
                $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }],
            };
        }
    } else if (state) {
        state = parseInt(state);
        if (keyword) {
            const reg = new RegExp(keyword, 'i');
            conditions = {
                $and: [
                    { $or: [{ state: state }] },
                    { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] },
                ],
            };
        } else {
            conditions = { state };
        }
    }
    let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
    let responseData = {
        count: 0,
        list: [],
    };
    Article.countDocuments(conditions, (err, count) => {
        if (err) {
            console.log('Error:' + err);
        } else {
            responseData.count = count;
            // 待返回的字段
            let fields = {
                title: 1,
                author: 1,
                keyword: 1,
                content: 1,
                desc: 1,
                img_url: 1,
                tags: 1,
                category: 1,
                state: 1,
                type: 1,
                origin: 1,
                comments: 1,
                like_User_id: 1,
                meta: 1,
                create_time: 1,
                update_time: 1,
            };
            let options = {
                skip: skip,
                limit: pageSize,
                sort: { create_time: -1 },
            };
            Article.find(conditions, fields, options, (error, result) => {
                if (err) {
                    console.error('Error:' + error);
                    // throw error;
                } else {
                    if (likes) {
                        result.sort((a, b) => {
                            return b.meta.likes - a.meta.likes;
                        });
                    }
                    responseData.list = result;
                    responseClient(res, 200, 0, '操作成功!', responseData);
                }
            })
                .populate([
                    { path: 'tags', },
                    { path: 'comments',  },
                    { path: 'category',  },
                ])
                .exec((err, doc) => {});
        }
    });
};
// 文章点赞
exports.likeArticle = (req, res) => {
    if (!req.session.userInfo) {
        responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
        return;
    }
    let { id, user_id } = req.body;
    Article.findOne({ _id: id })
        .then(data => {
            let fields = {};
            data.meta.likes = data.meta.likes + 1;
            fields.meta = data.meta;
            let like_users_arr = data.like_users.length ? data.like_users : [];
            User.findOne({ _id: user_id })
                .then(user => {
                    let new_like_user = {};
                    new_like_user.id = user._id;
                    new_like_user.name = user.name;
                    new_like_user.avatar = user.avatar;
                    new_like_user.create_time = user.create_time;
                    new_like_user.type = user.type;
                    new_like_user.introduce = user.introduce;
                    like_users_arr.push(new_like_user);
                    fields.like_users = like_users_arr;
                    Article.update({ _id: id }, fields)
                        .then(result => {
                            responseClient(res, 200, 0, '操作成功!', result);
                        })
                        .catch(err => {
                            console.error('err :', err);
                            throw err;
                        });
                })
                .catch(err => {
                    responseClient(res);
                    console.error('err 1:', err);
                });
        })
        .catch(err => {
            responseClient(res);
            console.error('err 2:', err);
        });
};
// 文章详情
exports.getArticleDetailByType = (req, res) => {
    let { type } = req.body;
    if (!type) {
        responseClient(res, 200, 1, '文章不存在 !');
        return;
    }
    Article.findOne({ type: type }, (Error, data) => {
        if (Error) {
            console.error('Error:' + Error);
            // throw error;
        } else {
            data.meta.views = data.meta.views + 1;
            Article.updateOne({ type: type }, { meta: data.meta })
                .then(result => {
                    responseClient(res, 200, 0, '操作成功 !', data);
                })
                .catch(err => {
                    console.error('err :', err);
                    throw err;
                });
        }
    })
        .populate([
            { path: 'tags', select: '-_id' },
            { path: 'category', select: '-_id' },
            { path: 'comments', select: '-_id' },
        ])
        .exec((err, doc) => {
            // console.log("doc:");          // aikin
            // console.log("doc.tags:",doc.tags);          // aikin
            // console.log("doc.category:",doc.category);           // undefined
        });
};
// 文章详情
exports.getArticleDetail = (req, res) => {
    let { id } = req.body;
    let type = Number(req.body.type) || 1; //文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
    console.log('type:', type);
    if (type === 1) {
        if (!id) {
            responseClient(res, 200, 1, '文章不存在 !');
            return;
        }
        Article.findOne({ _id: id }, (Error, data) => {
            if (Error) {
                console.error('Error:' + Error);
                // throw error;
            } else {
                data.meta.views = data.meta.views + 1;
                Article.updateOne({ _id: id }, { meta: data.meta })
                    .then(result => {
                        responseClient(res, 200, 0, '操作成功 !', data);
                    })
                    .catch(err => {
                        console.error('err :', err);
                        throw err;
                    });
            }
        })
            .populate([
                { path: 'tags',  },
                { path: 'category',  },
                { path: 'comments',  },
            ])
            .exec((err, doc) => {
                // console.log("doc:");          // aikin
                // console.log("doc.tags:",doc.tags);          // aikin
                // console.log("doc.category:",doc.category);           // undefined
            });
    } else {
        Article.findOne({ type: type }, (Error, data) => {
            if (Error) {
                console.log('Error:' + Error);
                // throw error;
            } else {
                if (data) {
                    data.meta.views = data.meta.views + 1;
                    Article.updateOne({ type: type }, { meta: data.meta })
                        .then(result => {
                            responseClient(res, 200, 0, '操作成功 !', data);
                        })
                        .catch(err => {
                            console.error('err :', err);
                            throw err;
                        });
                } else {
                    responseClient(res, 200, 1, '文章不存在 !');
                    return;
                }
            }
        })
            .populate([
                { path: 'tags',  },
                { path: 'category',  },
                { path: 'comments',  },
            ])
            .exec((err, doc) => {});
    }
};


6.3 评论


评论是有状态的:状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论。

管理一级和三级评论是设置前台能不能展示的,默认是展示,如果管理员看了,是条垃圾评论就 设置为 -1 或者 -2 ,进行隐藏,前台就不会展现了。


import { responseClient } from '../util/util';
import Comment from '../models/comment';
import User from '../models/user';
import Article from '../models/article';
//获取全部评论
exports.getCommentList = (req, res) => {
    let keyword = req.query.keyword || null;
    let comment_id = req.query.comment_id || null;
    let pageNum = parseInt(req.query.pageNum) || 1;
    let pageSize = parseInt(req.query.pageSize) || 10;
    let conditions = {};
    if (comment_id) {
        if (keyword) {
            const reg = new RegExp(keyword, 'i'); //不区分大小写
            conditions = {
                _id: comment_id,
                content: { $regex: reg },
            };
        } else {
            conditions = {
                _id: comment_id,
            };
        }
    } else {
        if (keyword) {
            const reg = new RegExp(keyword, 'i'); //不区分大小写
            conditions = {
                content: { $regex: reg },
            };
        }
    }
    let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
    let responseData = {
        count: 0,
        list: [],
    };
    Comment.countDocuments(conditions, (err, count) => {
        if (err) {
            console.error('Error:' + err);
        } else {
            responseData.count = count;
            // 待返回的字段
            let fields = {
                article_id: 1,
                content: 1,
                is_top: 1,
                likes: 1,
                user_id: 1,
                user: 1,
                other_comments: 1,
                state: 1,
                create_time: 1,
                update_time: 1,
            };
            let options = {
                skip: skip,
                limit: pageSize,
                sort: { create_time: -1 },
            };
            Comment.find(conditions, fields, options, (error, result) => {
                if (err) {
                    console.error('Error:' + error);
                    // throw error;
                } else {
                    responseData.list = result;
                    responseClient(res, 200, 0, '操作成功!', responseData);
                }
            });
        }
    });
};
// 添加一级评论
exports.addComment = (req, res) => {
    if (!req.session.userInfo) {
        responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
        return;
    }
    let { article_id, user_id, content } = req.body;
    User.findById({
        _id: user_id,
    })
        .then(result => {
            // console.log('result :', result);
            if (result) {
                let userInfo = {
                    user_id: result._id,
                    name: result.name,
                    type: result.type,
                    avatar: result.avatar,
                };
                let comment = new Comment({
                    article_id: article_id,
                    content: content,
                    user_id: user_id,
                    user: userInfo,
                });
                comment
                    .save()
                    .then(commentResult => {
                        Article.findOne({ _id: article_id }, (errors, data) => {
                            if (errors) {
                                console.error('Error:' + errors);
                                // throw errors;
                            } else {
                                data.comments.push(commentResult._id);
                                data.meta.comments = data.meta.comments + 1;
                                Article.updateOne({ _id: article_id }, { comments: data.comments, meta: data.meta })
                                    .then(result => {
                                        responseClient(res, 200, 0, '操作成功 !', commentResult);
                                    })
                                    .catch(err => {
                                        console.error('err :', err);
                                        throw err;
                                    });
                            }
                        });
                    })
                    .catch(err2 => {
                        console.error('err :', err2);
                        throw err2;
                    });
            } else {
                responseClient(res, 200, 1, '用户不存在');
            }
        })
        .catch(error => {
            console.error('error :', error);
            responseClient(res);
        });
};
// 添加第三者评论
exports.addThirdComment = (req, res) => {
    if (!req.session.userInfo) {
        responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
        return;
    }
    let { article_id, comment_id, user_id, content } = req.body;
    Comment.findById({
        _id: comment_id,
    })
        .then(commentResult => {
            User.findById({
                _id: user_id,
            })
                .then(userResult => {
                    if (userResult) {
                        let userInfo = {
                            user_id: userResult._id,
                            name: userResult.name,
                            type: userResult.type,
                            avatar: userResult.avatar,
                        };
                        let item = {
                            user: userInfo,
                            content: content,
                        };
                        commentResult.other_comments.push(item);
                        Comment.updateOne(
                            { _id: comment_id },
                            {
                                other_comments: commentResult,
                            },
                        )
                            .then(result => {
                                responseClient(res, 200, 0, '操作成功', result);
                                Article.findOne({ _id: article_id }, (errors, data) => {
                                    if (errors) {
                                        console.error('Error:' + errors);
                                        // throw errors;
                                    } else {
                                        data.meta.comments = data.meta.comments + 1;
                                        Article.updateOne({ _id: article_id }, { meta: data.meta })
                                            .then(result => {
                                                // console.log('result :', result);
                                                responseClient(res, 200, 0, '操作成功 !', result);
                                            })
                                            .catch(err => {
                                                console.log('err :', err);
                                                throw err;
                                            });
                                    }
                                });
                            })
                            .catch(err1 => {
                                console.error('err1:', err1);
                                responseClient(res);
                            });
                    } else {
                        responseClient(res, 200, 1, '用户不存在');
                    }
                })
                .catch(error => {
                    console.error('error :', error);
                    responseClient(res);
                });
        })
        .catch(error2 => {
            console.error('error2 :', error2);
            responseClient(res);
        });
};
// 管理一级评论
exports.changeComment = (req, res) => {
    if (!req.session.userInfo) {
        responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
        return;
    }
    let { id, state } = req.body;
    Comment.updateOne(
        { _id: id },
        {
            state: Number(state),
        },
    )
        .then(result => {
            responseClient(res, 200, 0, '操作成功', result);
        })
        .catch(err => {
            console.error('err:', err);
            responseClient(res);
        });
};
// 管理第三者评论
exports.changeThirdComment = (req, res) => {
    if (!req.session.userInfo) {
        responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
        return;
    }
    let { comment_id, state, index } = req.body;
    Comment.findById({
        _id: comment_id,
    })
        .then(commentResult => {
            let i = index ? Number(index) : 0;
            if (commentResult.other_comments.length) {
                commentResult.other_comments[i].state = Number(state);
                Comment.updateOne(
                    { _id: comment_id },
                    {
                        other_comments: commentResult,
                    },
                )
                    .then(result => {
                        responseClient(res, 200, 0, '操作成功', result);
                    })
                    .catch(err1 => {
                        console.error('err1:', err1);
                        responseClient(res);
                    });
            } else {
                responseClient(res, 200, 1, '第三方评论不存在!', result);
            }
        })
        .catch(error2 => {
            console.log('error2 :', error2);
            responseClient(res);
        });
};


其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。


7. Build Setup ( 构建安装 )


# install dependencies
npm install 
# serve with hot reload at localhost: 3000
npm start
# build for production with minification


请使用 pm2 ,可以永久运行在服务器上,且不会一报错 node程序就挂了。


8. 项目地址


如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。

项目地址:


前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
28天前
|
JavaScript 前端开发 中间件
探索后端技术:Node.js与Express框架的完美融合
【10月更文挑战第7天】 在当今数字化时代,Web应用已成为日常生活不可或缺的一部分。本文将深入探讨后端技术的两大重要角色——Node.js和Express框架,分析它们如何通过其独特的特性和优势,为现代Web开发提供强大支持。我们将从Node.js的非阻塞I/O和事件驱动机制,到Express框架的简洁路由和中间件特性,全面解析它们的工作原理及应用场景。此外,本文还将分享一些实际开发中的小技巧,帮助你更有效地利用这些技术构建高效、可扩展的Web应用。无论你是刚入门的新手,还是经验丰富的开发者,相信这篇文章都能为你带来新的启发和思考。
|
2月前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实战指南
【9月更文挑战第6天】在数字化时代的潮流中,后端开发作为支撑现代Web和移动应用的核心,其重要性不言而喻。本文将深入浅出地介绍如何使用Node.js及其流行的框架Express来搭建一个高效、可扩展的后端服务。通过具体的代码示例和实践技巧,我们将探索如何利用这两个强大的工具提升开发效率和应用性能。无论你是后端开发的新手还是希望提高现有项目质量的老手,这篇文章都将为你提供有价值的见解和指导。
|
3月前
|
JavaScript 前端开发 中间件
构建高效后端服务:Node.js与Express框架的完美搭档
【8月更文挑战第28天】在追求高性能、可扩展和易维护的后端开发领域,Node.js和Express框架的组合提供了一种轻量级且灵活的解决方案。本文将深入探讨如何利用这一组合打造高效的后端服务,并通过实际代码示例展示其实现过程。
|
3月前
|
JavaScript 中间件 API
深入浅出Node.js后端框架——Express
【8月更文挑战第27天】在这篇文章中,我们将一起探索Node.js的热门框架Express。Express以其简洁、高效的特点,成为了许多Node.js开发者的首选框架。本文将通过实例引导你了解Express的核心概念和使用方法,让你快速上手构建自己的Web应用。
|
7天前
|
JavaScript 中间件 关系型数据库
构建高效的后端服务:Node.js 与 Express 的实践指南
在后端开发领域,Node.js 与 Express 的组合因其轻量级和高效性而广受欢迎。本文将深入探讨如何利用这一组合构建高性能的后端服务。我们将从 Node.js 的事件驱动和非阻塞 I/O 模型出发,解释其如何优化网络请求处理。接着,通过 Express 框架的简洁 API,展示如何快速搭建 RESTful API。文章还将涉及中间件的使用,以及如何结合 MySQL 数据库进行数据操作。最后,我们将讨论性能优化技巧,包括异步编程模式和缓存策略,以确保服务的稳定性和扩展性。
|
14天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的完美结合
【10月更文挑战第21天】本文将引导你走进Node.js和Express框架的世界,探索它们如何共同打造一个高效、可扩展的后端服务。通过深入浅出的解释和实际代码示例,我们将一起理解这一组合的魅力所在,并学习如何利用它们来构建现代Web应用。
38 1
|
2天前
|
Web App开发 JavaScript 前端开发
探索后端开发:Node.js与Express的完美结合
【10月更文挑战第33天】本文将带领读者深入了解Node.js和Express的强强联手,通过实际案例揭示它们如何简化后端开发流程,提升应用性能。我们将一起探索这两个技术的核心概念、优势以及它们如何共同作用于现代Web开发中。准备好,让我们一起开启这场技术之旅!
10 0
|
2天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
4天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的融合之道
【10月更文挑战第31天】在追求快速、灵活和高效的后端开发领域,Node.js与Express框架的结合如同咖啡遇见了奶油——完美融合。本文将带你探索这一组合如何让后端服务搭建变得既轻松又充满乐趣,同时确保你的应用能够以光速运行。
10 0
|
1月前
|
Web App开发 JavaScript API
构建高效后端系统:Node.js与Express框架的实践之路
【9月更文挑战第37天】在数字化时代的浪潮中,后端开发作为技术架构的核心,承载着数据处理和业务逻辑的重要职责。本文将深入探讨如何利用Node.js及其强大的Express框架来搭建一个高效、可扩展的后端系统。我们将从基础概念讲起,逐步引导读者理解并实践如何设计、开发和维护一个高性能的后端服务。通过实际代码示例和清晰的步骤说明,本文旨在为初学者和有经验的开发者提供一个全面的指南,帮助他们在后端开发的旅途上走得更远。
43 3