import { $fetch } from 'ofetch' import type { ApiEnvelope } from '~/app/types' type RuntimeConfig = ReturnType /** app_template 表行(仅取解析目录名所需字段) */ interface AppTemplateRow { id?: number /** 模板标识,形如 'template-08',与前端 app/templates// 目录一一对应 */ code?: string name?: string } interface AppTemplatePage { list?: AppTemplateRow[] count?: number } /** * 模板主键 → 模板标识(code)映射 * * 【为什么需要这层映射】 * 上游 getSiteInfo 只返回 cms_website.template_id(app_template 自增主键), * 而前端模板目录名与 app_template.code 对应。历史上主键出现过跳号(id=8 缺失), * 导致「按主键补零推导目录名」整体错位一位(gxhrtc.shoplnk.cn 事故)。 * * 因此 SSR 侧统一在此反查模板表,把权威的 code 回填进 siteInfo.templateCode, * 前端 resolveTemplateKey() 优先按 code 解析,主键仅兜底。 * * 待上游 getSiteInfo 直接返回 templateCode 后,本模块会自动降级为「上游未返回时的兜底」, * 无需改动前端解析逻辑(tenant.ts 中已判断 templateCode 为空才查表)。 */ /** 缓存有效期:模板表极少变动,10 分钟足够,且避免每次 SSR 都打上游 */ const CACHE_TTL = 10 * 60 * 1000 let cache: { map: Map; expireAt: number } | null = null /** 并发去重:同一时刻多个 SSR 请求只发起一次上游查询 */ let inflight: Promise> | null = null /** * 拉取全量模板表并构建 id → code 映射 * 后端: GET {appApiBase}/api/app/template/page(无需鉴权) */ async function fetchTemplateMap(config: RuntimeConfig): Promise> { const appApiBase = config.public.appApiBase as string const map = new Map() const res = await $fetch | AppTemplatePage>( '/api/app/template/page', { baseURL: appApiBase, query: { page: 1, limit: 200 }, timeout: 5000, retry: 1 } ) const data = (res as ApiEnvelope)?.data ?? (res as AppTemplatePage) for (const row of data?.list || []) { if (row?.id && typeof row.code === 'string' && row.code.trim()) { map.set(Number(row.id), row.code.trim().toLowerCase()) } } return map } /** * 获取 id → code 映射(带缓存 + 并发去重 + 失败降级) * 上游异常时返回上一次的有效缓存(若有),否则返回空映射,由调用方回退主键推导。 */ async function getTemplateMap(config: RuntimeConfig): Promise> { const now = Date.now() if (cache && cache.expireAt > now) return cache.map if (inflight) return inflight inflight = fetchTemplateMap(config) .then((map) => { // 空结果不覆盖有效缓存,避免上游偶发返回空表导致全站回退主键推导 if (map.size > 0) { cache = { map, expireAt: Date.now() + CACHE_TTL } } return cache?.map ?? map }) .catch(() => cache?.map ?? new Map()) .finally(() => { inflight = null }) return inflight } /** * 按模板主键反查模板标识(code) * * @returns 形如 'template-08';查不到返回 ''(调用方回退主键推导) */ export async function getTemplateCodeById( templateId: number | string | null | undefined, config: RuntimeConfig ): Promise { if (templateId === null || templateId === undefined || templateId === '') return '' // 已经是目录名形式(上游直接返回 code),无需查表 if (typeof templateId === 'string' && templateId.startsWith('template-')) { return templateId.trim().toLowerCase() } const id = Number(templateId) if (!id || Number.isNaN(id) || id <= 0) return '' try { const map = await getTemplateMap(config) return map.get(id) || '' } catch { return '' } } /** 清空缓存(后台改动模板表后可通过重启或调用此方法立即生效,主要供测试使用) */ export function clearTemplateMapCache(): void { cache = null }