Files
hjc-web/server/utils/template-map.ts
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

124 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { $fetch } from 'ofetch'
import type { ApiEnvelope } from '~/app/types'
type RuntimeConfig = ReturnType<typeof useRuntimeConfig>
/** app_template 表行(仅取解析目录名所需字段) */
interface AppTemplateRow {
id?: number
/** 模板标识,形如 'template-08',与前端 app/templates/<code>/ 目录一一对应 */
code?: string
name?: string
}
interface AppTemplatePage {
list?: AppTemplateRow[]
count?: number
}
/**
* 模板主键 → 模板标识(code)映射
*
* 【为什么需要这层映射】
* 上游 getSiteInfo 只返回 cms_website.template_idapp_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<number, string>; expireAt: number } | null = null
/** 并发去重:同一时刻多个 SSR 请求只发起一次上游查询 */
let inflight: Promise<Map<number, string>> | null = null
/**
* 拉取全量模板表并构建 id → code 映射
* 后端: GET {appApiBase}/api/app/template/page(无需鉴权)
*/
async function fetchTemplateMap(config: RuntimeConfig): Promise<Map<number, string>> {
const appApiBase = config.public.appApiBase as string
const map = new Map<number, string>()
const res = await $fetch<ApiEnvelope<AppTemplatePage> | AppTemplatePage>(
'/api/app/template/page',
{
baseURL: appApiBase,
query: { page: 1, limit: 200 },
timeout: 5000,
retry: 1
}
)
const data = (res as ApiEnvelope<AppTemplatePage>)?.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<Map<number, string>> {
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<number, string>())
.finally(() => {
inflight = null
})
return inflight
}
/**
* 按模板主键反查模板标识(code)
*
* @returns 形如 'template-08';查不到返回 ''(调用方回退主键推导)
*/
export async function getTemplateCodeById(
templateId: number | string | null | undefined,
config: RuntimeConfig
): Promise<string> {
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
}