| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- /**
- * 网络请求封装(对接 jeesharp 后端:统一响应 AjaxResult { code, msg, data })
- *
- * 鉴权策略:
- * - ERP 小程序为强制登录应用,所有业务接口均带 token 三件套
- * (access_token + Authorization: Bearer + satoken,与管理端 axios 拦截器一致)。
- * - code:200 成功;code:201 或 HTTP 401 → token 失效,清态跳登录页;
- * code:205 → 无该功能权限,友好 toast。
- */
- import { apiConfig } from '@/config/api'
- import { getAccessToken, getSsoToken, clearAuthState } from './tokenStorage'
- import { getDeviceId, getAppType, getMobileClientHeaders } from './device'
- import type { ApiResponse } from '@/types'
- import {
- NeedLoginError,
- isNeedLoginError,
- promptReLogin,
- shouldSuppressAuthErrorToast
- } from './needLoginError'
- // 请求配置
- export interface RequestConfig {
- url: string
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
- data?: any
- params?: any
- header?: Record<string, string>
- showLoading?: boolean
- loadingText?: string
- showErrorToast?: boolean
- /** 跳过 token 注入(用于免登录接口:getRsaPublicKey / login/captcha / login) */
- skipAuth?: boolean
- }
- // 请求拦截器
- function requestInterceptor(config: RequestConfig): RequestConfig {
- // 添加 Token(与管理端 axios 拦截器一致:access_token + Authorization + satoken)
- const token = config.skipAuth ? null : getAccessToken()
- if (token) {
- const satoken = getSsoToken()
- config.header = {
- ...config.header,
- access_token: token,
- Authorization: `Bearer ${token}`,
- ...(satoken ? { satoken } : {})
- }
- }
- // 添加设备信息(与 upload 共用 X-Platform / X-Version,便于后端与网关识别)
- config.header = {
- ...config.header,
- ...getMobileClientHeaders(),
- 'X-Device-Id': getDeviceId(),
- 'X-App-Type': getAppType(),
- 'Content-Type': 'application/json'
- }
- return config
- }
- /** 无权限错误:业务 catch 中可识别做降级展示 */
- export class NoPermissionError extends Error {
- readonly noPermission = true
- constructor(message?: string) {
- super(message || '无该功能权限')
- this.name = 'NoPermissionError'
- }
- }
- export function isNoPermissionError(error: unknown): boolean {
- return Boolean(
- error && typeof error === 'object' && (error as NoPermissionError).noPermission === true
- )
- }
- // 响应拦截器
- function responseInterceptor<T = any>(
- response: UniApp.RequestSuccessCallbackResult,
- skipAuth = false
- ): ApiResponse<T> {
- // HTTP 401 兜底(jeesharp 正常以业务码 201 表达 token 失效,此处双通道处理)
- if (response.statusCode === 401) {
- if (!skipAuth) {
- clearAuthState()
- promptReLogin()
- }
- throw new NeedLoginError()
- }
- const res = response.data as ApiResponse<T>
- // Token 过期或无效
- if (res.code === 201) {
- if (!skipAuth) {
- clearAuthState()
- promptReLogin()
- }
- throw new NeedLoginError(res.msg)
- }
- // 无权限(jeesharp 业务码 205)
- if (res.code === 205) {
- throw new NoPermissionError(res.msg)
- }
- // 其他错误
- if (res.code !== 200) {
- throw new Error(res.msg || '请求失败')
- }
- return res
- }
- function sleep(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms))
- }
- /**
- * 请求方法
- * - GET 幂等请求失败(网络层错误)自动重试 apiConfig.retryCount 次
- * - 写操作(POST/PUT/DELETE)带提交锁:同 url+payload 300ms 内重复调用直接返回进行中 Promise,防连点重复提交
- */
- const pendingWrites = new Map<string, Promise<any>>()
- function writeLockKey(config: RequestConfig): string {
- return `${config.method}:${config.url}:${JSON.stringify(config.data ?? {})}`
- }
- export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
- const method = (config.method || 'GET').toUpperCase()
- // 写操作提交锁
- if (method !== 'GET') {
- const key = writeLockKey(config)
- const pending = pendingWrites.get(key)
- if (pending) return pending
- const p = doRequest<T>(config).finally(() => {
- // 稍延迟释放,覆盖连点场景
- setTimeout(() => pendingWrites.delete(key), 300)
- })
- pendingWrites.set(key, p)
- return p
- }
- return doRequestWithRetry<T>(config)
- }
- async function doRequestWithRetry<T>(config: RequestConfig): Promise<ApiResponse<T>> {
- let lastError: unknown = null
- for (let attempt = 0; attempt <= apiConfig.retryCount; attempt++) {
- try {
- return await doRequest<T>(config)
- } catch (error) {
- lastError = error
- // 业务错误(含 201/205/400 系)不重试,仅网络层失败重试
- if (!(error instanceof Error && /^网络请求失败|request:fail|timeout/i.test(error.message))) {
- throw error
- }
- if (attempt < apiConfig.retryCount) {
- await sleep(apiConfig.retryDelay)
- }
- }
- }
- throw lastError
- }
- function doRequest<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
- return new Promise((resolve, reject) => {
- // 请求拦截
- const finalConfig = requestInterceptor({
- ...apiConfig,
- ...config
- })
- // 显示加载提示
- if (finalConfig.showLoading) {
- uni.showLoading({
- title: finalConfig.loadingText || '加载中...',
- mask: true
- })
- }
- // 发送请求
- const method = (finalConfig.method || 'GET').toUpperCase()
- const requestData = method === 'GET' ? (finalConfig.params ?? finalConfig.data) : finalConfig.data
- uni.request({
- url: finalConfig.url.startsWith('http')
- ? finalConfig.url
- : `${apiConfig.baseURL}${finalConfig.url}`,
- method: finalConfig.method || 'GET',
- data: requestData,
- header: finalConfig.header,
- timeout: apiConfig.timeout,
- success: (res) => {
- if (finalConfig.showLoading) {
- uni.hideLoading()
- }
- try {
- const result = responseInterceptor<T>(res, finalConfig.skipAuth)
- resolve(result)
- } catch (error) {
- if (
- finalConfig.showErrorToast &&
- !isNeedLoginError(error) &&
- !shouldSuppressAuthErrorToast(error)
- ) {
- uni.showToast({
- title: (error as Error).message || '请求失败',
- icon: 'none'
- })
- }
- reject(error)
- }
- },
- fail: (error) => {
- if (finalConfig.showLoading) {
- uni.hideLoading()
- }
- const errorMsg = error.errMsg || '网络请求失败'
- if (finalConfig.showErrorToast) {
- uni.showToast({
- title: errorMsg,
- icon: 'none'
- })
- }
- reject(new Error(errorMsg))
- }
- })
- })
- }
- /**
- * GET 请求
- */
- export function get<T = any>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
- return request<T>({
- url,
- method: 'GET',
- params,
- ...config
- })
- }
- /**
- * POST 请求
- */
- export function post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
- return request<T>({
- url,
- method: 'POST',
- data,
- ...config
- })
- }
- /**
- * PUT 请求
- */
- export function put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
- return request<T>({
- url,
- method: 'PUT',
- data,
- ...config
- })
- }
- /**
- * DELETE 请求
- */
- export function del<T = any>(url: string, config?: Partial<RequestConfig>): Promise<ApiResponse<T>> {
- return request<T>({
- url,
- method: 'DELETE',
- ...config
- })
- }
- export default request
|