2026-09-20 10:52:10 +08:00
|
|
|
/** Shared request, configuration and session infrastructure. */
|
2026-09-22 17:25:52 +08:00
|
|
|
import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts'
|
2026-09-24 11:50:07 +08:00
|
|
|
import { clearAccountCache } from './cache-lifecycle.uts'
|
|
|
|
|
import { clearAssessmentFlow } from './assessment-flow.uts'
|
2026-09-22 17:25:52 +08:00
|
|
|
export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts'
|
2026-09-11 20:23:10 +08:00
|
|
|
const REQUEST_TIMEOUT = 10000
|
|
|
|
|
const SESSION_KEY = 'aiFinanceSession'
|
2026-09-10 10:57:40 +08:00
|
|
|
|
2026-10-04 08:29:53 +08:00
|
|
|
export class ApiRequestError extends Error {
|
|
|
|
|
status: number
|
|
|
|
|
errorCode: string
|
|
|
|
|
constructor(message: string, status: number, errorCode: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.status = status
|
|
|
|
|
this.errorCode = errorCode
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 17:25:52 +08:00
|
|
|
const wait = (duration: number) => new Promise<void>((resolve) => { setTimeout(() => resolve(), duration) })
|
2026-09-11 20:23:10 +08:00
|
|
|
|
2026-09-22 17:25:52 +08:00
|
|
|
export function saveSession(token: string, phone: string, userId: string, expiresAt: string): void {
|
|
|
|
|
if (token.length == 0 || userId.length == 0 || expiresAt.length == 0) throw new Error('登录响应不完整')
|
|
|
|
|
const expiry = Date.parse(expiresAt)
|
|
|
|
|
if (isNaN(expiry) || expiry <= Date.now()) throw new Error('登录凭证已过期')
|
2026-09-24 11:50:07 +08:00
|
|
|
const previous = getSession()
|
|
|
|
|
if (previous != null && previous['userId'] != userId) clearAccountCache(previous['userId'] as string)
|
2026-09-22 17:25:52 +08:00
|
|
|
uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() })
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getSession(): UTSJSONObject | null {
|
2026-09-22 17:25:52 +08:00
|
|
|
const value = uni.getStorageSync(SESSION_KEY)
|
|
|
|
|
if (value == null || typeof value != 'object') return null
|
|
|
|
|
const session = value as UTSJSONObject
|
|
|
|
|
const token = session['token'] as string | null
|
|
|
|
|
const userId = session['userId'] as string | null
|
|
|
|
|
const expiresAt = session['expiresAt'] as string | null
|
|
|
|
|
if (token == null || userId == null || expiresAt == null || token.length == 0 || userId.length == 0 || token.startsWith('demo-')) {
|
2026-09-24 11:50:07 +08:00
|
|
|
clearSession()
|
2026-09-22 17:25:52 +08:00
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const expiry = Date.parse(expiresAt)
|
2026-09-24 11:50:07 +08:00
|
|
|
if (isNaN(expiry) || expiry <= Date.now()) { clearSession(); return null }
|
2026-09-22 17:25:52 +08:00
|
|
|
return session
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 17:25:52 +08:00
|
|
|
export function isLoggedIn(): boolean { return getSession() != null }
|
2026-09-24 11:50:07 +08:00
|
|
|
export function clearSession(): void {
|
|
|
|
|
clearAssessmentFlow()
|
|
|
|
|
const value = uni.getStorageSync(SESSION_KEY)
|
|
|
|
|
if (value != null && typeof value == 'object') {
|
|
|
|
|
const userId = (value as UTSJSONObject)['userId'] as string | null
|
|
|
|
|
if (userId != null) clearAccountCache(userId)
|
|
|
|
|
}
|
|
|
|
|
uni.removeStorageSync(SESSION_KEY)
|
|
|
|
|
}
|
2026-09-22 17:25:52 +08:00
|
|
|
const currentToken = (): string => { const session = getSession(); return session == null ? '' : session['token'] as string }
|
2026-09-11 20:23:10 +08:00
|
|
|
|
2026-09-15 16:16:55 +08:00
|
|
|
const apiMessage = (error: any | null, fallback: string): string => {
|
2026-09-22 17:25:52 +08:00
|
|
|
if (error == null) return fallback
|
|
|
|
|
if (error instanceof Error) return error.message
|
|
|
|
|
const text = error.toString()
|
|
|
|
|
return text.length > 0 ? text : fallback
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject): Promise<UTSJSONObject> => {
|
2026-09-22 17:25:52 +08:00
|
|
|
return new Promise<UTSJSONObject>((resolve, reject) => {
|
|
|
|
|
if (API_BASE_URL.length == 0 || (APP_ENVIRONMENT == 'production' && !API_BASE_URL.startsWith('https://'))) {
|
|
|
|
|
reject(new Error('服务地址尚未配置')); return
|
|
|
|
|
}
|
|
|
|
|
const sentToken = currentToken()
|
|
|
|
|
const header: UTSJSONObject = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
|
|
if (sentToken.length > 0) header['Authorization'] = 'Bearer ' + sentToken
|
|
|
|
|
uni.request({
|
|
|
|
|
url: API_BASE_URL + path, method, data: payload, header, timeout: REQUEST_TIMEOUT,
|
|
|
|
|
success: (response) => {
|
|
|
|
|
// 旧请求不得覆盖刚登录的另一账户,也不能因旧401清除新会话。
|
|
|
|
|
if (currentToken() != sentToken) { reject(new Error('登录状态已变化,请重新操作')); return }
|
|
|
|
|
if (response.statusCode == 401 && sentToken.length > 0) clearSession()
|
|
|
|
|
if (response.data == null || typeof response.data != 'object') {
|
|
|
|
|
reject(new Error('服务响应格式错误')); return
|
|
|
|
|
}
|
|
|
|
|
const body = response.data as UTSJSONObject
|
|
|
|
|
const code = body['code'] as number | null
|
|
|
|
|
const message = body['msg'] as string | null
|
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300 || code != 1) {
|
|
|
|
|
if (code == 401 && sentToken.length > 0) clearSession()
|
2026-10-04 08:29:53 +08:00
|
|
|
const details = body['data']
|
|
|
|
|
const errorCode = details != null && typeof details == 'object' ? (details as UTSJSONObject).getString('errorCode') ?? '' : ''
|
|
|
|
|
reject(new ApiRequestError(message != null && message.length > 0 ? message : '请求失败,请稍后重试', response.statusCode, errorCode)); return
|
2026-09-22 17:25:52 +08:00
|
|
|
}
|
|
|
|
|
const data = body['data']
|
|
|
|
|
if (data == null) resolve(new UTSJSONObject())
|
|
|
|
|
else if (typeof data == 'object') resolve(data as UTSJSONObject)
|
|
|
|
|
else reject(new Error('服务响应数据格式错误'))
|
|
|
|
|
},
|
|
|
|
|
fail: () => { reject(new Error('网络连接失败,请稍后重试')) }
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
2026-09-10 10:57:40 +08:00
|
|
|
|
2026-09-20 10:52:10 +08:00
|
|
|
export { apiMessage, requestJson, wait }
|
2026-09-24 11:50:07 +08:00
|
|
|
|
|
|
|
|
export function uploadJson(path: string, filePath: string): Promise<UTSJSONObject> {
|
|
|
|
|
return new Promise<UTSJSONObject>((resolve, reject) => {
|
|
|
|
|
const sentToken = currentToken()
|
|
|
|
|
if (sentToken.length == 0) { reject(new Error('请先登录')); return }
|
|
|
|
|
if (API_BASE_URL.length == 0 || (APP_ENVIRONMENT == 'production' && !API_BASE_URL.startsWith('https://'))) {
|
|
|
|
|
reject(new Error('服务地址尚未配置')); return
|
|
|
|
|
}
|
|
|
|
|
uni.uploadFile({ url: API_BASE_URL + path, filePath, name: 'file', timeout: REQUEST_TIMEOUT,
|
|
|
|
|
header: { Authorization: 'Bearer ' + sentToken, 'X-Requested-With': 'XMLHttpRequest' },
|
|
|
|
|
success: (response) => {
|
|
|
|
|
if (currentToken() != sentToken) { reject(new Error('登录状态已变化,请重新操作')); return }
|
|
|
|
|
if (response.statusCode == 401) clearSession()
|
|
|
|
|
try {
|
|
|
|
|
const body = JSON.parse<UTSJSONObject>(response.data)
|
|
|
|
|
if (body == null) throw new Error('上传响应格式错误')
|
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300 || body['code'] != 1) {
|
|
|
|
|
if (body['code'] == 401) clearSession()
|
|
|
|
|
throw new Error((body['msg'] as string | null) ?? '头像上传失败')
|
|
|
|
|
}
|
|
|
|
|
const data = body['data'] as UTSJSONObject | null
|
|
|
|
|
if (data == null) throw new Error('上传响应不完整')
|
|
|
|
|
resolve(data)
|
|
|
|
|
} catch (error) { reject(error) }
|
|
|
|
|
}, fail: () => { reject(new Error('头像上传失败,请检查网络后重试')) }
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|