项目说明:
该项目采用前后端分离结构,权限认证采用 token 来实现
目录结构:
| public
| src
| | config ------------------配置文件
| | controller --------------控制层
| | database ----------------数据库相关
| | middleware --------------中间件
| | serviece ----------------服务层
| | utils -------------------工具类
| | app.js ------------------入口文件
| .babelrc
| .editorconfig
| .eslintrc
| .gitignore
| index.js ------------------启动文件
| package.json
技术栈:
- Node.js
- Koa
- MongoDB
项目启动:
# 安装依赖
yarn install 或者 npm install
# 启动项目
yarn start 或者 npm run start
代码说明:
- 项目的日志管理使用 log4js 模块,具体配置如下:
// 配置文件
const log4jsConf = {
appenders: {
console: {
type: 'console'
},
http: {
type: 'dateFile',
filename: path.resolve(__dirname, '../../log/http'),
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
},
error: {
type: 'file',
filename: path.resolve(__dirname, '../../log/error.log'),
maxLogSize: 1048576,
backups: 10
}
},
categories: {
default: {
appenders: ['console'],
level: 'info'
},
http: {
appenders: ['http'],
level: 'info'
},
error: {
appenders: ['error'],
level: 'error'
}
}
}
// 日志管理
export default app => {
app.use(async (ctx, next) => {
const { method, host, url, headers } = ctx
const httpInfo = {
method,
host,
url,
referer: headers['referer'],
userAgent: headers['user-agent']
}
httpLogger.info(JSON.stringify(httpInfo))
await next()
})
- 统一错误处理则使用了 Decorator 装饰器来实现,从而减少一定的代码量:
//装饰器配置
export function handleError(target, name, descriptor) {
const oldValue = descriptor.value
descriptor.value = async function() {
try {
return await oldValue.apply(this, arguments)
} catch (error) {
return Result.error(
ERROR_INFO.DatabaseError.code,
ERROR_INFO.DatabaseError.msg
)
}
}
}
//使用装饰器
...
@handleError
async publish(headers) {
const userid = await getUserId(headers)
const category = await Category.findOne({
_id: this.categoryid,
author: userid
})
const article = new Article({
author: userid,
title: xss(this.title),
content: xss(this.content),
category: category._id,
privated: this.privated
})
await article.save()
return Result.success(null)
}
...
API 接口功能:
- 用户登录
- 用户注册
- 修改资料
- 上传头像
- 收藏文章
- 发布文章
- 删除文章
- 更新文章
- 模糊查询
- 分页查询
- 阅读统计
- 用户评论
- 文章分类
…
import fs from 'fs'
import path from 'path'
import jwt from 'jsonwebtoken'
import xss from 'xss'
import Crypto from '../utils/crypto'
import { User, Article } from '../database/model'
import { TOKEN_SECRET, TOKEN_EXPIRESIN, PASSWORD_SALT, DEFAULT_AVATAR } from '../config/application'
import Result, { ERROR_INFO } from '../utils/result'
import { getToken, getUserId } from '../utils/roles'
import { handleError } from '../utils/decorator'
export default class UserService {
constructor({ username, password, avatar, introduction, collect }) {
this.username = username
this.password = password
this.avatar = avatar
this.introduction = introduction
this.collect = collect
}
@handleError
static async getUserInfo(headers) {
const token = getToken(headers)
const userData = await User.findOne({ token })
try {
jwt.verify(userData.token, TOKEN_SECRET)
const _userData = await User.findOne({ token }, { password: 0 }).populate({
path: 'collect',
populate: { path: 'author', select: 'username avatar' }
})
return Result.success(_userData)
} catch (error) {
return Result.error(ERROR_INFO.InvalidToken.code, ERROR_INFO.InvalidToken.msg)
}
}
@handleError
async register() {
const userData = await User.findOne({ username: this.username })
if (userData) {
return Result.error(ERROR_INFO.ExistAccount.code, ERROR_INFO.ExistAccount.msg)
} else if (this.username.length > 10) {
return Result.error(ERROR_INFO.BeyondLength.code, ERROR_INFO.BeyondLength.msg)
} else {
const token = jwt.sign({ username: this.username }, TOKEN_SECRET, {
expiresIn: TOKEN_EXPIRESIN
})
const crypto = new Crypto(PASSWORD_SALT, this.password)
const user = new User({
username: this.username,
password: crypto.encrypt(),
token
})
await user.save()
return Result.success(
await User.findOne({ username: this.username }, { password: 0 }).populate('collect')
)
}
}
@handleError
async login() {
const crypto = new Crypto(PASSWORD_SALT, this.password)
const userData = await User.findOne(
{ username: this.username, password: crypto.encrypt() },
{ password: 0 }
)
if (userData) {
const token = jwt.sign({ username: this.username }, TOKEN_SECRET, {
expiresIn: TOKEN_EXPIRESIN
})
await User.updateOne({ username: this.username }, { token })
return Result.success(
await User.findOne({ username: this.username }, { password: 0 }).populate('collect')
)
} else {
return Result.error(ERROR_INFO.BadAccount.code, ERROR_INFO.BadAccount.msg)
}
}
@handleError
async updateIntroduction(headers) {
const token = getToken(headers)
await User.updateOne({ token }, { introduction: xss(this.introduction) })
const userData = await User.findOne({ token }, { introduction: 1 })
return Result.success(userData)
}
@handleError
async uploadAvatar(headers) {
const avatarType = ['image/jpeg', 'image/png']
if (!avatarType.includes(this.avatar.type)) {
return Result.error(ERROR_INFO.InvalidFile.code, ERROR_INFO.InvalidFile.msg)
}
// 删除之前的头像
const token = await getToken(headers)
const userData = await User.findOne({ token })
const username = userData.username
const oldUploadPath = path.resolve(__dirname, `../../public${userData.avatar}`)
userData.avatar === `/upload/${DEFAULT_AVATAR}` ? null : fs.unlinkSync(oldUploadPath)
// 添加时间戳,以禁止缓存
const avatarName = username + Date.now().toString() + path.extname(this.avatar.name)
const uploadPath = path.resolve(__dirname, `../../public/upload/${avatarName}`)
const reader = fs.createReadStream(this.avatar.path)
const writer = fs.createWriteStream(uploadPath)
const avatar = `/upload/${avatarName}`
reader.pipe(writer)
await User.updateOne({ username }, { avatar })
const _userData = await User.findOne({ username }, { avatar: 1 })
return Result.success(_userData)
}
@handleError
static async handleCollect(articleid, cancelCollect, headers) {
const userId = await getUserId(headers)
const _article = await Article.findOne({ _id: articleid, privated: false })
const article = _article ? _article : await Article.findOne({ author: userId, _id: articleid })
if (cancelCollect) {
await User.update({ _id: userId }, { $pull: { collect: article._id } })
} else {
await User.update({ _id: userId }, { $addToSet: { collect: article._id } })
}
return Result.success(null)
}
}