diff --git a/docs/game.md b/docs/game.md
index 7a1fd9e1c8..4ea56bf15e 100644
--- a/docs/game.md
+++ b/docs/game.md
@@ -149,6 +149,28 @@ Example: `https://store.steampowered.com/search/?specials=1&term=atelier` 中的
+## TapTap
+
+### 游戏论坛
+
+
+
+| 全部 | 精华 | 官方 |
+| ---- | ----- | -------- |
+| all | elite | official |
+
+### 游戏更新
+
+
+
+### 游戏评价
+
+
+
+| 最新 | 最热 | 游戏时长 |
+| ------ | ---- | -------- |
+| update | hot | spent |
+
## 篝火营地
### 游戏资讯
diff --git a/lib/router.js b/lib/router.js
index d004edd7c2..0bd4e13e3e 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1749,4 +1749,9 @@ router.get('/haimaoba/:id?', require('./routes/haimaoba/comics'));
// 蒲公英
router.get('/pgyer/:app?', require('./routes/pgyer/app'));
+// TAPTAP
+router.get('/taptap/topic/:id/:label?', require('./routes/taptap/topic'));
+router.get('/taptap/changelog/:id', require('./routes/taptap/changelog'));
+router.get('/taptap/review/:id/:order?', require('./routes/taptap/review'));
+
module.exports = router;
diff --git a/lib/routes/taptap/changelog.js b/lib/routes/taptap/changelog.js
new file mode 100644
index 0000000000..b9864f420d
--- /dev/null
+++ b/lib/routes/taptap/changelog.js
@@ -0,0 +1,39 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const id = ctx.params.id;
+
+ const url = `https://www.taptap.com/app/${id}`;
+
+ const app_response = await got.get(url);
+ const $ = cheerio.load(app_response.data);
+
+ const app_img = $('.header-icon-body > img').attr('src');
+ const app_name = $('.breadcrumb > li.active').text();
+ const app_description = $('.body-description-paragraph').text();
+
+ const response = await got({
+ method: 'get',
+ url: `https://www.taptap.com/ajax/apk/v1/list-by-app?app_id=${id}&from=0&limit=10`,
+ headers: {
+ Referer: url,
+ },
+ });
+
+ const list = response.data.data.list;
+
+ ctx.state.data = {
+ title: `TapTap 更新记录 ${app_name}`,
+ description: app_description,
+ link: url,
+ image: app_img,
+ item: list.map((item) => ({
+ title: item.version_label,
+ description: `${item.whatsnew.text}`,
+ pubDate: new Date(item.update_date),
+ link: url,
+ guid: item.version_label,
+ })),
+ };
+};
diff --git a/lib/routes/taptap/review.js b/lib/routes/taptap/review.js
new file mode 100644
index 0000000000..588c1f0c0b
--- /dev/null
+++ b/lib/routes/taptap/review.js
@@ -0,0 +1,54 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const id = ctx.params.id;
+ const order = ctx.params.order;
+
+ let url = `https://www.taptap.com/app/${id}/review`;
+
+ if (order === 'update' || order === 'hot' || order === 'spent') {
+ url += `?order=${order}`;
+ }
+
+ const reviews_list_response = await got.get(url);
+ const $ = cheerio.load(reviews_list_response.data);
+
+ const app_img = $('.header-icon-body > img').attr('src');
+ const app_name = $('.breadcrumb > li.active').text();
+ const order_name = $('.taptap-review-title.section-title li.active')
+ .text()
+ .trim();
+ const reviews_list = $('.review-item-text').toArray();
+
+ ctx.state.data = {
+ title: `TapTap评价 ${app_name} - ${order_name}排序`,
+ link: url,
+ image: app_img,
+ item: reviews_list.map((review) => {
+ review = $(review);
+ const score =
+ review
+ .find('.colored')
+ .attr('style')
+ .match(/\d+/)[0] / 14;
+ const author = review
+ .find('.item-text-header > .taptap-user ')
+ .first()
+ .text()
+ .trim();
+ return {
+ title: `${author} - ${score}星`,
+ author: author,
+ description: review.find('.item-text-body').html(),
+ link: review.find('a.text-header-time').attr('href'),
+ pubDate: new Date(
+ review
+ .find('a.text-header-time [data-dynamic-time]')
+ .text()
+ .trim()
+ ),
+ };
+ }),
+ };
+};
diff --git a/lib/routes/taptap/topic.js b/lib/routes/taptap/topic.js
new file mode 100644
index 0000000000..d0589bcc3a
--- /dev/null
+++ b/lib/routes/taptap/topic.js
@@ -0,0 +1,85 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const id = ctx.params.id;
+ const label = ctx.params.label;
+
+ let url = `https://www.taptap.com/app/${id}/topic`;
+
+ if (label === 'elite' || label === 'official' || label === 'all') {
+ url += `?type=${label}`;
+ } else if (/^\d+$/.test(label)) {
+ url += `?group_label_id=${label}`;
+ }
+
+ const topics_list_response = await got.get(url);
+ const topics_list_data = topics_list_response.data;
+ const $ = cheerio.load(topics_list_data);
+
+ const app_img = $('.group-info > a > img').attr('src');
+ const app_name = $('.breadcrumb > li.active').text();
+ const label_name = $('.tab-label a.active span').text();
+ const topics_list = $('.item-content').toArray();
+
+ const parseContent = (htmlString) => {
+ const $ = cheerio.load(htmlString);
+
+ const author = $('.user-name-identity');
+ const content = $('.topic-content > .bbcode-body');
+ const time = $('.topic-update-time').length === 0 ? $('.topic-info [data-dynamic-time]') : $('.topic-update-time [data-dynamic-time]');
+ const pub_date = time.length === 0 ? new Date() : new Date(time.text().trim());
+
+ const images = $('img');
+ for (let k = 0; k < images.length; k++) {
+ $(images[k]).replaceWith(`
`);
+ }
+
+ return {
+ author: author.text().trim(),
+ description: content.html(),
+ pubDate: pub_date,
+ };
+ };
+
+ const out = await Promise.all(
+ topics_list.map(async (item) => {
+ const $ = cheerio.load(item);
+ const title = $('.item-title > a');
+ const link = title.attr('href');
+
+ const cache = await ctx.cache.get(link);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const topic = {
+ title: title.text().trim(),
+ link: link,
+ };
+
+ try {
+ const topic_response = await got.get(link);
+ const result = parseContent(topic_response.data);
+ if (!result) {
+ return Promise.resolve('');
+ }
+
+ topic.author = result.author;
+ topic.description = result.description;
+ topic.pubDate = result.pubDate;
+ } catch (err) {
+ return Promise.resolve('');
+ }
+ ctx.cache.set(link, JSON.stringify(topic));
+ return Promise.resolve(topic);
+ })
+ );
+
+ ctx.state.data = {
+ title: `TapTap论坛 ${app_name} - ${label_name}`,
+ link: url,
+ image: app_img,
+ item: out.filter((item) => item !== ''),
+ };
+};