Files
gongxue-base/apps/taro/src/services/api.ts
2026-06-30 01:36:04 +08:00

144 lines
4.8 KiB
TypeScript

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<TenantContext>(TENANT_KEY);
}
export function saveTenantContext(tenant: TenantContext) {
setStorage(TENANT_KEY, tenant);
}
export function clearTenantContext() {
removeStorage(TENANT_KEY);
}
export function getSession() {
return getStorage<ApiSession>(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<string, string | number | boolean | null | undefined>) {
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<T>(
path: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
query?: Record<string, string | number | boolean | null | undefined>;
body?: unknown;
tenantId?: string | null;
token?: string | null;
authMode?: ApiAuthMode;
headers?: Record<string, string>;
} = {},
): Promise<T> {
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<string, unknown>;
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;
}