import { buildApiUrl } from './apiBase' import { clearAuthSession } from './authSession' const SESSION_EXPIRED_TOAST = 'Session expired. Please sign in again.' const LOGIN_PATH = '/pages/login/login' let sessionExpiredHandling = false /** 已触发登出跳转时抛出,调用方可忽略二次 Toast */ export class UsAppSessionExpiredError extends Error { constructor(message = 'Unauthorized') { super(message) this.name = 'UsAppSessionExpiredError' } } export function isUsAppSessionExpiredError(e: unknown): e is UsAppSessionExpiredError { return e instanceof UsAppSessionExpiredError } function handleSessionExpiredAndGoLogin(): void { if (sessionExpiredHandling) return sessionExpiredHandling = true clearAuthSession() uni.showToast({ title: SESSION_EXPIRED_TOAST, icon: 'none', duration: 2500, }) setTimeout(() => { sessionExpiredHandling = false uni.reLaunch({ url: LOGIN_PATH }) }, 400) } /** * 取出真实业务负载:支持 ABP `result`、以及统一包装 `{ succeeded, data }` */ export function unwrapApiPayload(data: unknown): T { if (data == null || typeof data !== 'object') return data as T const o = data as Record if ('result' in o && o.result !== undefined) { return o.result as T } const payload = o.data ?? o.Data if (payload !== undefined && payload !== null) { return payload as T } return data as T } export function parseApiErrorMessage(data: unknown): string { if (data == null) return 'Request failed' if (typeof data === 'string') return data if (typeof data === 'object') { const o = data as Record const errorsRaw = o.errors ?? o.Errors if (typeof errorsRaw === 'string' && errorsRaw.trim()) return errorsRaw.trim() if (Array.isArray(errorsRaw)) { const parts = errorsRaw.map((x) => String(x)).filter(Boolean) if (parts.length) return parts.join('; ') } const err = o.error as Record | undefined if (err && typeof err.message === 'string') return err.message if (typeof o.message === 'string') return o.message if (typeof o.error_description === 'string') return o.error_description } return 'Request failed' } function isBusinessFailurePayload(data: unknown): boolean { if (data == null || typeof data !== 'object') return false const o = data as Record if (o.succeeded === false || o.Succeeded === false) return true const inner = o.statusCode ?? o.StatusCode if (typeof inner === 'number' && inner >= 400) return true return false } /** HTTP 200 但 JSON 内 statusCode 为 401(少数网关/包装) */ function isBodyUnauthorized(data: unknown): boolean { if (data == null || typeof data !== 'object') return false const o = data as Record const inner = o.statusCode ?? o.StatusCode return inner === 401 } export type UsAppApiRequestOptions = { path: string method: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: unknown auth?: boolean /** 为 true 时收到 401 不清理会话、不跳转(用于登录等匿名接口) */ skipUnauthorizedRedirect?: boolean } /** * 美国版 App 统一请求:401 时 Toast(英文)+ 清会话 + 回登录页 */ export function usAppApiRequest(options: UsAppApiRequestOptions): Promise { const header: Record = { 'Content-Type': 'application/json', Accept: 'application/json', } if (options.auth) { const token = uni.getStorageSync('access_token') if (token) header.Authorization = `Bearer ${token}` } const skipRedirect = !!options.skipUnauthorizedRedirect return new Promise((resolve, reject) => { uni.request({ url: buildApiUrl(options.path), method: options.method, data: options.data, header, success: (res) => { const status = res.statusCode ?? 0 if (status === 401) { if (!skipRedirect) { handleSessionExpiredAndGoLogin() reject(new UsAppSessionExpiredError(parseApiErrorMessage(res.data) || 'Unauthorized')) } else { reject(new Error(parseApiErrorMessage(res.data) || 'Unauthorized')) } return } if (status >= 400) { reject(new Error(parseApiErrorMessage(res.data))) return } if (!skipRedirect && isBodyUnauthorized(res.data)) { handleSessionExpiredAndGoLogin() reject(new UsAppSessionExpiredError(parseApiErrorMessage(res.data) || 'Unauthorized')) return } if (isBusinessFailurePayload(res.data)) { reject(new Error(parseApiErrorMessage(res.data))) return } try { const body = unwrapApiPayload(res.data as unknown) resolve(body) } catch { reject(new Error('Invalid response')) } }, fail: (err) => { reject(new Error(err.errMsg || 'Network error')) }, }) }) }