前言
此 blog-node 项目是基于 koa 搭建,采用了主流的前后端分离思想,提供符合 RESTful 风格的 API 接口
功能描述
已经实现功能
- 文章管理
- 评论管理
- 评论回复管理
- 菜单管理
- 功能管理
- 友情链接管理
- 留言管理
- 项目管理
- 励志语言管理
- 统计管理
- 标签管理
- 上传文件管理
- 用户管理
- 角色管理
- API文档管理
node技术
- bcryptjs (密码加密)
- connect-history-api-fallback (支持前端history模式)
- jsonwebtoken (提供token用户验证)
- koa (node框架)
- koa-body (解析json和表单数据)
- koa-logger (日志记录)
- koa-router (路由中间件)
- koa-static (静态资源中间件)
- koa2-cors (跨域处理)
- moment (时间格式处理)
- mongoose (mongodb操作)
- nodemailer (邮件发送)
- svg-captcha (验证码)
项目结构
- apiDesc API文档注释形式,可单独在接口设置或统一处理
- apidoc 通过注释形式生成API文档
- config
- connect mongodb数据库连接
- constant 常量数据
- globalHandle 路由拦截验证token
- jwt token生成以及校验封装
- utils 响应回复、数据库操作请求、发送邮件、验证码和时间格式化等一些常用方法封装
- controllers
- article 文章增删改查、详情和点赞
- comment 一级评论增删改查和置顶评论
- baseController 封装解决class无法获取this指向问题
- functionOper 功能列表增删改查
- index 引入所有路由接口
- link 友情链接增删改查
- menu 菜单功能增删改查以及权限列表树形结构数据
- message 留言增删改查
- project 项目增删改查
- replyComment 回复评论增删改查
- role 角色增删改查、获取和设置角色权限列表、批量导入和移除用户
- statement 前端博客显示励志语句增改查
- statistics 访客、用户、文章、留言按年月日周或时间段统计以及排名
- tag 文章标签增删改查
- upload 上传资源增删查以及下载
- user 用户增删改查、登录注册和邮件发送、验证码获取
- routers 路由请求
- models 模式类型,定义文档的字段属性以及校验
- mongodb mongodb数据集合备份(初始化恢复数据,包括菜单、角色和test用户)
- static 图片和资源
- apidoc.json apidoc文档配置,输出名称、版本和顺序等
- app.js 初始化以及配置
主文件app
const Koa = require('koa')
const app = new Koa()
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-body')
const cors = require('koa2-cors')
// const history = require('connect-history-api-fallback')
const router = require('./routers')
const logger = require("koa-logger");
const utils = require('./config/utils')
// 跨域处理
app.use(
cors({
origin: function(ctx) { //设置允许来自指定域名请求
return '*'; //只允许http://localhost:3000这个域名的请求
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: false, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法
allowHeaders: ['Content-Type', 'Authorization', 'Accept'], //设置服务器支持的所有头信息字段
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
})
)
app.use(logger());
// error handler
onerror(app)
// 前端使用history模式
// app.use(history({
// htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
// }))
// middlewares
app.use(bodyparser({
multipart:true,
jsonLimit: '30mb',
formLimit: '30mb',
}))
app.use(json())
app.use(require('koa-static')(__dirname + '/static'))
app.use(require('koa-static')(__dirname + '/views'))
app.use(require('koa-static')(__dirname + '/apidoc'))
// logger
// app.use(async (ctx, next) => {
// const start = new Date()
// await next()
// const ms = new Date() - start
// console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
// })
// 配置
require("./config/globalHandle")(app) // 检查token
require("./config/connect") // MongoDB数据库连接
// 初始化路由
router(app)
// error-handling
app.on('error', (err, ctx) => {
utils.severErr(err, ctx)
});
module.exports = app
token拦截验证
const JwtUtil = require('./jwt');
const CONSTANT = require('./constant');
const HTTP_CODE = CONSTANT.HTTP_CODE
const utils = require('./utils');
module.exports = app => {
let whitelist = [
'/blogAdmin/user/login',
'/blogAdmin/user/register',
'/blogAdmin/user/getCode',
'/blogAdmin/user/resetPwd',
'/blogAdmin/user/sendEmail',
'/blogAdmin/file/down',
'/blogAdmin/file/down/',
'/blogPage/user/login',
'/blogPage/statistics/tagList',
'/blogPage/statistics/articleArchive',
'/blogPage/statistics/randomMessage',
'/blogPage/statistics/randomArticle',
'/blogPage/article/list',
'/blogPage/article/detail',
'/blogPage/comment/list',
'/blogPage/statement/list',
'/blogPage/message/list',
'/blogPage/project/list',
'/blogPage/link/list'
]
app.use( async(ctx, next)=> {
let {request:req, response:res} = ctx
let url = req.url.indexOf('?') > -1?req.url.split('?')[0]:req.url
if (!whitelist.includes(url)) {
let token = req.headers.authorization;
if(token){
let jwt = new JwtUtil(token);
let result = jwt.verifyToken();
req.tokenMessage = result
// 如果考验通过就next,否则就返回登陆信息不正确
if (result == 'err') {
utils.responseClient(ctx, HTTP_CODE.unauthorized, '登录已过期,请重新登录', null, HTTP_CODE.unauthorized)
} else {
await next();
}
}else{
utils.responseClient(ctx, HTTP_CODE.unauthorized, 'token不存在', null, HTTP_CODE.unauthorized)
}
} else {
let token = req.headers.authorization;
if(token){
let jwt = new JwtUtil(token);
let result = jwt.verifyToken();
req.tokenMessage = result || '';
}
await next();
}
});
}
models 数据模型
user用户模型,模型有设置默认值default,校验required、validate和match,枚举enum
const mongoose = require('mongoose');
const moment = require('moment');
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: {
type: String,
required: true,
validate: (val)=> {
return val.length < 10
}
},
email: {
type: String,
required: true,
match: /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/i
},
phone: {
type: String,
match: /^1[3|4|5|6|7|8|9][0-9]\d{4,8}$/
},
password: {
type: String,
select: false, // 返回对象数据中不会显示这个字段信息
required: true
},
info: {
type: String,
validate: (val)=> {
return val.length < 40
}
},
status: {
type: String,
default: '1',
enum: ['0', '1']
},
avatarId: {
type: String,
default: ''
},
mark: {
type: String,
default: 'xxxxxx4xxxyxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
})
},
roleId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Role',
},
createTime: {
type: String,
default: () => moment().format('YYYY-MM-DD HH:mm:ss')
},
updateTime: {
type: String,
default: () => moment().format('YYYY-MM-DD HH:mm:ss')
}
}, {
versionKey: false,
collection: 'user' //生成 collection 的自定义名称,默认会以复数形式
// timestamps: { createdAt: 'createTime', updatedAt: 'updateTime' }
})
module.exports = mongoose.model('User', userSchema);
routers 路由
index.js依次加载routers路由中的所有文件
/*
*加载所有的路由接口
*/
const fs = require('fs')
module.exports = (app)=>{
fs.readdirSync(__dirname).forEach(file=>{
if(file === 'index.js'){
return
}
const router = require(`./${file}`)
app.use(router.routes()).use(router.allowedMethods())
})
}
获取article文章路由模块
const Router = require('koa-router')
const router = new Router()
const { articleList, articleDetail, articleAdd, articleLike, articleUpdate, articleDel} = require('../controllers/article')
const baseUrl = '/blogAdmin'
const basePageUrl = '/blogPage'
router.get(baseUrl + '/article/list', articleList)
router.get(baseUrl + '/article/detail', articleDetail)
router.post(baseUrl + '/article/add', articleAdd)
router.put(baseUrl + '/article/like', articleLike)
router.put(baseUrl + '/article/update', articleUpdate)
router.delete(baseUrl + '/article/del/:id', articleDel)
router.get(basePageUrl + '/article/list', articleList)
router.get(basePageUrl + '/article/detail', articleDetail)
module.exports = router
controllers控制模块
baseController 基本模块
解决class中this指向问题
class BaseController {
resolve(){
return new Proxy(this, {
get(target, name) {
return target[name].bind(target)
}
})
}
}
module.exports = BaseController
文章模块
-
列表查询根据参数进行模糊匹配,分页和排序,如果角色不是超级用户则只能获取属于该账号下文章列表
-
使用populate填充获取整个对象
-
使用pull或$push来对数组元素进行删除或添加,如增加文章一级评论时,在文章模型中修改对应数据
const BaseController = require('./baseController')
const Article = require("../models/article");
const Tag = require("../models/tag");
const Comment = require("../models/comment");
const History = require("../models/history");
const ReplyComment = require("../models/replyComment");
const CONSTANT = require('../config/constant');
const RES_CODE = CONSTANT.RES_CODE
const ROLE_TYPE = CONSTANT.ROLE_TYPE
const utils = require('../config/utils');
class ArticleCtl extends BaseController{
constructor() {
super()
}
//文章列表
async articleList(ctx){
let req = ctx.request
let conditions = utils.blurSelect(req.query) //模糊查询
let pageObj = utils.pageSelect(req.query) //分页查询
if(req.url.indexOf('blogPage') === -1){ //数据筛选拦截和平台判断
let userMessage = req.tokenMessage.userMessage
if(userMessage.roleId !== ROLE_TYPE.superRole){
conditions['createUser.mark'] = userMessage.mark
}
}else{
conditions.status = '1'
}
if(!pageObj.sort){ //排序
pageObj.sort = {
createTime: '-1'
}
}
let count = await Article.countDocuments(conditions)
let fields = {
_id: 1,
title: 1,
description: 1,
imgId: 1,
status: 1,
meta: 1,
createUser: 1,
createTime: 1,
}
let docs = await Article.find(conditions, fields, pageObj).populate([
{ path: 'tags', select: '_id name bgColor' },
{ path: 'createUser', select: '_id name mark' }
])
if (docs) {
let data = {
count,
data: docs
}
utils.responseClient(ctx, RES_CODE.reqSuccess, "获取文章列表成功", data)
} else {
utils.responseClient(ctx, RES_CODE.dataFail, "获取文章列表失败")
}
}
//文章详情
async articleDetail(ctx){
let req = ctx.request;
let tokenMessage = req.tokenMessage;
let articleId = req.query.id;
let doc = await Article.findByIdAndUpdate(articleId, {$inc: {'meta.viewTotal': 1}}, {new: true}).populate([
{ path: 'tags', select: '_id name bgColor' },
{ path: 'linkUser', select: '_id avatarId name' },
{
path: 'commentList',
populate: {path: 'createUser', select: '_id avatarId name'} //文章评论分页需要单独查评论列表
},
{ path: 'createUser', select: '_id avatarId name' },
{ path: 'updateUser', select: '_id avatarId name' }
])
if(doc){
utils.responseClient(ctx, RES_CODE.reqSuccess, "获取文章详情成功", doc)
if(tokenMessage && tokenMessage.userMessage){
this.historyHandle({
userId: tokenMessage.userMessage.id,
articleId,
type: '1'
})
}
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "获取文章详情失败")
}
}
// 文章新增
async articleAdd(ctx){
let req = ctx.request
let body = req.body
let conditions = utils.completeSelect(body)
let userMessage = req.tokenMessage.userMessage
conditions.tags = conditions.tags?conditions.tags.split(','):[]
conditions.createUser = userMessage.id
conditions.updateUser = userMessage.id
conditions['meta.txtTotal'] = utils.getPostWordCount(body.content)
let newArticle = new Article(conditions)
let docs = await Article.findOne({
title: body.title
})
if (docs) {
utils.responseClient(ctx, RES_CODE.dataAlready, "文章标题已存在")
} else {
let doc = await newArticle.save()
doc?utils.responseClient(ctx, RES_CODE.reqSuccess, "文章新增成功"):utils.responseClient(ctx, RES_CODE.dataFail, "文章新增失败")
}
}
// 文章点赞
async articleLike(ctx){
let req = ctx.request
let userMessage = req.tokenMessage.userMessage
let {type, id} = req.body
if(type === '1'){
// 点赞
let docs = await Article.findById(id)
if(docs){
let likeTotal = docs.meta.likeTotal + 1;
// 判断是否已经点赞过
if(docs.linkUser.includes(userMessage.id)){
return utils.responseClient(ctx, RES_CODE.dataAlready, "已点赞,不要重复点赞")
}
docs.linkUser.push(userMessage.id);
let doc = await Article.findByIdAndUpdate(id, {'meta.likeTotal': likeTotal, linkUser: docs.linkUser}, {new: true})
if(doc){
utils.responseClient(ctx, RES_CODE.reqSuccess, "文章点赞成功")
this.historyHandle({
userId: userMessage.id,
articleId: id,
type: '2'
})
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "文章点赞失败")
}
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "获取文章失败")
}
}else{
//取消点赞
let docs = await Article.findById(id)
if(docs){
let likeTotal = docs.meta.likeTotal - 1;
if(!docs.linkUser.includes(userMessage.id)){
return utils.responseClient(ctx, RES_CODE.dataNot, "暂无点赞")
}
let linkUser = docs.linkUser.filter((item)=>{
return item.toString() !== userMessage.id
})
let doc = await Article.findByIdAndUpdate(id, {'meta.likeTotal': likeTotal, linkUser}, {new: true})
if(doc){
utils.responseClient(ctx, RES_CODE.reqSuccess, "取消点赞成功")
await History.findOneAndRemove({
type: '2',
articleId: id
})
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "取消点赞失败")
}
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "获取文章失败")
}
}
}
// 文章更新
async articleUpdate(ctx){
let req = ctx.request
let body = req.body
let conditions = utils.completeSelect(body)
if(conditions.tags){
conditions.tags = conditions.tags.split(',')
}
let doc = await Article.find({
title: body.title
})
if(doc.length === 1 && doc[0]._id.toString() != body.id){
return utils.responseClient(ctx, RES_CODE.dataAlready, "文章标题已存在")
}
conditions.updateTime = utils.currentDayDate()
conditions.updateUser = req.tokenMessage.userMessage.id
if(body.content){
conditions['meta.txtTotal'] = utils.getPostWordCount(body.content)
}
let docs = await Article.findByIdAndUpdate(body.id, conditions, {new: true})
docs?utils.responseClient(ctx, RES_CODE.reqSuccess, "更新文章成功"):utils.responseClient(ctx, RES_CODE.dataFail, "更新文章失败")
}
// 文章删除
async articleDel (ctx){
let id = ctx.params.id
let docs = await Article.findByIdAndRemove(id)
if(docs){
utils.responseClient(ctx, RES_CODE.reqSuccess, "删除文章成功")
await Comment.deleteMany({articleId: id})
await ReplyComment.deleteMany({articleId: id})
await History.deleteMany({articleId: id})
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "删除文章失败")
}
}
//文章操作历史
async historyHandle(historyData){
let historyResult = await History.findOne(historyData)
if(historyResult){
let updateTimeObj = {
updateTime: utils.currentDayDate()
}
await History.findByIdAndUpdate(historyResult._id, updateTimeObj, {new: true})
}else{
new History(historyData).save();
}
}
}
module.exports = new ArticleCtl().resolve()
用户模块
//使用map结构来缓存随机验证码和邮箱验证码,设置有效期为十分钟
// 邮箱验证码验证
const emailCodeList = new Map(); //缓存邮箱验证码信息列表
emailCodeFind(ctx) {
let { email, emailCode } = ctx.request.body;
let getEmailCode = emailCodeList.get(email)
let result = false;
if (getEmailCode) {
if (getEmailCode.email === email && getEmailCode.code === emailCode) {
let diffTime = utils.timeDiff(getEmailCode.createTime, utils.currentDayDate())
if (diffTime <= 10) {
result = true;
} else {
emailCodeList.delete(email)
utils.responseClient(ctx, RES_CODE.timeOver, "邮箱验证码有效期已超时" + (diffTime - 10) + '分钟')
}
} else {
utils.responseClient(ctx, RES_CODE.codeFail, "邮箱验证码错误")
}
} else {
utils.responseClient(ctx, RES_CODE.codeFail, "邮箱验证码错误")
}
return result
}
//使用bcrypt对密码进行加密
pwdBcrypt(password) {
return new Promise((resolve, reject) => {
bcrypt.genSalt(10, function (error, salt) {
bcrypt.hash(password, salt, function (err, hashPassword) {
//store hash in your password DB.
if (err) {
reject(err)
throw new Error('加密失败');
}
resolve(hashPassword)
});
})
})
}
// token储存所需用户信息
async login(ctx){
let req = ctx.request
const { name, password } = req.body;
if (this.randomCodeFind(req)) {
return utils.responseClient(ctx, RES_CODE.randomFail, "随机验证码错误或超过有效期10分钟")
}
// 查询数据库
let user = await User.findOne({ $or: [{ name }, { email: name }] }, "+password").populate([
{ path: 'roleId' }
])
if(!user){
return utils.responseClient(ctx, RES_CODE.dataFail, "邮箱或用户名不存在")
}
if (user.status === '0') {
return utils.responseClient(ctx, RES_CODE.statusFail, "该用户处于禁用状态")
}
let isMatch = await bcrypt.compare(password, user.password)
if (isMatch) {
let userMessage = {
id: user._id,
name: user.name,
avatarId: user.avatarId,
functionList: user.roleId ? user.roleId.functionList : [],
roleId: user.roleId ? user.roleId._id : null,
mark: user.mark
};
let tokenMessage = new JwtUtil({ userMessage });
let token = tokenMessage.generateToken();
user.password = null;
let data = {
user: user,
token
// token: "Bearer " + token
}
utils.responseClient(ctx, RES_CODE.reqSuccess, "登录成功", data)
let accessTime = utils.currentDayDate().split(' ')[0]
let doc = await AccessUser.find({ userName: user.name})
if (doc.length > 0) {
let timeArr = []
doc.forEach((item)=>{
timeArr.push(item.accessTime.split(' ')[0])
})
if (!timeArr.includes(accessTime)) {
let newAccessUser = new AccessUser({
userName: user.name
})
newAccessUser.save()
}
} else {
let newAccessUser = new AccessUser({
userName: user.name
})
newAccessUser.save()
}
} else {
utils.responseClient(ctx, RES_CODE.pwdFail, "密码错误")
}
}
评论置顶
// 置顶评论,根据isTop和topUpdateTime排序
async commentSticky(ctx){
let req = ctx.request
let { commentId, isTop} = req.body
isTop = isTop === '1' || isTop == 'true'?true:false
let updateResult = await Comment.findByIdAndUpdate(commentId, { isTop, topUpdateTime: utils.currentDayDate() }, { new: true })
updateResult?utils.responseClient(ctx, RES_CODE.reqSuccess, "置顶更换成功", updateResult):utils.responseClient(ctx, RES_CODE.dataFail, "置顶更换失败")
}
菜单模块
// 菜单功能扁平数据结构转化成树形数据结构返回
treeData(source){
let cloneData = JSON.parse(JSON.stringify(source)) // 对源数据深度克隆
return cloneData.filter(father=>{
let branchArr = cloneData.filter(child=>father._id == child.parentId) //返回每一项的子级数组
branchArr.length>0 ? father.children = branchArr : '' //如果存在子级,则给父级添加一个children属性,并赋值
return father.parentId=='0'; //返回第一层
})
}
//对数据权限进行拦截处理
let userMessage = req.tokenMessage.userMessage
if(!userMessage.functionList.includes('5e834ff2fb69305aa091e836')){
return utils.responseClient(ctx, RES_CODE.dataFail, "无该功能权限")
}
统计模块
//按日、周、月、年或自定义时间段统计数据
// 统计文章数量
async articleStatistics(ctx){
let req = ctx.request
let { type, startTime, endTime } = req.query
startTime = startTime || utils.currentDayDate('day')
let options = {}
if (type === 'day') {
options.startTime = startTime
options.endTime = startTime
options.substrData = ["$createTime", 11, 2]
} else if (type === 'week') {
let weekArr = utils.weekFirstLast(startTime)
options.startTime = weekArr[0]
options.endTime = weekArr[1]
options.substrData = ["$createTime", 0, 10]
} else if (type === 'month') {
let monthArr = utils.monthFirstLast(startTime)
options.startTime = monthArr[0]
options.endTime = monthArr[1]
options.substrData = ["$createTime", 0, 10]
} else if (type === 'year') {
let yearArr = utils.yearFirstLast(startTime)
options.startTime = yearArr[0]
options.endTime = yearArr[1]
options.substrData = ["$createTime", 5, 2]
} else {
options.startTime = startTime
options.endTime = endTime
options.substrData = ["$createTime", 0, 10]
}
let data = await Article.aggregate([
{
$match: {
status: '1',
createTime: { '$gte': options.startTime + ' 00:00:00', '$lt': options.endTime + ' 23:59:59' }
}
},
{
$project: {
hour: {
$substr: options.substrData
}
},
},
{
$group: {
_id: "$hour",
count: { $sum: 1 }
}
},
{
$sort: { _id: 1 }
}
])
data?utils.responseClient(ctx, RES_CODE.reqSuccess, "获取文章统计", data):utils.responseClient(ctx, RES_CODE.dataFail, "获取文章统计失败")
}
//按排名最多排序,时间字符串转换时间格式,使用new Date("$time")全部会转换成1970-01-01T00:00:00.000Z
async articleList(ctx){
let req = ctx.request
let { type } = req.query
let date = null
if(type === 'day'){
date = {
$substr: ["$createTime", 11, 2]
}
}else if(type === 'month'){
date = {
$substr: ["$createTime", 5, 2]
}
}else{
date = {
$isoDayOfWeek: {
$dateFromString: {
dateString: "$createTime"
}
}
}
}
let data = await Article.aggregate([
{
$match: {
status: '1'
}
},
{
$project: {
date,
createTime: 1
}
},
{
$group: {
_id: "$date",
count: { $sum: 1 }
}
},
{
$sort: { count: -1 }
}
])
data?utils.responseClient(ctx, RES_CODE.reqSuccess, "获取文章排名统计", data):utils.responseClient(ctx, RES_CODE.dataFail, "获取文章排名统计失败")
}
//$lookup联表查询,$group分组字段需要放到_id中
async tagList(ctx){
let data = await Article.aggregate([
{
$match: {
status: '1'
}
},
{
$lookup: {
from: 'tag',
localField: 'tags',
foreignField: '_id',
as: 'articleTag'
}
},
{
$unwind: "$articleTag"
},
{
$project: {
title: 1,
'articleTag._id': 1,
'articleTag.name': 1,
'articleTag.description': 1,
'articleTag.bgColor': 1
}
},
{
$group: {
_id: {
"id": "$articleTag._id",
"name": "$articleTag.name",
"description": "$articleTag.description",
"bgColor": "$articleTag.bgColor"
},
count: { $sum: 1 }
}
},
{
$sort: { count: -1 }
}
])
data?utils.responseClient(ctx, RES_CODE.reqSuccess, "获取文章标签统计", data):utils.responseClient(ctx, RES_CODE.dataFail, "获取文章标签统计失败")
}
文件上传和下载
//文件上传
async uploadFile(ctx){
let file = ctx.request.files.file
if(file){
// 创建可读流
const reader = fs.createReadStream(file.path);
let timeValue = utils.timeValue()
let filePath = path.join(__dirname, '../static/img/') + `${timeValue}.${file.type.split('/')[1]}`;
let imgUrl = ctx.protocol + '://'+ ctx.headers.host + '/blogAdmin/file/down?downId=' + timeValue
// 创建可写流
const upStream = fs.createWriteStream(filePath);
// 可读流通过管道写入可写流
reader.pipe(upStream);
const newSource = new Source({
sourceId: timeValue,
name: file.name,
type: file.type,
url: imgUrl
})
let source = await newSource.save()
source?utils.responseClient(ctx, RES_CODE.reqSuccess, "上传成功", source):utils.responseClient(ctx, RES_CODE.dataFail, "上传失败")
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "获取文件失败")
}
}
//文件下载
async downFile(ctx) {
let req = ctx.request
let sourceId = req.query.downId
let source = await Source.findOne({
sourceId
})
if(source){
let pathUrl = `static/img/${source.sourceId}.${source.type.split('/')[1]}`
//下载文件
ctx.attachment(pathUrl)
await send(ctx, pathUrl)
}else{
utils.responseClient(ctx, RES_CODE.dataFail, "获取资源失败")
}
}
说明
-
默认超级管理员,账户:test,密码:123456
-
开发环境使用 nodeman,一旦报错,程序断开,生产环境使用 pm2,把 node 设置为进程,不会因报错而断开服务
-
全局安装npm install apidoc@0.25.0 -g(0.26.0版本会出现测试请求无法隐藏),npm run apidoc生成apidoc文档,本地访问为 http://localhost:3000/ ,线上为 sdjblog.cn:3000/
建立安装
数据库mongodb安装,mongodb按教程安装下载,然后配置:
1、下载mongodb在D:\mongodb位置(自定义),data文件夹下新建db文件夹
2、在D:\mongodb\bin中执行.\mongod --dbpath D:\mongodb\data\db,查看是否安装成功
3、配置文件安装服务,mongod -config " D:\mongodb\bin\mongod.cfg" -install -serviceName "MongoDB"
4、在D:\mongodb\bin中执行./mongo或配置系统变量使用mongo来创建超级用户:
use admin
db.createUser({user:"admin",pwd:"123456",roles:["root"]})
5、新建数据库:
use blogNode
db.createUser({user:"admin",pwd:"123456",roles:[{role:"dbOwner",db:"blogNode"}]})
(dbOwner:该数据库的所有者,具有该数据库的全部权限)
6、在mongod.cfg中配置需要权限认证,重启服务
security:
authorization: enabled
7、安装navicat for mongodb 可视化数据库,导入恢复mongodb的数据
8、全局安装npm install -g nodemon来监听重启
9、安装依赖,npm install
10、启动服务,npm run dev,默认端口3000
项目地址:
项目系列文章: