mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 21:12:00 +08:00
fix: 修复token过期未重新登录的问题 (#803)
* fix: 修复token过期未重新登录的问题 实现统一的401错误处理机制: - 创建httpClient封装fetch API,添加响应拦截器 - 401时自动清理localStorage和React状态 - 显示"请先登录"提示并延迟1.5秒后跳转登录页 - 保存当前URL到sessionStorage用于登录后返回 - 改造所有API调用使用httpClient统一处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix: 添加401处理的单例保护防止并发竞态 问题: - 多个API同时返回401会导致多个通知叠加 - 多个style元素被添加到DOM造成内存泄漏 - 可能触发多次登录页跳转 解决方案: - 添加静态标志位 isHandling401 防止重复处理 - 第一个401触发完整处理流程 - 后续401直接抛出错误,避免重复操作 - 确保只显示一次通知和一次跳转 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix: 修复isHandling401标志永不重置的问题 问题: - isHandling401标志在401处理后永不重置 - 导致用户重新登录后,后续401会被静默忽略 - 页面刷新或取消重定向后标志仍为true 解决方案: - 在HttpClient中添加reset401Flag()公开方法 - 登录成功后调用reset401Flag()重置标志 - 页面加载时调用reset401Flag()确保新会话正常 影响范围: - web/src/lib/httpClient.ts: 添加reset方法和导出函数 - web/src/contexts/AuthContext.tsx: 在登录和页面加载时重置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(auth): consume returnUrl after successful login (BLOCKING-1) 修复登录后未跳转回原页面的问题。 问题: - httpClient在401时保存returnUrl到sessionStorage - 但登录成功后没有读取和使用returnUrl - 导致用户登录后停留在登录页,无法回到原页面 修复: - 在loginAdmin、verifyOTP、completeRegistration三个登录方法中 - 添加returnUrl检查和跳转逻辑 - 登录成功后优先跳转到returnUrl,如果没有则使用默认页面 影响: - 用户token过期后重新登录,会自动返回之前访问的页面 - 提升用户体验,避免手动导航 测试场景: 1. 用户访问/traders → token过期 → 登录 → 自动回到/traders ✅ 2. 用户直接访问/login → 登录 → 跳转到默认页面(/dashboard或/traders) ✅ Related: BLOCKING-1 in PR #803 code review --------- Co-authored-by: sue <177699783@qq.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
CompetitionData,
|
||||
} from '../types'
|
||||
import { CryptoService } from './crypto'
|
||||
import { httpClient } from './httpClient'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
@@ -33,51 +34,51 @@ function getAuthHeaders(): Record<string, string> {
|
||||
export const api = {
|
||||
// AI交易员管理接口
|
||||
async getTraders(): Promise<TraderInfo[]> {
|
||||
const res = await fetch(`${API_BASE}/my-traders`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(`${API_BASE}/my-traders`, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取trader列表失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 获取公开的交易员列表(无需认证)
|
||||
async getPublicTraders(): Promise<any[]> {
|
||||
const res = await fetch(`${API_BASE}/traders`)
|
||||
const res = await httpClient.get(`${API_BASE}/traders`)
|
||||
if (!res.ok) throw new Error('获取公开trader列表失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||
const res = await fetch(`${API_BASE}/traders`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
const res = await httpClient.post(
|
||||
`${API_BASE}/traders`,
|
||||
request,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('创建交易员失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
async deleteTrader(traderId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.delete(
|
||||
`${API_BASE}/traders/${traderId}`,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('删除交易员失败')
|
||||
},
|
||||
|
||||
async startTrader(traderId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/start`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.post(
|
||||
`${API_BASE}/traders/${traderId}/start`,
|
||||
undefined,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('启动交易员失败')
|
||||
},
|
||||
|
||||
async stopTrader(traderId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.post(
|
||||
`${API_BASE}/traders/${traderId}/stop`,
|
||||
undefined,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('停止交易员失败')
|
||||
},
|
||||
|
||||
@@ -85,18 +86,19 @@ export const api = {
|
||||
traderId: string,
|
||||
customPrompt: string
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/prompt`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ custom_prompt: customPrompt }),
|
||||
})
|
||||
const res = await httpClient.put(
|
||||
`${API_BASE}/traders/${traderId}/prompt`,
|
||||
{ custom_prompt: customPrompt },
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('更新自定义策略失败')
|
||||
},
|
||||
|
||||
async getTraderConfig(traderId: string): Promise<any> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/config`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(
|
||||
`${API_BASE}/traders/${traderId}/config`,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('获取交易员配置失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -105,27 +107,25 @@ export const api = {
|
||||
traderId: string,
|
||||
request: CreateTraderRequest
|
||||
): Promise<TraderInfo> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
const res = await httpClient.put(
|
||||
`${API_BASE}/traders/${traderId}`,
|
||||
request,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('更新交易员失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// AI模型配置接口
|
||||
async getModelConfigs(): Promise<AIModel[]> {
|
||||
const res = await fetch(`${API_BASE}/models`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(`${API_BASE}/models`, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取模型配置失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 获取系统支持的AI模型列表(无需认证)
|
||||
async getSupportedModels(): Promise<AIModel[]> {
|
||||
const res = await fetch(`${API_BASE}/supported-models`)
|
||||
const res = await httpClient.get(`${API_BASE}/supported-models`)
|
||||
if (!res.ok) throw new Error('获取支持的模型失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -149,27 +149,25 @@ export const api = {
|
||||
)
|
||||
|
||||
// 发送加密数据
|
||||
const res = await fetch(`${API_BASE}/models`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(encryptedPayload),
|
||||
})
|
||||
const res = await httpClient.put(
|
||||
`${API_BASE}/models`,
|
||||
encryptedPayload,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('更新模型配置失败')
|
||||
},
|
||||
|
||||
|
||||
// 交易所配置接口
|
||||
async getExchangeConfigs(): Promise<Exchange[]> {
|
||||
const res = await fetch(`${API_BASE}/exchanges`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(`${API_BASE}/exchanges`, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取交易所配置失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 获取系统支持的交易所列表(无需认证)
|
||||
async getSupportedExchanges(): Promise<Exchange[]> {
|
||||
const res = await fetch(`${API_BASE}/supported-exchanges`)
|
||||
const res = await httpClient.get(`${API_BASE}/supported-exchanges`)
|
||||
if (!res.ok) throw new Error('获取支持的交易所失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -177,11 +175,11 @@ export const api = {
|
||||
async updateExchangeConfigs(
|
||||
request: UpdateExchangeConfigRequest
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/exchanges`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
const res = await httpClient.put(
|
||||
`${API_BASE}/exchanges`,
|
||||
request,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('更新交易所配置失败')
|
||||
},
|
||||
|
||||
@@ -207,11 +205,11 @@ export const api = {
|
||||
)
|
||||
|
||||
// 发送加密数据
|
||||
const res = await fetch(`${API_BASE}/exchanges`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(encryptedPayload),
|
||||
})
|
||||
const res = await httpClient.put(
|
||||
`${API_BASE}/exchanges`,
|
||||
encryptedPayload,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('更新交易所配置失败')
|
||||
},
|
||||
|
||||
@@ -220,9 +218,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/status?trader_id=${traderId}`
|
||||
: `${API_BASE}/status`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取系统状态失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -232,7 +228,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/account?trader_id=${traderId}`
|
||||
: `${API_BASE}/account`
|
||||
const res = await fetch(url, {
|
||||
const res = await httpClient.request(url, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
@@ -250,9 +246,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/positions?trader_id=${traderId}`
|
||||
: `${API_BASE}/positions`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取持仓列表失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -262,9 +256,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/decisions?trader_id=${traderId}`
|
||||
: `${API_BASE}/decisions`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取决策日志失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -274,9 +266,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/decisions/latest?trader_id=${traderId}`
|
||||
: `${API_BASE}/decisions/latest`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取最新决策失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -286,9 +276,7 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/statistics?trader_id=${traderId}`
|
||||
: `${API_BASE}/statistics`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取统计信息失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -298,21 +286,15 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
||||
: `${API_BASE}/equity-history`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取历史数据失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 批量获取多个交易员的历史数据(无需认证)
|
||||
async getEquityHistoryBatch(traderIds: string[]): Promise<any> {
|
||||
const res = await fetch(`${API_BASE}/equity-history-batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ trader_ids: traderIds }),
|
||||
const res = await httpClient.post(`${API_BASE}/equity-history-batch`, {
|
||||
trader_ids: traderIds,
|
||||
})
|
||||
if (!res.ok) throw new Error('获取批量历史数据失败')
|
||||
return res.json()
|
||||
@@ -320,14 +302,14 @@ export const api = {
|
||||
|
||||
// 获取前5名交易员数据(无需认证)
|
||||
async getTopTraders(): Promise<any[]> {
|
||||
const res = await fetch(`${API_BASE}/top-traders`)
|
||||
const res = await httpClient.get(`${API_BASE}/top-traders`)
|
||||
if (!res.ok) throw new Error('获取前5名交易员失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 获取公开交易员配置(无需认证)
|
||||
async getPublicTraderConfig(traderId: string): Promise<any> {
|
||||
const res = await fetch(`${API_BASE}/trader/${traderId}/config`)
|
||||
const res = await httpClient.get(`${API_BASE}/trader/${traderId}/config`)
|
||||
if (!res.ok) throw new Error('获取公开交易员配置失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -337,16 +319,14 @@ export const api = {
|
||||
const url = traderId
|
||||
? `${API_BASE}/performance?trader_id=${traderId}`
|
||||
: `${API_BASE}/performance`
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(url, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取AI学习数据失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
// 获取竞赛数据(无需认证)
|
||||
async getCompetition(): Promise<CompetitionData> {
|
||||
const res = await fetch(`${API_BASE}/competition`)
|
||||
const res = await httpClient.get(`${API_BASE}/competition`)
|
||||
if (!res.ok) throw new Error('获取竞赛数据失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -356,9 +336,10 @@ export const api = {
|
||||
coin_pool_url: string
|
||||
oi_top_url: string
|
||||
}> {
|
||||
const res = await fetch(`${API_BASE}/user/signal-sources`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(
|
||||
`${API_BASE}/user/signal-sources`,
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('获取用户信号源配置失败')
|
||||
return res.json()
|
||||
},
|
||||
@@ -367,14 +348,14 @@ export const api = {
|
||||
coinPoolUrl: string,
|
||||
oiTopUrl: string
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/user/signal-sources`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
const res = await httpClient.post(
|
||||
`${API_BASE}/user/signal-sources`,
|
||||
{
|
||||
coin_pool_url: coinPoolUrl,
|
||||
oi_top_url: oiTopUrl,
|
||||
}),
|
||||
})
|
||||
},
|
||||
getAuthHeaders()
|
||||
)
|
||||
if (!res.ok) throw new Error('保存用户信号源配置失败')
|
||||
},
|
||||
|
||||
@@ -383,9 +364,7 @@ export const api = {
|
||||
public_ip: string
|
||||
message: string
|
||||
}> {
|
||||
const res = await fetch(`${API_BASE}/server-ip`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
const res = await httpClient.get(`${API_BASE}/server-ip`, getAuthHeaders())
|
||||
if (!res.ok) throw new Error('获取服务器IP失败')
|
||||
return res.json()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user