node + koa + mongodb 博客接口开发

1,110

首页

前言

此 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填充获取整个对象

  • 使用inc来减少或增加数量,inc来减少或增加数量,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

项目地址:

前台展示:https://gitee.com/sdj_work/blog-page(Vue/Nuxt/uni-app)

管理后台:https://gitee.com/sdj_work/blog-admin(Vue/React)

后端Node:https://gitee.com/sdj_work/blog-node(Express/Koa)

博客地址:https://sdjBlog.cn/

项目系列文章:

Vue+Nuxt 博客展示

Vue+uniapp 博客展示

Vue+ElementUI 后台博客管理

node + koa + mongodb 博客接口开发

node + express + mongodb 博客接口开发