本博客的一些设计思路
前言
本人比较菜,了解的知识很少,所以肯定存在不少问题,如果您有哪些地方存在问题或建议,欢迎在留言区指出。本博客还存在不少问题,完善后会在github上开源,感兴趣可以话可以看看。这次重构主要是为了SEO优化,完善用户系统和后台。代码拆分为三个项目,客户端、后台、服务端。
本站已开源 https://gitee.com/yoshinosk/blog ,欢迎感兴趣的朋友一起学习。
技术栈
前端: NuxtJS 、 Bootstrap 、 ElementUI 、 Vue.js
数据库: MongoDB 、Redis
原本有些功能(登录、排行榜、签到、点赞)是用redis做的,但是做完后觉得反正数据量不大,没必要用redis,就改成用mongodb了。
UI
可能不少人会觉得本站的UI花里胡哨的,我也觉得。但我的思路就是要个性化,毕竟是个人博客,主要还是为自己服务,自己喜欢就好了。
登录
使用Refresh Token保持登录状态。实现思路:用户登录后生成并返回access_token和refresh_token,服务端将access_token和refresh_token存进数据库,用于校验是否匹配,将access_token存在Vuex中,refresh_token存在cookie实现持久化,access_token过期时调用刷新TOKEN接口,返回并保存新的access_token。
自动登录
检测cookie中是否存在refresh_token,存在则调用刷新TOKEN接口,返回用户信息和新的access_token。请求时如果token过期,则拦截本次请求,调用刷新TOKEN接口,拿到新的token后重新请求。
文章
普通用户发布文章需要管理员审核后才会显示,编辑文章后需要重新审核,审核结果会发送消息给用户,通过后可以获得经验值和硬币。发布文章时会将标签保存到文章标签数据库。每日签到可以获得1-10硬币和(10+连续签到天数 *5)经验值。发布评论可以获得10经验值,不做限制(反正没用户)。
匿名点赞,点赞后将点赞的文章存在cookie中判断是否已点赞该文章。
富文本编辑器使用 wangEditor 。服务端对文章内容使用xss过滤。
数据库模型
const mongoose = require("mongoose");
// 文章模型
const ArticleScheam = new mongoose.Schema({
_id: { type: Number, unique: true, index: true, required: true },
title: { type: String, trim: true, required: true }, // 标题
content: { type: String, required: true },// 内容
desc: { type: String, required: true },// 描述
author: { type: Number, required: true, ref:'User' }, //作者id
createTime: { type: Number , default: () => Math.ceil(Date.now() / 1000) },// 创建时间
updateTime: { type: Number },// 编辑时间
deleteTime: {type:Number},// 删除时间
sort: { type: Number, default: 0 ,ref: 'Article_Sort' },// 分类id
views: { type: Number, default: 0 },// 浏览数
tag: { type: Array },// 标签
coverImg: { type: String }, // 封面图
status: { type: Number, default: 0 },// -1:已删除 0:待审核 1:审核通过 2:审核未通过 3:隐藏
order: { type: Number, default: 0 }, // 排序 越大越靠前
goods: { type: Number, default: 0 },// 点赞
allowComment:{ type:Number,default: 1 }, // 允许评论
type:{ type:Number,default: 1 }, // 文章类型 默认1 - HTML 2 - MARKDOWN
},{ _id: false, autoIndex: false , versionKey: false})
// 文章分类模型
const SortScheam = new mongoose.Schema({
_id: { type: Number, require: true , index: true},
name: { type: String, require: true , index: true},
desc: { type: String, require: true },
order: { type: Number, default: 0 }, // 排序 越大越靠前
},{ _id: false, autoIndex: false ,versionKey: false})
// 文章标签模型
const TagsScheam = new mongoose.Schema({
name: { type: String, require: true },
total: { type: Number, default: 1 }
},{ versionKey: false})
module.exports = {
Article: mongoose.model('Article', ArticleScheam),
Sort: mongoose.model('Article_Sort', SortScheam),
Tags: mongoose.model('Article_Tags', TagsScheam),
}评论
需要登录后才能发表评论,点赞功能因为没用户所以感觉没必要就没做。就是懒,哪天闲了再加上
数据库模型
const Schema = new mongoose.Schema({
article: { type: Number, required: true, ref: "Article" }, // 文章id
from: { type: Number, required: true, ref: "User" }, // 用户id
comment: { type: mongoose.Types.ObjectId, ref: "Comment" }, // 顶级评论id
reply: { type: mongoose.Types.ObjectId, ref: "Comment" }, // 回复的楼中楼id
content: { type: String, required: true, trim: true},
createTime: { type: Number, default: () => Math.ceil(Date.now() / 1000) },// 创建时间
goods: { type:Number, default: 0 }, //点赞数
bads: { type:Number, default: 0 }, // 踩
status: { type: Number, default: 1 } // 1 正常 0 已删除
}, {versionKey: false})用户
为了避免一些麻烦需要邮箱验证后才能注册,昵称不可重复。用户等级参考了四糸乃吧的等级。
数据库模型
const UserScheam = new mongoose.Schema({
_id: { type: Number, unique: true, index: true },
nickname: { type: String, trim: true, required: true, unique: true },// 昵称
password: {
type: String, trim: true, required: true, set(val) {
return md5(val + SECRET_PWD)
}
},
ip: { type: String },
email: {
type: String, trim: true, required: true, unique: true, index: true, set(val) {
return String(val).toLowerCase()
}
},
avatar: {
type: String, default: () => `/static/img/default_avatar.png`, get: (val) => {
return new URL(val, HOST).toString()
}
}, // 头像
registerTime: { type: Number, default: () => Math.ceil(Date.now() / 1000) }, // 注册时间
roles: { type: Array, default: ['user'] }, // 角色组
lastLoginTime: { type: Number },// 最后登录日期
exp: { type: Number, default: 0 }, // 经验
coin: { type: Number, default: 0 },// 积分
signInTol: { type: Number, default: 0 }, // 累计签到
usersign: { type: String }, // 用户签名
status: { type: Number, default: 1 } // 状态 0-封禁 1-正常
}, { _id: false, autoIndex: false, versionKey: false, toJSON: { virtuals: true, getters: true }, toObject: { getters: true } })
UserScheam.virtual('role', {
ref: 'Role',
localField: 'roles',
foreignField: 'name',
justOne: false,
})留言板
允许匿名留言,不做限制。
数据库模型
const Schema = new mongoose.Schema({
from: { type: Number, ref: "User" }, // 用户id
email: { type: String },
nickname: { type: String },
content: { type: String, required: true, trim: true },
createTime: { type: Number, default: () => Math.ceil(Date.now() / 1000) },// 创建时间
ip: { type: String }
}, { versionKey: false })过滤器
服务端在发布文章和编辑文字处对内容加入了xss过滤,其他允许用户输入的地方加入了违规词检测,虽然感觉没必要(反正小破站没人看),过滤器使用mint-filter。词库在github找的,感觉也蛮全的,共5w条。但为了避免一些麻烦还是加上了。
鉴权
一个用户可以拥有多个角色,每个角色都包含了不同的权限。在需要鉴权的接口加入中间件,判断用户是否拥有指定权限。
数据库模型
/**
* @description 用户角色模型
*/
const RoleScheam = new mongoose.Schema({
_id: { type: Number, unique: true, index: true, required: true },
name: { type: String, trim: true, unique: true, required: true }, // 角色名
title: { type: String, default: '' }, // 称号
rights: { type: Array, default: [] } // 权限数组
},{ _id: false, autoIndex: false ,versionKey: false})
/**
* @description 用户权限模型
*/
const RightsScheam = new mongoose.Schema({
_id: { type: Number, unique: true, index: true, required: true },
name: { type: String, trim: true, unique: true, required: true }, // 权限名
field: { type: String, unique: true, index: true }, // 列名
pid: { type: Number, default: null }, // 父级
isRights: { type: Number, default: 0, set(val){ // 是否为权限菜单
if(typeof val == 'boolean'){
return val ? 1 : 0
}else return val ;
} }
}, { _id: false, autoIndex: false ,versionKey: false})完成本项目后学到不少东西,觉得最重要的还是一些设计思路。期间也踩过不少坑,我遇到觉得最难的问题是查询回复用户的消息,类似贴吧的消息,
需求:
1、在用户发布的文章下的评论 comment.article = 文章(article.author = uid) ;
2、回复用户发布的评论 comment.comment = 评论(comment.from = uid);
3、楼中楼里的回复 comment.comment = 评论(comment.reply.from = uid);
mongodb里联合查询实在麻烦,里面又需要嵌套查询,所以不知道怎么用管道操作符写,基础不够扎实 ,
最后的实现方式是先把用户发布的文章和评论都查出来,然后再根据条件查一遍,也就是要查三遍,但如果用管道实现的话只用查一遍,不知道这两者性能差距有多少。
代码:
// 先筛选出用户发表过的文章和评论
let userArticle = await Article.find({ author: uid }, { _id: 1 }).lean();
let userComment = await Comment.find({ from: uid }, { _id: 1 }).lean();
let commentIds = userComment.map(item => item._id)
let list = await Comment.find({
$or:
[
{
article: { $in: userArticle.map(item => item._id) }
},
{
comment: { $in: commentIds }
},
{
comment: { $nin: commentIds },
reply: { $in: commentIds }
}
],
from: { $ne: uid },
status: 1
})
.sort({ createTime: -1 })
.skip((page - 1) * pageSize).limit(pageSize)
.populate('from', 'nickname avatar')
.populate('comment', 'content')
.populate('reply', 'content')
.populate('article', 'title coverImg')数据库模型:
const Schema = new mongoose.Schema({
article: { type: Number, required: true, ref: "Article" }, // 文章id
from: { type: Number, required: true, ref: "User" }, // 用户id
comment: { type: mongoose.Types.ObjectId, ref: "Comment" }, // 顶级评论id
reply: { type: mongoose.Types.ObjectId, ref: "Comment" }, // 回复的楼中楼id
content: { type: String, required: true, trim: true},
createTime: { type: Number, default: () => Math.ceil(Date.now() / 1000) },// 创建时间
goods: { type:Number, default: 0 }, //点赞数
bads: { type:Number, default: 0 }, // 踩
status: { type: Number, default: 1 } // 1 正常 0 已删除
}, {versionKey: false})后台
使用了模板vue-manage-system


