|
@@ -9,7 +9,7 @@ interface RequestConfig {
|
|
|
timeout?: number
|
|
timeout?: number
|
|
|
headers?: Record<string, string>
|
|
headers?: Record<string, string>
|
|
|
signal?: AbortSignal
|
|
signal?: AbortSignal
|
|
|
- // Next.js 缓存选项(仅服务端有效)
|
|
|
|
|
|
|
+ // Next.js 缓存选项
|
|
|
cache?: "force-cache" | "no-store" | "no-cache" | "default"
|
|
cache?: "force-cache" | "no-store" | "no-cache" | "default"
|
|
|
next?: {
|
|
next?: {
|
|
|
revalidate?: number | false
|
|
revalidate?: number | false
|
|
@@ -17,10 +17,8 @@ interface RequestConfig {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 扩展 GET/DELETE 请求的配置(增加数组参数格式化)
|
|
|
|
|
-interface GetDeleteRequestConfig extends RequestConfig {
|
|
|
|
|
- arrayFormat?: "indices" | "repeat" | "comma"
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 定义可能的错误类型
|
|
|
|
|
+// type HttpClientError = Error
|
|
|
|
|
|
|
|
// 定义可能的错误类型
|
|
// 定义可能的错误类型
|
|
|
type HttpClientError = Error & {
|
|
type HttpClientError = Error & {
|
|
@@ -28,11 +26,10 @@ type HttpClientError = Error & {
|
|
|
originalError?: Error // 新增:保留原始错误
|
|
originalError?: Error // 新增:保留原始错误
|
|
|
isClientError?: boolean // 新增:标记是否为客户端错误
|
|
isClientError?: boolean // 新增:标记是否为客户端错误
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
class ServerHttpClient {
|
|
class ServerHttpClient {
|
|
|
- private readonly baseURL: string
|
|
|
|
|
- private readonly defaultTimeout: number
|
|
|
|
|
- private readonly defaultHeaders: Record<string, string>
|
|
|
|
|
|
|
+ private baseURL: string
|
|
|
|
|
+ private defaultTimeout: number
|
|
|
|
|
+ private defaultHeaders: Record<string, string>
|
|
|
|
|
|
|
|
constructor() {
|
|
constructor() {
|
|
|
this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"
|
|
this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"
|
|
@@ -107,35 +104,12 @@ class ServerHttpClient {
|
|
|
return responseData
|
|
return responseData
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
clearTimeout(timeoutId) // 确保超时器被清理
|
|
clearTimeout(timeoutId) // 确保超时器被清理
|
|
|
- throw this.handleError(error as Error, isClientRequest)
|
|
|
|
|
|
|
+ throw this.handleError(error as Error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 服务端 GET 请求方法
|
|
|
|
|
- */
|
|
|
|
|
- async serverGet<TResponse = any, TParams extends Record<string, any> | null = null>(
|
|
|
|
|
- url: string,
|
|
|
|
|
- params?: TParams,
|
|
|
|
|
- config?: GetDeleteRequestConfig,
|
|
|
|
|
- ): Promise<ApiResponse<TResponse>> {
|
|
|
|
|
- return this.request<TResponse>("GET", url, { params, config })
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 服务端 POST 请求方法
|
|
|
|
|
- */
|
|
|
|
|
- async serverPost<TResponse = any, TData extends Record<string, any> | null = null>(
|
|
|
|
|
- url: string,
|
|
|
|
|
- data?: TData,
|
|
|
|
|
- config?: RequestConfig,
|
|
|
|
|
- ): Promise<ApiResponse<TResponse>> {
|
|
|
|
|
- return this.request<TResponse>("POST", url, { data, config })
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 客户端 POST 请求方法(新增)
|
|
* 客户端 POST 请求方法(新增)
|
|
|
- * 适配浏览器环境,自动移除服务端缓存配置,优化错误提示
|
|
|
|
|
*/
|
|
*/
|
|
|
async clientPost<TResponse = any, TData extends Record<string, any> | null = null>(
|
|
async clientPost<TResponse = any, TData extends Record<string, any> | null = null>(
|
|
|
url: string,
|
|
url: string,
|
|
@@ -149,115 +123,213 @@ class ServerHttpClient {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 服务端 PUT 请求方法
|
|
|
|
|
- */
|
|
|
|
|
- async serverPut<TResponse = any, TData extends Record<string, any> | null = null>(
|
|
|
|
|
|
|
+ async serverGet<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
|
|
+ url: string,
|
|
|
|
|
+ params?: TParams,
|
|
|
|
|
+ config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
|
|
|
+ ): Promise<ApiResponse<TResponse>> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "GET",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return data
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ throw this.handleError(error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
data?: TData,
|
|
data?: TData,
|
|
|
config?: RequestConfig,
|
|
config?: RequestConfig,
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
- return this.request<TResponse>("PUT", url, { data, config })
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: data ? JSON.stringify(data) : undefined,
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ throw this.handleError(error)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 服务端 DELETE 请求方法
|
|
|
|
|
- */
|
|
|
|
|
- async serverDelete<TResponse = any, TParams extends Record<string, any> | null = null>(
|
|
|
|
|
|
|
+ async serverPut<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
|
|
+ url: string,
|
|
|
|
|
+ data?: TData,
|
|
|
|
|
+ config?: RequestConfig,
|
|
|
|
|
+ ): Promise<ApiResponse<TResponse>> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "PUT",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: data ? JSON.stringify(data) : undefined,
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ throw this.handleError(error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async serverDelete<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
params?: TParams,
|
|
params?: TParams,
|
|
|
- config?: GetDeleteRequestConfig,
|
|
|
|
|
|
|
+ config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
- return this.request<TResponse>("DELETE", url, { params, config })
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "DELETE",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ throw this.handleError(error)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 构建带参数的 URL
|
|
|
|
|
- */
|
|
|
|
|
private buildUrl(
|
|
private buildUrl(
|
|
|
url: string,
|
|
url: string,
|
|
|
params?: Record<string, any> | null,
|
|
params?: Record<string, any> | null,
|
|
|
arrayFormat: "indices" | "repeat" | "comma" = "indices",
|
|
arrayFormat: "indices" | "repeat" | "comma" = "indices",
|
|
|
): string {
|
|
): string {
|
|
|
- if (!params) return `${this.baseURL}${url}`
|
|
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
|
|
|
- const searchParams = new URLSearchParams()
|
|
|
|
|
|
|
+ if (!params) {
|
|
|
|
|
+ return fullUrl
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- for (const [key, value] of Object.entries(params)) {
|
|
|
|
|
- if (value === undefined || value === null) continue
|
|
|
|
|
|
|
+ const searchParams = new URLSearchParams()
|
|
|
|
|
|
|
|
|
|
+ for (const key in params) {
|
|
|
|
|
+ const value = params[key]
|
|
|
if (Array.isArray(value)) {
|
|
if (Array.isArray(value)) {
|
|
|
switch (arrayFormat) {
|
|
switch (arrayFormat) {
|
|
|
case "repeat":
|
|
case "repeat":
|
|
|
- value.forEach(v => searchParams.append(key, String(v)))
|
|
|
|
|
|
|
+ value.forEach((v) => searchParams.append(key, String(v)))
|
|
|
break
|
|
break
|
|
|
case "comma":
|
|
case "comma":
|
|
|
- searchParams.set(key, value.map(String).join(","))
|
|
|
|
|
|
|
+ searchParams.set(key, value.map((v) => String(v)).join(","))
|
|
|
break
|
|
break
|
|
|
case "indices":
|
|
case "indices":
|
|
|
default:
|
|
default:
|
|
|
value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v)))
|
|
value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v)))
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
|
|
+ } else if (value !== undefined && value !== null) {
|
|
|
searchParams.set(key, String(value))
|
|
searchParams.set(key, String(value))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const queryString = searchParams.toString()
|
|
const queryString = searchParams.toString()
|
|
|
- return queryString ? `${this.baseURL}${url}?${queryString}` : `${this.baseURL}${url}`
|
|
|
|
|
|
|
+ return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 统一错误处理(区分客户端/服务端错误)
|
|
|
|
|
- */
|
|
|
|
|
- private handleError(error: Error, isClientRequest = false): HttpClientError {
|
|
|
|
|
- const httpError = error as HttpClientError
|
|
|
|
|
- httpError.originalError = { ...error } // 保留原始错误信息
|
|
|
|
|
- httpError.isClientError = isClientRequest // 标记客户端错误
|
|
|
|
|
-
|
|
|
|
|
|
|
+ private handleError(error: any): HttpClientError {
|
|
|
if (error.name === "AbortError") {
|
|
if (error.name === "AbortError") {
|
|
|
- httpError.message = isClientRequest
|
|
|
|
|
- ? "请求超时,请检查网络或稍后重试" // 客户端更友好的提示
|
|
|
|
|
- : "请求超时"
|
|
|
|
|
- httpError.statusCode = 408
|
|
|
|
|
- } else if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
|
|
|
- httpError.message = isClientRequest
|
|
|
|
|
- ? "网络连接失败,请检查您的网络设置" // 客户端更友好的提示
|
|
|
|
|
- : "网络连接失败"
|
|
|
|
|
- httpError.statusCode = 0
|
|
|
|
|
- } else if (!httpError.statusCode) {
|
|
|
|
|
- httpError.message = httpError.message || (isClientRequest
|
|
|
|
|
- ? "请求失败,请联系客服"
|
|
|
|
|
- : "未知错误,请联系管理员")
|
|
|
|
|
- httpError.statusCode = 500
|
|
|
|
|
|
|
+ return new Error("请求超时")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return httpError
|
|
|
|
|
|
|
+ if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
|
|
|
+ return new Error("网络连接失败")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return error instanceof Error ? error : new Error("未知错误")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 获取客户端配置信息
|
|
|
|
|
- */
|
|
|
|
|
getInstance() {
|
|
getInstance() {
|
|
|
return {
|
|
return {
|
|
|
baseURL: this.baseURL,
|
|
baseURL: this.baseURL,
|
|
|
timeout: this.defaultTimeout,
|
|
timeout: this.defaultTimeout,
|
|
|
- headers: { ...this.defaultHeaders }, // 返回拷贝,避免外部修改内部配置
|
|
|
|
|
|
|
+ headers: this.defaultHeaders,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 创建服务端 HTTP 客户端实例(单例)
|
|
|
|
|
|
|
+// 创建服务端 HTTP 客户端实例
|
|
|
const serverHttpClient = new ServerHttpClient()
|
|
const serverHttpClient = new ServerHttpClient()
|
|
|
|
|
|
|
|
-// 导出请求方法(绑定实例上下文)
|
|
|
|
|
export const serverGet = serverHttpClient.serverGet.bind(serverHttpClient)
|
|
export const serverGet = serverHttpClient.serverGet.bind(serverHttpClient)
|
|
|
export const serverPost = serverHttpClient.serverPost.bind(serverHttpClient)
|
|
export const serverPost = serverHttpClient.serverPost.bind(serverHttpClient)
|
|
|
-export const clientPost = serverHttpClient.clientPost.bind(serverHttpClient) // 导出新增的clientPost
|
|
|
|
|
export const serverPut = serverHttpClient.serverPut.bind(serverHttpClient)
|
|
export const serverPut = serverHttpClient.serverPut.bind(serverHttpClient)
|
|
|
export const serverDelete = serverHttpClient.serverDelete.bind(serverHttpClient)
|
|
export const serverDelete = serverHttpClient.serverDelete.bind(serverHttpClient)
|
|
|
|
|
+export const clientPost = serverHttpClient.clientPost.bind(serverHttpClient) // 导出新增的clientPost
|
|
|
|
|
|
|
|
-// 导出类型和实例
|
|
|
|
|
-export type { ApiResponse, RequestConfig, GetDeleteRequestConfig, HttpClientError }
|
|
|
|
|
|
|
+export type { ApiResponse, RequestConfig, HttpClientError }
|
|
|
export { serverHttpClient }
|
|
export { serverHttpClient }
|