import Taro from '@tarojs/taro'; import { ClientApp } from '@dukang/shared-types'; import { forceReloadAfterAccountMerge } from './auth-nav'; import { resetStoresSessionBootstrap } from './stores-session'; import { resetHomeCatalogBootstrap } from './home-catalog-session'; import { reportClientValidationError } from './client-error'; function resolveApiBase(): string { const origin = typeof TARO_APP_API_ORIGIN !== 'undefined' && TARO_APP_API_ORIGIN ? TARO_APP_API_ORIGIN : process.env.TARO_ENV === 'h5' ? 'http://localhost:3010' : ''; if (origin) { return `${origin.replace(/\/$/, '')}/api/v1`; } return '/api/v1'; } export const API_BASE = resolveApiBase(); const TOKEN_KEY = 'user_access_token'; const REFRESH_KEY = 'user_refresh_token'; /** H5 产物走公众号体系(USER_H5);小程序原生走 USER_MINI。勿混用,否则 JSAPI 会出现 appid 与 openid 不匹配 */ export const CLIENT_APP = process.env.TARO_ENV === 'h5' ? ClientApp.USER_H5 : ClientApp.USER_MINI; export function getToken(): string { try { return Taro.getStorageSync(TOKEN_KEY) || ''; } catch { return ''; } } export function getRefreshToken(): string { try { return Taro.getStorageSync(REFRESH_KEY) || ''; } catch { return ''; } } export function saveAuth(data: { accessToken: string; refreshToken?: string }) { Taro.setStorageSync(TOKEN_KEY, data.accessToken); if (data.refreshToken) { Taro.setStorageSync(REFRESH_KEY, data.refreshToken); } } export function clearAuth() { Taro.removeStorageSync(TOKEN_KEY); Taro.removeStorageSync(REFRESH_KEY); } export function isLoggedIn(): boolean { return !!getToken(); } export function logout() { clearAuth(); // 主动退出才重置门店/首页「当次登录」会话;401 清 token 不要打断筛选 resetStoresSessionBootstrap(); resetHomeCatalogBootstrap(); Taro.reLaunch({ url: '/pages/home/index' }); } type ReqOptions = { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; data?: Record | unknown; auth?: boolean; }; function parseBody(data: unknown): { code?: number; message?: string } { if (data && typeof data === 'object') { return data as { code?: number; message?: string }; } return {}; } /** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */ export async function request(path: string, options: ReqOptions = {}): Promise { const header: Record = { 'Content-Type': 'application/json', 'X-Client-App': CLIENT_APP, }; const token = getToken(); if (token) header.Authorization = `Bearer ${token}`; const res = await Taro.request({ url: `${API_BASE}${path}`, method: options.method ?? 'GET', data: options.data as Record, header, }); const status = res.statusCode; const body = parseBody(res.data); if (status === 401 || body?.code === 401) { // 仅清掉「发起本请求时」仍在使用的 token,避免登录页旧 /auth/me 竞态清掉刚写入的新 token const stillCurrent = !!token && getToken() === token; if (stillCurrent) { clearAuth(); const mergedMsg = body?.message || ''; if (/账号已合并/.test(mergedMsg)) { // 合并后旧 JWT 失效:强制刷新,不引导「重新登录」 forceReloadAfterAccountMerge(); } } throw new Error(body?.message || '登录已过期,请重新登录'); } if (status === 404 && body.code === undefined) { throw new Error('接口不可达,请确认 API 服务已启动'); } if (status >= 400 || body.code !== 0) { if (status === 400 || body?.code === 400) { reportClientValidationError({ message: body?.message || `请求失败(${status})`, apiPath: path, }); } throw new Error(body?.message || `请求失败(${status})`); } return (res.data as { data: T }).data; } export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none') { Taro.showToast({ title, icon, duration: 1800 }); } export type SessionPayload = { accessToken: string; refreshToken?: string; phoneVerified?: boolean; accountMerged?: boolean; }; export type UserProfile = { id: string; phone?: string | null; nickname?: string | null; avatarUrl?: string | null; phoneVerified?: boolean; hasWechat?: boolean; };