export const BRAND = { red: '#A02D30', yellow: '#FFC107', bg: '#f5f5f5', text: '#333', muted: '#999', }; export const apiBase = '/api/v1'; const CLIENT_APP = 'USER_H5'; export type UserProfile = { id: string; userNo: string; phone: string | null; phoneVerified: boolean; nickname: string | null; avatarUrl: string | null; hasWechat: boolean; }; export type SessionPayload = { accessToken: string; refreshToken: string; deviceKey?: string; phoneVerified: boolean; user?: UserProfile; }; const DEVICE_KEY = 'deviceKey'; const ACCESS_TOKEN = 'accessToken'; const REFRESH_TOKEN = 'refreshToken'; export function getDeviceKey() { return localStorage.getItem(DEVICE_KEY); } export function saveSession(data: SessionPayload) { localStorage.setItem(ACCESS_TOKEN, data.accessToken); localStorage.setItem(REFRESH_TOKEN, data.refreshToken); if (data.deviceKey) localStorage.setItem(DEVICE_KEY, data.deviceKey); } export function saveAuth(data: { accessToken: string; refreshToken?: string; deviceKey?: string }) { localStorage.setItem(ACCESS_TOKEN, data.accessToken); if (data.refreshToken) localStorage.setItem(REFRESH_TOKEN, data.refreshToken); if (data.deviceKey) localStorage.setItem(DEVICE_KEY, data.deviceKey); } export function clearAuth() { localStorage.removeItem(ACCESS_TOKEN); localStorage.removeItem(REFRESH_TOKEN); } export function isLoggedIn() { return !!localStorage.getItem(ACCESS_TOKEN); } const AUTH_RECOVERY_EXEMPT_PATHS = ['/auth/session/bootstrap', '/auth/token/refresh']; async function recoverSession(): Promise { const refreshed = await refreshSession(); if (refreshed) return refreshed; clearAuth(); return bootstrapSession(); } async function rawRequest( path: string, options: RequestInit = {}, token?: string | null, ): Promise { const headers: Record = { 'Content-Type': 'application/json', 'X-Client-App': CLIENT_APP, ...(options.headers as Record), }; const authToken = token ?? localStorage.getItem(ACCESS_TOKEN); if (authToken) headers.Authorization = `Bearer ${authToken}`; const res = await fetch(`${apiBase}${path}`, { ...options, headers }); const json = await res.json(); if (json.code !== 0) { const err = new Error(json.message || '请求失败') as Error & { status?: number }; err.status = json.code; throw err; } return json.data as T; } async function requestWithAuthRetry( path: string, options: RequestInit = {}, retried = false, ): Promise { try { return await rawRequest(path, options); } catch (e) { const err = e as Error & { status?: number }; const canRecover = err.status === 401 && !retried && !AUTH_RECOVERY_EXEMPT_PATHS.some((p) => path.startsWith(p)); if (!canRecover) throw e; await recoverSession(); return requestWithAuthRetry(path, options, true); } } export async function request( _clientApp: string, path: string, options: RequestInit = {}, ): Promise { if (!isLoggedIn() && !AUTH_RECOVERY_EXEMPT_PATHS.some((p) => path.startsWith(p))) { await bootstrapSession(); } return requestWithAuthRetry(path, options); } export async function bootstrapSession(): Promise { const deviceKey = getDeviceKey(); const data = await rawRequest( '/auth/session/bootstrap', { method: 'POST', body: JSON.stringify(deviceKey ? { deviceKey } : {}), }, null, ); saveSession(data); return data; } export async function refreshSession(): Promise { const refreshToken = localStorage.getItem(REFRESH_TOKEN); if (!refreshToken) return null; try { const data = await rawRequest( '/auth/token/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }), }, null, ); saveSession(data); return data; } catch { return null; } } export async function ensureSession(): Promise { if (isLoggedIn()) { try { const me = await rawRequest('/auth/me'); return { accessToken: localStorage.getItem(ACCESS_TOKEN) ?? '', refreshToken: localStorage.getItem(REFRESH_TOKEN) ?? '', deviceKey: getDeviceKey() ?? undefined, phoneVerified: !!me.phoneVerified, user: me, }; } catch (e) { const err = e as Error & { status?: number }; if (err.status === 401) { clearAuth(); } else { const refreshed = await refreshSession(); if (refreshed) return refreshed; } } } return bootstrapSession(); } export async function bindPhone(phone: string, code: string): Promise { if (!isLoggedIn()) { await bootstrapSession(); } const data = await requestWithAuthRetry('/auth/phone/bind', { method: 'POST', body: JSON.stringify({ phone, code }), }); saveSession(data); return data; }