Blame view

泰额版/Food Labeling Management App UniApp/src/utils/usAppApiRequest.ts 4.89 KB
59e51671   “wangming”   1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
  import { buildApiUrl } from './apiBase'
  import { clearAuthSession } from './authSession'
  
  const SESSION_EXPIRED_TOAST = 'Session expired. Please sign in again.'
  const LOGIN_PATH = '/pages/login/login'
  
  let sessionExpiredHandling = false
  
  /** 已触发登出跳转时抛出,调用方可忽略二次 Toast */
  export class UsAppSessionExpiredError extends Error {
    constructor(message = 'Unauthorized') {
      super(message)
      this.name = 'UsAppSessionExpiredError'
    }
  }
  
  export function isUsAppSessionExpiredError(e: unknown): e is UsAppSessionExpiredError {
    return e instanceof UsAppSessionExpiredError
  }
  
  function handleSessionExpiredAndGoLogin(): void {
    if (sessionExpiredHandling) return
    sessionExpiredHandling = true
    clearAuthSession()
    uni.showToast({
      title: SESSION_EXPIRED_TOAST,
      icon: 'none',
      duration: 2500,
    })
    setTimeout(() => {
      sessionExpiredHandling = false
      uni.reLaunch({ url: LOGIN_PATH })
    }, 400)
  }
  
  /**
   * 取出真实业务负载:支持 ABP `result`、以及统一包装 `{ succeeded, data }`
   */
  export function unwrapApiPayload<T>(data: unknown): T {
    if (data == null || typeof data !== 'object') return data as T
    const o = data as Record<string, unknown>
    if ('result' in o && o.result !== undefined) {
      return o.result as T
    }
    const payload = o.data ?? o.Data
    if (payload !== undefined && payload !== null) {
      return payload as T
    }
    return data as T
  }
  
  export function parseApiErrorMessage(data: unknown): string {
    if (data == null) return 'Request failed'
    if (typeof data === 'string') return data
    if (typeof data === 'object') {
      const o = data as Record<string, unknown>
      const errorsRaw = o.errors ?? o.Errors
      if (typeof errorsRaw === 'string' && errorsRaw.trim()) return errorsRaw.trim()
      if (Array.isArray(errorsRaw)) {
        const parts = errorsRaw.map((x) => String(x)).filter(Boolean)
        if (parts.length) return parts.join('; ')
      }
      const err = o.error as Record<string, unknown> | undefined
      if (err && typeof err.message === 'string') return err.message
      if (typeof o.message === 'string') return o.message
      if (typeof o.error_description === 'string') return o.error_description
    }
    return 'Request failed'
  }
  
  function isBusinessFailurePayload(data: unknown): boolean {
    if (data == null || typeof data !== 'object') return false
    const o = data as Record<string, unknown>
    if (o.succeeded === false || o.Succeeded === false) return true
    const inner = o.statusCode ?? o.StatusCode
    if (typeof inner === 'number' && inner >= 400) return true
    return false
  }
  
  /** HTTP 200 但 JSON 内 statusCode 为 401(少数网关/包装) */
  function isBodyUnauthorized(data: unknown): boolean {
    if (data == null || typeof data !== 'object') return false
    const o = data as Record<string, unknown>
    const inner = o.statusCode ?? o.StatusCode
    return inner === 401
  }
  
  export type UsAppApiRequestOptions = {
    path: string
    method: 'GET' | 'POST' | 'PUT' | 'DELETE'
    data?: unknown
    auth?: boolean
    /** 为 true 时收到 401 不清理会话、不跳转(用于登录等匿名接口) */
    skipUnauthorizedRedirect?: boolean
  }
  
  /**
   * 美国版 App 统一请求:401 时 Toast(英文)+ 清会话 + 回登录页
   */
  export function usAppApiRequest<T>(options: UsAppApiRequestOptions): Promise<T> {
    const header: Record<string, string> = {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    }
    if (options.auth) {
      const token = uni.getStorageSync('access_token')
      if (token) header.Authorization = `Bearer ${token}`
    }
  
    const skipRedirect = !!options.skipUnauthorizedRedirect
  
    return new Promise((resolve, reject) => {
      uni.request({
        url: buildApiUrl(options.path),
        method: options.method,
        data: options.data,
        header,
        success: (res) => {
          const status = res.statusCode ?? 0
  
          if (status === 401) {
            if (!skipRedirect) {
              handleSessionExpiredAndGoLogin()
              reject(new UsAppSessionExpiredError(parseApiErrorMessage(res.data) || 'Unauthorized'))
            } else {
              reject(new Error(parseApiErrorMessage(res.data) || 'Unauthorized'))
            }
            return
          }
  
          if (status >= 400) {
            reject(new Error(parseApiErrorMessage(res.data)))
            return
          }
  
          if (!skipRedirect && isBodyUnauthorized(res.data)) {
            handleSessionExpiredAndGoLogin()
            reject(new UsAppSessionExpiredError(parseApiErrorMessage(res.data) || 'Unauthorized'))
            return
          }
  
          if (isBusinessFailurePayload(res.data)) {
            reject(new Error(parseApiErrorMessage(res.data)))
            return
          }
  
          try {
            const body = unwrapApiPayload<T>(res.data as unknown)
            resolve(body)
          } catch {
            reject(new Error('Invalid response'))
          }
        },
        fail: (err) => {
          reject(new Error(err.errMsg || 'Network error'))
        },
      })
    })
  }