forked from wangziqi/gongxue-base
144 lines
4.8 KiB
TypeScript
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;
|
|
}
|