import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts'; // token 持久化 key const ACCESS_TOKEN_KEY = 'akreq_access_token'; const REFRESH_TOKEN_KEY = 'akreq_refresh_token'; const EXPIRES_AT_KEY = 'akreq_expires_at'; // 优化:用静态变量缓存 token,只有 set/clear 时同步 storage let _accessToken : string | null = null; let _refreshToken : string | null = null; let _expiresAt : number | null = null; export class AkReq { static setToken(token : string, refreshToken : string, expiresAt : number) { _accessToken = token; _refreshToken = refreshToken; _expiresAt = expiresAt; uni.setStorageSync(ACCESS_TOKEN_KEY, token); uni.setStorageSync(REFRESH_TOKEN_KEY, refreshToken); uni.setStorageSync(EXPIRES_AT_KEY, expiresAt); } static getToken() : string | null { if (_accessToken != null) return _accessToken; const t = uni.getStorageSync(ACCESS_TOKEN_KEY) as string | null; _accessToken = t; return t; } static getRefreshToken() : string | null { if (_refreshToken != null) return _refreshToken; const t = uni.getStorageSync(REFRESH_TOKEN_KEY) as string | null; _refreshToken = t; return t; } static getExpiresAt() : number | null { const val = _expiresAt; if (val != null) return val; const t = uni.getStorageSync(EXPIRES_AT_KEY) as number | null; _expiresAt = t; return t; } static clearToken() { _accessToken = null; _refreshToken = null; _expiresAt = null; uni.removeStorageSync(ACCESS_TOKEN_KEY); uni.removeStorageSync(REFRESH_TOKEN_KEY); uni.removeStorageSync(EXPIRES_AT_KEY); } // 判断 token 是否即将过期(提前5分钟刷新) static isTokenExpiring() : boolean { const expiresAt = this.getExpiresAt(); if (expiresAt === null || expiresAt == 0) { return true; } const now = Math.floor(Date.now() / 1000); return (expiresAt - now) < 300; // 提前5分钟刷新 } // 自动刷新 token,返回 true=已刷新,false=未刷新 static async refreshTokenIfNeeded(apikey ?: string) : Promise { // 没有 access_token 直接返回,不刷新 const accessToken = this.getToken(); if (accessToken === null || accessToken === "") { return false; } if (!this.isTokenExpiring()) { return false; } const refreshToken = this.getRefreshToken(); if (refreshToken === null || refreshToken === "") { this.clearToken(); return false; } // 构造 header,必须带 apikey let headers = {} as UTSJSONObject; if (apikey !== null && apikey !== "") { headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject; } try { const res = await this.request({ url: 'https://ak3.oulog.com/auth/v1/token?grant_type=refresh_token', method: 'POST', data: ({ refresh_token: refreshToken } as UTSJSONObject), headers: headers, contentType: 'application/json' }, true); // skipRefresh=true,避免递归 const data = res.data as UTSJSONObject | null; let accessToken : string | null = null; let refreshTokenNew : string | null = null; let expiresAt : number | null = null; if (data != null && typeof data.getString === 'function' && typeof data.getNumber === 'function') { accessToken = data.getString('access_token'); refreshTokenNew = data.getString('refresh_token'); expiresAt = data.getNumber('expires_at'); } if (accessToken !== null && refreshTokenNew !== null && expiresAt !== null) { this.setToken(accessToken, refreshTokenNew, expiresAt); return true; } else { this.clearToken(); return false; } } catch (e) { this.clearToken(); return false; } } // options: AkReqOptions, skipRefresh: boolean = false static async request(options : AkReqOptions, skipRefresh ?: boolean) : Promise> { // 自动刷新 token if (skipRefresh != true) { let apikey : string | null = null; const headersObj = options.headers; if (headersObj != null && typeof headersObj.getString === 'function') { apikey = headersObj.getString('apikey'); } await this.refreshTokenIfNeeded(apikey); } // 统一 header,自动带上 Authorization/Content-Type/Accept let headers = options.headers ?? ({} as UTSJSONObject); const token = this.getToken(); if (token != null && token != "") { headers = Object.assign({}, headers, { Authorization: `Bearer ${token}` }) as UTSJSONObject; } let contentType = options.contentType ?? ''; if (headers != null && typeof headers.getString === 'function') { const headerContentType = headers.getString('Content-Type'); if (headerContentType != null) { contentType = headerContentType; } } if (contentType != null && contentType != "") { headers = Object.assign({}, headers, { 'Content-Type': contentType }) as UTSJSONObject; } // 默认 Accept headers = Object.assign({ Accept: 'application/json' } as UTSJSONObject, headers) as UTSJSONObject; const timeout = options.timeout ?? 10000; const maxRetry = Math.max(0, options.retryCount ?? 0); const baseDelay = Math.max(0, options.retryDelayMs ?? 300); const doOnce = (): Promise> => { return new Promise>((resolve) => { uni.request({ url: options.url, method: options.method ?? 'GET', data: options.data, header: headers, timeout: timeout, success: (res) => { // HEAD 请求特殊处理:没有响应体,只有 headers if (options.method == 'HEAD') { const result = AkReq.createResponse( res.statusCode, [] as Array, res.header as UTSJSONObject ); resolve(result); return; } // 兼容 res.data 可能为 string 或 UTSJSONObject 或 UTSArray let data : UTSJSONObject | Array | null; if (typeof res.data == 'string') { const strData = res.data as string; if (strData.length > 0 && /[^\s]/.test(strData)) { try { data = JSON.parse(strData) as UTSJSONObject; } catch (e) { data = null; } } else { data = null; } } else if (Array.isArray(res.data)) { data = res.data as UTSJSONObject[]; } else { const objData = res.data as UTSJSONObject | null; data = objData; if (objData != null) { const accessToken = objData.getString('access_token'); const refreshTokenNew = objData.getString('refresh_token'); const expiresAt = objData.getNumber('expires_at'); if (accessToken !== null && refreshTokenNew !== null && expiresAt !== null) { AkReq.setToken(accessToken, refreshTokenNew, expiresAt); } } } const result = AkReq.createResponse( res.statusCode, data ?? {}, res.header as UTSJSONObject ); resolve(result); }, fail: (err) => { const result = AkReq.createResponse( err.errCode, err.data ?? {}, {} as UTSJSONObject, new UniError('uni-request', err.errCode, err.errMsg ?? 'request fail') ); resolve(result); } }); }); }; let attempt = 0; let lastRes: AkReqResponse | null = null; while (attempt <= maxRetry) { const res = await doOnce(); lastRes = res; // 仅网络失败/超时(errCode 非 0 且 status 非 2xx/3xx)时重试 const status = res.status ?? 0; const isOk = status >= 200 && status < 400; if (isOk) return res; if (attempt === maxRetry) break; // 简单退避 const delay = baseDelay * Math.pow(2, attempt); await new Promise((r) => { setTimeout(() => { r(); }, delay); }); attempt++; } return lastRes!!; } // 新增 upload 方法,支持 uni.uploadFile,自动带 token/apikey static async upload(options : AkReqUploadOptions) : Promise> { // 上传前尝试刷新 token(若即将过期)。优先从 options.headers 或 apikey 字段获取 apikey let apikey: string | null = null; const hdr = options.headers; if (hdr != null && typeof hdr.getString === 'function') { apikey = hdr.getString('apikey'); } if (apikey == null && options.apikey != null) apikey = options.apikey; await this.refreshTokenIfNeeded(apikey != null ? apikey : null); let headers = options.headers ?? ({} as UTSJSONObject); const token = this.getToken(); if (token != null && token !== "") { headers = Object.assign({}, headers, { Authorization: `Bearer ${token}` }) as UTSJSONObject; } if (apikey != null && apikey !== "") { headers = Object.assign({}, headers, { apikey: apikey }) as UTSJSONObject; } // 默认 Accept headers = Object.assign({ Accept: 'application/json' } as UTSJSONObject, headers) as UTSJSONObject; const timeout = options.timeout ?? 10000; const maxRetry = Math.max(0, options.retryCount ?? 0); const baseDelay = Math.max(0, options.retryDelayMs ?? 300); const doOnce = (): Promise> => { return new Promise>((resolve) => { const task = uni.uploadFile({ url: options.url, filePath: options.filePath, name: options.name, formData: options.formData ?? {}, header: headers, timeout: timeout, success: (res : UploadFileSuccess) => { let parsed: UTSJSONObject | null = null; try { parsed = JSON.parse(res.data) as UTSJSONObject; } catch (e) { parsed = null; } if (parsed != null) { const accessToken = parsed.getString('access_token'); const refreshTokenNew = parsed.getString('refresh_token'); const expiresAt = parsed.getNumber('expires_at'); if (accessToken !== null && refreshTokenNew !== null && expiresAt !== null) { AkReq.setToken(accessToken, refreshTokenNew, expiresAt); } } const result = AkReq.createResponse( res.statusCode, parsed ?? {}, headers ); resolve(result); }, fail: (err) => { const result = AkReq.createResponse( err.errCode, err.data ?? {}, {} as UTSJSONObject, new UniError('uni-upload', err.errCode, err.errMsg ?? 'upload fail') ); resolve(result); } }); if (options.onProgress != null && task != null) { const progressCallback = (res: OnProgressUpdateResult) => { const percent = res.progress as number; // 0-100 const sent = res.totalBytesSent as number | null; const expected = res.totalBytesExpectedToSend as number | null; if (options.onProgress != null) { options.onProgress(percent, sent, expected); } }; task.onProgressUpdate(progressCallback); } }); }; let attempt = 0; let lastRes: AkReqResponse | null = null; while (attempt <= maxRetry) { const res = await doOnce(); lastRes = res; const status = res.status ?? 0; const isOk = status >= 200 && status < 400; if (isOk) return res; if (attempt === maxRetry) break; const delay = baseDelay * Math.pow(2, attempt); await new Promise((resolve) => { setTimeout(() => { resolve(); }, delay); }); attempt++; } return lastRes!!; } // 辅助方法:创建 AkReqResponse 对象,避免类型推断问题 static createResponse( status: number, data: T | Array , headers: UTSJSONObject, error: UniError | null = null, total: number | null = null, page: number | null = null, limit: number | null = null, hasmore: boolean | null = null, origin: any | null = null ): AkReqResponse { return { status, data, headers, error, total, page, limit, hasmore, origin }; } // 新增:支持类型转换的请求方法 static async requestAs(options : AkReqOptions, skipRefresh ?: boolean) : Promise>> { const response = await this.request(options, skipRefresh); // 如果原始 data 是 null,直接返回 null // if (response.data == null) { // return { // status: response.status, // data: null, // headers: response.headers, // error: response.error, // total: response.total, // page: response.page, // limit: response.limit, // hasmore: response.hasmore, // origin: response.origin // } as AkReqResponse>; // } // 尝试类型转换 let convertedData: T | null = null; try { // #ifdef APP-ANDROID if (response.data instanceof UTSJSONObject) { convertedData = response.data.parse(); } else if (Array.isArray(response.data)) { const convertedArray: Array = []; const dataArray = response.data; for (let i = 0; i < dataArray.length; i++) { const item = dataArray[i]; if (item instanceof UTSJSONObject) { const parsed = item.parse(); if (parsed != null) { convertedArray.push(parsed); } } else { convertedArray.push(item); } } convertedData = convertedArray as T; } // #endif // #ifndef APP-ANDROID convertedData = response.data as T; // #endif } catch (e) { console.warn('类型转换失败,使用原始 UTSJSONObject:', e); // 转换失败时,返回原始 UTSJSONObject convertedData = response.data as T; } const aaa = { status: response.status, data: convertedData!!, headers: response.headers, error: response.error, total: response.total, page: response.page, limit: response.limit, hasmore: response.hasmore, origin: response.origin } ; return aaa } } export default AkReq;