Skip to content

Commit 7b31ad2

Browse files
YeungKCJacksonTian
authored andcommitted
fest: add topic api (#128)
* refactor: sub router related * fix: reply unit test error content * feat: add topic api * refactor: 修复 await 和 then 一起使用的问题;原本用 async function 的地方统一使用箭头函数 * test: Improve topic unit coverage & bug fix
1 parent 3169ec2 commit 7b31ad2

File tree

9 files changed

+415
-13
lines changed

9 files changed

+415
-13
lines changed

app/controller/api/topic.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
'use strict';
2+
3+
const Controller = require('egg').Controller;
4+
const _ = require('lodash');
5+
6+
class TopicController extends Controller {
7+
async index(ctx) {
8+
const tab = ctx.query.tab || 'all';
9+
const mdrender = ctx.query.mdrender !== 'false';
10+
11+
const query = {};
12+
if (!tab || tab === 'all') {
13+
query.tab = { $nin: [ 'job', 'dev' ] };
14+
} else {
15+
if (tab === 'good') {
16+
query.good = true;
17+
} else {
18+
query.tab = tab;
19+
}
20+
}
21+
22+
let topics = await ctx.service.topic.getTopicsByQuery(query,
23+
// TODO 修改 eslint 支持在 {} 内使用 ...,栗子:{ sort: '-top -last_reply_at', ...ctx.pagination }
24+
Object.assign({ sort: '-top -last_reply_at' }, ctx.pagination));
25+
topics = topics.map(topic => {
26+
topic.content = mdrender ? ctx.helper.markdown(topic.content) : topic.content;
27+
topic.author = _.pick(topic.author, [ 'loginname', 'avatar_url' ]);
28+
topic.id = topic._id;
29+
return _.pick(topic, [ 'id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
30+
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author' ]);
31+
});
32+
33+
ctx.body = {
34+
success: true,
35+
data: topics,
36+
};
37+
}
38+
39+
async create(ctx) {
40+
const all_tabs = ctx.app.config.tabs.map(tab => {
41+
return tab[ 0 ];
42+
});
43+
44+
// TODO: 此处可以优化,将所有使用 egg_validate 的 rules 集中管理,避免即时新建对象
45+
ctx.validate({
46+
title: {
47+
type: 'string',
48+
max: 100,
49+
min: 5,
50+
},
51+
tab: { type: 'enum', values: all_tabs },
52+
content: { type: 'string' },
53+
});
54+
55+
const body = ctx.request.body;
56+
57+
// 储存新主题帖
58+
const topic = await ctx.service.topic.newAndSave(
59+
body.title,
60+
body.content,
61+
body.tab,
62+
ctx.request.user.id
63+
);
64+
65+
// 发帖用户增加积分,增加发表主题数量
66+
await ctx.service.user.incrementScoreAndReplyCount(topic.author_id, 5, 1);
67+
68+
// 通知被@的用户
69+
await ctx.service.at.sendMessageToMentionUsers(
70+
body.content,
71+
topic.id,
72+
ctx.request.user.id
73+
);
74+
75+
ctx.body = {
76+
success: true,
77+
topic_id: topic.id,
78+
};
79+
}
80+
81+
async show(ctx) {
82+
ctx.validate({
83+
id: {
84+
type: 'string',
85+
max: 24,
86+
min: 24,
87+
},
88+
}, ctx.params);
89+
90+
const topic_id = String(ctx.params.id);
91+
const mdrender = ctx.query.mdrender !== 'false';
92+
const user = await ctx.service.user.getUserByToken(ctx.query.accesstoken);
93+
94+
let [ topic, author, replies ] = await ctx.service.topic.getFullTopic(topic_id);
95+
96+
if (!topic) {
97+
ctx.status = 404;
98+
ctx.body = {
99+
success: false,
100+
error_msg: '此话题不存在或已被删除',
101+
};
102+
return;
103+
}
104+
105+
// 增加 visit_count
106+
topic.visit_count += 1;
107+
// 写入 DB
108+
await ctx.service.topic.incrementVisitCount(topic_id);
109+
110+
topic.content = mdrender ? ctx.helper.markdown(topic.content) : topic.content;
111+
topic.id = topic._id;
112+
topic = _.pick(topic, [ 'id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
113+
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author' ]);
114+
115+
topic.author = _.pick(author, [ 'loginname', 'avatar_url' ]);
116+
117+
topic.replies = replies.map(reply => {
118+
reply.content = mdrender ? ctx.helper.markdown(reply.content) : reply.content;
119+
120+
reply.author = _.pick(reply.author, [ 'loginname', 'avatar_url' ]);
121+
reply.id = reply._id;
122+
reply = _.pick(reply, [ 'id', 'author', 'content', 'ups', 'create_at', 'reply_id' ]);
123+
reply.reply_id = reply.reply_id || null;
124+
125+
reply.is_uped = !!(reply.ups && user && reply.ups.indexOf(user.id) !== -1);
126+
127+
return reply;
128+
});
129+
130+
topic.is_collect = user ? !!await ctx.service.topicCollect.getTopicCollect(
131+
user.id,
132+
topic_id
133+
) : false;
134+
135+
ctx.body = {
136+
success: true,
137+
data: topic,
138+
};
139+
}
140+
141+
async update(ctx) {
142+
143+
const all_tabs = ctx.app.config.tabs.map(tab => {
144+
return tab[ 0 ];
145+
});
146+
147+
ctx.validate({
148+
topic_id: {
149+
type: 'string',
150+
max: 24,
151+
min: 24,
152+
},
153+
title: {
154+
type: 'string',
155+
max: 100,
156+
min: 5,
157+
},
158+
tab: { type: 'enum', values: all_tabs },
159+
content: { type: 'string' },
160+
});
161+
162+
const body = ctx.request.body;
163+
164+
let { topic } = await ctx.service.topic.getTopicById(body.topic_id);
165+
if (!topic) {
166+
ctx.status = 404;
167+
ctx.body = { success: false, error_msg: '此话题不存在或已被删除。' };
168+
return;
169+
}
170+
171+
if (!topic.author_id.equals(ctx.request.user._id) && !ctx.request.is_admin) {
172+
ctx.status = 403;
173+
ctx.body = {
174+
success: false,
175+
error_msg: '对不起,你不能编辑此话题',
176+
};
177+
return;
178+
}
179+
180+
delete body.accesstoken;
181+
topic = Object.assign(topic, body);
182+
topic.update_at = new Date();
183+
184+
await topic.save();
185+
186+
// 通知被 @ 的人
187+
await ctx.service.at.sendMessageToMentionUsers(
188+
topic.content,
189+
topic.id,
190+
ctx.request.user.id
191+
);
192+
193+
ctx.body = {
194+
success: true,
195+
topic_id: topic.id,
196+
};
197+
}
198+
}
199+
200+
module.exports = TopicController;

app/middleware/pagination.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
4+
module.exports = () => {
5+
return async (ctx, next) => {
6+
if (!ctx.pagination) {
7+
8+
const query = ctx.query;
9+
const config = ctx.app.config;
10+
const pagination = {};
11+
12+
// 这里限制了最大 limit,不知道实际上需不需要
13+
pagination.limit = Math.min(100, parseInt(query.limit || config.default_limit, 10));
14+
const page = Math.max(1, parseInt(query.page || config.default_page, 10));
15+
pagination.skip = (page - 1) * pagination.limit;
16+
17+
ctx.pagination = pagination;
18+
}
19+
await next();
20+
};
21+
};
22+

app/router/api.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,29 @@
44
* @param {Egg.Application} app - egg application
55
*/
66
module.exports = app => {
7-
const { router, controller, middleware } = app;
7+
const apiV1Router = app.router.namespace('/api/v1');
8+
const { controller, middleware } = app;
89

9-
const { user, message } = controller.api;
10+
const { user, message, topic } = controller.api;
1011

1112
const tokenRequired = middleware.tokenRequired();
13+
const pagination = middleware.pagination();
1214
// const createTopicLimit = middleware.createTopicLimit(config.topic);
1315
// const createUserLimit = middleware.createUserLimit(config.create_user_per_ip);
1416

1517
// 用户
16-
router.get('/api/v1/user/:loginname', user.show);
17-
router.post('/api/v1/accesstoken', tokenRequired, user.verify);
18+
apiV1Router.get('/user/:loginname', user.show);
19+
apiV1Router.post('/accesstoken', tokenRequired, user.verify);
1820

1921
// 消息通知
20-
router.get('/api/v1/message/count', tokenRequired, message.count);
21-
router.get('/api/v1/messages', tokenRequired, message.list);
22-
router.post('/api/v1/message/mark_all', tokenRequired, message.markAll);
23-
router.post('/api/v1/message/mark_one/:msg_id', tokenRequired, message.markOne);
22+
apiV1Router.get('/message/count', tokenRequired, message.count);
23+
apiV1Router.get('/messages', tokenRequired, message.list);
24+
apiV1Router.post('/message/mark_all', tokenRequired, message.markAll);
25+
apiV1Router.post('/message/mark_one/:msg_id', tokenRequired, message.markOne);
26+
27+
// 主题
28+
apiV1Router.get('/topics', pagination, topic.index);
29+
apiV1Router.get('/topic/:id', topic.show);
30+
apiV1Router.post('/topics', tokenRequired, topic.create);
31+
apiV1Router.post('/topics/update', tokenRequired, topic.update);
2432
};

config/config.default.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,8 @@ module.exports = appInfo => {
166166
},
167167
};
168168

169+
config.default_page = 1;
170+
config.default_limit = 20;
171+
169172
return config;
170173
};

config/plugin.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ exports.validate = {
4343
enable: true,
4444
package: 'egg-validate',
4545
};
46+
47+
exports.routerPlus = {
48+
enable: true,
49+
package: 'egg-router-plus',
50+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"egg-passport-github": "^1.0.0",
1515
"egg-passport-local": "^1.2.1",
1616
"egg-redis": "^2.0.0",
17+
"egg-router-plus": "^1.2.0",
1718
"egg-scripts": "^2.5.0",
1819
"egg-validate": "^1.0.0",
1920
"egg-view-ejs": "^2.0.0",

0 commit comments

Comments
 (0)