44 * - 未设置:开发环境默认连仓库里的远程后端;生产环境默认同域(与上面空字符串一致)。
55 * 注意:不要用 `||`,否则空字符串会被当成假值而误用默认 IP。
66 */
7- import {
8- clearAuth ,
9- getStoredAccessToken ,
10- getStoredRefreshToken ,
11- hasUsableRefreshToken ,
12- isJwtExpired ,
13- isLikelyJwt ,
14- parseJwtPayload
15- } from '../auth/session'
7+ import { clearAuth , getStoredAccessToken } from '../auth/session'
168
179function resolveApiBase ( ) {
1810 const v = import . meta. env . VITE_API_BASE
@@ -24,107 +16,21 @@ function resolveApiBase() {
2416
2517export const API_BASE = resolveApiBase ( )
2618
19+ const baseUrl = API_BASE
20+
2721function buildUrl ( path ) {
2822 // 后端统一是 /api/v1 前缀
2923 if ( path . startsWith ( 'http://' ) || path . startsWith ( 'https://' ) ) return path
30- return `${ API_BASE } ${ path } `
31- }
32-
33- /** 并发刷新合并为同一 Promise */
34- let refreshInflight = null
35-
36- /**
37- * 使用 refreshToken 换发双 token(不经过 request,避免环)。
38- * 失败时会 clearAuth。
39- */
40- export function refreshSessionTokens ( ) {
41- if ( refreshInflight ) return refreshInflight
42-
43- refreshInflight = ( async ( ) => {
44- const rt = getStoredRefreshToken ( )
45- if ( ! rt || ! isLikelyJwt ( rt ) || isJwtExpired ( rt ) ) {
46- clearAuth ( )
47- const err = new Error ( '登录已失效' )
48- err . httpStatus = 401
49- throw err
50- }
51-
52- const res = await fetch ( buildUrl ( '/api/v1/auth/refresh' ) , {
53- method : 'POST' ,
54- headers : { 'Content-Type' : 'application/json' } ,
55- body : JSON . stringify ( { refreshToken : rt } )
56- } )
57-
58- const text = await res . text ( )
59- let json = null
60- try {
61- json = text ? JSON . parse ( text ) : null
62- } catch {
63- // ignore
64- }
65-
66- if ( ! res . ok || ( json && typeof json . code === 'number' && json . code !== 0 ) ) {
67- clearAuth ( )
68- const err = new Error ( json ?. message || '登录已失效' )
69- err . httpStatus = res . status || 401
70- err . code = json ?. code
71- throw err
72- }
73-
74- const data = json ?. data
75- if ( ! data ?. accessToken || ! data ?. refreshToken ) {
76- clearAuth ( )
77- const err = new Error ( '刷新返回异常' )
78- err . httpStatus = 401
79- throw err
80- }
81-
82- localStorage . setItem ( 'accessToken' , data . accessToken )
83- localStorage . setItem ( 'refreshToken' , data . refreshToken )
84- return data
85- } ) ( )
86-
87- return refreshInflight . finally ( ( ) => {
88- refreshInflight = null
89- } )
90- }
91-
92- function accessExpiresWithinSeconds ( seconds ) {
93- const t = getStoredAccessToken ( )
94- if ( ! t || ! isLikelyJwt ( t ) ) return true
95- const p = parseJwtPayload ( t )
96- if ( typeof p . exp !== 'number' ) return false
97- return p . exp * 1000 - Date . now ( ) < seconds * 1000
98- }
99-
100- function redirectConsoleLoginIfNeeded ( ) {
101- if ( typeof window !== 'undefined' && window . location . pathname . startsWith ( '/console' ) ) {
102- const redirect = window . location . pathname + window . location . search
103- window . location . assign ( '/console/login?redirect=' + encodeURIComponent ( redirect ) )
104- }
24+ return `${ baseUrl } ${ path } `
10525}
10626
10727export async function request ( path , options = { } ) {
108- const isRetry = Boolean ( options . _refreshRetried )
109- const extraHeaders = options . headers && typeof options . headers === 'object' ? options . headers : { }
11028 const headers = {
11129 'Content-Type' : 'application/json' ,
112- ...extraHeaders
30+ ...( options . headers || { } )
11331 }
11432
11533 if ( options . withAuth ) {
116- if (
117- ! isRetry &&
118- hasUsableRefreshToken ( ) &&
119- ( ! getStoredAccessToken ( ) || accessExpiresWithinSeconds ( 90 ) )
120- ) {
121- try {
122- await refreshSessionTokens ( )
123- } catch {
124- // 无 access 时交给后续分支;将过期 access 时可能仍失败,再由 401 重试
125- }
126- }
127-
12834 const token = getStoredAccessToken ( )
12935 if ( ! token ) {
13036 const err = new Error ( '未登录' )
@@ -134,46 +40,31 @@ export async function request(path, options = {}) {
13440 headers . Authorization = `Bearer ${ token } `
13541 }
13642
137- const fetchOpts = { ...options }
138- delete fetchOpts . withAuth
139- delete fetchOpts . _refreshRetried
140- delete fetchOpts . headers
141-
14243 const res = await fetch ( buildUrl ( path ) , {
143- ... fetchOpts ,
144- headers
44+ headers ,
45+ ... options
14546 } )
14647
14748 const text = await res . text ( )
14849 let json = null
149- if ( ! _skipJson ) {
150- try {
151- json = text ? JSON . parse ( text ) : null
152- } catch {
153- // ignore
154- }
50+ try {
51+ json = text ? JSON . parse ( text ) : null
52+ } catch {
53+ // ignore
15554 }
15655
157- if ( res . status === 401 && options . withAuth && ! isRetry ) {
158- try {
159- await refreshSessionTokens ( )
160- return request ( path , { ...options , _refreshRetried : true } )
161- } catch {
162- redirectConsoleLoginIfNeeded ( )
163- const err = new Error ( '登录已失效' )
164- err . httpStatus = 401
165- throw err
166- }
167- }
168-
169- if ( res . status === 401 && options . withAuth && isRetry ) {
56+ if ( res . status === 401 && options . withAuth ) {
17057 clearAuth ( )
171- redirectConsoleLoginIfNeeded ( )
58+ if ( typeof window !== 'undefined' && window . location . pathname . startsWith ( '/console' ) ) {
59+ const redirect = window . location . pathname + window . location . search
60+ window . location . assign ( '/console/login?redirect=' + encodeURIComponent ( redirect ) )
61+ }
17262 const err = new Error ( '登录已失效' )
17363 err . httpStatus = 401
17464 throw err
17565 }
17666
67+ // API 统一返回 ApiResponse,HTTP 层可能仍为 200
17768 if ( json && typeof json . code === 'number' && json . code !== 0 ) {
17869 const msg = json . message || '请求失败'
17970 const err = new Error ( msg )
@@ -192,3 +83,4 @@ export async function request(path, options = {}) {
19283
19384 return json ?. data ?? json
19485}
86+
0 commit comments