usAppApiRequest.ts
4.89 KB
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'))
},
})
})
}