SEC_Warehouse/src/http/client.ts

117 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Taro from '@tarojs/taro'
import { store } from '@/store'
import { clearAuth } from '@/store/authSlice'
import { ROUTES } from '@/constants/routes'
import { reLaunch } from '@/utils/navigation'
import { ApiError, BusinessError, AuthExpiredError } from './errors'
// 测试环境(内网,开发者工具需勾选「不校验合法域名」);正式域名为占位符,上线前必须替换(spec D2)
const BASE_URL = process.env.NODE_ENV === 'development'
? 'http://192.168.10.11:39101/api/biz'
: 'https://PROD_PLACEHOLDER/api/biz'
// 后端约定的未登录业务码(占位 '401',联调确认后更新——spec D5/Open Questions 1)
const UNAUTHORIZED_CODES = new Set(['401'])
// 防重入:跳转登录页进行中不重复触发(并发 401 只踢一次)
let redirectingToLogin = false
function redirectToLogin(tokenAtStart: string | null): void {
if (redirectingToLogin) return
// 仅当发起请求时的 token 仍是当前登录态才踢,避免在途旧请求 401 误清重新登录后的新会话
// (token 为 null 的未登录请求 401 时 null === null 成立会踢,属合理行为)
if (store.getState().auth.token !== tokenAtStart) return
redirectingToLogin = true
store.dispatch(clearAuth())
reLaunch(ROUTES.LOGIN)
.catch((e: unknown) => {
// 跳转失败显式记录,避免 unhandled rejection 噪音(debug-first:不静默吞)
console.error('[http] 踢回登录页失败', e)
})
.finally(() => {
redirectingToLogin = false
})
}
export interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
headers?: Record<string, string>
timeout?: number
needAuth?: boolean // 默认 true;登录接口自身传 false
}
// 后端统一响应格式(大写 key、字符串状态码)
export interface ApiEnvelope<T> {
Code: string
Message?: string
Content: T
RecordCount?: number
PageIndex?: number
PageSize?: number
}
export async function request<T>(
url: string,
params?: Record<string, unknown>,
config: RequestConfig = {}
): Promise<T> {
const method = config.method ?? 'GET'
const timeout = config.timeout ?? 10000
// 请求发起时刻的 token 快照:headers 注入与 401 陈旧竞态判断共用同一份
const tokenAtStart = store.getState().auth.token
// token 注入:从 authSlice 读取
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...config.headers
}
if (config.needAuth !== false) {
if (tokenAtStart) headers.Authorization = `Bearer ${tokenAtStart}`
}
let res: Taro.request.SuccessCallbackResult<ApiEnvelope<T>>
try {
res = await Taro.request({
url: BASE_URL + url,
method,
// Taro.request 的 GET 参数同样走 data,运行时自动序列化为 query string
data: params,
dataType: 'json',
timeout,
header: headers
})
} catch (e) {
throw new ApiError(e instanceof Error ? e.message : '网络异常')
}
if (res.statusCode !== 200) {
if (res.statusCode === 401) {
redirectToLogin(tokenAtStart)
throw new AuthExpiredError('登录已过期')
}
throw new ApiError(`HTTP ${res.statusCode}`, res.statusCode)
}
// 网关返回 200 + 非 envelope 形态(HTML 拦截页/null 等)属传输层异常,不能当业务错分流
const body = res.data as ApiEnvelope<T> | null | string
if (body === null || typeof body !== 'object' || body.Code == null) {
throw new ApiError('响应格式异常', res.statusCode)
}
const bizCode = String(body.Code)
if (UNAUTHORIZED_CODES.has(bizCode)) {
redirectToLogin(tokenAtStart)
throw new AuthExpiredError(body.Message || '登录已过期')
}
if (bizCode !== '200') {
throw new BusinessError(body.Message || '业务错误', bizCode)
}
return body.Content
}
request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
request<T>(url, params, { ...config, method: 'GET' })
request.post = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
request<T>(url, params, { ...config, method: 'POST' })