var express = require('express'); var router = express.Router(); const models = require('../models'); const config = require('../config/app'); const redis = require('../libs/redis'); const categories = require('../config/category'); const tags = require('../config/tag'); const languages = require('../config/language'); const translate = require('../config/translate'); const meta = require('../config/meta'); const utils = require('../libs/utils'); const DailyFactory = require('../models/daily-factory'); const datefns = require('date-fns'); const { format } = require('date-fns'); const { getListBuilder } = require('../libs/pager'); const CACHE_PREFIX = "art_v1"; // const CACHE_EXPIRES = 60; // 60s刷新一次 const CACHE_EXPIRES = 3600; // 1小时刷新一次即可 const artSelect = 'name title desc width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId'; // 路由:设置语言 router.post('/set-lang', (req, res) => { const lang = req.body.lang; const uri = req.body.uri; // 设置cookie来保存用户选择的语言 res.cookie('lang', lang, config.cookie); // 重定向会指定的uri页,但是语言要变过来 let newUri = `/${lang}${uri.substring(3)}`; res.redirect(newUri); }); // 首页不带语言路由,确定lang,然后重定向到相关语言版本 router.get('/', (req, res, next) => { let locale = utils.lang.getLocale(req.acceptsLanguages()); let lang = utils.lang.ensureLanguage(locale); console.log(`lang=${lang}, req.cookies.lang=${req.cookies.lang}`); // 如果cookies中带有lang,就用cookies的,cookies没有则设置cookies if (req.cookies.lang) { lang = utils.lang.ensureLanguage(req.cookies.lang); } if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); console.log(`${req.originalUrl} set cookie: lang=${lang}`); } return res.redirect(`/${lang}`); }); // 首页路由 // router.get('/:lang/', function (req, res, next) { router.get(/^\/(en|zh|es|pt|ja)$/, function (req, res, next) { // 限制严格一点 (async function () { // let lang = utils.lang.ensureLanguage(req.params.lang); let params = Object.values(req.params); let [lang] = params; if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); console.log(`${req.originalUrl} set cookie: lang=${lang}`); } let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_home_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let host = config.cdnHost ?? config.resHost; // 今日daily图 let todayDoc; let dailyModel = await DailyFactory.getModel('art'); let dailydocs = await dailyModel .find({ dailyDate: { $lte: datefns.startOfTomorrow(Date.now()) } }) .sort({ dailyDate: 'desc' }) .limit(1) .lean() .exec(); if (dailydocs && dailydocs.length >= 1) { todayDoc = dailydocs[0]; todayDoc.artInfo.thumb = `${host}/thumbs/coloring-page/page/480/${todayDoc.artInfo._id}.${imageType}`; } let baseSort = { publishTime: 'desc' }; // 最新上线 let latest = await models.Art .find({ open: true, status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(latest, lang, imageType); // 热门精选 let recommend = await models.Art .find({ open: true, tags: 'data_good', status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(recommend, lang, imageType); // special 专区 let special = await models.Art .find({ open: true, hasSpecial: true, status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(special, lang, imageType); // 专辑 let albums = await models.ArtAlbum .find({ pid: 'art', enabled: true }) .sort({ order: 'asc' }) .populate('title') .populate('slogon') .select('tag title slogon contents') .limit(6) .lean() .exec(); for (let doc of albums) { doc.icon = `${host}/thumbs/coloring-page/album_icon/320/${doc._id}.${imageType}`; doc.cover = `${host}/thumbs/coloring-page/album_cover/480/${doc._id}.${imageType}`; doc.title = doc.title ? doc.title[lang] : ''; doc.slogon = doc.slogon ? doc.slogon[lang] : ''; doc.size = doc.contents.length; } // 设计师 let designers = await models.Art.aggregate([ // 首先,过滤出 status = 9000 的文档 { $match: { open: true, status: 9000 } }, // 首先,根据 user 字段进行分组,并计算每个 user 出现的次数 { $group: { _id: '$user', count: { $sum: 1 } } }, // 然后,按照 count 字段进行降序排列 { $sort: { count: -1 } }, // 接着,与 users 集合进行连接,以获取用户的详细信息 { $lookup: { from: 'users', // 要连接的集合名称 localField: '_id', // 本地字段(即上一步分组后的 _id 字段) foreignField: '_id', // 要连接的集合中的字段 as: 'userDetails' // 连接后结果存储在新字段 userDetails 中 } }, // 展开 userDetails 数组,以便将用户信息提升到顶层 { $unwind: '$userDetails' }, // 调整输出格式,只保留需要的字段 { $project: { _id: 1, user: '$_id', count: 1, username: '$userDetails.username', name: '$userDetails.name' } }, // 限制返回的记录数量 { $limit: 6 } ]); for (let doc of designers) { doc.avatar = `/thumbs/v1/avatar/320/${doc._id}.${imageType}`; } let data = { title: meta.homePageTile[lang], description: meta.homePageDescription[lang], today: datefns.format(Date.now(), 'yyyy-MM-dd'), daily: todayDoc, latest, recommend, special, albums, designers, translate, categories, languages, lang, tags, uri: `/${lang}`, }; // 渲染EJS模板到内存中 res.render('index', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 分类页不带tag,redirect 到 latest router.get('/:lang/category', (req, res, next) => { return res.redirect(`${req.originalUrl}/latest`); }); // 分类页路由 router.get('/:lang/category/:tag?', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let tag = req.params.tag; if (!tag) tag = 'latest'; let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_category_${tag}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let query = { page: req.query.page, length: req.query.length, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: tag == 'latest' ? {} : { tags: tag }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { title: meta.categoryTitle[lang], description: meta.categoryDescription[lang], data: result.data, page: result.page, search: req.query.search, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, categories, languages, lang, tag, uri: `/${lang}/category/${tag}`, }; // 渲染EJS模板到内存中 res.render('category', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 标签页路由 router.get('/:lang/tag/:tag?', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let tag = req.params.tag; if (!tag) tag = 'latest'; let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_tag_${tag}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: tag == 'latest' ? {} : { tags: tag }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { title: meta.tagTitle[lang], description: meta.tagDescription[lang], data: result.data, page: result.page, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, categories, languages, lang, tag, tags, uri: `/${lang}/tag/${tag}`, }; // 渲染EJS模板到内存中 res.render('tag', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 搜索页路由 router.get('/:lang/search', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let search = req.query.search; let cacheKey = `${CACHE_PREFIX}_search_${search}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: {}, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { title: meta.searchTitle[lang], description: meta.searchDescription[lang], data: result.data, page: result.page, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, categories, languages, lang, uri: `/${lang}/search?search=${search}`, }; // 渲染EJS模板到内存中 res.render('search', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // special页路由 router.get('/:lang/special', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_special_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let query = { title: meta.specialTitle[lang], description: meta.specialDescription[lang], lang, page: req.query.page, length: req.query.length, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: { hasSpecial: true }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { title: meta.specialTitle[lang], description: meta.specialDescription[lang], data: result.data, page: result.page, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, languages, lang, uri: `/${lang}/special`, }; // 渲染EJS模板到内存中 res.render('special', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 专辑页路由 router.get('/:lang/albums', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, { maxAge: 900000, httpOnly: true }); } let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_albums_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { // 专辑 let albums = await models.ArtAlbum .find({ pid: 'art', enabled: true }) .sort({ order: 'asc' }) .populate('title') .populate('slogon') .select('tag title slogon contents') .lean() .exec(); let host = config.cdnHost ?? config.resHost; for (let doc of albums) { doc.icon = `${host}/thumbs/coloring-page/album_icon/320/${doc._id}.${imageType}`; doc.cover = `${host}/thumbs/coloring-page/album_cover/480/${doc._id}.${imageType}`; doc.title = doc.title ? doc.title[lang] : ''; doc.slogon = doc.slogon ? doc.slogon[lang] : ''; doc.size = doc.contents.length; } let data = { title: meta.albumsTitle[lang], description: meta.albumsDescription[lang], data: albums, length: albums.length, translate, languages, lang, uri: `/${lang}/albums`, }; // 渲染EJS模板到内存中 res.render('albums', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 专辑详情页路由 router.get('/:lang/album/:id', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let id = req.params.id; utils.validators.validateId(id); let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_album_${id}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { // 专辑 let doc = await models.ArtAlbum .findById(id) .populate('title') .populate('slogon') .populate({ path: 'contents', select: artSelect }) .select('tag title slogon contents') .lean() .exec(); if (!doc) throw createError(404, 'Album Not Found!'); let host = config.cdnHost ?? config.resHost; doc.icon = `${host}/thumbs/coloring-page/album_icon/320/${doc._id}.${imageType}`; doc.cover = `${host}/thumbs/coloring-page/album_cover/480/${doc._id}.${imageType}`; doc.title = doc.title ? doc.title[lang] : ''; doc.slogon = doc.slogon ? doc.slogon[lang] : ''; doc.size = doc.contents.length; organizeData(doc.contents, lang, imageType); let data = { title: `${doc.title} | ${translate.coloringPageAlbum[lang]}`, description: `${doc.slogon} | ${translate.coloringPageAlbum[lang]}`, data: doc, translate, languages, lang, uri: `/${lang}/album/${id}`, }; // 渲染EJS模板到内存中 res.render('album', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 设计师专栏路由 router.get('/:lang/designers', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_designers_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let docs = await models.Art.aggregate([ // 首先,过滤出 status = 9000 的文档 { $match: { open: true, status: 9000 } }, // 首先,根据 user 字段进行分组,并计算每个 user 出现的次数 { $group: { _id: '$user', count: { $sum: 1 } } }, // 然后,按照 count 字段进行降序排列 { $sort: { count: -1 } }, // 接着,与 users 集合进行连接,以获取用户的详细信息 { $lookup: { from: 'users', // 要连接的集合名称 localField: '_id', // 本地字段(即上一步分组后的 _id 字段) foreignField: '_id', // 要连接的集合中的字段 as: 'userDetails' // 连接后结果存储在新字段 userDetails 中 } }, // 展开 userDetails 数组,以便将用户信息提升到顶层 { $unwind: '$userDetails' }, // 调整输出格式,只保留需要的字段 { $project: { _id: 1, user: '$_id', count: 1, username: '$userDetails.username', name: '$userDetails.name' } }, // 限制返回的记录数量 { $limit: 40 } ]); for (let doc of docs) { doc.avatar = `/thumbs/v1/avatar/320/${doc._id}.${imageType}`; } let data = { title: meta.designersTitle[lang], description: meta.designersDescription[lang], data: docs, length: docs.length, translate, languages, lang, uri: `/${lang}/designers`, }; // 渲染EJS模板到内存中 res.render('designers', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 设计师详情页路由 router.get('/:lang/designer/:id', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let id = req.params.id; utils.validators.validateId(id); let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_designer_${id}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let user = await models.User.findById(id).select('name username'); if (!user) throw createError(404, 'User Not Found!'); let count = await models.Art.countDocuments({ user: id, status: 9000 }); user.count = count; user.avatar = `/thumbs/v1/avatar/320/${user._id}.${imageType}`; // find user arts // 算法: 排除掉主流的tag,用剩下的tag去检索,取最多12条记录 let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: { user: id }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { title: `${meta.designerTitle[lang]}: ${user.username}`, description: meta.designerDescription[lang], user, data: result.data, page: result.page, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, lang, languages, uri: `/${lang}/designer/${id}`, }; // 渲染EJS模板到内存中 res.render('designer', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); function getRealId(str) { let id = str; let lastIndex = str.lastIndexOf('-'); if (lastIndex !== -1) { id = str.substring(lastIndex + 1); } return id; } // 详情页路由(seo url) router.get('/:lang/coloring-page/:str', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let str = req.params.str; // 拟人化的id,形如 beautiful-house-daldkaghlda3232, 最后一个-后面的才是真正的id let id = getRealId(str); utils.validators.validateId(id); let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_detail_${id}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let doc = await models.Art .findById(id) .select(artSelect) .populate('user', 'username name') .lean() .exec(); if (!doc) throw createError(404, 'Art Not Found!'); organizeDetail(doc, lang, imageType); // find relate let tags = [...doc.tags]; let cates = categories.map(e => e.id); tags = tags.filter(e => !cates.includes(e)); let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: { tags }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { // title: `${doc.name.replace(/[_]+/g, '-')}`, title: `${doc.title} ${translate.printableColoringPage[lang]}`, description: `${doc.desc}`, // description: `${meta.detailDescription[lang]}: ${doc.desc}`, detail: doc, data: result.data, page: result.page, length: result.length, recordsFiltered: result.recordsFiltered, recordsTotal: result.recordsTotal, translate, lang, languages, relates: result.data, uri: doc.uri, }; // 渲染EJS模板到内存中 res.render('detail', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // 详情页路由(简单版不带seo) router.get('/:lang/detail/:id', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); if (!req.cookies.lang || req.cookies.lang != lang) { res.cookie('lang', lang, config.cookie); } let id = req.params.id; utils.validators.validateId(id); let imageType = req.headers.accept?.includes('image/webp') ? 'webp' : 'jpeg'; // 浏览器支持webp就用webp let cacheKey = `${CACHE_PREFIX}_detail_${id}_${lang}_${imageType}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let doc = await models.Art .findById(id) .select(artSelect) .populate('user', 'username name') .lean() .exec(); if (!doc) throw createError(404, 'Art Not Found!'); organizeDetail(doc, lang, imageType); // find relate let tags = [...doc.tags]; let cates = categories.map(e => e.id); tags = tags.filter(e => !cates.includes(e)); let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { open: true, status: 9000 }, filters: { tags }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang, imageType); let data = { // title: `${doc.name.replace(/[_]+/g, '-')}`, title: `${doc.title} ${translate.printableColoringPage[lang]}`, description: `${doc.desc}`, // description: `${meta.detailDescription[lang]}: ${doc.desc}`, data: doc, translate, lang, languages, relates, uri: `${lang}/detail/${id}`, }; // 渲染EJS模板到内存中 res.render('detail', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); // play页路由 router.get('/play/:id', function (req, res, next) { (async function () { let id = req.params.id; utils.validators.validateId(id); let cacheKey = `${CACHE_PREFIX}_play_${id}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let doc = await models.Art.findById(id); if (!doc) throw createError(404, 'Art Not Found!'); let data = { id, title: meta.playTitle.en, description: meta.playDescription.en, }; // 渲染EJS模板到内存中 res.render('play', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); router.get('/:lang/info', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); let cacheKey = `${CACHE_PREFIX}_info_${lang}`; let htmlData = await redis.getAsync(cacheKey); if (!htmlData) { let data = { title: meta.infoTitle[lang], description: meta.infoDescription[lang], lang, languages, translate, uri: `/${lang}/info`, }; // 渲染EJS模板到内存中 res.render('info', data, async (err, html) => { if (err) { // 如果渲染出错,调用next()传递错误 return next(err); } // 渲染成功,存redis, 发送数据到客户端 htmlData = html; try { await redis.set(cacheKey, htmlData, 'EX', CACHE_EXPIRES); } catch (e) { console.error(e); } res.send(htmlData); }); } else { // 缓存命中, 直接发送缓存数据 res.set({ 'X-From-Cache': 'true' }); res.send(htmlData); } })().catch(next); }); const organizeData = (data, lang, imageType) => { data.forEach(doc => { let host = config.cdnHost ?? config.resHost; let publishVersion = doc.publishVersion || 0; let version = publishVersion + 1500; doc.thumb = `${host}/thumbs/coloring-page/page/480/${doc._id}.${imageType}`; doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`; let uriTitle = doc.name; if (doc.title) { try { let titleJson = JSON.parse(doc.title); doc.title = titleJson && titleJson[lang] ? titleJson[lang] : doc.name; if (titleJson.en) uriTitle = titleJson.en; // URL 里还是尽量用英文title } catch (e) { console.error(e.message); } } else { doc.title = doc.name; } doc.tags = doc.tags.filter(str => !/[\u4e00-\u9fa5]/.test(str)); // 过滤掉tags中的中文 let utf8name = encodeURIComponent(uriTitle.replace(/[\s_]+/g, '-')).toLowerCase(); doc.uri = `/${lang}/coloring-page/${utf8name}-${doc._id}`; delete doc.hasSpecial; delete doc.useSpecialThumb; delete doc.publishVersion; delete doc.pageId; delete doc.desc; }) } const organizeDetail = (doc, lang, imageType) => { let host = config.cdnHost ?? config.resHost; let publishVersion = doc.publishVersion || 0; let version = publishVersion + 1500; doc.thumb = `${host}/thumbs/coloring-page/page/480/${doc._id}.${imageType}`; doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip` // doc.title = translate.titleTest[lang]; // doc.desc = translate.descTest[lang]; let uriTitle = doc.name; if (doc.title) { try { let titleJson = JSON.parse(doc.title); doc.title = titleJson && titleJson[lang] ? titleJson[lang] : doc.name; if (titleJson.en) uriTitle = titleJson.en; // URL 里还是尽量用英文title } catch (e) { console.error(e.message); } } else { doc.title = doc.name; } if (doc.desc) { try { let descJson = JSON.parse(doc.desc); doc.desc = descJson && descJson[lang] ? descJson[lang] : ''; } catch (e) { console.error(e.message); } } doc.publishTime = format(new Date(doc.publishTime), 'yyyy/MM/dd'); doc.tags = doc.tags.filter(str => !/[\u4e00-\u9fa5]/.test(str)); // 过滤掉tags中的中文 let utf8name = encodeURIComponent(uriTitle.replace(/[\s_]+/g, '-')).toLowerCase(); doc.uri = `/${lang}/coloring-page/${utf8name}-${doc._id}`; doc.downlink = `${host}/thumbs/coloring-page/page/1200/${doc._id}.${imageType}`; delete doc.hasSpecial; delete doc.useSpecialThumb; delete doc.publishVersion; delete doc.pageId; } module.exports = router;