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:
2026-09-25 05:22:58 +08:00
parent 3d3a377221
commit 4a5a8f306a
12 changed files with 582 additions and 564 deletions
+137 -129
View File
@@ -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
}
}