/** * API工具类 - 统一封装后端API请求 */ import type { ApiResponse, TotalAssetsResponse, GetPortfoliosResponse, PortfolioDetailResponse, CreatePortfolioRequest, CreatePortfolioResponse, UpdatePortfolioRequest, GetTransactionsResponse, CreateTransactionRequest, CreateTransactionResponse, NavHistoryResponse, BackfillNavResponse, StrategyListResponse, StrategyDetailResponse, CreateStrategyRequest, UpdateStrategyRequest, StrategyResponse, DeleteStrategyResponse, StrategySignalResponse, UserInfoResponse, UserStatsResponse, UpdateUserRequest, UpdateUserResponse, } from '@/types'; // API基础URL const BASE_URL: string = import.meta.env.VITE_API_BASE_URL || 'https://localhost:7040/'; // 请求超时时间 const TIMEOUT = 10000; // 登录锁,防止并发登录 let loginLock: Promise | null = null; // 全局loading计数 let loadingCount = 0; const showLoading = (): void => { if (loadingCount === 0) { uni.showLoading({ title: '加载中...', mask: true }); } loadingCount++; }; const hideLoading = (): void => { loadingCount--; if (loadingCount <= 0) { loadingCount = 0; uni.hideLoading(); } }; /** * 获取微信登录码 */ const getWechatCode = (): Promise => { return new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: (res) => { if (res.code) { resolve(res.code); } else { reject(new Error(`微信登录码获取失败: ${res.errMsg}`)); } }, fail: (err) => { reject(new Error(`微信登录失败: ${err.errMsg}`)); } }); }); }; /** * 执行微信登录 */ const doWechatLogin = async (): Promise> => { const code = await getWechatCode(); const res = await new Promise((resolve, reject) => { uni.request({ url: `${BASE_URL}api/auth/wechat-login`, method: 'POST', data: { code }, header: { 'Content-Type': 'application/json' }, timeout: TIMEOUT, success: resolve, fail: reject }); }); if (res.statusCode === 200 && res.data?.code === 200 && res.data?.data?.token) { uni.setStorageSync('token', res.data.data.token); return res.data; } else { throw new Error(res.data?.message || '微信登录失败'); } }; /** * 带重试机制的请求方法 */ const requestWithRetry = async ( url: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data: Record = {}, headers: Record = {}, retryCount: number = 0 ): Promise => { try { let token = uni.getStorageSync('token') as string | undefined; // 如果没有token,先执行微信登录 if (!token) { if (loginLock) { await loginLock; token = uni.getStorageSync('token'); } else { loginLock = doWechatLogin(); await loginLock; loginLock = null; token = uni.getStorageSync('token'); } } // 处理URL拼接 let fullUrl: string; if (BASE_URL.endsWith('/') && url.startsWith('/')) { fullUrl = BASE_URL + url.substring(1); } else if (!BASE_URL.endsWith('/') && !url.startsWith('/')) { fullUrl = BASE_URL + '/' + url; } else { fullUrl = BASE_URL + url; } showLoading(); const res = await new Promise((resolve, reject) => { uni.request({ url: fullUrl, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', ...headers }, timeout: TIMEOUT, success: resolve, fail: reject }); }); hideLoading(); if (res.statusCode === 200) { return res.data as T; } else if (res.statusCode === 401) { uni.removeStorageSync('token'); if (retryCount < 3) { if (loginLock) { await loginLock; } else { loginLock = doWechatLogin(); await loginLock; loginLock = null; } return requestWithRetry(url, method, data, headers, retryCount + 1); } else { uni.showToast({ title: '系统异常,请稍后重试', icon: 'none' }); throw new Error('登录已过期,重试次数超限'); } } else { throw new Error(`请求失败: ${res.statusCode}`); } } catch (error: any) { hideLoading(); if (retryCount < 3 && error.message?.includes('网络请求失败')) { return requestWithRetry(url, method, data, headers, retryCount + 1); } if (retryCount >= 2) { uni.showToast({ title: '系统异常,请稍后重试', icon: 'none', duration: 2000 }); } throw error; } }; /** * 基础请求方法 */ const request = ( url: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data: Record = {}, headers: Record = {} ): Promise => { return requestWithRetry(url, method, data, headers); }; /** * GET请求 */ export const get = ( url: string, params: Record = {}, headers: Record = {} ): Promise => { const queryString = Object.keys(params) .map(key => `${key}=${encodeURIComponent(params[key])}`) .join('&'); const fullUrl = queryString ? `${url}?${queryString}` : url; return request(fullUrl, 'GET', {}, headers); }; /** * POST请求 */ export const post = ( url: string, data: Record = {}, headers: Record = {} ): Promise => { return request(url, 'POST', data, headers); }; /** * PUT请求 */ export const put = ( url: string, data: Record = {}, headers: Record = {} ): Promise => { return request(url, 'PUT', data, headers); }; /** * DELETE请求 */ export const del = ( url: string, headers: Record = {} ): Promise => { return request(url, 'DELETE', {}, headers); }; /** * API接口封装 */ export const api = { assets: { getAssetData: (): Promise> => get('/api/v1/portfolio/assets'), getHoldings: (): Promise> => get('/api/v1/portfolio'), getPortfolio: (id: string | number): Promise> => get(`/api/v1/portfolio/${id}`), getTransactions: (params: { portfolioId?: string; limit: number; offset: number }): Promise> => get('/api/v1/portfolio/transactions', params), createTransaction: (data: CreateTransactionRequest): Promise> => post('/api/v1/portfolio/transactions', data), createPortfolio: (data: CreatePortfolioRequest): Promise> => post('/api/v1/portfolio', data), updatePortfolio: (id: string | number, data: UpdatePortfolioRequest): Promise> => put(`/api/v1/portfolio/${id}`, data), getPortfolioSignal: (id: string | number): Promise> => get(`/api/v1/portfolio/${id}/signal`), getNavHistory: (id: string | number, params?: { startDate?: string; endDate?: string; interval?: string }): Promise> => get(`/api/v1/portfolio/${id}/nav-history`, params), backfillNavHistory: (id: string | number, force: boolean = false): Promise> => post(`/api/v1/portfolio/${id}/nav-history/backfill`, { force }), }, strategies: { getStrategies: (): Promise> => get('/api/v1/strategy/strategies'), getStrategy: (id: string | number): Promise> => get(`/api/v1/strategy/${id}`), createStrategy: (data: CreateStrategyRequest): Promise> => post('/api/v1/strategy', data), updateStrategy: (id: string | number, data: UpdateStrategyRequest): Promise> => put(`/api/v1/strategy/${id}`, data), deleteStrategy: (id: string | number): Promise> => del(`/api/v1/strategy/${id}`), }, user: { getUserInfo: (): Promise> => get('/api/v1/user/info'), getUserStats: (): Promise> => get('/api/v1/user/stats'), updateUserInfo: (data: UpdateUserRequest): Promise> => put('/api/v1/user/info', data), }, ticker: { search: (keyword: string, limit: number = 20): Promise> => get('/api/v1/ticker/search', { keyword, limit }), } }; export default api;