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 { format } = require('date-fns'); const { getListBuilder } = require('../libs/pager'); const CACHE_PREFIX = "art_v1"; const CACHE_EXPIRES = 60; // 60s刷新一次 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, { maxAge: 900000, httpOnly: true }); // 重定向会指定的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); // 如果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, { maxAge: 900000, httpOnly: true }); } return res.redirect(`/${lang}`); }); // 首页路由 router.get('/:lang/', 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 baseSort = { publishTime: 'desc' }; // 最新上线 let latest = await models.Art .find({ status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(latest, lang); // 热门精选 let recommend = await models.Art .find({ tags: 'data_good', status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(recommend, lang); // special 专区 let special = await models.Art .find({ hasSpecial: true, status: 9000 }) .select(artSelect) .sort(baseSort) .limit(12) .lean() .exec(); organizeData(special, lang); // 专辑 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 = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`; doc.cover = `${config.resHost}/res/coloring/album_cover/320/${doc._id}.jpeg`; 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: { 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}.jpeg`; } let data = { title: meta.homePageTile[lang], description: meta.homePageDescription[lang], latest, recommend, special, albums, designers, translate, categories, languages, lang, tags, uri: `/${lang}`, }; res.render('index', data); })().catch(next); }); // 分类页路由 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, { maxAge: 900000, httpOnly: true }); } let tag = req.params.tag; if (!tag) tag = 'latest'; let query = { page: req.query.page, length: req.query.length, orderBy: 'publishTime', order: 'desc', base: { status: 9000 }, filters: tag == 'latest' ? {} : { tags: tag }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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}`, }; res.render('category', data); })().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, { maxAge: 900000, httpOnly: true }); } let tag = req.params.tag; if (!tag) tag = 'latest'; let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { status: 9000 }, filters: tag == 'latest' ? {} : { tags: tag }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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}`, }; res.render('tag', data); })().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, { maxAge: 900000, httpOnly: true }); } let search = req.query.search; let query = { page: req.query.page, length: req.query.length, search: req.query.search, orderBy: 'publishTime', order: 'desc', base: { status: 9000 }, filters: {}, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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}`, }; res.render('search', data); })().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, { maxAge: 900000, httpOnly: true }); } let query = { title: meta.specialTitle[lang], description: meta.specialDescription[lang], lang, page: req.query.page, length: req.query.length, orderBy: 'publishTime', order: 'desc', base: { status: 9000 }, filters: { hasSpecial: true }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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`, }; res.render('special', data); })().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 albums = await models.ArtAlbum .find({ pid: 'art', enabled: true }) .sort({ order: 'asc' }) .populate('title') .populate('slogon') .select('tag title slogon contents') .lean() .exec(); for (let doc of albums) { doc.icon = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`; doc.cover = `${config.resHost}/res/coloring/album_cover/640/${doc._id}.jpeg`; 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`, }; res.render('albums', data); })().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, { maxAge: 900000, httpOnly: true }); } let id = req.params.id; utils.validators.validateId(id); // 专辑 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!'); doc.icon = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`; doc.cover = `${config.resHost}/res/coloring/album_cover/640/${doc._id}.jpeg`; doc.title = doc.title ? doc.title[lang] : ''; doc.slogon = doc.slogon ? doc.slogon[lang] : ''; doc.size = doc.contents.length; organizeData(doc.contents, lang); let data = { title: `${doc.title} | ${translate.coloringPageAlbum[lang]}`, description: `${doc.slogon} | ${translate.coloringPageAlbum[lang]}`, data: doc, translate, languages, lang, uri: `/${lang}/album/${id}`, }; res.render('album', data); })().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, { maxAge: 900000, httpOnly: true }); } let docs = await models.Art.aggregate([ // 首先,过滤出 status = 9000 的文档 { $match: { 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}.jpeg`; } let data = { title: meta.designersTitle[lang], description: meta.designersDescription[lang], data: docs, length: docs.length, translate, languages, lang, uri: `/${lang}/designers`, }; res.render('designers', data); })().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, { maxAge: 900000, httpOnly: true }); } let id = req.params.id; utils.validators.validateId(id); 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}.jpeg`; // 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: { status: 9000 }, filters: { user: id }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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}`, }; res.render('designer', data); })().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, { maxAge: 900000, httpOnly: true }); } let str = req.params.str; // 拟人化的id,形如 beautiful-house-daldkaghlda3232, 最后一个-后面的才是真正的id let id = getRealId(str); utils.validators.validateId(id); 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); // 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: { status: 9000 }, filters: { tags }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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, }; res.render('detail', data); })().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, { maxAge: 900000, httpOnly: true }); } let id = req.params.id; utils.validators.validateId(id); 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); // 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: { status: 9000 }, filters: { tags }, } let result = await getListBuilder(query, models.Art); organizeData(result.data, lang); 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}`, }; res.render('detail', data); })().catch(next); }); // play页路由 router.get('/play/:id', function (req, res, next) { (async function () { let id = req.params.id; let data = { id, title: meta.playTitle.en, description: meta.playDescription.en, }; utils.validators.validateId(id); let doc = await models.Art.findById(id); if (!doc) throw createError(404, 'Art Not Found!'); res.render('play', data); })().catch(next); }); // print页路由 router.get('/print/:id', function (req, res, next) { (async function () { let id = req.params.id; let data = { id, title: meta.playTitle.en, description: meta.playDescription.en, }; utils.validators.validateId(id); let doc = await models.Art.findById(id); if (!doc) throw createError(404, 'Art Not Found!'); let host = config.resHost; let size = Math.min(doc.width, 2000); let downlink = `${host}/thumbs/coloring-page/page/${size}/${doc._id}.jpeg`; data.downlink = downlink; res.render('print', data); })().catch(next); }); router.get('/:lang/info', function (req, res, next) { (async function () { let lang = utils.lang.ensureLanguage(req.params.lang); let data = { title: meta.infoTitle[lang], description: meta.infoDescription[lang], lang, languages, translate, uri: `/${lang}/info`, }; res.render('info', data); })().catch(next); }); const organizeData = (data, lang) => { data.forEach(doc => { let host = config.resHost; let publishVersion = doc.publishVersion || 0; let version = publishVersion + 1500; doc.thumb = `${host}/thumbs/coloring-page/page/480/${doc._id}.jpeg`; if (doc.mystery) { // 神秘图固定用一张 doc.thumb = `${host}/thumbs/coloring-page/mystery/480/${doc._id}.jpeg`; } else if (doc.hasSpecial) { // special图有切线图、渐变切线图、灰度图、上传图3中可选 let str = 'special_outline'; if (doc.useSpecialThumb == 1) { str = 'special_gray'; } else if (doc.useSpecialThumb == 2) { str = 'special_thumb'; } else if (doc.useSpecialThumb == 3) { str = 'special_gradient'; } doc.thumb = `${host}/thumbs/coloring-page/${str}/480/${doc._id}.jpeg`; } doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`; if (doc.title) { try { let titleJson = JSON.parse(doc.title); doc.title = titleJson && titleJson[lang] ? titleJson[lang] : doc.name; } catch (e) { console.error(e.message); } } else { doc.title = doc.name; } let utf8name = encodeURIComponent(doc.title.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) => { let host = config.resHost; let publishVersion = doc.publishVersion || 0; let version = publishVersion + 1500; doc.thumb = `${host}/thumbs/coloring-page/page/480/${doc._id}.jpeg`; if (doc.mystery) { // 神秘图固定用一张 doc.thumb = `${host}/thumbs/coloring-page/mystery/480/${doc._id}.jpeg`; } else if (doc.hasSpecial) { // special图有切线图、渐变切线图、灰度图、上传图3中可选 let str = 'special_outline'; if (doc.useSpecialThumb == 1) { str = 'special_gray'; } else if (doc.useSpecialThumb == 2) { str = 'special_thumb'; } else if (doc.useSpecialThumb == 3) { str = 'special_gradient'; } doc.thumb = `${host}/thumbs/coloring-page/${str}/480/${doc._id}.jpeg`; } doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip` // doc.title = translate.titleTest[lang]; // doc.desc = translate.descTest[lang]; if (doc.title) { try { let titleJson = JSON.parse(doc.title); doc.title = titleJson && titleJson[lang] ? titleJson[lang] : doc.name; } 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'); let utf8name = encodeURIComponent(doc.title.replace(/[\s_]+/g, '-')).toLowerCase(); doc.uri = `/${lang}/coloring-page/${utf8name}-${doc._id}`; let size = Math.min(doc.width, 2000); doc.downlink = `${host}/thumbs/coloring-page/page/${size}/${doc._id}.jpeg`; delete doc.hasSpecial; delete doc.useSpecialThumb; delete doc.publishVersion; delete doc.pageId; } module.exports = router;