|
@@ -2,221 +2,181 @@ import Taro from '@tarojs/taro'
|
|
|
import { hc } from 'hono/client'
|
|
import { hc } from 'hono/client'
|
|
|
import { ResponsePolyfill } from './response-polyfill'
|
|
import { ResponsePolyfill } from './response-polyfill'
|
|
|
|
|
|
|
|
-export interface RpcClientConfig {
|
|
|
|
|
- /** API基础URL,例如: http://localhost:3000 */
|
|
|
|
|
- apiBaseUrl: string
|
|
|
|
|
- /** token存储键名,默认: 'token' */
|
|
|
|
|
- tokenStorageKey?: string
|
|
|
|
|
- /** 刷新token存储键名,默认: 'refresh_token' */
|
|
|
|
|
- refreshTokenStorageKey?: string
|
|
|
|
|
- /** 刷新token的API路径,默认: '/api/v1/auth/refresh-token' */
|
|
|
|
|
- refreshTokenPath?: string
|
|
|
|
|
- /** 登录页面路径,token刷新失败时跳转,默认: '/pages/login/index' */
|
|
|
|
|
- loginPagePath?: string
|
|
|
|
|
- /** 用户信息存储键名,默认: 'userInfo' */
|
|
|
|
|
- userInfoStorageKey?: string
|
|
|
|
|
- /** 认证类型前缀,默认: 'Bearer ' */
|
|
|
|
|
- authPrefix?: string
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 刷新token的函数
|
|
|
|
|
+let isRefreshing = false
|
|
|
|
|
+let refreshSubscribers: ((token: string) => void)[] = []
|
|
|
|
|
+
|
|
|
|
|
+// 执行token刷新
|
|
|
|
|
+const refreshToken = async (): Promise<string | null> => {
|
|
|
|
|
+ if (isRefreshing) {
|
|
|
|
|
+ // 如果已经在刷新,等待结果
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ refreshSubscribers.push((token) => {
|
|
|
|
|
+ resolve(token)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// 默认配置
|
|
|
|
|
-const defaultConfig: RpcClientConfig = {
|
|
|
|
|
- apiBaseUrl: process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000',
|
|
|
|
|
- tokenStorageKey: 'token',
|
|
|
|
|
- refreshTokenStorageKey: 'refresh_token',
|
|
|
|
|
- refreshTokenPath: '/api/v1/auth/refresh-token',
|
|
|
|
|
- loginPagePath: '/pages/login/index',
|
|
|
|
|
- userInfoStorageKey: 'userInfo',
|
|
|
|
|
- authPrefix: 'Bearer '
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ isRefreshing = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const refreshToken = Taro.getStorageSync('enterprise_refresh_token')
|
|
|
|
|
+ if (!refreshToken) {
|
|
|
|
|
+ throw new Error('未找到刷新token')
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// 刷新token的管理器
|
|
|
|
|
-class TokenRefreshManager {
|
|
|
|
|
- private isRefreshing = false
|
|
|
|
|
- private refreshSubscribers: ((token: string | null) => void)[] = []
|
|
|
|
|
|
|
+ // 调用刷新token接口
|
|
|
|
|
+ const response = await Taro.request({
|
|
|
|
|
+ url: `${process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'}/api/v1/yongren/auth/refresh-token`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'Authorization': `Bearer ${refreshToken}`
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- constructor(private config: RpcClientConfig) {}
|
|
|
|
|
|
|
+ if (response.statusCode === 200) {
|
|
|
|
|
+ const { token, refresh_token: newRefreshToken } = response.data
|
|
|
|
|
+ Taro.setStorageSync('enterprise_token', token)
|
|
|
|
|
+ if (newRefreshToken) {
|
|
|
|
|
+ Taro.setStorageSync('enterprise_refresh_token', newRefreshToken)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- async refreshToken(): Promise<string | null> {
|
|
|
|
|
- if (this.isRefreshing) {
|
|
|
|
|
- // 如果已经在刷新,等待结果
|
|
|
|
|
- return new Promise((resolve) => {
|
|
|
|
|
- this.refreshSubscribers.push((token) => {
|
|
|
|
|
- resolve(token)
|
|
|
|
|
- })
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // 通知所有等待的请求
|
|
|
|
|
+ refreshSubscribers.forEach(callback => callback(token))
|
|
|
|
|
+ refreshSubscribers = []
|
|
|
|
|
+ return token
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error('刷新token失败')
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('刷新token失败:', error)
|
|
|
|
|
+ // 清除token,跳转到登录页
|
|
|
|
|
+ Taro.removeStorageSync('enterprise_token')
|
|
|
|
|
+ Taro.removeStorageSync('enterprise_refresh_token')
|
|
|
|
|
+ Taro.removeStorageSync('enterpriseUserInfo')
|
|
|
|
|
+
|
|
|
|
|
+ // 跳转到登录页
|
|
|
|
|
+ Taro.showToast({
|
|
|
|
|
+ title: '登录已过期,请重新登录',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ Taro.redirectTo({
|
|
|
|
|
+ url: '/pages/login/index'
|
|
|
|
|
+ })
|
|
|
|
|
+ }, 1500)
|
|
|
|
|
|
|
|
- this.isRefreshing = true
|
|
|
|
|
- try {
|
|
|
|
|
- const refreshToken = Taro.getStorageSync(this.config.refreshTokenStorageKey!)
|
|
|
|
|
- if (!refreshToken) {
|
|
|
|
|
- throw new Error('未找到刷新token')
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return null
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isRefreshing = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 调用刷新token接口
|
|
|
|
|
- const response = await Taro.request({
|
|
|
|
|
- url: `${this.config.apiBaseUrl}${this.config.refreshTokenPath}`,
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- header: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- 'Authorization': `${this.config.authPrefix}${refreshToken}`
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+// API配置
|
|
|
|
|
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'
|
|
|
|
|
|
|
|
- if (response.statusCode === 200) {
|
|
|
|
|
- const { token, refresh_token: newRefreshToken } = response.data
|
|
|
|
|
- Taro.setStorageSync(this.config.tokenStorageKey!, token)
|
|
|
|
|
- if (newRefreshToken) {
|
|
|
|
|
- Taro.setStorageSync(this.config.refreshTokenStorageKey!, newRefreshToken)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 完整的API地址
|
|
|
|
|
+// const BASE_URL = `${API_BASE_URL}/api/${API_VERSION}`
|
|
|
|
|
|
|
|
- // 通知所有等待的请求
|
|
|
|
|
- this.refreshSubscribers.forEach(callback => callback(token))
|
|
|
|
|
- this.refreshSubscribers = []
|
|
|
|
|
- return token
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error('刷新token失败')
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('刷新token失败:', error)
|
|
|
|
|
- // 清除token,跳转到登录页
|
|
|
|
|
- this.clearAuthData()
|
|
|
|
|
-
|
|
|
|
|
- // 跳转到登录页
|
|
|
|
|
- Taro.showToast({
|
|
|
|
|
- title: '登录已过期,请重新登录',
|
|
|
|
|
- icon: 'none'
|
|
|
|
|
- })
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- Taro.redirectTo({
|
|
|
|
|
- url: this.config.loginPagePath!
|
|
|
|
|
- })
|
|
|
|
|
- }, 1500)
|
|
|
|
|
-
|
|
|
|
|
- // 通知所有等待的请求刷新失败
|
|
|
|
|
- this.refreshSubscribers.forEach(callback => callback(null))
|
|
|
|
|
- this.refreshSubscribers = []
|
|
|
|
|
- return null
|
|
|
|
|
- } finally {
|
|
|
|
|
- this.isRefreshing = false
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 创建自定义fetch函数,适配Taro.request,支持token自动刷新
|
|
|
|
|
+const taroFetch: any = async (input, init) => {
|
|
|
|
|
+ const url = typeof input === 'string' ? input : input.url
|
|
|
|
|
+ const method = init.method || 'GET'
|
|
|
|
|
+
|
|
|
|
|
+ const requestHeaders: Record<string, string> = init.headers;
|
|
|
|
|
|
|
|
- clearAuthData(): void {
|
|
|
|
|
- Taro.removeStorageSync(this.config.tokenStorageKey!)
|
|
|
|
|
- Taro.removeStorageSync(this.config.refreshTokenStorageKey!)
|
|
|
|
|
- Taro.removeStorageSync(this.config.userInfoStorageKey!)
|
|
|
|
|
|
|
+ const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
|
|
|
|
|
+ if (!keyOfContentType) {
|
|
|
|
|
+ requestHeaders['content-type'] = 'application/json'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- getToken(): string | null {
|
|
|
|
|
- return Taro.getStorageSync(this.config.tokenStorageKey!)
|
|
|
|
|
|
|
+ // 构建Taro请求选项
|
|
|
|
|
+ const options: Taro.request.Option = {
|
|
|
|
|
+ url,
|
|
|
|
|
+ method: method as any,
|
|
|
|
|
+ data: init.body,
|
|
|
|
|
+ header: requestHeaders
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-// 创建自定义fetch函数,适配Taro.request,支持token自动刷新
|
|
|
|
|
-const createTaroFetch = (config: RpcClientConfig, tokenManager: TokenRefreshManager) => {
|
|
|
|
|
- return async (input: RequestInfo | URL, init?: RequestInit): Promise<any> => {
|
|
|
|
|
- const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
|
|
|
|
|
- const method = init?.method || 'GET'
|
|
|
|
|
- const requestHeaders: Record<string, string> = { ...init?.headers } as Record<string, string>
|
|
|
|
|
-
|
|
|
|
|
- // 确保有Content-Type
|
|
|
|
|
- const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
|
|
|
|
|
- if (!keyOfContentType) {
|
|
|
|
|
- requestHeaders['content-type'] = 'application/json'
|
|
|
|
|
|
|
+ // 添加token - 优先使用企业token,兼容mini_token
|
|
|
|
|
+ let token = Taro.getStorageSync('enterprise_token')
|
|
|
|
|
+ if (!token) {
|
|
|
|
|
+ token = Taro.getStorageSync('mini_token')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (token) {
|
|
|
|
|
+ options.header = {
|
|
|
|
|
+ ...options.header,
|
|
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 构建Taro请求选项
|
|
|
|
|
- const options: Taro.request.Option = {
|
|
|
|
|
- url,
|
|
|
|
|
- method: method as any,
|
|
|
|
|
- data: init?.body,
|
|
|
|
|
- header: requestHeaders
|
|
|
|
|
|
|
+ // 发送请求
|
|
|
|
|
+ const sendRequest = async (): Promise<any> => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('API请求:', options.url)
|
|
|
|
|
+ const response = await Taro.request(options)
|
|
|
|
|
+
|
|
|
|
|
+ const responseHeaders = response.header;
|
|
|
|
|
+
|
|
|
|
|
+ // 处理204 No Content响应,不设置body
|
|
|
|
|
+ const body = response.statusCode === 204
|
|
|
|
|
+ ? null
|
|
|
|
|
+ : responseHeaders['content-type']!.includes('application/json')
|
|
|
|
|
+ ? JSON.stringify(response.data)
|
|
|
|
|
+ : response.data;
|
|
|
|
|
+
|
|
|
|
|
+ return new ResponsePolyfill(
|
|
|
|
|
+ body,
|
|
|
|
|
+ {
|
|
|
|
|
+ status: response.statusCode,
|
|
|
|
|
+ statusText: response.errMsg || 'OK',
|
|
|
|
|
+ headers: responseHeaders
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('API Error:', error)
|
|
|
|
|
+ throw error
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 添加token
|
|
|
|
|
- let token = tokenManager.getToken()
|
|
|
|
|
- if (token) {
|
|
|
|
|
- options.header = {
|
|
|
|
|
- ...options.header,
|
|
|
|
|
- 'Authorization': `${config.authPrefix}${token}`
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ let response = await sendRequest()
|
|
|
|
|
|
|
|
- // 发送请求
|
|
|
|
|
- const sendRequest = async (): Promise<any> => {
|
|
|
|
|
- try {
|
|
|
|
|
- console.log('API请求:', options.url)
|
|
|
|
|
- const response = await Taro.request(options)
|
|
|
|
|
-
|
|
|
|
|
- const responseHeaders = response.header;
|
|
|
|
|
-
|
|
|
|
|
- // 处理204 No Content响应,不设置body
|
|
|
|
|
- const body = response.statusCode === 204
|
|
|
|
|
- ? null
|
|
|
|
|
- : responseHeaders['content-type']!.includes('application/json')
|
|
|
|
|
- ? JSON.stringify(response.data)
|
|
|
|
|
- : response.data;
|
|
|
|
|
-
|
|
|
|
|
- return new ResponsePolyfill(
|
|
|
|
|
- body,
|
|
|
|
|
- {
|
|
|
|
|
- status: response.statusCode,
|
|
|
|
|
- statusText: response.errMsg || 'OK',
|
|
|
|
|
- headers: responseHeaders
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('API Error:', error)
|
|
|
|
|
- throw error
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 检查是否为401错误,尝试刷新token
|
|
|
|
|
+ if (response.status === 401 && token) {
|
|
|
|
|
+ console.log('检测到401错误,尝试刷新token...')
|
|
|
|
|
+ const newToken = await refreshToken()
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- let response = await sendRequest()
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否为401错误,尝试刷新token
|
|
|
|
|
- if (response.status === 401 && token) {
|
|
|
|
|
- console.log('检测到401错误,尝试刷新token...')
|
|
|
|
|
- const newToken = await tokenManager.refreshToken()
|
|
|
|
|
-
|
|
|
|
|
- if (newToken) {
|
|
|
|
|
- // 更新请求header中的token
|
|
|
|
|
- options.header = {
|
|
|
|
|
- ...options.header,
|
|
|
|
|
- 'Authorization': `${config.authPrefix}${newToken}`
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 重试原始请求
|
|
|
|
|
- response = await sendRequest()
|
|
|
|
|
- } else {
|
|
|
|
|
- // 刷新失败,返回原始401响应
|
|
|
|
|
- return response
|
|
|
|
|
|
|
+ if (newToken) {
|
|
|
|
|
+ // 更新请求header中的token
|
|
|
|
|
+ options.header = {
|
|
|
|
|
+ ...options.header,
|
|
|
|
|
+ 'Authorization': `Bearer ${newToken}`
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- return response
|
|
|
|
|
- } catch (error: any) {
|
|
|
|
|
- console.error('API请求失败:', error)
|
|
|
|
|
- Taro.showToast({
|
|
|
|
|
- title: error.message || '网络错误',
|
|
|
|
|
- icon: 'none'
|
|
|
|
|
- })
|
|
|
|
|
- throw error
|
|
|
|
|
|
|
+ // 重试原始请求
|
|
|
|
|
+ response = await sendRequest()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 刷新失败,返回原始401响应
|
|
|
|
|
+ return response
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return response
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('API请求失败:', error)
|
|
|
|
|
+ Taro.showToast({
|
|
|
|
|
+ title: error.message || '网络错误',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ throw error
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 创建Hono RPC客户端
|
|
// 创建Hono RPC客户端
|
|
|
-export const createRpcClient = <T extends any>(customConfig?: Partial<RpcClientConfig>) => {
|
|
|
|
|
- const config = { ...defaultConfig, ...customConfig }
|
|
|
|
|
- const tokenManager = new TokenRefreshManager(config)
|
|
|
|
|
- const fetch = createTaroFetch(config, tokenManager)
|
|
|
|
|
-
|
|
|
|
|
|
|
+export const rpcClient = <T extends any>(apiBasePath?: string) => {
|
|
|
// @ts-ignore
|
|
// @ts-ignore
|
|
|
- return hc<T>(`${config.apiBaseUrl}`, { fetch })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 导出类型
|
|
|
|
|
-export type RpcClient<T> = ReturnType<typeof createRpcClient<T>>
|
|
|
|
|
|
|
+ return hc<T>(`${API_BASE_URL}${apiBasePath}`, {
|
|
|
|
|
+ fetch: taroFetch
|
|
|
|
|
+ })
|
|
|
|
|
+}
|