fix: NUXT_PUBLIC_APP_ID 改为数字主键,接口基址切正式域名
1. appId 语义修正
NUXT_PUBLIC_APP_ID 之前填的是 productCode('website'),但下游拿它去请求
/api/app/product/detail/{productId},后端按 Integer 解析主键,于是转换失败。
改为 app_product.product_id 数字主键(种子数据为 42);nuxt.config.ts 的
缺省值同步去掉 'website' 回退——留空时下游按空值降级,绝不回退到编码值。
2. 接口基址切 hjc-api.sitelnk.cn
.env.example / .env.production / nuxt.config.ts 的缺省值与 preconnect、
dns-prefetch 一并从 hjc-api.websoft.top 改为正式域名;scripts/ 与
outputs/ 文档里的旧地址同步更新。
注:本提交含若干文件的 CRLF/LF 行尾归一,实际内容改动见 git diff --ignore-cr-at-eol。
This commit is contained in:
+137
-129
@@ -1,129 +1,137 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
// 防御:productId 是 app_product.product_id 数字主键。
|
||||
// 传入 productCode(如 'website')会打到 /api/app/product/detail/{productId}
|
||||
// 导致后端 Integer 转换失败,且每个请求都会重复报错。直接短路返回 null。
|
||||
if (!/^\d+$/.test(String(productId))) {
|
||||
console.warn('[app] getAppProductById 收到非数字 productId=%s,跳过查询', 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user