/** Shared request, configuration and session infrastructure. */ import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts' import { clearAccountCache } from './cache-lifecycle.uts' import { clearAssessmentFlow } from './assessment-flow.uts' export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts' const REQUEST_TIMEOUT = 10000 const SESSION_KEY = 'aiFinanceSession' const wait = (duration: number) => new Promise((resolve) => { setTimeout(() => resolve(), duration) }) 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('登录凭证已过期') const previous = getSession() if (previous != null && previous['userId'] != userId) clearAccountCache(previous['userId'] as string) uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() }) } export function getSession(): UTSJSONObject | null { 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-')) { clearSession() return null } const expiry = Date.parse(expiresAt) if (isNaN(expiry) || expiry <= Date.now()) { clearSession(); return null } return session } export function isLoggedIn(): boolean { return getSession() != null } 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) } const currentToken = (): string => { const session = getSession(); return session == null ? '' : session['token'] as string } const apiMessage = (error: any | null, fallback: string): string => { if (error == null) return fallback if (error instanceof Error) return error.message const text = error.toString() return text.length > 0 ? text : fallback } const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject): Promise => { return new Promise((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() reject(new Error(message != null && message.length > 0 ? message : '请求失败,请稍后重试')); return } 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('网络连接失败,请稍后重试')) } }) }) } export { apiMessage, requestJson, wait } export function uploadJson(path: string, filePath: string): Promise { return new Promise((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(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('头像上传失败,请检查网络后重试')) } }) }) }