Files
YwxAppThink/addon/wxchat/YwxMakeFriends/utlis/http-api.uts
T

620 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// simple-http.uts
// 简单强大的 UniappX UTS 网络请求封装
// 支持 token 管理、无感刷新、多方法兼容
// ================ 类型定义 ================
// 响应接口
import HttpError, {
IResponse, IRequestOption, IHttpError, IFileInfo, INetworkInfo, IDownRespone, IUploadInfo, IUploadOption, IUploadResult, IResponseToken
} from "@/types/http.uts"
import userState, { getUserToken, setUserToken, clearToken } from '@/stores/user.uts'
// ================ 简单 HTTP 请求类 ================
class SimpleHttp {
// 配置
private baseOptions : UTSJSONObject = {
baseURL: 'http://localhost:8000',
timeout: 15000,
tokenKey: 'access_token',
refreshTokenKey: 'refresh_token',
tokenExpireKey: 'token_expire',
tokenType: 'Bearer',
enableRefreshToken: true,
maxRetryCount: 3,
retryDelay: 1000,
debug: true
} as UTSJSONObject
// 状态
private isRefreshing = false
private refreshSubscribers : Array<() => void> = []
private requestQueue : Array<() => Promise<any>> = []
private isProcessingQueue = false
constructor(options : UTSJSONObject = {}) {
this.baseOptions = { ...this.baseOptions, ...options }
this.setupNetworkListener()
}
// ================ 核心请求方法 ================
/**
* 发送请求
*/
async request(options : IRequestOption) : Promise<IResponse> {
try {
//await this.checkNetwork() // 检查网络
if (options.showLoading != null && options.showLoading == true) {
uni.showLoading({
title: options.loadingText ?? '加载中...',
mask: true
})
}
const requestConfig = this.buildRequestConfig(options)
const response = await this.sendRequest(requestConfig)
const { statusCode, data } = response
if (statusCode >= 200 && statusCode < 300 && data != null) {
if (data?.access_token != null) {
const access_token = data.access_token ?? ''
const access_expired = data.access_expired ?? 0
const refresh_token = data.refresh_token ?? ''
const refresh_expired = data.refresh_expired ?? 0
setUserToken(access_token, access_expired, refresh_token, refresh_expired)
}
return Promise.resolve(data)
}
else if (statusCode == 401) {
return this.handleTokenExpired(options)
} else {
throw HttpError({
errCode: statusCode,
errMsg: `HTTP 错误: ${statusCode}`,
options: options
})
}
} catch (error) {
console.log(error)
throw HttpError({
errCode: 5000,
options,
cause: error as Error
})
} finally {
if (options.showLoading != null && options.showLoading == true) {
uni.hideLoading()
}
}
}
/**
* GET 请求
*/
async get(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
//const mergedConfig = config != null ? config : { url: '' }
return this.request({
...(config),
url,
method: 'GET',
data
} as IRequestOption)
}
/**
* POST 请求
*/
async post(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
//const mergedConfig = config != null ? config : { url: '' }
return this.request({
...(config),
url,
method: 'POST',
data
} as IRequestOption)
}
/**
* PUT 请求
*/
async put(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
//const mergedConfig = config != null ? config : { url: '' }
return this.request({
...(config),
url,
method: 'PUT',
data
})
}
/**
* DELETE 请求
*/
async delete(url : string, data ?: any, config : IRequestOption = { url: '' }) : Promise<IResponse> {
//const mergedConfig = config != null ? config : { url: '' }
return this.request({
...(config),
url,
method: 'DELETE'
} as IRequestOption)
}
/**
* PATCH 请求
*/
async patch(url : string, data ?: any, config : IRequestOption = { url: '' }) : Promise<IResponse> {
//const mergedConfig = config != null ? config : { url: '' }
return this.request({
...(config),
url,
method: 'PATCH',
data
} as IRequestOption)
}
/**
* 检查 Token 是否有效
*/
isTokenValid() : boolean {
const token = getUserToken()
return (token != null)
}
// ================ Token 无感刷新 ================
/**
* 刷新 Token
*/
private async refreshToken() : Promise<boolean> {
if (this.isRefreshing) {
return new Promise((resolve) => {
this.refreshSubscribers.push(() => resolve(true))
})
}
this.isRefreshing = true
try {
const tokenResult = getUserToken()
if (tokenResult == null) {
clearToken()
this.goToLogin()
return false
}
const { refreshToken } = tokenResult
if (refreshToken == null) {
this.log('无刷新 Token')
clearToken()
this.goToLogin()
return false
}
// 调用刷新接口(需后端实现 Login::refresh 并返回 { access_token, refresh_token, ... }
const response = await this.post('/wxchat/api/login/refresh', {
refresh_token: refreshToken
}, {
url: "",
needToken: false,
showLoading: false
})
if (response.code == 0) {
const { access_token, access_expired, refresh_token, refresh_expired } = response
setUserToken(access_token ?? '', access_expired ?? 0, refresh_token ?? '', refresh_expired ?? 0)
// 通知所有等待的请求
this.refreshSubscribers.forEach(callback => callback())
this.refreshSubscribers = []
return true
} else {
this.log('刷新 Token 失败:', response.message)
clearToken()
this.goToLogin()
return false
}
} catch (error) {
this.log('刷新 Token 异常:', error)
clearToken()
this.goToLogin()
return false
} finally {
this.isRefreshing = false
}
}
/**
* 处理 Token 过期
*/
private async handleTokenExpired(options : IRequestOption) : Promise<IResponse> {
if (this.baseOptions.enableRefreshToken == null) {
throw HttpError({
errCode: 401,
errMsg: 'Token 已过期',
options: options
})
}
const success = await this.refreshToken()
if (success) {
return this.request(options)
} else {
throw HttpError({
errCode: 401,
errMsg: '登录已过期,请重新登录',
options: options
})
}
}
/**
* 跳转到登录页
*/
private goToLogin() : void {
uni.showModal({
title: '登录提示',
content: '登录已过期,请重新登录',
showCancel: false,
confirmText: '去登录',
success: () => {
uni.reLaunch({
url: '/pages/login/index'
})
}
})
}
// ================ 请求处理 ================
/**
* 构建请求配置
*/
private buildRequestConfig(options : IRequestOption) : IRequestOption {
const { url, contentType } = options
options.url = url?.startsWith('http') == true ? url! : `${this.baseOptions.baseURL}${url?.startsWith('/') == true ? url! : '/' + (url ?? '')}`
if (options.header == null) {
options.header = {} as UTSJSONObject
}
// #ifdef APP
const sysInfo = uni.getSystemInfoSync()
options.header['App-Platform'] = sysInfo.osName
options.header['App-Version-Code'] = sysInfo.appVersionCode
options.header['App-Version-Name'] = sysInfo.appVersion
options.header['App-Channel'] = sysInfo.osName
options.header['App-Device-ID'] = sysInfo.deviceId
options.header['App-Device-Model'] = sysInfo.deviceModel
options.header['App-OS-Version'] = sysInfo.osVersion
// #endif
switch (contentType) {
case 'json':
options.header['Content-Type'] = 'application/json'
break
case 'form':
options.header['Content-Type'] = 'application/x-www-form-urlencoded'
break
case 'multipart':
options.header['Content-Type'] = 'multipart/form-data'
break
}
if (options.needToken != null && options.needToken) {
const tokenResult = getUserToken()
if (tokenResult != null) {
const { accessToken } = tokenResult
if (accessToken != null) {
options.header['Authorization'] = `${this.baseOptions.tokenType} ${accessToken}`
}
}
}
return options;
}
/**
* 发送请求
*/
private async sendRequest(options : IRequestOption) : Promise<RequestSuccess<IResponse>> {
return new Promise((resolve, reject) => {
uni.request<IResponse>({
url: options.url,
data: options.data,
header: options.header,
method: options.method,
timeout: options.timeout,
enableChunked: true,
success: (res : RequestSuccess<IResponse>) => {
resolve(res)
},
fail: (err : RequestFail) => {
reject(HttpError({
errCode: err.errCode,
errMsg: err.errMsg,
options: options,
}))
},
complete: (option : any) => {
}
})
})
}
// ================ 网络状态 ================
/**
* 检查网络状态
*/
private async checkNetwork() : Promise<void> {
return new Promise((resolve, reject) => {
uni.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
reject(new Error('网络连接已断开'))
} else {
resolve()
}
},
fail: () => {
resolve() // 网络检查失败不阻止请求
}
})
})
}
/**
* 获取网络信息
*/
async getNetworkInfo() : Promise<INetworkInfo> {
return new Promise((resolve) => {
uni.getNetworkType({
success: (res) => {
resolve({
isConnected: res.networkType !== 'none',
networkType: res.networkType,
isWifi: res.networkType == 'wifi',
isCellular: ['2g', '3g', '4g', '5g'].includes(res.networkType)
})
},
fail: () => {
resolve({
isConnected: false,
networkType: 'unknown',
isWifi: false,
isCellular: false
})
}
})
})
}
/**
* 设置网络监听
*/
private setupNetworkListener() : void {
uni.onNetworkStatusChange((res) => {
if (!res.isConnected) {
uni.showToast({
title: '网络连接已断开',
icon: 'none',
duration: 3000
})
}
})
}
// ================ 工具方法 ================
/**
* 日志记录
*/
private log(...args : any[]) : void {
if (this.baseOptions.debug != null && this.baseOptions.debug == true) {
console.log('[SimpleHttp]', ...args)
}
}
/**
* 设置基础 URL
*/
setBaseURL(baseURL : string) : void {
this.baseOptions.baseURL = baseURL
}
/**
* 设置超时时间
*/
setTimeout(timeout : number) : void {
this.baseOptions.timeout = timeout
}
/**
* 设置调试模式
*/
setDebug(debug : boolean) : void {
this.baseOptions.debug = debug
}
/**
* 设置 Token 刷新开关
*/
setEnableRefreshToken(enable : boolean) : void {
this.baseOptions.enableRefreshToken = enable
}
/**
* 获取配置
*/
getConfig() : UTSJSONObject {
return { ...this.baseOptions }
}
// ================ 上传和下载 ================
/**
* 上传文件
*/
async upload(options : IUploadOption, onProgress ?: (current : number, total : number, percent : number) => void) : Promise<IResponse> {
let { url, filePath, name = 'file', formData, header } = options
const headers : UTSJSONObject = {
...(header ?? {} as UTSJSONObject)
}
const tokenResult = getUserToken()
if (tokenResult != null) {
const { accessToken } = tokenResult
if (accessToken != null) {
{
headers['Authorization'] = `${this.baseOptions.tokenType} ${accessToken}`
}
}
}
return await new Promise((resolve, reject) => {
const uploadTask = uni.uploadFile({
url: url.startsWith('http') ? url : `${this.baseOptions.baseURL}${url}`,
filePath,
name,
formData: formData ?? {},
header: headers,
success: (response : UploadFileSuccess) => {
const { statusCode, data } = response
// 处理 HTTP 204 等无内容响应
if (statusCode == 204 || data == null) {
//resolve({ code: 0, message: 'success', data: null } as IResponse)
reject(HttpError({ errCode: -1, errMsg: `响应解析失败,原始数据>: ${data.substring(0, 200)}`, options }))
}
if (statusCode >= 200 && statusCode < 300) {
try {
const obj = JSON.parse(data) as UTSJSONObject;
let resInfo = obj.parse<IResponse>()
const { access_token, access_expired, refresh_token, refresh_expired } = resInfo!!
if (resInfo?.access_token != null) {
setUserToken(access_token ?? '', access_expired ?? 0, refresh_token ?? '', refresh_expired ?? 0)
}
if (resInfo != null) {
resolve(resInfo)
}
// // 业务状态码判断(根据你的业务逻辑调整)
if (resInfo?.code != 0 && resInfo?.code != 200) {
reject(HttpError({ errCode: resInfo?.code ?? 500, errMsg: resInfo?.message ?? '业务错误', options }))
}
} catch (parseError) {
console.log(parseError)
// 返回的不是 JSON,可能是之前遇到的响应头问题
reject(HttpError({ errCode: -1, errMsg: `响应解析失败,原始数据: ${data.substring(0, 200)}`, options }))
}
return
}
// 401 未授权处理
if (statusCode == 401) {
// if (this.baseOptions.enableRefreshToken) {
// try {
// // const refreshSuccess = await this.refreshToken()
// // if (refreshSuccess) {
// // // 重试上传
// // const retryResult = await this.upload<T>(options)
// // resolve(retryResult)
// // return
// // }
// } catch (refreshError) {
// // refreshToken 失败,继续走登录过期逻辑
// }
// }
reject(HttpError({ errCode: 401, errMsg: '登录已过期,请重新登录', options }))
}
// 其他 HTTP 错误
reject(HttpError({ errCode: statusCode, errMsg: `HTTP 错误: ${statusCode}`, options }))
},
fail: (error : UploadFileFail) => {
console.log(error)
reject(HttpError({ errCode: error.errCode ?? -1, errMsg: error.errMsg ?? '上传失败', options }))
}
})
uploadTask.onProgressUpdate((res) => {
console.log('上传进度', res.progress)
})
})
}
/**
* 并行上传多个文件
*/
async uploadMultipleFilesParallel(options : IUploadOption, files : IFileInfo[]) : Promise<IFileInfo[]> {
// 显式指定 map 回调的返回类型为 Promise<IFileInfo>
const uploadPromises = files.map((file) : Promise<IFileInfo> => {
// 显式指定 upload 的泛型参数,并添加 return 返回 Promise
return this.upload({
url: options.url,
header: options.header,
timeout: options.timeout,
filePath: file.path,
//name: file.name ?? 'file',
}, null).then((response : IResponse) : IFileInfo => { // 显式指定 then 的返回类型
let { code, data } = response
if (code == 0 && data != null) {
const result = (data as UTSJSONObject).parse<IUploadInfo>()
file.fid = result?.fid ?? 0
file.name = result?.name ?? file.name
file.path = result?.url ?? file.path
}
return file // 直接返回 file,不要用 Promise.resolve
}).catch((error) : IFileInfo => {
console.log(error);
return file
})
})
return Promise.all(uploadPromises)
}
// /**
// * 带整体进度回调的并行上传
// */
// async uploadMultipleFilesWithProgress<T = any>(options : IUploadOption, files : IFileInfo[], onProgress ?: (completed : number, total : number, results : IUploadResult<T>[]) => void) : Promise<IUploadResult<T>[]> {
// const results : IUploadResult<T>[] = new Array()
// let completedCount = 0
// const uploadPromises = files.map((file) => {
// return this.upload<T>({
// url: options.url,
// header: options.header,
// timeout: options.timeout,
// filePath: file.path,
// name: file.name ?? 'file',
// formData: {
// ...(options.formData ?? {}),
// fileIndex: file.index.toString(),
// fileCount: files.length.toString()
// }
// }).then((result) => {
// results[file.index] = {
// index: file.index,
// success: true,
// data: result
// }
// completedCount++
// onProgress?.(completedCount, files.length, [...results])
// return results[file.index]
// }).catch((error) => {
// results[file.index] = {
// index: file.index,
// success: false,
// error: error
// }
// completedCount++
// onProgress?.(completedCount, files.length, [...results])
// return results[file.index]
// })
// })
// await Promise.all(uploadPromises)
// return results
// }
}
const httpApi = new SimpleHttp()
// 导出
export default httpApi
export type { IResponse, IRequestOption, INetworkInfo, IDownRespone, IUploadOption, IFileInfo }
// import http, { SimpleHttp, IResponse } from './simple-http'
// // 使用默认配置
// const api = http
// // 或者创建自定义实例
// const customApi = new SimpleHttp('https://api.yourservice.com', {
// timeout: 20000,
// debug: true,
// tokenType: 'Bearer',
// enableRefreshToken: true
// })