Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
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<boolean> {
// 没有 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<AkReqResponse<any>> {
// 自动刷新 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<AkReqResponse<any>> => {
return new Promise<AkReqResponse<any>>((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<any>(
res.statusCode,
[] as Array<any>,
res.header as UTSJSONObject
);
resolve(result);
return;
}
// 兼容 res.data 可能为 string 或 UTSJSONObject 或 UTSArray
let data : UTSJSONObject | Array<UTSJSONObject> | 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<any>(
res.statusCode,
data ?? {},
res.header as UTSJSONObject
);
resolve(result);
},
fail: (err) => {
const result = AkReq.createResponse<any>(
err.errCode,
err.data ?? {},
{} as UTSJSONObject,
new UniError('uni-request', err.errCode, err.errMsg ?? 'request fail')
);
resolve(result);
}
});
});
};
let attempt = 0;
let lastRes: AkReqResponse<any> | 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<void>((r) => { setTimeout(() => { r(); }, delay); });
attempt++;
}
return lastRes!!;
}
// 新增 upload 方法,支持 uni.uploadFile自动带 token/apikey
static async upload(options : AkReqUploadOptions) : Promise<AkReqResponse<any>> {
// 上传前尝试刷新 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<AkReqResponse<any>> => {
return new Promise<AkReqResponse<any>>((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<any>(
res.statusCode,
parsed ?? {},
headers
);
resolve(result);
},
fail: (err) => {
const result = AkReq.createResponse<any>(
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<any> | 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<void>((resolve) => {
setTimeout(() => {
resolve();
}, delay);
});
attempt++;
}
return lastRes!!;
}
// 辅助方法:创建 AkReqResponse 对象,避免类型推断问题
static createResponse<T>(
status: number,
data: T | Array<T> ,
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<T> {
return {
status,
data,
headers,
error,
total,
page,
limit,
hasmore,
origin
};
}
// 新增:支持类型转换的请求方法
static async requestAs<T = any>(options : AkReqOptions, skipRefresh ?: boolean) : Promise<AkReqResponse<T|Array<T>>> {
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<T|Array<T>>;
// }
// 尝试类型转换
let convertedData: T | null = null;
try {
// #ifdef APP-ANDROID
if (response.data instanceof UTSJSONObject) {
convertedData = response.data.parse<T>();
} else if (Array.isArray(response.data)) {
const convertedArray: Array<any> = [];
const dataArray = response.data;
for (let i = 0; i < dataArray.length; i++) {
const item = dataArray[i];
if (item instanceof UTSJSONObject) {
const parsed = item.parse<T>();
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;

View File

@@ -0,0 +1,2 @@
export * from './interface.uts';
export * from './ak-req.uts';

View File

@@ -0,0 +1,48 @@
// ak-req 类型定义
export type AkReqOptions = {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' |'HEAD';
data?: UTSJSONObject | Array<UTSJSONObject>;
headers?: UTSJSONObject;
timeout?: number;
contentType?: string; // 新增,支持顶级 contentType
// 可选:重试设置(仅网络错误/超时触发)。默认重试 0 次
retryCount?: number; // 最大重试次数,默认 0
retryDelayMs?: number; // 首次重试延迟,默认 300ms指数退避
};
// 上传参数类型定义
export type AkReqUploadOptions = {
url: string,
filePath: string,
name: string,
formData?: UTSJSONObject,
headers?: UTSJSONObject,
apikey?: string,
timeout?: number,
// 进度回调0-100注意H5/APP 平台支持不同)
onProgress?: (progress: number, transferredBytes?: number, totalBytes?: number) => void,
// 可选:重试设置(仅网络错误/超时触发)。默认 0
retryCount?: number,
retryDelayMs?: number
};
export type AkReqResponse<T = any> = {
status: number;
data: T | Array<T> | null; // 支持 null
headers: UTSJSONObject;
error: UniError | null;
total:number |null;
page: number |null;
limit: number |null;
hasmore:boolean |null;
origin: any | null;
};
export class AkReqError extends Error {
code: number;
constructor(message: string, code: number = 0) {
super(message);
this.code = code;
this.name = 'AkReqError';
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "ak-req",
"version": "0.0.1",
"main": "ak-req.uts",
"types": "interface.uts",
"uni_modules": {
"uni_modules": true
}
}