项目开始
yarn init -y
初始化package.json
- 或者 使用
npm install koa-generator -g
生成项目koa2 koa-restful-api
{
"name": "koa2_restful",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon app",
"start": "cross-env NODE_ENV=production node app"
},
"dependencies": {
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"koa-json-error": "^3.1.2",
"koa-jwt": "^4.0.3",
"koa-parameter": "^3.0.1",
"koa-router": "^12.0.0",
"mongoose": "^6.7.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"nodemon": "^2.0.20"
}
}
理解 koa 中间件 洋葱模型
- koa 中间件的执行顺序是洋葱模型,先执行最外层的中间件,然后执行里面的中间件,最后执行最里面的中间件,然后再执行外层的中间件,依次类推,直到最外层的中间件执行完毕。
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log(1)
// 下一个中间件
await next()
console.log(2)
// 下一个中间件执行完毕后将执行后面代码
ctx.body = 'Hello World'
})
// 中间件2
app.use(async (ctx, next) => {
console.log(3)
await next()
console.log(4)
})
// 中间件3
app.use(async (ctx, next) => {
console.log(5)
await next()
console.log(6)
})
// 1 3 5 6 4 2
// === 中间件 洋葱模型 ===
app.listen(3000)
koa 添加路由
yarn add koa-router
const Koa = require('koa')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
const router = new Router()
// 模拟用户验证鉴权中间件
const auth = async (ctx, next) => {
if (ctx.url !== '/users') {
ctx.throw(401)
}
await next()
}
// 用户路由
const userRouter = new Router({ prefix: '/user' })
// 首页
router.get('/', (ctx, next) => {
ctx.body = 'Hello World'
})
// 用户列表
userRouter.get('/', (ctx, next) => {
// 设置请求头
ctx.set('Allow', 'GET, POST')
ctx.body = [
{ name: 'tom', age: 20 },
{ name: 'jerry', age: 18 },
]
})
// 创建用户
userRouter.post('/', (ctx, next) => {
console.log('ctx.request.body', ctx.request.body)
ctx.body = { name: 'tom', age: 20 }
})
// 用户详情
userRouter.get('/:id', auth, (ctx, next) => {
ctx.body = { name: 'tom', age: 20 }
})
// 修改用户
userRouter.put('/:id', auth, (ctx, next) => {
ctx.body = { name: 'tom2', age: 21 }
})
// 删除用户
userRouter.delete('/:id', auth, (ctx, next) => {
ctx.status = 204
})
// body解析
app.use(bodyParser())
// 注册路由
app.use(router.routes())
// 注册用户路由
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
// http options
// 检测服务器所支持的http请求方法
// CORS中的预检请求
// allowMethods: 用于响应OPTIONS请求
// 相应的请求方法不在路由中时,返回405 (不允许)例如: delete 请求 没有实现
// 相应的请求方法在路由中,但是没有对应的处理函数时,返回501 (未实现)例如:link 请求
// 获取http请求参数
// 1. query 如:http://localhost:3000/user?id=1&name=2
// 2. params 如:http://localhost:3000/user/1
// 3. body 如:post请求 {name: 'tom', age: 20}
// 4. header 如:Authorization Bearer token
// 发送http响应
// 1. 发送 status 例如: 200/400等
// 2. 发送 body 例如: {name: 'tom', age: 20}
// 3. 发送 header 例如: Content-Type: application/json
app.listen(3000)
目录重构
- routes 目录放路由文件
- controllers 目录放控制器文件
koa-parameter
参数校验koa-json-error
错误处理
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const parameter = require('koa-parameter')
const app = new Koa()
const router = require('./routes')
// koa-json-error
app.use(
error({
postFormat: (e, { stack, ...rest }) =>
process.env.NODE_ENV === 'production' ? rest : { stack, ...rest },
})
)
// koa-bodyparser 解析请求体
app.use(bodyParser())
// koa-parameter 参数校验
app.use(parameter(app))
// 注册路由
router(app)
// 异常状况有哪些?
// 1. 404 Not Found 412 Precondition Failed (先决条件失败) 422 Unprocessable Entity (无法处理的实体,参数格式不对)
// 2. 500 服务器错误 运行时错误
// 3. 401 未授权
// 4. 403 禁止访问
// 5. 400 Bad Request 请求参数错误
// 6. 405 Method Not Allowed 请求方法不被允许
// 7. 406 Not Acceptable 请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体
// 8. 409 Conflict 通常是由于资源的当前状态和请求的条件冲突造成的
// jwt vs session
// 1. jwt 无状态,session 有状态
// 2. jwt 无法在多个服务器之间共享,session 可以
// jwt
// 1. 无状态
// 无状态是指服务器不需要记录用户的状态,每次请求都是独立的,不需要记录上一次的请求信息,也不需要记录用户的登录状态,这样就可以实现服务器的水平扩展,即可以增加服务器的数量来提高服务器的处理能力,而不会影响用户的登录状态。
// 2. 无法在多个服务器之间共享
app.listen(3000)
const fs = require('fs')
// 批量导出路由
module.exports = (app) => {
fs.readdirSync(__dirname).forEach((file) => {
if (file === 'index.js') return
const route = require(`./${file}`)
app.use(route.routes()).use(route.allowedMethods())
})
}
const Router = require('koa-router')
const router = new Router({ prefix: '/users' })
// 模拟用户验证鉴权中间件
const auth = async (ctx, next) => {
if (ctx.url !== '/users') {
ctx.throw(401)
}
await next()
}
// 用户列表
router.get('/', (ctx, next) => {
// 设置请求头
ctx.set('Allow', 'GET, POST')
ctx.body = [
{ name: 'tom', age: 20 },
{ name: 'jerry', age: 18 },
]
})
// 创建用户
router.post('/', (ctx, next) => {
console.log('ctx.request.body', ctx.request.body)
ctx.body = { name: 'tom', age: 20 }
})
// 用户详情
router.get('/:id', auth, (ctx, next) => {
ctx.body = { name: 'tom', age: 20 }
})
// 修改用户
router.put('/:id', auth, (ctx, next) => {
ctx.body = { name: 'tom2', age: 21 }
})
// 删除用户
router.delete('/:id', auth, (ctx, next) => {
ctx.status = 204
})
module.exports = router
const Router = require('koa-router')
const router = new Router()
// 首页
router.get('/', (ctx, next) => {
ctx.body = 'Hello World'
})
module.exports = router
- controllers 使用类文件与路由关联
// home 控制器
class HomeCtl {
async index(ctx) {
ctx.body = 'Hello World'
}
}
module.exports = new HomeCtl()
const Router = require('koa-router')
const router = new Router()
const { index } = require('../controllers/home')
// 首页
router.get('/', index)
module.exports = router
// 用户控制器
class UsersCtl {
// 获取用户列表
async find(ctx) {
// 模拟抛出 500 错误
// a.b
// ctx.throw(412, '先决条件失败')
ctx.body = [
{ name: 'tom', age: 20 },
{ name: 'jerry', age: 18 },
]
}
// 获取用户详情
async findById(ctx) {
ctx.body = { name: 'tom', age: 20 }
}
// 创建用户
async create(ctx) {
// 参数校验
ctx.verifyParams({
name: { type: 'string', required: true },
age: { type: 'number', required: false },
})
ctx.body = { name: 'tom', age: 20 }
}
// 修改用户
async update(ctx) {
// 参数校验
ctx.verifyParams({
name: { type: 'string', required: false },
age: { type: 'number', required: false },
})
ctx.body = { name: 'tom2', age: 21 }
}
// 删除用户
async del(ctx) {
ctx.status = 204
}
}
module.exports = new UsersCtl()
const Router = require('koa-router')
const router = new Router({ prefix: '/users' })
const { find, findById, create, update, del } = require('../controllers/users')
// 模拟用户验证鉴权中间件
const auth = async (ctx, next) => {
if (ctx.url !== '/users') {
ctx.throw(401)
}
await next()
}
// 用户列表
router.get('/', find)
// 创建用户
router.post('/', create)
// 用户详情
router.get('/:id', auth, findById)
// 修改用户
// 全量修改使用 put
// router.put('/:id', update)
// 部分修改使用 patch
router.patch('/:id', update)
// 删除用户
router.delete('/:id', del)
module.exports = router
连接数据库,使用 mongoose
- mongodb 申请云数据库,也可以本地安装
mongodb
- mongoose 文档
module.exports = {
// 数据库配置
connectStr:
'mongodb+srv://xmllein:<password>@cluster0.9wopstp.mongodb.net/?retryWrites=true&w=majority',
}
const mongoose = require('mongoose')
// 连接数据库
mongoose.connect(
connectStr,
{ useNewUrlParser: true, useUnifiedTopology: true },
() => console.log('MongoDB 连接成功')
)
mongoose.connection.on('error', console.error)
// 用户模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const userShechma = new Schema({
name: { type: String, required: true },
password: { type: String, required: true, select: false },
})
module.exports = model('User', userShechma)
const User = require('../models/users')
const { secret } = require('../config')
// 用户控制器
class UsersCtl {
// 获取用户列表
async find(ctx) {
// 模拟抛出 500 错误
// a.b
// ctx.throw(412, '先决条件失败')
ctx.body = await User.find()
}
// 获取用户详情
async findById(ctx) {
console.log(ctx.state.user)
const user = await User.findById(ctx.params.id)
if (!user) {
ctx.throw(404, '用户不存在')
}
ctx.body = user
}
// 创建用户
async create(ctx) {
// 参数校验
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true },
})
console.log('ctx.request.body', ctx.request.body)
// 重复用户名校验
const { name } = ctx.request.body
const repeatedUser = await User.findOne({ name })
if (repeatedUser) {
ctx.throw(409, '用户已存在')
}
const user = await new User(ctx.request.body).save()
ctx.body = user
}
// 修改用户
async update(ctx) {
// 参数校验
ctx.verifyParams({
name: { type: 'string', required: false },
password: { type: 'string', required: false },
})
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
if (!user) {
ctx.throw(404, '用户不存在')
}
ctx.body = user
}
// 删除用户
async del(ctx) {
const user = await User.findByIdAndRemove(ctx.params.id)
if (!user) {
ctx.throw(404, '用户不存在')
}
ctx.status = 204
}
}
module.exports = new UsersCtl()
jwt 在 koa 中使用
jsonwebtoken
生成 token, 认证 tokenkoa-jwt
中间件 来作为 koa 验证 token 中间件
// 登录
async login(ctx) {
// 参数校验
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true },
})
// 是否有该用户
const user = await User.findOne(ctx.request.body)
if (!user) {
ctx.throw(401, '用户名或密码不正确')
}
// 生成 token
const { _id, name } = user
const token = jsonwebtoken.sign({ _id, name }, secret, { expiresIn: '1d' })
ctx.body = { token }
}
// 检查用户是否是自己(是本人才能修改数据和删除数据)
async checkOwner(ctx, next) {
if (ctx.params.id !== ctx.state.user._id) {
ctx.throw(403, '没有权限')
}
await next()
}
const Router = require('koa-router')
// const jsonwebtoken = require('jsonwebtoken')
const koajwt = require('koa-jwt')
const { secret } = require('../config')
const router = new Router({ prefix: '/users' })
const {
find,
findById,
create,
update,
del,
login,
checkOwner,
} = require('../controllers/users')
// 模拟用户验证鉴权中间件
const auth = async (ctx, next) => {
if (ctx.url !== '/users') {
ctx.throw(401)
}
await next()
}
// const authToken = async (ctx, next) => {
// const { authorization = '' } = ctx.request.header
// const token = authorization.replace('Bearer ', '')
// try {
// const user = jsonwebtoken.verify(token, secret)
// ctx.state.user = user
// } catch (err) {
// ctx.throw(401, err.message)
// }
// await next()
// }
// koa-jwt 替换上面自定义的 authToken 中间件
const authToken = koajwt({ secret })
// 用户列表
router.get('/', find)
// 创建用户
router.post('/', create)
// 用户详情
router.get('/:id', authToken, findById)
// 修改用户
// 全量修改使用 put
// router.put('/:id', update)
// 部分修改使用 patch
router.patch('/:id', authToken, checkOwner, update)
// 删除用户
router.delete('/:id', authToken, checkOwner, del)
// 登录
router.post('/login', login)
module.exports = router
REST Client
使用 vscode 插件 - 测试接口,替换 postman 测试
### 变量
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzdhZjY3NDE2NWRjZDAxMzJkYTkxMjYiLCJuYW1lIjoidGVzdCIsImlhdCI6MTY2OTAwOTg3NCwiZXhwIjoxNjY5MDk2Mjc0fQ.o6VSajZt3eIncTVnsbZDif-zONLAlRdTHVaR3MxoAik
### 请求用户列表
GET http://localhost:3000/users/ HTTP/1.1
### 用户详情
GET http://localhost:3000/users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
### 新增用户
POST http://localhost:3000/users HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
### 修改用户
PATCH http://localhost:3000/users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test100",
"password": "123456"
}
### 删除用户
DELETE http://localhost:3000/users/637af42c7c3fd94d5c7916ba HTTP/1.1
### 用户登录
POST http://localhost:3000/users/login HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
上传图片
- 使用
koa-body@4.1.0
版本 - 新建存放图片目录
/public/uploads
// koa-body 解析请求体 multipart/form-data 上传文件
const koaBody = require('koa-body')
//最新版本
// const { koaBody } = require('koa-body')
....
app.use(
koaBody({
multipart: true,
formidable: {
maxFileSize: 200 * 1024 * 1024,
uploadDir: path.join(__dirname, '/public/uploads'),
keepExtensions: true,
},
})
)
// 上传
upload(ctx) {
const file = ctx.request.files.file
const basename = path.basename(file.path)
ctx.body = { url: `${ctx.origin}/uploads/${basename}` }
}
const { index, upload } = require('../controllers/home')
...
// 上传文件
router.post('/upload', upload)
### 变量
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzdhZjY3NDE2NWRjZDAxMzJkYTkxMjYiLCJuYW1lIjoidGVzdCIsImlhdCI6MTY2OTAwOTg3NCwiZXhwIjoxNjY5MDk2Mjc0fQ.o6VSajZt3eIncTVnsbZDif-zONLAlRdTHVaR3MxoAik
@BASE_URL = http://localhost:3000/
### 请求用户列表
GET {{BASE_URL}}users/ HTTP/1.1
### 用户详情
GET {{BASE_URL}}users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
### 新增用户
POST {{BASE_URL}}users HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
### 修改用户
PATCH {{BASE_URL}}users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test100",
"password": "123456"
}
### 删除用户
DELETE {{BASE_URL}}users/637af42c7c3fd94d5c7916ba HTTP/1.1
### 用户登录
POST {{BASE_URL}}users/login HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
### 文件上传
POST {{BASE_URL}}upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: image/jpeg
< ./1.jpg
------WebKitFormBoundary7MA4YWxkTrZu0gW--
个人资料完善
- 完善用户信息
// 用户模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const userShechma = new Schema({
__v: { type: Number, select: false },
name: { type: String, required: true },
password: { type: String, required: true, select: false },
// 头像
avatar_url: { type: String },
// 性别
gender: {
type: String,
enum: ['male', 'female'],
default: 'male',
required: true,
},
// 简介
headline: { type: String },
// 位置
locations: { type: [{ type: String }], select: false },
// 行业
business: { type: String, select: false },
// 职业经历
employments: {
type: [{ company: { type: String }, job: { type: String } }],
select: false,
},
// 教育经历
educations: {
type: [
{
school: { type: String },
major: { type: String },
diploma: { type: Number, enum: [1, 2, 3, 4, 5] },
// 入学时间
entrance_year: { type: Number },
// 毕业时间
graduation_year: { type: Number },
},
],
select: false,
},
})
module.exports = model('User', userShechma)
### 变量
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzdhZjY3NDE2NWRjZDAxMzJkYTkxMjYiLCJuYW1lIjoidGVzdCIsImlhdCI6MTY2OTAwOTg3NCwiZXhwIjoxNjY5MDk2Mjc0fQ.o6VSajZt3eIncTVnsbZDif-zONLAlRdTHVaR3MxoAik
@BASE_URL = http://localhost:3000/
### 请求用户列表
GET {{BASE_URL}}users/ HTTP/1.1
### 用户详情
GET {{BASE_URL}}users/637af674165dcd0132da9126?fields=business;locations HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
### 新增用户
POST {{BASE_URL}}users HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
### 修改用户
PATCH {{BASE_URL}}users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test100",
"password": "123456",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"gender": "male",
"headline": "test",
"locations": ["北京", "上海"],
"business": "IT",
"employments": [
{
"company": "阿里巴巴",
"job": "前端专家"
}
],
"educations": [
{
"school": "清华大学",
"major": "计算机科学与技术",
"diploma": 3,
"entrance_year": 2010,
"graduation_year": 2014
}
]
}
### 删除用户
DELETE {{BASE_URL}}users/637af42c7c3fd94d5c7916ba HTTP/1.1
### 用户登录
POST {{BASE_URL}}users/login HTTP/1.1
Content-Type: application/json
{
"name": "test",
"password": "123456"
}
### 文件上传
POST {{BASE_URL}}upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: image/jpeg
< ./1.jpg
------WebKitFormBoundary7MA4YWxkTrZu0gW--
// 获取用户详情
async findById(ctx) {
// 用户要显示的字段
const { fields } = ctx.query
const selectFields = fields
.split(';')
.filter((f) => f)
.map((f) => ' +' + f)
.join('')
const user = await User.findById(ctx.params.id).select(selectFields)
if (!user) {
ctx.throw(404, '用户不存在')
}
ctx.body = user
}
关注与粉丝模块
- 用户与粉丝的关系,多对多的关系
- 修改用户
model
- 关注与取消关注时候,需要校验用户是否存在
// 关注者
following: {
type: [{ type: Schema.Types.ObjectId, ref: 'User' }],
select: false,
},
// 获取关注列表
async listFollowing(ctx) {
const user = await User.findById(ctx.params.id)
.select('+following')
.populate('following')
if (!user) {
ctx.throw(404)
}
ctx.body = user.following
}
// 关注用户
async follow(ctx) {
const me = await User.findById(ctx.state.user._id).select('+following')
// 判断是否已经关注
if (!me.following.map((id) => id.toString()).includes(ctx.params.id)) {
me.following.push(ctx.params.id)
me.save()
}
ctx.status = 204
}
// 校验用户是否存在 中间件
async checkUserExist(ctx, next) {
const user = await User.findById(ctx.params.id)
if (!user) {
ctx.throw(404, '用户不存在')
}
await next()
}
// 取消关注
async unfollow(ctx) {
const me = await User.findById(ctx.state.user._id).select('+following')
// 判断是否已经关注
const index = me.following.map((id) => id.toString()).indexOf(ctx.params.id)
if (index > -1) {
me.following.splice(index, 1)
me.save()
}
ctx.status = 204
}
// 获取粉丝列表
async listFollowers(ctx) {
const users = await User.find({ following: ctx.params.id })
ctx.body = users
}
const {
find,
findById,
create,
update,
del,
login,
checkOwner,
listFollowing,
listFollowers,
checkUserExist,
follow,
unfollow,
} = require('../controllers/users')
...
// 获取关注列表
router.get('/:id/following', listFollowing)
// 关注某人
router.put('/following/:id', authToken, checkUserExist, follow)
// 取消关注某人
router.delete('/following/:id', authToken, checkUserExist, unfollow)
// 获取粉丝列表
router.get('/:id/followers', listFollowers)
### 获取某个用户关注人列表
GET {{BASE_URL}}users/637af674165dcd0132da9126/following HTTP/1.1
### 关注某个用户
PUT {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 取消关注
DELETE {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 获取某个用户粉丝列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/followers HTTP/1.1
话题模块
- 话题的增删改查
- 话题模糊搜索
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const TopicSchema = new Schema({
__v: { type: Number, select: false },
// 话题名称
name: { type: String, required: true },
// 话题头像
avatar_url: { type: String },
// 话题简介
introduction: { type: String, select: false },
})
module.exports = model('Topic', TopicSchema)
// 话题控制器
const Topic = require('../models/topics')
class TopicCtl {
// 获取话题列表
async find(ctx) {
const { per_page = 10 } = ctx.query
const page = Math.max(ctx.query.page * 1, 1) - 1
const perPage = Math.max(per_page * 1, 1)
// 模糊查询 q=xxx
ctx.body = await Topic.find({ name: new RegExp(ctx.query.q) })
.limit(perPage)
.skip(page * perPage)
}
// 获取特定话题
async findById(ctx) {
const { fields = '' } = ctx.query
const selectFields = fields
.split(';')
.filter((f) => f)
.map((f) => ' +' + f)
.join('')
const topic = await Topic.findById(ctx.params.id).select(selectFields)
ctx.body = topic
}
// 创建话题
async create(ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
avatar_url: { type: 'string', required: false },
introduction: { type: 'string', required: false },
})
const topic = await new Topic(ctx.request.body).save()
ctx.body = topic
}
// 修改话题
async update(ctx) {
ctx.verifyParams({
name: { type: 'string', required: false },
avatar_url: { type: 'string', required: false },
introduction: { type: 'string', required: false },
})
const topic = await Topic.findByIdAndUpdate(ctx.params.id, ctx.request.body)
ctx.body = topic
}
// 删除话题
async delete(ctx) {
await Topic.findByIdAndRemove(ctx.params.id)
ctx.status = 204
}
}
module.exports = new TopicCtl()
// 话题路由
const Router = require('koa-router')
const router = new Router({ prefix: '/topics' })
const {
find,
findById,
create,
update,
delete: del,
} = require('../controllers/topics')
// 获取话题列表
router.get('/', find)
// 获取特定话题
router.get('/:id', findById)
// 创建话题
router.post('/', create)
// 修改话题
router.patch('/:id', update)
// 删除话题
router.delete('/:id', del)
module.exports = router
## 话题列表
GET {{BASE_URL}}topics?page=2&per_page=2&q=t HTTP/1.1
### 话题详情
GET {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/1.1
### 新增话题
POST {{BASE_URL}}topics HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test3",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"test3"
}
### 修改话题
PATCH {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test2",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"test2"
}
### 删除话题
DELETE {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/1.1
Authorization: Bearer {{token}}
- 用户关注话题 , 用户可以关注多个话题,话题也可以被多个用户关注
// 用户模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const userShechma = new Schema({
__v: { type: Number, select: false },
name: { type: String, required: true },
password: { type: String, required: true, select: false },
// 头像
avatar_url: { type: String },
// 性别
gender: {
type: String,
enum: ['male', 'female'],
default: 'male',
required: true,
},
// 简介
headline: { type: String },
// 位置
locations: {
type: [{ type: Schema.Types.ObjectId, ref: 'Topic' }],
select: false,
},
// 行业
business: { type: Schema.Types.ObjectId, ref: 'Topic', select: false },
// 职业经历
employments: {
type: [
{
company: { type: Schema.Types.ObjectId, ref: 'Topic' },
job: { type: Schema.Types.ObjectId, ref: 'Topic' },
},
],
select: false,
},
// 教育经历
educations: {
type: [
{
// 学校
school: { type: Schema.Types.ObjectId, ref: 'Topic' },
// 专业
major: { type: Schema.Types.ObjectId, ref: 'Topic' },
diploma: { type: Number, enum: [1, 2, 3, 4, 5] },
// 入学时间
entrance_year: { type: Number },
// 毕业时间
graduation_year: { type: Number },
},
],
select: false,
},
// 关注者
following: {
type: [{ type: Schema.Types.ObjectId, ref: 'User' }],
select: false,
},
// 关注的话题
followingTopics: {
type: [{ type: Schema.Types.ObjectId, ref: 'Topic' }],
select: false,
},
})
module.exports = model('User', userShechma)
// 获取关注话题列表
async listFollowingTopics(ctx) {
const user = await User.findById(ctx.params.id)
.select('+followingTopics')
.populate('followingTopics')
if (!user) {
ctx.throw(404)
}
ctx.body = user.followingTopics
}
// 关注话题
async followTopic(ctx) {
const me = await User.findById(ctx.state.user._id).select(
'+followingTopics'
)
// 判断是否已经关注
if (
!me.followingTopics.map((id) => id.toString()).includes(ctx.params.id)
) {
me.followingTopics.push(ctx.params.id)
me.save()
}
ctx.status = 204
}
// 取消关注话题
async unfollowTopic(ctx) {
const me = await User.findById(ctx.state.user._id).select(
'+followingTopics'
)
// 判断是否已经关注
const index = me.followingTopics
.map((id) => id.toString())
.indexOf(ctx.params.id)
if (index > -1) {
me.followingTopics.splice(index, 1)
me.save()
}
ctx.status = 204
}
const {
find,
findById,
create,
update,
del,
login,
checkOwner,
listFollowing,
listFollowers,
checkUserExist,
follow,
unfollow,
followTopic,
unfollowTopic,
listFollowingTopics,
} = require('../controllers/users')
const { checkTopicExist } = require('../controllers/topics')
...
// 关注话题
router.put('/followingTopics/:id', authToken, checkTopicExist, followTopic)
// 取消关注话题
router.delete('/followingTopics/:id', authToken, checkTopicExist, unfollowTopic)
// 获取关注的话题列表
router.get('/:id/followingTopics', listFollowingTopics)
// 关注话题是否存在 中间件
async checkTopicExist(ctx, next) {
const topic = await Topic.findById(ctx.params.id)
if (!topic) {
ctx.throw(404, '话题不存在')
}
await next()
}
// 获取话题的关注者
async listTopicFollowers(ctx) {
const users = await User.find({ followingTopics: ctx.params.id })
ctx.body = users
}
// 话题路由
const Router = require('koa-router')
const koajwt = require('koa-jwt')
const router = new Router({ prefix: '/topics' })
const {
find,
findById,
create,
update,
delete: del,
checkTopicExist,
listTopicFollowers,
} = require('../controllers/topics')
const { secret } = require('../config')
const auth = koajwt({ secret })
// 获取话题列表
router.get('/', find)
// 获取特定话题
router.get('/:id', checkTopicExist, findById)
// 创建话题
router.post('/', auth, create)
// 修改话题
router.patch('/:id', auth, checkTopicExist, update)
// 删除话题
router.delete('/:id', del)
// 获取话题的粉丝列表
router.get('/:id/followers', checkTopicExist, listTopicFollowers)
module.exports = router
### 变量
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzdiOTUzNmUwMjNmODlkOTE4YmE4YjgiLCJuYW1lIjoidGVzdDIiLCJpYXQiOjE2NjkwNDQ2MjIsImV4cCI6MTY2OTEzMTAyMn0.ZPLYEUEcriiWTGxT7HHPOUHLy1g-hSHLI8LN_4c6QVc
@BASE_URL = http://localhost:3000/
### 请求用户列表
GET {{BASE_URL}}users/ HTTP/1.1
### 用户详情
GET {{BASE_URL}}users/637af674165dcd0132da9126?fields=business;locations;educations;employments; HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
### 新增用户
POST {{BASE_URL}}users HTTP/1.1
Content-Type: application/json
{
"name": "test2",
"password": "123456"
}
### 修改用户
PATCH {{BASE_URL}}users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test100",
"password": "123456",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"gender": "male",
"headline": "test",
"locations": ["北京", "上海"],
"business": "IT",
"employments": [
{
"company": "阿里巴巴",
"job": "前端专家"
}
],
"educations": [
{
"school": "清华大学",
"major": "计算机科学与技术",
"diploma": 3,
"entrance_year": 2010,
"graduation_year": 2014
}
]
}
### 删除用户
DELETE {{BASE_URL}}users/637af42c7c3fd94d5c7916ba HTTP/1.1
### 用户登录
POST {{BASE_URL}}users/login HTTP/1.1
Content-Type: application/json
{
"name": "test2",
"password": "123456"
}
### 文件上传
POST {{BASE_URL}}upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: image/jpeg
< ./1.jpg
------WebKitFormBoundary7MA4YWxkTrZu0gW--
### 获取用户关注人列表
GET {{BASE_URL}}users/637af674165dcd0132da9126/following HTTP/1.1
### 关注用户
PUT {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 取消关注
DELETE {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 获取用户粉丝列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/followers HTTP/1.1
### 获取用户关注话题列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/followingTopics HTTP/1.1
### 关注某个话题
PUT {{BASE_URL}}users/followingTopics/637c235dd0f3a38dd1b9783a
Authorization: Bearer {{token}}
### 取消关注某个话题
DELETE {{BASE_URL}}users/followingTopics/637c235dd0f3a38dd1b9783a
Authorization: Bearer {{token}}
### 话题列表
GET {{BASE_URL}}topics?page=1&per_page=20&q= HTTP/1.1
### 话题详情
GET {{BASE_URL}}topics/637c1fec3d25426a6c8c10e9 HTTP/1.1
### 新增话题
POST {{BASE_URL}}topics HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "新的话题",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"新的话题"
}
### 修改话题
PATCH {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test2",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"test2"
}
### 删除话题
DELETE {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/1.1
Authorization: Bearer {{token}}
### 获取话题的粉丝列表
GET {{BASE_URL}}topics/637c235dd0f3a38dd1b9783a/followers HTTP/1.1
问题模块
- 问题列表(用户-问题一对多关系)
- 话题列表(问题-话题多对多关系)
- 用户关注问题(略)
// 关注的问题
followingQuestions: {
type: [{ type: Schema.Types.ObjectId, ref: 'Question' }],
select: false,
},
// 问题模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const QuestionSchema = new Schema({
__v: { type: Number, select: false },
// 问题标题
title: { type: String, required: true },
// 问题描述
description: { type: String, required: true },
// 问题关联的话题
topics: { type: Schema.Types.ObjectId, ref: 'Topic', required: true },
// 问题的创建者
questioner: { type: Schema.Types.ObjectId, ref: 'User', required: true },
})
module.exports = model('Question', QuestionSchema)
const Question = require('../models/questions')
// 问题控制器
class QuestionsCtl {
// 查询所有问题
async find(ctx) {
const { per_page = 10 } = ctx.query
const page = Math.max(ctx.query.page * 1, 1) - 1
const perPage = Math.max(per_page * 1, 1)
ctx.body = await Question.find({ title: new RegExp(ctx.query.q) })
.limit(perPage)
.skip(page * perPage)
}
// 根据id查询问题
async findById(ctx) {
const question = await Question.findById(ctx.params.id).populate(
'questioner topics'
)
ctx.body = question
}
// 创建问题
async create(ctx) {
ctx.verifyParams({
title: { type: 'string', required: true },
topics: { type: 'array', itemType: 'string', required: false },
description: { type: 'string', required: false },
})
const question = await new Question({
...ctx.request.body,
questioner: ctx.state.user._id,
}).save()
ctx.body = question
}
// 修改问题
async update(ctx) {
ctx.verifyParams({
title: { type: 'string', required: false },
topics: { type: 'array', itemType: 'string', required: false },
description: { type: 'string', required: false },
})
const question = await Question.findByIdAndUpdate(
ctx.params.id,
ctx.request.body
)
ctx.body = question
}
// 删除问题
async delete(ctx) {
const question = await Question.findByIdAndRemove(ctx.params.id)
ctx.status = 204
}
// 检查问题是否存在
async checkQuestionExist(ctx, next) {
const question = await Question.findById(ctx.params.id).select(
'+questioner'
)
if (!question) {
ctx.throw(404, '问题不存在')
}
await next()
}
}
module.exports = new QuestionsCtl()
// 问题路由
// 话题路由
const Router = require('koa-router')
const koajwt = require('koa-jwt')
const router = new Router({ prefix: '/questions' })
const {
find,
findById,
create,
update,
delete: del,
checkQuestionExist,
} = require('../controllers/questions')
const { secret } = require('../config')
const auth = koajwt({ secret })
// 获取问题列表
router.get('/', find)
// 获取特定问题
router.get('/:id', findById)
// 创建问题
router.post('/', auth, create)
// 修改问题
router.patch('/:id', auth, checkQuestionExist, update)
// 删除问题
router.delete('/:id', auth, checkQuestionExist, del)
module.exports = router
// 话题下面的问题列表
async listQuestions(ctx) {
const questions = await Question.find({ topics: ctx.params.id })
ctx.body = questions
}
// 获取话题的问题列表
router.get('/:id/questions', checkTopicExist, listQuestions)
### 变量
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzdiOTUzNmUwMjNmODlkOTE4YmE4YjgiLCJuYW1lIjoidGVzdDIiLCJpYXQiOjE2NjkwNDQ2MjIsImV4cCI6MTY2OTEzMTAyMn0.ZPLYEUEcriiWTGxT7HHPOUHLy1g-hSHLI8LN_4c6QVc
@BASE_URL = http://localhost:3000/
### 请求用户列表
GET {{BASE_URL}}users/ HTTP/1.1
### 用户详情
GET {{BASE_URL}}users/637af674165dcd0132da9126?fields=business;locations;educations;employments;followingQuestions; HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
### 新增用户
POST {{BASE_URL}}users HTTP/1.1
Content-Type: application/json
{
"name": "test2",
"password": "123456"
}
### 修改用户
PATCH {{BASE_URL}}users/637af674165dcd0132da9126 HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test100",
"password": "123456",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"gender": "male",
"headline": "test",
"locations": ["北京", "上海"],
"business": "IT",
"employments": [
{
"company": "阿里巴巴",
"job": "前端专家"
}
],
"educations": [
{
"school": "清华大学",
"major": "计算机科学与技术",
"diploma": 3,
"entrance_year": 2010,
"graduation_year": 2014
}
]
}
### 删除用户
DELETE {{BASE_URL}}users/637af42c7c3fd94d5c7916ba HTTP/1.1
### 用户登录
POST {{BASE_URL}}users/login HTTP/1.1
Content-Type: application/json
{
"name": "test2",
"password": "123456"
}
### 文件上传
POST {{BASE_URL}}upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: image/jpeg
< ./1.jpg
------WebKitFormBoundary7MA4YWxkTrZu0gW--
### 获取用户关注人列表
GET {{BASE_URL}}users/637af674165dcd0132da9126/following HTTP/1.1
### 关注用户
PUT {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 取消关注
DELETE {{BASE_URL}}users/following/637b9536e023f89d918ba8b8 HTTP/1.1
Authorization: Bearer {{token}}
### 获取用户粉丝列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/followers HTTP/1.1
### 获取用户关注话题列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/followingTopics HTTP/1.1
### 关注某个话题
PUT {{BASE_URL}}users/followingTopics/637c235dd0f3a38dd1b9783a
Authorization: Bearer {{token}}
### 取消关注某个话题
DELETE {{BASE_URL}}users/followingTopics/637c235dd0f3a38dd1b9783a
Authorization: Bearer {{token}}
### 话题列表
GET {{BASE_URL}}topics?page=1&per_page=20&q= HTTP/1.1
### 话题详情
GET {{BASE_URL}}topics/637c1fec3d25426a6c8c10e9 HTTP/1.1
### 新增话题
POST {{BASE_URL}}topics HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "新的话题",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"新的话题"
}
### 修改话题
PATCH {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "test2",
"avatar_url": "https://www.baidu.com/img/bd_logo1.png",
"introduction":"test2"
}
### 删除话题
DELETE {{BASE_URL}}topics/637ba02afdb8e477106fab5f HTTP/1.1
Authorization: Bearer {{token}}
### 获取话题的粉丝列表
GET {{BASE_URL}}topics/637c235dd0f3a38dd1b9783a/followers HTTP/1.1
### 获取话题的问题列表
GET {{BASE_URL}}topics/637c235dd0f3a38dd1b9783a/questions HTTP/1.1
### 用户问题列表
GET {{BASE_URL}}users/637b9536e023f89d918ba8b8/questions?page=1&per_page=20&q= HTTP/1.1
### 问题列表
GET {{BASE_URL}}questions?page=1&per_page=20&q= HTTP/1.1
### 问题详情
GET {{BASE_URL}}questions/637c42c9b517d4d4c692d44e HTTP/1.1
### 新增问题
POST {{BASE_URL}}questions HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "我的问题1",
"description": "我的问题1",
"topics": ["637c1e9d3d25426a6c8c10d5"]
}
### 修改问题
PATCH {{BASE_URL}}questions/637c42c9b517d4d4c692d44e HTTP/1.1
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "我修改问题",
"description": "我修改问题",
"topics": ["637c1fb63d25426a6c8c10e4"]
}
### 删除问题
DELETE {{BASE_URL}}questions/637c1f9b3d25426a6c8c10e8 HTTP
Authorization: Bearer {{token}}
答案模块
- 答案列表
- 问题-答案/ 用户-答案 一对多关系
// 答案模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const AnswerSchema = new Schema({
__v: { type: Number, select: false },
// 答案内容
content: { type: String, required: true },
// 答案的创建者
answerer: { type: Schema.Types.ObjectId, ref: 'User', required: true },
// 答案的问题
questionId: { type: String, required: true },
// 答案的点赞数
voteCount: { type: Number, required: true, default: 0 },
})
module.exports = model('Answer', AnswerSchema)
// 答案控制器
const Answer = require('../models/answers')
class AnswersCtl {
// 答案列表
async find(ctx) {
const { per_page = 10 } = ctx.query
const page = Math.max(ctx.query.page * 1, 1) - 1
const perPage = Math.max(per_page * 1, 1)
const q = new RegExp(ctx.query.q)
ctx.body = await Answer.find({
content: q,
questionId: ctx.params.questionId,
})
.limit(perPage)
.skip(page * perPage)
}
// 根据id查询答案
async findById(ctx) {
const answer = await Answer.findById(ctx.params.id).populate('answerer')
ctx.body = answer
}
// 创建答案
async create(ctx) {
ctx.verifyParams({
content: { type: 'string', required: true },
})
const answer = await new Answer({
...ctx.request.body,
answerer: ctx.state.user._id,
questionId: ctx.params.questionId,
}).save()
ctx.body = answer
}
// 修改答案
async update(ctx) {
ctx.verifyParams({
content: { type: 'string', required: false },
})
const answer = await Answer.findByIdAndUpdate(
ctx.params.id,
ctx.request.body
)
ctx.body = answer
}
// 删除答案
async delete(ctx) {
const answer = await Answer.findByIdAndRemove(ctx.params.id)
ctx.status = 204
}
// 检查答案是否存在
async checkAnswerExist(ctx, next) {
const answer = await Answer.findById(ctx.params.id).select('+answerer')
if (!answer) {
ctx.throw(404, '答案不存在')
}
await next()
}
// 检查答案是否是当前用户
async checkAnswerer(ctx, next) {
const answer = await Answer.findById(ctx.params.id).select('+answerer')
if (answer.answerer.toString() !== ctx.state.user._id) {
ctx.throw(403, '没有权限')
}
await next()
}
}
module.exports = new AnswersCtl()
// 答案路由
const Router = require('koa-router')
const koajwt = require('koa-jwt')
const router = new Router({ prefix: '/questions/:questionId/answers' })
const {
find,
findById,
create,
update,
delete: del,
checkAnswerExist,
checkAnswerer,
} = require('../controllers/answers')
const { secret } = require('../config')
const auth = koajwt({ secret })
// 获取答案列表
router.get('/', find)
// 获取特定答案
router.get('/:id', checkAnswerExist, findById)
// 创建答案
router.post('/', auth, create)
// 修改答案
router.patch('/:id', auth, checkAnswerExist, checkAnswerer, update)
// 删除答案
router.delete('/:id', auth, checkAnswerExist, checkAnswerer, del)
module.exports = router
- 赞同/踩 答案 (6 个接口,注意互斥)
// 赞过的答案
likingAnswers: {
type: [{ type: Schema.Types.ObjectId, ref: 'Answer' }],
select: false,
},
// 踩过的答案
dislikingAnswers: {
type: [{ type: Schema.Types.ObjectId, ref: 'Answer' }],
select: false,
},
// 获取用户的点赞列表
async listLikingAnswers(ctx) {
const user = await User.findById(ctx.params.id)
.select('+likingAnswers')
.populate('likingAnswers')
if (!user) {
ctx.throw(404)
}
ctx.body = user.likingAnswers
}
// 点赞答案
async likeAnswer(ctx, next) {
const me = await User.findById(ctx.state.user._id).select('+likingAnswers')
if (!me.likingAnswers.map((id) => id.toString()).includes(ctx.params.id)) {
me.likingAnswers.push(ctx.params.id)
me.save()
}
ctx.status = 204
await next()
}
// 取消点赞答案
async unlikeAnswer(ctx) {
const me = await User.findById(ctx.state.user._id).select('+likingAnswers')
const index = me.likingAnswers
.map((id) => id.toString())
.indexOf(ctx.params.id)
if (index > -1) {
me.likingAnswers.splice(index, 1)
me.save()
}
ctx.status = 204
}
// 获取用户的踩列表
async listDislikingAnswers(ctx) {
const user = await User.findById(ctx.params.id)
.select('+dislikingAnswers')
.populate('dislikingAnswers')
if (!user) {
ctx.throw(404)
}
ctx.body = user.dislikingAnswers
}
// 踩答案
async dislikeAnswer(ctx, next) {
const me = await User.findById(ctx.state.user._id).select(
'+dislikingAnswers'
)
if (
!me.dislikingAnswers.map((id) => id.toString()).includes(ctx.params.id)
) {
me.dislikingAnswers.push(ctx.params.id)
me.save()
}
ctx.status = 204
await next()
}
// 取消踩答案
async undislikeAnswer(ctx) {
const me = await User.findById(ctx.state.user._id).select(
'+dislikingAnswers'
)
const index = me.dislikingAnswers
.map((id) => id.toString())
.indexOf(ctx.params.id)
if (index > -1) {
me.dislikingAnswers.splice(index, 1)
me.save()
}
ctx.status = 204
}
const {
find,
findById,
create,
update,
del,
login,
checkOwner,
listFollowing,
listFollowers,
checkUserExist,
follow,
unfollow,
followTopic,
unfollowTopic,
listFollowingTopics,
listQuestions,
listLikingAnswers,
likeAnswer,
unlikeAnswer,
listDislikingAnswers,
dislikeAnswer,
undislikeAnswer,
} = require('../controllers/users')
...
// 用户点赞答案列表
router.get('/:id/likingAnswers', listLikingAnswers)
// 用户点赞答案
router.put(
'/likingAnswers/:id',
authToken,
checkAnswerExist,
likeAnswer,
undislikeAnswer
)
// 用户取消点赞答案
router.delete('/likingAnswers/:id', authToken, checkAnswerExist, unlikeAnswer)
// 用户踩答案列表
router.get('/:id/dislikingAnswers', listDislikingAnswers)
// 用户踩答案
router.put(
'/dislikingAnswers/:id',
authToken,
checkAnswerExist,
dislikeAnswer,
unlikeAnswer
)
// 用户取消踩答案
router.delete(
'/dislikingAnswers/:id',
authToken,
checkAnswerExist,
undislikeAnswer
)
- 收藏答案
// 收藏的答案
collectingAnswers: {
type: [{ type: Schema.Types.ObjectId, ref: 'Answer' }],
select: false,
},
// 获取用户的收藏列表
async listCollectingAnswers(ctx) {
const user = await User.findById(ctx.params.id)
.select('+collectingAnswers')
.populate('collectingAnswers')
if (!user) {
ctx.throw(404)
}
ctx.body = user.collectingAnswers
}
// 收藏答案
async collectAnswer(ctx) {
const me = await User.findById(ctx.state.user._id).select(
'+collectingAnswers'
)
if (
!me.collectingAnswers.map((id) => id.toString()).includes(ctx.params.id)
) {
me.collectingAnswers.push(ctx.params.id)
me.save()
}
ctx.status = 204
}
// 取消收藏答案
async uncollectAnswer(ctx) {
const me = await User.findById(ctx.state.user._id).select(
'+collectingAnswers'
)
const index = me.collectingAnswers
.map((id) => id.toString())
.indexOf(ctx.params.id)
if (index > -1) {
me.collectingAnswers.splice(index, 1)
me.save()
}
ctx.status = 204
}
const {
...
listCollectingAnswers,
collectAnswer,
uncollectAnswer,
} = require('../controllers/users')
...
// 用户收藏答案列表
router.get('/:id/collectingAnswers', listCollectingAnswers)
// 用户收藏答案
router.put('/collectingAnswers/:id', authToken, checkAnswerExist, collectAnswer)
// 用户取消收藏答案
router.delete(
'/collectingAnswers/:id',
authToken,
checkAnswerExist,
uncollectAnswer
)
评论模块
- 评论的增删改查
- 答案-评论/问题-评论/用户-评论 一对多关系
- 一级评论与二级评论 (增加连个字段,replyTo, rootCommentId)
// 评论模型
const mongoose = require('mongoose')
const { Schema, model } = mongoose
const CommentSchema = new Schema(
{
// 评论的内容
content: { type: String, required: true },
// 评论的创建者
commentator: { type: Schema.Types.ObjectId, ref: 'User', required: true },
// 评论的问题
questionId: { type: String, required: true },
// 评论的答案
answerId: { type: String, required: true },
// 评论的点赞数
voteCount: { type: Number, required: true, default: 0 },
// 评论的回复
replyTo: { type: Schema.Types.ObjectId, ref: 'User' },
// 根评论
rootCommentId: { type: String },
},
{ timestamps: true }
)
module.exports = model('Comment', CommentSchema)
// 评论控制器
const Comment = require('../models/comments')
class CommentsCtl {
// 评论列表
async find(ctx) {
const { per_page = 10 } = ctx.query
const page = Math.max(ctx.query.page * 1, 1) - 1
const perPage = Math.max(per_page * 1, 1)
const q = new RegExp(ctx.query.q)
const { rootCommentId, replyTo } = ctx.query
ctx.body = await Comment.find({
content: q,
questionId: ctx.params.questionId,
answerId: ctx.params.answerId,
rootCommentId,
})
.limit(perPage)
.skip(page * perPage)
.populate('commentator replyTo')
}
// 根据id查询评论
async findById(ctx) {
const comment = await Comment.findById(ctx.params.id).populate(
'commentator'
)
ctx.body = comment
}
// 创建评论
async create(ctx) {
ctx.verifyParams({
content: { type: 'string', required: true },
rootCommentId: { type: 'string', required: false },
replyTo: { type: 'string', required: false },
})
const comment = await new Comment({
...ctx.request.body,
commentator: ctx.state.user._id,
questionId: ctx.params.questionId,
answerId: ctx.params.answerId,
}).save()
ctx.body = comment
}
// 修改评论
async update(ctx) {
ctx.verifyParams({
content: { type: 'string', required: false },
})
const { content } = ctx.request.body
const comment = await Comment.findByIdAndUpdate(ctx.params.id, content)
ctx.body = comment
}
// 删除评论
async delete(ctx) {
const comment = await Comment.findByIdAndRemove(ctx.params.id)
ctx.status = 204
}
}
module.exports = new CommentsCtl()
// 评论路由
const Router = require('koa-router')
const koajwt = require('koa-jwt')
const { secret } = require('../config')
const router = new Router({
prefix: '/questions/:questionId/answers/:answerId/comments',
})
const auth = koajwt({ secret })
const {
find,
findById,
create,
update,
delete: del,
} = require('../controllers/comments')
// 获取评论列表
router.get('/', find)
// 获取特定评论
router.get('/:id', findById)
// 创建评论
router.post('/', create)
// 修改评论
router.patch('/:id', auth, update)
// 删除评论
router.delete('/:id', auth, del)
module.exports = router
- 评论的点赞与踩 (略)