var express = require('express'); var router = express.Router(); const redis = require('../../libs/redis'); const models = require('../../models'); const utils = require('../../libs/utils'); const common = require('./common'); const { getListBuilder } = require('../../libs/pager'); const categories = require('../../config/category'); const { tags } = require('../../config/tag'); const { coloringList } = require('./config'); const { getSimilarArts, getSimilarArtsSimple } = require('./similarArts'); const CACHE_PREFIX = "art_v2"; // const CACHE_EXPIRES = 60; // 60s刷新一次 const CACHE_EXPIRES = 600; const artSelect = 'name title desc seoTitle seoDescription width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion totalStartCount totalDoneCount completionRate'; // 详情页路由 router.get('/:id', function (req, res, next) { (async function () { let id = req.params.id; utils.validators.validateId(id); // let cacheKey = `${CACHE_PREFIX}_detail_${id}`; // let htmlData = await redis.getAsync(cacheKey); let htmlData = null; 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!'); common.organizeDetail(doc); // 关联推荐 // let mytags = [...doc.tags]; // let cates = categories.map(e => e.id); // let alltags = tags.map(e => e.tag); // mytags = mytags.filter(e => !cates.includes(e) && alltags.includes(e)); // if (mytags.length == 0) mytags = [...doc.tags]; // 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: mytags }, // } // let result = await getListBuilder(query, models.Art, [{ path: 'user', select: 'username' }]); // common.organizeData(result.data); // 用新的算法 console.time('getSimilarArts'); let result = await getSimilarArts(id, {limit: 30, candidateLimit: 1000, fields : artSelect }); console.timeEnd('getSimilarArts') common.organizeData(result); // 填色页合集推荐 const recmCollections = recommendColoringPages(doc, coloringList); // 评论 const comments = await models.Comment.find({ approved: true, page: doc._id }).sort({ createdAt: -1 }); // deeplink 相关 let applink = `https://art.pcoloring.com${req.originalUrl}`; let downlink = `https://art.pcoloring.com/app`; let openapplink = applink; if (!req.originalUrl.includes('check')) { openapplink = applink.concat(req.originalUrl.includes('?') ? '&check=1' : '?check=1'); } const userAgent = req.headers['user-agent']; console.log('User-Agent:', userAgent); if (userAgent) { const ua = userAgent.toLowerCase(); if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod')) { downlink = 'itms-apps://itunes.apple.com/app/id1575480118?utm_source=share'; } else if (ua.includes('android')) { downlink = 'https://play.google.com/store/apps/details?id=com.pcoloring.art.puzzle.color.by.number&utm_source=share'; } } let data = { title: `${doc.seoTitle}`, description: `${doc.seoDescription}`, detail: doc, // data: result.data, page: result.page, pageId: doc._id, // length: result.length, // recordsFiltered: result.recordsFiltered, // recordsTotal: result.recordsTotal, // relates: result.data, relates: result, uri: `/coloring-page/${doc._id}`, imageUrl: doc.thumb, pageUri: common.replaceUriParams, comments, dateFormat: common.dateFormat, collections: recmCollections, applink, downlink, openapplink, }; // 渲染EJS模板到内存中 res.render('v2/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); }); /** * 填色页推荐系统 - 根据标签相似度推荐相关填色页合集 * @param {Object} currentPage - 当前填色页对象,包含tags属性 * @param {Array} collections - 所有填色页合集列表 * @returns {Array} - 推荐的3个填色页合集 */ function recommendColoringPages(currentPage, collections) { // 计算每个候选页与当前页的标签匹配度 const scoredPages = collections.map(page => { const matchedTags = page.tags.filter(tag => currentPage.tags.some(currentTag => currentTag.toLowerCase() === tag.toLowerCase() ) ); return { ...page, score: matchedTags.length, matchedTags }; }); // 按匹配度降序排序 scoredPages.sort((a, b) => b.score - a.score); // 收集匹配度大于0的推荐 const tagMatchedRecommendations = scoredPages.filter(page => page.score > 0); // 如果标签匹配的推荐不足3个,从剩余的合集中随机选取补足 if (tagMatchedRecommendations.length < 3) { const remainingPages = scoredPages.filter(page => page.score === 0); // 打乱剩余页面顺序 const shuffledRemaining = remainingPages.sort(() => 0.5 - Math.random()); // 补足到3个推荐 return [...tagMatchedRecommendations, ...shuffledRemaining].slice(0, 3); } // 如果标签匹配的推荐超过3个,取前3个 return tagMatchedRecommendations.slice(0, 3); } module.exports = router;