import axios, { AxiosError, AxiosRequestConfig } from 'axios'; const TOKEN_KEY = 'olons_web_session'; const DEVICE_KEY = 'olons_web_device'; const LOGIN_NOTICE_PENDING_KEY = 'olons_login_notice_pending'; const reasonMessages: Record = { AUTH_REQUIRED: '登录已失效,请重新登录', FORBIDDEN: '当前账户无权执行此操作', INVALID_CREDENTIALS: '手机号或密码错误', ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册', REFERRAL_CODE_INVALID: '邀请码无效,请检查后重新输入', RATE_LIMITED: '操作过于频繁,请稍后再试', TASK_DISABLED: '领取功能暂未开放', OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内', INSUFFICIENT_BALANCE: '可用余额不足299元,暂时无法领取', DAILY_LIMIT_REACHED: '今日领取次数已用完', PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理', NO_TASK_AVAILABLE: '当前暂无可领取任务', ORDER_EXPIRED: '该任务已超时', ORDER_STATE_INVALID: '当前任务状态不允许此操作', TRADE_PASSWORD_NOT_SET: '请先设置支付密码', TRADE_PASSWORD_INVALID: '支付密码不正确', PAYMENT_ACCOUNT_FIELDS_INVALID: '绑卡信息填写不完整或格式不正确', QR_ATTACHMENT_INVALID: '二维码图片无效,请重新上传', IMAGE_INVALID: '图片格式或大小不符合要求', PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用', WITHDRAWAL_DISABLED: '取出功能暂未开放', WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则', WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完', WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道', RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则', RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道', RECHARGE_VOUCHER_INVALID: '储入凭证无效', SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试', CUSTOMER_SERVICE_NOT_FOUND: '客服已下线或不存在', CUSTOMER_SERVICE_NOT_BOUND: '客服账号尚未配置', IM_RELATION_PENDING: '好友申请已发送,请等待客服确认', IM_RELATION_NOT_ALLOWED: '当前无法与该客服建立会话', IM_SERVICE_UNAVAILABLE: '客服系统暂不可用,请稍后重试', INTERNAL: '服务异常,请稍后重试', }; export interface ApiEnvelope { code: number; data: T; message?: string; reason?: string; request_id?: string; server_time?: string; } export class MobileApiError extends Error { reason: string; status: number; constructor(message: string, reason = '', status = 0) { super(message); this.reason = reason; this.status = status; } } const client = axios.create({ baseURL: '', timeout: 15000, headers: { Accept: 'application/json' }, }); client.interceptors.request.use((config) => { const token = sessionStorage.getItem(TOKEN_KEY); if (token) config.headers.Authorization = `Bearer ${token}`; config.headers['X-Request-ID'] = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`; config.headers['X-Device-ID'] = getDeviceID(); return config; }); client.interceptors.response.use( (response) => response, (error: AxiosError>) => { const status = error.response?.status || 0; const payload = error.response?.data; if (status === 401) { clearSession(); const currentPath = window.location.hash.replace(/^#/, '') || '/app/home'; if (!currentPath.startsWith('/app/auth')) { window.location.hash = `/app/auth?redirect=${encodeURIComponent(currentPath)}`; } } const reason = payload?.reason || ''; return Promise.reject( new MobileApiError( reasonMessages[reason] || payload?.message || (status ? '请求失败,请稍后重试' : '网络连接失败'), reason, status ) ); } ); export async function request(config: AxiosRequestConfig): Promise { const method = String(config.method || 'GET').toUpperCase(); const attempts = method === 'GET' ? 3 : 1; let lastError: unknown; for (let attempt = 0; attempt < attempts; attempt += 1) { try { const response = await client.request>(config); if (response.data.code !== 0) { const reason = response.data.reason || ''; throw new MobileApiError( reasonMessages[reason] || response.data.message || '请求失败', reason, response.status ); } return response.data.data; } catch (error) { lastError = error; if (!shouldRetry(error, attempt, attempts)) throw error; await new Promise((resolve) => window.setTimeout(resolve, attempt === 0 ? 250 : 600)); } } throw lastError; } function shouldRetry(error: unknown, attempt: number, attempts: number) { if (attempt >= attempts - 1) return false; if (axios.isCancel(error)) return false; return error instanceof MobileApiError && (error.status === 0 || error.status === 503); } function getDeviceID() { let value = localStorage.getItem(DEVICE_KEY); if (!value) { value = crypto.randomUUID?.() || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`; localStorage.setItem(DEVICE_KEY, value); } return value; } export function deviceID() { return getDeviceID(); } export function saveSession(token: string) { sessionStorage.setItem(TOKEN_KEY, token); sessionStorage.setItem(LOGIN_NOTICE_PENDING_KEY, '1'); } export function clearSession() { sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(LOGIN_NOTICE_PENDING_KEY); } export function hasSession() { return Boolean(sessionStorage.getItem(TOKEN_KEY)); } export function shouldShowLoginNotice() { return sessionStorage.getItem(LOGIN_NOTICE_PENDING_KEY) !== '0'; } export function completeLoginNotice() { sessionStorage.setItem(LOGIN_NOTICE_PENDING_KEY, '0'); } export function idempotencyKey() { return crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; } export function money(value?: number, currency = 'CNY') { const amount = Number.isFinite(value) ? Number(value) / 100 : 0; return new Intl.NumberFormat('zh-CN', { style: 'currency', currency, minimumFractionDigits: 2, }).format(amount); } export function dateTime(value?: string) { if (!value) return '--'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '--'; return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }) .format(date) .replaceAll('/', '-'); } export function safeImage(url?: string) { if (!url) return ''; try { const parsed = new URL(url, window.location.origin); return parsed.protocol === 'https:' || parsed.origin === window.location.origin ? parsed.href : ''; } catch { return ''; } } export function errorText(error: unknown) { return error instanceof Error ? error.message : '操作失败,请稍后重试'; }