Files
juip-web/src/api/base.ts
2026-08-13 18:28:20 +08:00

211 lines
5.3 KiB
TypeScript

import {
API_BASE_URL,
type ApiResponse,
CLIENT_ID,
CLIENT_SECRET,
} from "@/lib/api"
import { clearSession, getUserToken } from "./token"
type CallMode = "public" | "device" | "user"
async function request<R = undefined>(
endpoint: string,
init: {
method: "GET" | "POST"
body?: string
auth?: string
},
): Promise<ApiResponse<R>> {
try {
const headers: HeadersInit = {}
if (init.body !== undefined) {
headers["Content-Type"] = "application/json"
}
if (init.auth) headers.Authorization = init.auth
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: init.method,
headers,
body: init.body,
})
const contentType = response.headers.get("Content-Type") ?? "text/plain"
if (contentType.includes("text/plain")) {
const text = await response.text()
if (!response.ok) {
return {
success: false,
status: response.status,
message: text || "请求失败",
}
}
return { success: true, data: undefined as R }
}
if (contentType.includes("application/json")) {
const json = (await response.json()) as R & {
message?: string
error_description?: string
Code?: number
Message?: string
Data?: unknown
data?: unknown
}
if (!response.ok) {
return {
success: false,
status: response.status,
message: json.message || json.error_description || "请求失败",
}
}
if (typeof json.Code === "number") {
if (json.Code === 10000) {
return {
success: true,
data: (json.Data ?? json.data ?? undefined) as R,
}
}
return {
success: false,
message: json.Message || "请求失败",
}
}
return { success: true, data: json }
}
throw new Error(`无法解析响应数据: ${contentType}`)
} catch (e) {
console.error("后端请求异常:", e)
return {
success: false,
status: 500,
message: (e as Error).message || "网络错误",
}
}
}
let deviceToken: string | null = null
let deviceTokenExpire = 0
let deviceTokenPromise: Promise<string | null> | null = null
async function acquireDeviceToken(): Promise<string | null> {
if (deviceToken && Date.now() < deviceTokenExpire) return deviceToken
if (!deviceTokenPromise) {
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
deviceTokenPromise = request<{
access_token: string
expires_in: number
}>("/api/auth/token", {
method: "POST",
body: JSON.stringify({ grant_type: "client_credentials" }),
auth: `Basic ${basic}`,
})
.then(resp => {
if (!resp.success) {
deviceToken = null
return null
}
deviceToken = resp.data.access_token
deviceTokenExpire = Date.now() + (resp.data.expires_in - 60) * 1000
return resp.data.access_token
})
.finally(() => {
deviceTokenPromise = null
})
}
return deviceTokenPromise
}
const inflight = new Map<string, Promise<ApiResponse<unknown>>>()
function dedupGet<R>(
endpoint: string,
run: () => Promise<ApiResponse<R>>,
): Promise<ApiResponse<R>> {
const existing = inflight.get(endpoint)
if (existing) return existing as Promise<ApiResponse<R>>
const promise = run().finally(() => inflight.delete(endpoint))
inflight.set(endpoint, promise)
return promise
}
async function call<R = undefined>(
mode: CallMode,
endpoint: string,
data?: unknown,
method: "GET" | "POST" = "POST",
): Promise<ApiResponse<R>> {
const body = data === undefined ? undefined : JSON.stringify(data)
const run = async (): Promise<ApiResponse<R>> => {
let auth: string | undefined
if (mode === "user") {
const token = getUserToken()
if (!token) {
return {
success: false,
status: 401,
message: "未登录或会话已过期",
}
}
auth = `Bearer ${token}`
} else if (mode === "device") {
const token = await acquireDeviceToken()
if (!token) {
return {
success: false,
status: 401,
message: "设备凭证获取失败",
}
}
auth = `Bearer ${token}`
}
const resp = await request<R>(endpoint, { method, body, auth })
if (!resp.success && resp.status === 401) {
if (mode === "user") {
clearSession()
} else if (mode === "device") {
deviceToken = null
deviceTokenExpire = 0
}
}
return resp
}
if (method === "GET" && data === undefined) {
return dedupGet<R>(endpoint, run)
}
return run()
}
async function callPublic<R = undefined>(
endpoint: string,
data?: unknown,
method: "GET" | "POST" = "POST",
): Promise<ApiResponse<R>> {
return call<R>("public", endpoint, data, method)
}
async function callByDevice<R = undefined>(
endpoint: string,
data?: unknown,
method: "GET" | "POST" = "POST",
): Promise<ApiResponse<R>> {
return call<R>("device", endpoint, data, method)
}
async function callByUser<R = undefined>(
endpoint: string,
data?: unknown,
method: "GET" | "POST" = "POST",
): Promise<ApiResponse<R>> {
return call<R>("user", endpoint, data, method)
}
export { callByDevice, callByUser, callPublic }