import Taro from '@tarojs/taro'; import { appEnv, ensureRuntimeConfigLoaded } from '@/env'; import type { ApiErrorPayload, ApiSession, TenantContext } from '@/types'; import { buildApiHeaders, resolveApiAuthorization } from './api-auth'; import type { ApiAuthMode } from './api-auth'; import { getStorage, removeStorage, setStorage } from './storage'; const TENANT_KEY = 'tiku:tenant'; const SESSION_KEY = 'tiku:session'; export class ApiError extends Error { status: number; code: string; details?: unknown; constructor(payload: ApiErrorPayload) { super(payload.message); this.name = 'ApiError'; this.status = payload.status; this.code = payload.code; this.details = payload.details; } } export function getTenantContext() { return getStorage(TENANT_KEY); } export function saveTenantContext(tenant: TenantContext) { setStorage(TENANT_KEY, tenant); } export function clearTenantContext() { removeStorage(TENANT_KEY); } export function getSession() { return getStorage(SESSION_KEY); } export function saveSession(session: ApiSession) { setStorage(SESSION_KEY, session); } export function clearSession() { removeStorage(SESSION_KEY); } export type { ApiAuthMode, SupabaseAccessTokenProvider } from './api-auth'; export { setSupabaseAccessTokenProviderForTest } from './api-auth'; function normalizeBaseUrl(baseUrl: string) { return baseUrl.replace(/\/+$/, ''); } function buildUrl(path: string, query?: Record) { const url = `${normalizeBaseUrl(appEnv.apiBaseUrl)}${path.startsWith('/') ? path : `/${path}`}`; const params = Object.entries(query || {}) .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); return params.length ? `${url}?${params.join('&')}` : url; } export async function apiRequest( path: string, options: { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; query?: Record; body?: unknown; tenantId?: string | null; token?: string | null; authMode?: ApiAuthMode; headers?: Record; } = {}, ): Promise { await ensureRuntimeConfigLoaded(); const tenant = getTenantContext(); const session = getSession(); const hasTenantOverride = Object.prototype.hasOwnProperty.call(options, 'tenantId'); const hasTokenOverride = Object.prototype.hasOwnProperty.call(options, 'token'); const tenantId = hasTenantOverride ? options.tenantId : tenant?.tenantId ?? null; const authMode = options.authMode || 'auto'; const token = await resolveApiAuthorization({ authMode, hasTokenOverride, explicitToken: options.token, legacyToken: session?.token, }); const headers = buildApiHeaders({ tenantId, token, extraHeaders: options.headers }); const response = await Taro.request({ url: buildUrl(path, options.query), method: options.method || 'GET', data: options.body === undefined ? undefined : options.body, header: headers, }); const payload = (response.data || {}) as Record; if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode === 401) clearSession(); throw new ApiError({ status: response.statusCode, code: String(payload.code || 'API_ERROR'), message: String(payload.message || '请求失败'), details: payload, }); } return payload as T; } export async function resolveTenant(input: { host?: string; tenantCode?: string } = {}) { const payload = await apiRequest<{ item?: TenantContext; tenant?: { id?: string; tenantId?: string; slug?: string; }; branding?: TenantContext['branding']; features?: TenantContext['features']; adminFeatures?: TenantContext['adminFeatures']; publicConfig?: TenantContext['publicConfig']; }>('/api/tenant/resolve', { query: { host: input.host, tenantCode: input.tenantCode || appEnv.tenantCode, }, tenantId: null, authMode: 'none', }); const tenantId = payload.item?.tenantId || payload.tenant?.tenantId || payload.tenant?.id; if (!tenantId) throw new ApiError({ status: 500, code: 'TENANT_RESOLVE_INVALID', message: '租户解析结果缺少 tenantId' }); const context: TenantContext = { tenantId, tenantSlug: payload.item?.tenantSlug || payload.tenant?.slug, host: input.host, branding: payload.item?.branding || payload.branding || {}, features: payload.item?.features || payload.features || {}, adminFeatures: payload.item?.adminFeatures || payload.adminFeatures || {}, publicConfig: payload.item?.publicConfig || payload.publicConfig || {}, }; saveTenantContext(context); return context; }