2026-07-30 13:55:28 +08:00
|
|
|
import {
|
|
|
|
|
API_BASE_URL,
|
|
|
|
|
type ApiResponse,
|
|
|
|
|
CLIENT_ID,
|
|
|
|
|
CLIENT_SECRET,
|
|
|
|
|
} from "@/lib/api"
|
2026-08-13 18:28:20 +08:00
|
|
|
import { clearSession, getUserToken } from "./token"
|
2026-07-30 13:55:28 +08:00
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
type CallMode = "public" | "device" | "user"
|
2026-07-30 13:55:28 +08:00
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
async function request<R = undefined>(
|
2026-07-30 13:55:28 +08:00
|
|
|
endpoint: string,
|
2026-08-13 18:28:20 +08:00
|
|
|
init: {
|
|
|
|
|
method: "GET" | "POST"
|
|
|
|
|
body?: string
|
|
|
|
|
auth?: string
|
|
|
|
|
},
|
|
|
|
|
): Promise<ApiResponse<R>> {
|
2026-07-30 13:55:28 +08:00
|
|
|
try {
|
2026-08-13 18:28:20 +08:00
|
|
|
const headers: HeadersInit = {}
|
|
|
|
|
if (init.body !== undefined) {
|
|
|
|
|
headers["Content-Type"] = "application/json"
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
2026-08-13 18:28:20 +08:00
|
|
|
if (init.auth) headers.Authorization = init.auth
|
2026-07-30 13:55:28 +08:00
|
|
|
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
2026-08-13 18:28:20 +08:00
|
|
|
method: init.method,
|
2026-07-30 13:55:28 +08:00
|
|
|
headers,
|
2026-08-13 18:28:20 +08:00
|
|
|
body: init.body,
|
2026-07-30 13:55:28 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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 || "请求失败",
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-13 18:28:20 +08:00
|
|
|
return { success: true, data: undefined as R }
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentType.includes("application/json")) {
|
2026-08-13 18:28:20 +08:00
|
|
|
const json = (await response.json()) as R & {
|
|
|
|
|
message?: string
|
|
|
|
|
error_description?: string
|
|
|
|
|
Code?: number
|
|
|
|
|
Message?: string
|
|
|
|
|
Data?: unknown
|
|
|
|
|
data?: unknown
|
|
|
|
|
}
|
2026-07-30 13:55:28 +08:00
|
|
|
if (!response.ok) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
status: response.status,
|
|
|
|
|
message: json.message || json.error_description || "请求失败",
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-13 18:28:20 +08:00
|
|
|
|
|
|
|
|
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 || "请求失败",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 13:55:28 +08:00
|
|
|
return { success: true, data: json }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(`无法解析响应数据: ${contentType}`)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("后端请求异常:", e)
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
status: 500,
|
|
|
|
|
message: (e as Error).message || "网络错误",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
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>(
|
2026-07-30 13:55:28 +08:00
|
|
|
endpoint: string,
|
2026-08-13 18:28:20 +08:00
|
|
|
run: () => Promise<ApiResponse<R>>,
|
2026-07-30 13:55:28 +08:00
|
|
|
): Promise<ApiResponse<R>> {
|
2026-08-13 18:28:20 +08:00
|
|
|
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
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
async function call<R = undefined>(
|
|
|
|
|
mode: CallMode,
|
2026-07-30 13:55:28 +08:00
|
|
|
endpoint: string,
|
|
|
|
|
data?: unknown,
|
2026-08-13 18:28:20 +08:00
|
|
|
method: "GET" | "POST" = "POST",
|
2026-07-30 13:55:28 +08:00
|
|
|
): Promise<ApiResponse<R>> {
|
2026-08-13 18:28:20 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
2026-08-13 18:28:20 +08:00
|
|
|
return resp
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
if (method === "GET" && data === undefined) {
|
|
|
|
|
return dedupGet<R>(endpoint, run)
|
|
|
|
|
}
|
|
|
|
|
return run()
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
async function callPublic<R = undefined>(
|
2026-07-30 13:55:28 +08:00
|
|
|
endpoint: string,
|
|
|
|
|
data?: unknown,
|
2026-08-13 18:28:20 +08:00
|
|
|
method: "GET" | "POST" = "POST",
|
2026-07-30 13:55:28 +08:00
|
|
|
): Promise<ApiResponse<R>> {
|
2026-08-13 18:28:20 +08:00
|
|
|
return call<R>("public", endpoint, data, method)
|
|
|
|
|
}
|
2026-07-30 13:55:28 +08:00
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
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)
|
2026-07-30 13:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { callByDevice, callByUser, callPublic }
|