import { eq, desc, asc } from 'drizzle-orm' import { useDB, schema } from '../../utils/db.js' // 防御性解析 tags:兼容历史脏数据(字符串字面量 "[]"、null、非法 JSON),始终返回数组 function safeParseTags(raw) { if (!raw) return [] try { let v = JSON.parse(raw) if (typeof v === 'string') { try { v = JSON.parse(v) } catch { return [] } } return Array.isArray(v) ? v : [] } catch { return [] } } /** * 实体表数据构建器 * 当 key 对应独立实体表时,从实体表动态读取数据, * 同时合并 site_content 中的元数据(summary/seo 等) */ function buildFromEntityTable(db, key, siteData) { switch (key) { case 'reviews': { const items = db.select().from(schema.reviews) .where(eq(schema.reviews.published, true)) .orderBy(asc(schema.reviews.sortOrder), desc(schema.reviews.id)) .all() .map(r => ({ id: r.id, nickname: r.author, travelDate: r.date, productVersion: r.title, screenshot: r.screenshot || '', content: r.content, rating: r.rating, scenes: r.scenes ? JSON.parse(r.scenes) : [], concerns: r.concerns ? JSON.parse(r.concerns) : [], })) // 高频关键词:从评价正文中按预设词表统计出现频次 const KEYWORD_DICT = [ '领队', '服务', '贴心', '耐心', '细心', '专业', '摄影', '拍照', '旅拍', '骑马', '越野车', '滑草', '营地', '烧烤', '住宿', '行程', '错峰', '不排队', '亲子', '小朋友', '孩子', '家人', '老人', '草原', '风景', '美', '惊艳', '难忘', '舒适', '安排', '推荐', '值得', '良心', '安心', '用心', ] const freq = {} for (const it of items) { const c = it.content || '' for (const kw of KEYWORD_DICT) if (c.includes(kw)) freq[kw] = (freq[kw] || 0) + 1 } const keywords = Object.entries(freq) .sort((a, b) => b[1] - a[1]) .slice(0, 12) .map(([w]) => w) const summary = { totalCount: siteData?.summary?.totalCount || items.length, approvalRate: siteData?.summary?.approvalRate || '98%', keywords: siteData?.summary?.keywords?.length ? siteData.summary.keywords : keywords, } return { summary, items } } case 'faq': { const items = db.select().from(schema.faqs) .where(eq(schema.faqs.published, true)) .orderBy(asc(schema.faqs.sortOrder), asc(schema.faqs.id)) .all() // site_content 有分类结构则保留,把实体表的问答作为「全部」 if (siteData?.categories) return siteData return { categories: [{ id: 'all', name: '常见问题', questions: items.map(f => ({ question: f.question, answer: f.answer, })), }], } } case 'gallery': { const items = db.select().from(schema.galleryItems) .where(eq(schema.galleryItems.published, true)) .orderBy(asc(schema.galleryItems.sortOrder), asc(schema.galleryItems.id)) .all() const works = items.map(g => ({ id: g.id, title: g.title, category: g.category, location: g.location, description: g.description, image: g.image, orientation: g.orientation, })) const categories = siteData?.categories || [{key:"all",label:"全部作品"}] return { seo: siteData?.seo || {}, intro: siteData?.intro || {}, categories, works, } } case 'stories': { const items = db.select().from(schema.stories) .where(eq(schema.stories.published, true)) .orderBy(asc(schema.stories.sortOrder), desc(schema.stories.id)) .all() .map(s => ({ ...s, tags: s.tags ? (() => { try { return JSON.parse(s.tags) } catch { return [] } })() : [], sections: s.sections ? JSON.parse(s.sections) : [], keyMoments: s.keyMoments ? JSON.parse(s.keyMoments) : [], })) return { stories: items } } case 'news': { const items = db.select().from(schema.articles) .where(eq(schema.articles.published, true)) .orderBy(desc(schema.articles.createdAt), desc(schema.articles.id)) .all() .filter(a => a.type === 'news') .map(a => ({ id: a.slug, title: a.title, summary: a.summary, content: a.content, category: a.category, coverImage: a.coverImage, author: a.author, tags: safeParseTags(a.tags), date: a.createdAt, })) return { articles: items } } case 'blog': { const items = db.select().from(schema.articles) .where(eq(schema.articles.published, true)) .orderBy(desc(schema.articles.id)) .all() .filter(a => a.type === 'blog') .map(a => ({ id: a.slug, title: a.title, summary: a.summary, content: a.content, category: a.category, coverImage: a.coverImage, author: a.author, tags: safeParseTags(a.tags), seoTitle: a.seoTitle, seoDescription: a.seoDesc, seoKeywords: a.seoKeywords, date: a.createdAt, })) return { articles: items } } default: return null } } // 有entity表的 key 列表 const ENTITY_KEYS = ['reviews', 'faq', 'gallery', 'stories', 'news', 'blog'] export default defineEventHandler(async (event) => { const key = getRouterParam(event, 'key') const db = useDB() // 读site_content(作为元数据或兜底) const item = db.select().from(schema.siteContent) .where(eq(schema.siteContent.key, key)) .get() const siteData = item ? JSON.parse(item.data) : null // 如果是实体表key,优先从实体表构建 if (ENTITY_KEYS.includes(key)) { const result = buildFromEntityTable(db, key, siteData) if (result) { setHeader(event, 'Cache-Control', 'no-store, must-revalidate') return result } } // 虚拟 key:summer-camp 从 products.summerCamp 提取 if (key === 'summer-camp' && !siteData) { const productsItem = db.select().from(schema.siteContent) .where(eq(schema.siteContent.key, 'products')) .get() const productsData = productsItem ? JSON.parse(productsItem.data) : null if (productsData?.summerCamp) { setHeader(event, 'Cache-Control', 'no-store, must-revalidate') return productsData.summerCamp } } // 普通 site_content key if (!siteData) { throw createError({ statusCode: 404, message: `未找到配置项: ${key}` }) } setHeader(event, 'Cache-Control', 'no-store, must-revalidate') return siteData })