8f83624ffe
Avoid conflict with another local service on 3000; align Vite proxies and docs.
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
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<string, unknown> | 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<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> {
|
|
const header: Record<string, string> = {
|
|
'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<string, unknown>,
|
|
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;
|
|
};
|