Lesson-1 答案模块需求分析
答案模块功能点
- 答案的增删改查
- 问题-答案/用户 - 答案一对多
- 赞/踩答案
- 收藏答案
Lesson-2 问题-答案模块二级嵌套的增删改查接口
操作步骤
- 设计数据库 Schema
- 实现增删改查接口
设计数据库 Schema
// models/answers.js
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, select: false },
questionId: { type: String, select: true }
});
module.exports = model('Answer', AnswerSchema);
实现增删改查接口
// controllers/answers.js
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;
const perPage = Math.max(+ctx.query.per_page, 1);
const q = new RegExp(ctx.query.q);
ctx.body = await Answer.find( { content: q, questionId: ctx.params.questionId }).limit(perPage).skip(page * perPage); // limit: 返回多少数量,skip:跳过多少数量
}
async findById (ctx) {
const { fields = '' } = ctx.query;
const selectFields = fields.split(';').filter(item => item).map(item => ' +'+item).join('');
const answer = await Answer.findById(ctx.params.id).select(selectFields).populate('answerer');
if(!answer) ctx.throw(404, '答案不存在');
ctx.body = answer;
}
async create (ctx) {
ctx.verifyParams({
content: { type: 'string', required: true }
});
const answerer = ctx.state.user._id;
const { questionId } = ctx.params;
const answer = await new Answer({...ctx.request.body, answerer, questionId }).save();
ctx.body = answer;
}
async update (ctx) {
ctx.verifyParams({
content: { type: 'string', required: false }
});
await ctx.state.answer.update(ctx.request.body);
ctx.body = ctx.state.answer;
}
async delete (ctx) {
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 || answer.questionId !== ctx.params.questionId) ctx.throw(404, '答案不存在');
ctx.state.answer = answer;
await next();
}
async checkAnswerer (ctx, next) {
const { answer } = ctx.state;
if (answer.answerer.toString() !== ctx.state.user._id) ctx.throw(403, '没有权限');
await next();
}
}
module.exports = new AnswersCtl();
// routes/answers.js
const jwt = require('koa-jwt');
const Router = require('koa-router');
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 = jwt({ secret });
// 获取问题列表
router.get('/', find);
// 增加问题
router.post('/', auth, create);
// 获取特定问题
router.get('/:id',checkAnswerExist, findById);
// 修改特定问题
router.patch('/:id', auth, checkAnswerExist, checkAnswerer, update);
// 删除问题
router.delete('/:id', auth, checkAnswerExist, checkAnswerer, del);
module.exports = router;
Lesson-3 互斥关系的赞踩答案接口设计与实现
操作步骤
- 设计数据库 Schema
- 实现接口
设计数据库 Schema
首先需要在用户Schema添加喜欢跟踩的属性
// models/users.js
const userSchema = new Schema({
likingAnswers: {
type: [{
type: Schema.Types.ObjectId, // 答案ID
ref: 'Answer'
}],
select: false
},
dislikingAnswers: {
type: [{
type: Schema.Types.ObjectId, // 答案ID
ref: 'Answer'
}],
select: false
}
});
然后我们需要对答案增加一个票数,这个用于统计该答案总的点赞数
// models/answers.js
const AnswerSchema = new Schema({
__v: { type: Number, select: false },
content: { type: String, required: true },
answerer: { type: Schema.Types.ObjectId, ref: 'User', required: true, select: false },
questionId: { type: String, select: true },
voteCount: { type: Number, required: true, default: 0 }
});
实现接口
// controllers/users.js
// 答案点赞列表
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();
// 赞同数加1
await Answer.findByIdAndUpdate(ctx.params.id, { $inc: { voteCount: 1 } });
}
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();
// 赞同数减1
await Answer.findByIdAndUpdate(ctx.params.id, { $inc: { voteCount: -1 } });
}
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;
}
修改一下判断答案是否存在的中间件,因为赞和踩的时候并没有questionId
// controllers/answers.js
async checkAnswerExist (ctx, next) {
const answer = await Answer.findById(ctx.params.id).select('+answerer');
if (!answer) ctx.throw(404, '答案不存在');
// 只有删改查答案的时候才检查此逻辑,赞和踩答案不检查
if(ctx.params.questionId && answer.questionId !== ctx.params.questionId) ctx.throw(404, '该问题下没有此答案');
ctx.state.answer = answer;
await next();
}
修改用户路由,添加这六个方法请求
// router/users.js
// 获取喜欢的答案列表
router.get('/:id/likingAnswers', listLikingAnswers);
// 点赞答案
router.put('/likingAnswers/:id', auth, checkAnswerExist, likeAnswer, undislikeAnswer);
// 取消点赞答案
router.delete('/likingAnswers/:id', auth, checkAnswerExist, unlikeAnswer);
// 获取踩的答案列表
router.get('/:id/dislikingAnswers', listDisLikingAnswers);
// 踩答案
router.put('/dislikingAnswers/:id', auth, checkAnswerExist, dislikeAnswer, unlikeAnswer);
// 取消踩答案
router.delete('/dislikingAnswers/:id', auth, checkAnswerExist, undislikeAnswer);
Lesson-4 RESTful 风格的收藏答案接口
这节跟之前的实现还是一样的思路,所以不多说直接上代码
// models/users.js
const userSchema = new Schema({
collectingAnswers: { // 收藏答案
type: [{
type: Schema.Types.ObjectId, // 答案ID
ref: 'Answer'
}],
select: false
}
});
// controllers/users.js
// 收藏答案列表
async listCollectAnswers (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, next) {
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;
await next();
}
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;
}
// router/users.js
// 获取收藏答案列表
router.get('/:id/collectAnswers', listCollectAnswers);
// 收藏答案
router.put('/collectAnswers/:id', auth, checkAnswerExist, collectAnswer);
// 取消收藏答案
router.delete('/collectAnswers/:id', auth, checkAnswerExist, uncollectAnswer);
网友评论