2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import type { AppProduct, AppDomain, ApiEnvelope } from '~/app/types'
|
||
import { $fetch } from 'ofetch'
|
||
|
||
type RuntimeConfig = ReturnType<typeof useRuntimeConfig>
|
||
|
||
/**
|
||
* 从统一响应封装中取出 data
|
||
* 后端可能返回 { code, data } 也可能直接返回对象
|
||
*
|
||
* 关键:未找到时上游返回的是「无 data 字段的信封」,
|
||
* 例如 { "code": 0, "message": "操作成功" }。
|
||
* 旧实现在无 data 字段时会把整个信封当结果返回(truthy),
|
||
* 导致调用方误判为「命中」,进而跳过了未授权 403 拦截。
|
||
* 因此:无 data 字段的信封一律视为「未找到」返回 null。
|
||
*/
|
||
function unwrap<T>(res: ApiEnvelope<T> | T): T | null {
|
||
if (!res || typeof res !== 'object') return null
|
||
const obj = res as Record<string, unknown>
|
||
// 标准信封:存在 data 字段则取 data(未找到时 data 为 null)
|
||
if ('data' in obj) {
|
||
return (obj.data as T) ?? null
|
||
}
|
||
// 信封但无 data(如 { code, message })→ 视为无结果
|
||
if ('code' in obj || 'message' in obj) {
|
||
return null
|
||
}
|
||
// 直接返回对象(无信封包裹)的情况
|
||
return (res as T) ?? null
|
||
}
|
||
|
||
/** 不应暴露到前端 / SSR payload 的敏感字段 */
|
||
const SENSITIVE_KEYS = ['productSecret', 'apiUrl', 'reviewerId', 'rejectReason']
|
||
|
||
/**
|
||
* 脱敏:移除应用密钥等敏感字段
|
||
*/
|
||
function sanitize(app: AppProduct | null): AppProduct | null {
|
||
if (!app) return null
|
||
const clean = { ...app }
|
||
for (const key of SENSITIVE_KEYS) {
|
||
delete clean[key]
|
||
}
|
||
return clean
|
||
}
|
||
|
||
/**
|
||
* 按 productId 查询应用产品信息
|
||
* 后端: GET {appApiBase}/api/app/product/detail/{productId}
|
||
* 用于本地开发环境(.env 指定 NUXT_PUBLIC_APP_ID)
|
||
*/
|
||
export async function getAppProductById(
|
||
productId: string | number,
|
||
config: RuntimeConfig
|
||
): Promise<AppProduct | null> {
|
||
const appApiBase = config.public.appApiBase as string
|
||
if (!productId) return null
|
||
|
||
try {
|
||
const res = await $fetch<ApiEnvelope<AppProduct> | AppProduct>(
|
||
`/api/app/product/detail/${productId}`,
|
||
{
|
||
baseURL: appApiBase,
|
||
timeout: 5000,
|
||
retry: 1
|
||
}
|
||
)
|
||
return sanitize(unwrap<AppProduct>(res))
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按域名查询应用产品信息
|
||
* 后端: GET {appApiBase}/api/app/product/getByDomain?domain={domain}
|
||
* 用于生产环境(根据当前访问域名解析应用)
|
||
* 后端已改造为直接返回完整 AppProduct(含 tenantId / templateId)
|
||
*/
|
||
export async function getAppProductByDomain(
|
||
domain: string,
|
||
config: RuntimeConfig
|
||
): Promise<AppProduct | null> {
|
||
const appApiBase = config.public.appApiBase as string
|
||
if (!domain) return null
|
||
|
||
try {
|
||
const res = await $fetch<ApiEnvelope<AppProduct> | AppProduct>(
|
||
'/api/app/product/getByDomain',
|
||
{
|
||
baseURL: appApiBase,
|
||
query: { domain },
|
||
timeout: 5000,
|
||
retry: 1
|
||
}
|
||
)
|
||
return sanitize(unwrap<AppProduct>(res))
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按域名查询应用域名绑定(appDomain 表)
|
||
* 后端: GET {appApiBase}/api/app/domain/getByDomain?domain={domain}
|
||
* 用于生产环境兜底解析(主解析 app_product.domain 未命中时回退到此表)
|
||
* 返回绑定关系(含 tenantId / productId),用于取出租户与回查完整应用信息
|
||
*/
|
||
export async function getAppDomainByDomain(
|
||
domain: string,
|
||
config: RuntimeConfig
|
||
): Promise<AppDomain | null> {
|
||
const appApiBase = config.public.appApiBase as string
|
||
if (!domain) return null
|
||
|
||
try {
|
||
const res = await $fetch<ApiEnvelope<AppDomain> | AppDomain>(
|
||
'/api/app/domain/getByDomain',
|
||
{
|
||
baseURL: appApiBase,
|
||
query: { domain },
|
||
timeout: 5000,
|
||
retry: 1
|
||
}
|
||
)
|
||
return unwrap<AppDomain>(res)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|