1744 lines
58 KiB
TypeScript
Executable File
1744 lines
58 KiB
TypeScript
Executable File
// API 工具类 - 封装网络请求和认证逻辑
|
||
import {
|
||
IAppOption,
|
||
ILoginResponse,
|
||
IApiResponse,
|
||
IRecognitionResponse,
|
||
IAuditHistoryResponse,
|
||
IUserInfo,
|
||
IDailySummaryResponse,
|
||
YdWordDetail
|
||
} from '../types/app';
|
||
import { BASE_URL } from './config';
|
||
import { cloudConfig } from './cloud.config';
|
||
|
||
// 添加对 ICloud 命名空间的引用
|
||
declare namespace ICloud {
|
||
interface CallContainerResult {
|
||
data: any;
|
||
header: Record<string, string>;
|
||
statusCode: number;
|
||
errMsg: string;
|
||
}
|
||
}
|
||
|
||
// 音频缓存映射
|
||
const audioCache: Map<string, string> = new Map()
|
||
// 图片缓存映射
|
||
const imageCache: Map<string, string> = new Map()
|
||
|
||
// 缓存大小统计(以字节为单位)
|
||
const cacheStats: Map<string, number> = new Map()
|
||
|
||
// 在类外部添加初始化缓存的方法
|
||
function initPersistentCache() {
|
||
try {
|
||
console.log('开始初始化持久化缓存')
|
||
const startTime = Date.now()
|
||
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
// 检查音频缓存目录是否存在
|
||
fs.accessSync(audioCacheDir)
|
||
|
||
// 递归遍历目录并重建缓存映射
|
||
traverseDirectory(audioCacheDir, '', fs)
|
||
|
||
const endTime = Date.now()
|
||
console.log('持久化缓存初始化完成,耗时:', endTime - startTime, 'ms,音频文件数量:', audioCache.size)
|
||
} catch (e) {
|
||
// 目录不存在,这是正常的,不需要处理
|
||
console.log('音频缓存目录不存在,无需初始化')
|
||
}
|
||
}
|
||
|
||
// 递归遍历目录
|
||
function traverseDirectory(baseDir: string, relativePath: string, fs: any) {
|
||
try {
|
||
const currentPath = relativePath ? `${baseDir}/${relativePath}` : baseDir
|
||
|
||
// 限制遍历深度,避免过深的目录结构
|
||
const pathDepth = relativePath.split('/').filter(p => p.length > 0).length
|
||
if (pathDepth > 5) {
|
||
console.warn('目录深度超过限制,跳过:', relativePath)
|
||
return
|
||
}
|
||
|
||
const items = fs.readdirSync(currentPath)
|
||
|
||
// 限制文件数量,避免过多文件导致性能问题
|
||
const maxItems = 100
|
||
if (items.length > maxItems) {
|
||
console.warn('目录文件数量超过限制,仅处理前', maxItems, '个文件')
|
||
items.splice(maxItems)
|
||
}
|
||
|
||
items.forEach((item: string) => {
|
||
const itemRelativePath = relativePath ? `${relativePath}/${item}` : item
|
||
const itemFullPath = `${baseDir}/${itemRelativePath}`
|
||
|
||
try {
|
||
const stat = fs.statSync(itemFullPath)
|
||
if (stat.isFile()) {
|
||
// 限制文件大小,避免处理过大的文件
|
||
const maxSize = 10 * 1024 * 1024 // 10MB
|
||
if (stat.size > maxSize) {
|
||
console.warn('文件过大,跳过:', itemRelativePath, '大小:', stat.size)
|
||
return
|
||
}
|
||
|
||
// 将文件添加到缓存映射中
|
||
audioCache.set(itemRelativePath, itemFullPath)
|
||
|
||
// 获取文件大小并更新统计
|
||
const fileSize = stat.size
|
||
cacheStats.set(`audio_${itemRelativePath}`, fileSize)
|
||
console.log(`初始化缓存文件: ${itemRelativePath}, 大小: ${fileSize} bytes`)
|
||
} else if (stat.isDirectory()) {
|
||
// 递归处理子目录
|
||
traverseDirectory(baseDir, itemRelativePath, fs)
|
||
}
|
||
} catch (err) {
|
||
// 单个文件处理出错不影响其他文件
|
||
console.warn(`处理项目 ${itemRelativePath} 时出错:`, err)
|
||
}
|
||
})
|
||
} catch (err) {
|
||
// 如果目录不存在或无法读取,记录日志但不中断
|
||
if (relativePath === '') {
|
||
console.log(`无法访问音频缓存目录:`, err)
|
||
} else {
|
||
console.warn(`遍历目录 ${relativePath} 时出错:`, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 异步初始化缓存,避免阻塞主线程
|
||
function initPersistentCacheAsync() {
|
||
// 使用 setTimeout 将初始化操作推迟到下一个事件循环
|
||
setTimeout(() => {
|
||
try {
|
||
initPersistentCache()
|
||
} catch (e) {
|
||
console.error('初始化持久化缓存时出错:', e)
|
||
}
|
||
}, 0)
|
||
}
|
||
|
||
// 在文件末尾调用异步初始化方法
|
||
initPersistentCacheAsync()
|
||
|
||
// 词典等级映射
|
||
const DICT_LEVEL_OPTIONS = {
|
||
LEVEL1: "初级",
|
||
LEVEL2: "中级",
|
||
LEVEL3: "高级"
|
||
}
|
||
|
||
class ApiManager {
|
||
private loginPromise: Promise<ILoginResponse | null> | null = null // 防止重复登录
|
||
private isAudioPlaying: boolean = false // 音频播放状态标志
|
||
|
||
// 获取请求头
|
||
private getHeaders(): Record<string, string> {
|
||
const app = getApp<IAppOption>()
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
|
||
if (app.globalData.token) {
|
||
headers['Authorization'] = `Bearer ${app.globalData.token}`
|
||
}
|
||
|
||
return headers
|
||
}
|
||
|
||
// 专用于登录的请求方法(不处理401重试)
|
||
private loginRequest<T>(
|
||
url: string,
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||
data?: any,
|
||
showLoading: boolean = true
|
||
): Promise<IApiResponse<T>> {
|
||
return new Promise((resolve, reject) => {
|
||
if (showLoading) {
|
||
wx.showLoading({ title: '登录中...' })
|
||
}
|
||
|
||
console.log('发起登录请求:', {
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
hasData: !!data
|
||
})
|
||
|
||
wx.request({
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
data,
|
||
header: {
|
||
'Content-Type': 'application/json'
|
||
// 登录请求不需要Authorization头
|
||
},
|
||
success: (res) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.log('登录请求响应:', {
|
||
statusCode: res.statusCode,
|
||
hasData: !!res.data,
|
||
success: true
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
const response = res.data as IApiResponse<T>
|
||
if (response.code === 0 || response.code === 200) {
|
||
resolve(response)
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '登录失败'
|
||
console.error('登录业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} else {
|
||
console.error('登录HTTP错误:', res.statusCode, res.data)
|
||
let errorMsg = '登录失败'
|
||
if (res.statusCode === 401) {
|
||
errorMsg = '登录凭据无效'
|
||
} else if (res.statusCode >= 500) {
|
||
errorMsg = '服务器错误,请稍后重试'
|
||
}
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.error('登录请求失败:', error)
|
||
wx.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
})
|
||
reject(new Error('网络连接失败: ' + JSON.stringify(error)))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 通用请求方法(支持401自动重试登录)
|
||
private async request<T>(
|
||
url: string,
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||
data?: any,
|
||
showLoading: boolean = true,
|
||
retryCount: number = 0
|
||
): Promise<IApiResponse<T>> {
|
||
const maxRetries = 1 // 最多重试1次(首次请求 + 1次重试)
|
||
|
||
if (BASE_URL.includes('https://prod')){
|
||
return this.wx_request<T>(url, method, data, showLoading)
|
||
}
|
||
else {
|
||
return new Promise(async (resolve, reject) => {
|
||
if (showLoading && retryCount === 0) {
|
||
// wx.showLoading({ title: '加载中...' })
|
||
}
|
||
|
||
console.log('发起请求:', {
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
hasData: !!data,
|
||
hasHeaders: !!this.getHeaders(),
|
||
retryCount
|
||
})
|
||
|
||
wx.request({
|
||
url: `${BASE_URL}${url}`,
|
||
method,
|
||
data,
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.log('请求响应:', {
|
||
statusCode: res.statusCode,
|
||
hasData: !!res.data,
|
||
retryCount,
|
||
success: true
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
const response = res.data as IApiResponse<T>
|
||
if (response.code === 0 || response.code === 200) {
|
||
resolve(response)
|
||
} else {
|
||
// 业务错误
|
||
const errorMsg = response.message || response.msg || '请求失败'
|
||
console.error('业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
console.warn('401认证失败,尝试自动登录', { retryCount, maxRetries })
|
||
|
||
// 如果还有重试机会,尝试自动登录并重试请求
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
console.log('开始401自动登录重试')
|
||
|
||
// 清理过期的认证数据
|
||
this.clearAuthData()
|
||
|
||
// 进行智能登录
|
||
const loginResult = await this.smartLogin(true) // 强制刷新token
|
||
|
||
if (loginResult) {
|
||
console.log('401自动登录成功,重试请求')
|
||
// 登录成功后重试原请求
|
||
try {
|
||
const retryResult = await this.request<T>(url, method, data, false, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
console.error('401自动登录失败')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录失败,请重新登录'))
|
||
}
|
||
} catch (loginError) {
|
||
console.error('401自动登录发生错误:', loginError)
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录失败,请重新登录'))
|
||
}
|
||
} else {
|
||
// 重试次数耗尽,直接处理token过期
|
||
console.error('401重试次数耗尽,跳转登录页')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else if (res.statusCode === 400) {
|
||
const response = res.data as IApiResponse<T>
|
||
const errorMsg = response.msg || '请求失败'
|
||
console.error('400 错误:', errorMsg, response)
|
||
reject(new Error(errorMsg))
|
||
} else if (res.statusCode === 403) {
|
||
const response = res.data as IApiResponse<T>
|
||
const errorMsg = response.msg || '请求失败'
|
||
console.error('403 错误:', errorMsg, response)
|
||
reject(new Error(errorMsg))
|
||
} else if (res.statusCode === 404 ) {
|
||
const response = res.data as IApiResponse<T>
|
||
const errorMsg = response.msg || '请求失败'
|
||
console.error('404 错误:', errorMsg, response)
|
||
reject(new Error(errorMsg))
|
||
} else {
|
||
console.error('HTTP错误:', res.statusCode, res.data)
|
||
wx.showToast({
|
||
title: `网络请求失败 (${res.statusCode})`,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
if (showLoading) {
|
||
wx.hideLoading()
|
||
}
|
||
|
||
console.error('请求失败:', error)
|
||
wx.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
})
|
||
reject(new Error('网络连接失败: ' + JSON.stringify(error)))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// 微信云托管请求方法
|
||
private async wx_request<T>(
|
||
path: string,
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||
data?: any,
|
||
showLoading: boolean = true
|
||
): Promise<ICloud.CallContainerResult> {
|
||
return new Promise((resolve, reject) => {
|
||
if (showLoading) {
|
||
wx.showLoading({ title: '加载中...' });
|
||
}
|
||
|
||
console.log('发起微信云托管请求:', {
|
||
path,
|
||
method,
|
||
hasData: !!data
|
||
});
|
||
|
||
wx.cloud.callContainer({
|
||
config: {
|
||
env: cloudConfig.env // 使用配置文件中的环境ID
|
||
},
|
||
path: path.startsWith('/') ? path : `/${path}`, // 确保路径以 / 开头
|
||
method,
|
||
header: {
|
||
'X-WX-SERVICE': cloudConfig.service // 使用配置文件中的服务名称
|
||
},
|
||
data: data || {}, // 确保数据对象不为 undefined
|
||
success: (res: ICloud.CallContainerResult) => {
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
|
||
console.log('微信云托管请求响应:', {
|
||
statusCode: res.statusCode,
|
||
hasData: !!res.data,
|
||
success: true
|
||
});
|
||
|
||
// 直接返回响应数据,由调用方处理业务逻辑
|
||
resolve(res.data);
|
||
},
|
||
fail: (error: any) => {
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
|
||
console.error('微信云托管请求失败:', error);
|
||
wx.showToast({
|
||
title: '网络连接失败',
|
||
icon: 'none'
|
||
});
|
||
reject(new Error('网络连接失败: ' + JSON.stringify(error)));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 处理 token 过期
|
||
private handleTokenExpired() {
|
||
console.log('处理token过期,清理状态并跳转登录页')
|
||
|
||
const app = getApp<IAppOption>()
|
||
// 清理全局状态
|
||
app.globalData.isLoggedIn = false
|
||
app.globalData.token = undefined
|
||
app.globalData.userInfo = undefined
|
||
app.globalData.dictLevel = undefined
|
||
|
||
// 清理本地存储
|
||
wx.removeStorageSync('token')
|
||
wx.removeStorageSync('userInfo')
|
||
wx.removeStorageSync('tokenExpiry')
|
||
wx.removeStorageSync('sessionUuid')
|
||
wx.removeStorageSync('dictLevel')
|
||
|
||
// 优化用户提示,不要太突充
|
||
console.log('登录状态已过期,即将跳转到登录页')
|
||
|
||
// 立即跳转到登录页,减少等待时间
|
||
wx.reLaunch({
|
||
url: '/pages/index/index'
|
||
})
|
||
}
|
||
|
||
// 智能登录(优先检查本地token)
|
||
async smartLogin(forceRefresh: boolean = false): Promise<ILoginResponse | null> {
|
||
// 如果已经在登录中,返回已有的Promise
|
||
if (this.loginPromise && !forceRefresh) {
|
||
console.log('登录正在进行中,等待现有登录完成')
|
||
return this.loginPromise
|
||
}
|
||
|
||
const app = getApp<IAppOption>()
|
||
console.log('开始智能登录,强制刷新:', forceRefresh)
|
||
|
||
// 如果不是强制刷新,检查本地存储中的token和过期时间
|
||
if (!forceRefresh) {
|
||
const authInfo = this.getStoredAuthInfo()
|
||
console.log('本地认证信息:', {
|
||
hasToken: !!authInfo?.token,
|
||
hasExpiry: !!authInfo?.tokenExpiry,
|
||
tokenLength: authInfo?.token?.length || 0
|
||
})
|
||
|
||
if (authInfo && authInfo.token && authInfo.tokenExpiry) {
|
||
// 检查token是否过期
|
||
const isExpired = this.isTokenExpired()
|
||
if (!isExpired) {
|
||
console.log('Token未过期,使用本地token')
|
||
|
||
// 更新全局状态
|
||
app.globalData.isLoggedIn = true
|
||
app.globalData.token = authInfo.token
|
||
app.globalData.dictLevel = authInfo.dictLevel || 'PRIMARY'
|
||
|
||
// 返回本地存储的登录信息
|
||
return {
|
||
access_token: authInfo.token,
|
||
access_token_expire_time: new Date(authInfo.tokenExpiry).toISOString(),
|
||
session_uuid: authInfo.sessionUuid || '',
|
||
dict_level: authInfo.dictLevel || 'PRIMARY'
|
||
}
|
||
} else {
|
||
console.log('Token已过期,需要重新登录')
|
||
// 清理过期的token
|
||
this.clearAuthData()
|
||
}
|
||
} else {
|
||
console.log('未找到本地token或过期时间,需要登录')
|
||
}
|
||
} else {
|
||
console.log('强制刷新token,重新登录')
|
||
// 清理现有数据
|
||
this.clearAuthData()
|
||
}
|
||
|
||
// Token过期或不存在,进行新的登录
|
||
console.log('发起新的登录请求')
|
||
|
||
// 创建新的登录Promise
|
||
this.loginPromise = this.login().then(
|
||
(result) => {
|
||
console.log('智能登录成功')
|
||
this.loginPromise = null // 清理Promise
|
||
return result
|
||
},
|
||
(error) => {
|
||
console.error('智能登录失败:', error)
|
||
this.loginPromise = null // 清理Promise
|
||
throw error
|
||
}
|
||
)
|
||
|
||
return this.loginPromise
|
||
}
|
||
|
||
// 清理认证数据
|
||
private clearAuthData() {
|
||
const app = getApp<IAppOption>()
|
||
|
||
// 清理全局状态
|
||
app.globalData.isLoggedIn = false
|
||
app.globalData.token = undefined
|
||
app.globalData.userInfo = undefined
|
||
app.globalData.dictLevel = undefined
|
||
|
||
// 清理本地存储
|
||
wx.removeStorageSync('token')
|
||
wx.removeStorageSync('userInfo')
|
||
wx.removeStorageSync('tokenExpiry')
|
||
wx.removeStorageSync('sessionUuid')
|
||
wx.removeStorageSync('dictLevel')
|
||
}
|
||
|
||
// 用户登录(带重试机制)
|
||
async login(retryCount: number = 0): Promise<ILoginResponse> {
|
||
const maxRetries = 5 // 最多重试5次(首轮 + 5次重试,共最多6次尝试)
|
||
|
||
return new Promise((resolve, reject) => {
|
||
if (retryCount > 0) {
|
||
console.log(`登录重试 ${retryCount}/${maxRetries + 1}`)
|
||
}
|
||
|
||
wx.login({
|
||
success: async (loginRes) => {
|
||
if (loginRes.code) {
|
||
try {
|
||
const response = await this.loginRequest<ILoginResponse>('/api/v1/wx/login', 'POST', {
|
||
code: loginRes.code
|
||
})
|
||
|
||
// 处理新的数据结构
|
||
const loginData = response.data
|
||
|
||
if (!loginData.access_token) {
|
||
throw new Error('服务器返回数据中缺少access_token')
|
||
}
|
||
|
||
// 存储 access_token
|
||
wx.setStorageSync('token', loginData.access_token)
|
||
|
||
// 处理过期时间,将字符串时间转换为时间戳
|
||
if (loginData.access_token_expire_time) {
|
||
const expireTime = new Date(loginData.access_token_expire_time).getTime()
|
||
if (isNaN(expireTime)) {
|
||
console.warn('过期时间格式错误,使用默认值')
|
||
const defaultExpiry = Date.now() + 30 * 24 * 60 * 60 * 1000
|
||
wx.setStorageSync('tokenExpiry', defaultExpiry)
|
||
} else {
|
||
wx.setStorageSync('tokenExpiry', expireTime)
|
||
}
|
||
} else {
|
||
const defaultExpiry = Date.now() + 30 * 24 * 60 * 60 * 1000
|
||
wx.setStorageSync('tokenExpiry', defaultExpiry)
|
||
}
|
||
|
||
// 存储 session_uuid
|
||
if (loginData.session_uuid) {
|
||
wx.setStorageSync('sessionUuid', loginData.session_uuid)
|
||
}
|
||
|
||
// 存储 dict_level
|
||
if (loginData.dict_level) {
|
||
wx.setStorageSync('dictLevel', loginData.dict_level)
|
||
// 更新全局状态
|
||
const app = getApp<IAppOption>()
|
||
app.globalData.dictLevel = loginData.dict_level
|
||
}
|
||
|
||
console.log('登录成功,数据存储完成')
|
||
resolve(loginData)
|
||
} catch (error: any) {
|
||
console.error(`登录请求失败:`, error.message)
|
||
|
||
// 如果还有重试次数,尝试重试
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1500) // 减少重试间隔
|
||
} else {
|
||
reject(error)
|
||
}
|
||
}
|
||
} else {
|
||
const error = new Error('微信登录失败:未能获取到登录code')
|
||
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1000)
|
||
} else {
|
||
reject(error)
|
||
}
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('微信登录接口调用失败:', error)
|
||
|
||
if (retryCount < maxRetries) {
|
||
setTimeout(async () => {
|
||
try {
|
||
const result = await this.login(retryCount + 1)
|
||
resolve(result)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
}, 1000)
|
||
} else {
|
||
reject(new Error('微信登录接口调用失败: ' + JSON.stringify(error)))
|
||
}
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 上传文件(第一步:上传文件获取ID)
|
||
async uploadFile(filePath: string, retryCount: number = 0): Promise<string> {
|
||
const maxRetries = 1 // 最多重试1次
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
console.log('上传文件请求:', { filePath, retryCount })
|
||
|
||
wx.uploadFile({
|
||
url: `${BASE_URL}/api/v1/file/upload`,
|
||
filePath,
|
||
name: 'file', // 根据API要求调整参数名
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
console.log('上传文件响应:', {
|
||
statusCode: res.statusCode,
|
||
data: res.data,
|
||
retryCount
|
||
})
|
||
|
||
if (res.statusCode === 200) {
|
||
try {
|
||
const response = JSON.parse(res.data) as IApiResponse<{ id: string }>
|
||
if (response.code === 0 || response.code === 200) {
|
||
if (response.data?.id) {
|
||
console.log('文件上传成功,获得ID:', response.data.id)
|
||
resolve(response.data.id)
|
||
} else {
|
||
reject(new Error('服务器返回数据中缺少文件ID'))
|
||
}
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '文件上传失败'
|
||
console.error('文件上传业务错误:', errorMsg, response)
|
||
wx.showToast({
|
||
title: errorMsg,
|
||
icon: 'none'
|
||
})
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} catch (error) {
|
||
console.error('文件上传响应解析错误:', error)
|
||
wx.showToast({
|
||
title: '数据解析错误',
|
||
icon: 'none'
|
||
})
|
||
reject(error)
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
console.warn('文件上传401认证失败,尝试自动登录', { retryCount, maxRetries })
|
||
|
||
// 如果还有重试机会,尝试自动登录并重试请求
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
console.log('开始文件上传401自动登录重试')
|
||
|
||
// 清理过期的认证数据
|
||
this.clearAuthData()
|
||
|
||
// 进行智能登录
|
||
const loginResult = await this.smartLogin(true) // 强制刷新token
|
||
|
||
if (loginResult) {
|
||
console.log('文件上传401自动登录成功,重试请求')
|
||
// 登录成功后重试原请求
|
||
try {
|
||
const retryResult = await this.uploadFile(filePath, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
console.error('文件上传401自动登录失败')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} catch (loginError) {
|
||
console.error('文件上传401自动登录发生错误:', loginError)
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
// 重试次数耗尽,直接处理token过期
|
||
console.error('文件上传401重试次数耗尽,跳转登录页')
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
// Check for 413 Request Entity Too Large error
|
||
if (res.statusCode === 413) {
|
||
// Get file size for debugging
|
||
const fs = wx.getFileSystemManager();
|
||
fs.getFileInfo({
|
||
filePath: filePath,
|
||
success: (infoRes) => {
|
||
const fileSizeInMB = (infoRes.size / (1024 * 1024)).toFixed(4);
|
||
console.error('文件上传413错误 - 请求实体过大:', {
|
||
statusCode: res.statusCode,
|
||
fileSize: `${fileSizeInMB} MB`,
|
||
fileSizeInBytes: infoRes.size,
|
||
filePath: filePath,
|
||
data: res.data
|
||
});
|
||
},
|
||
fail: (infoError) => {
|
||
console.error('文件上传413错误 - 无法获取文件大小:', {
|
||
statusCode: res.statusCode,
|
||
filePath: filePath,
|
||
fileInfoError: infoError,
|
||
data: res.data
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
console.error('文件上传HTTP错误:', res.statusCode, res.data);
|
||
}
|
||
|
||
wx.showToast({
|
||
title: '文件上传失败',
|
||
icon: 'none'
|
||
});
|
||
reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('文件上传请求失败:', error)
|
||
wx.showToast({
|
||
title: '文件上传失败',
|
||
icon: 'none'
|
||
})
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 图片识别(第二步:通过文件ID进行识别)
|
||
async recognizeImageAsync(fileId: string, type: string = 'word'): Promise<{task_id: string, status: string, message: string}> {
|
||
console.log('开始图片识别请求:', { fileId, type })
|
||
|
||
// 获取当前的词典等级配置
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
|
||
|
||
const response = await this.request<{task_id: string, status: string, message: string}>('/api/v1/image/recognize/async', 'POST', {
|
||
file_id: fileId,
|
||
type: type,
|
||
dict_level: dictLevel // 添加词典等级参数
|
||
})
|
||
|
||
console.log('图片识别成功:', response.data)
|
||
return response.data
|
||
}
|
||
|
||
// 获取识别结果
|
||
async recognizeGetTask(taskId: string | number): Promise<IRecognitionResponse> {
|
||
|
||
const response = await this.request<IRecognitionResponse>(`/api/v1/image/recognize/task/${taskId}`, 'GET')
|
||
|
||
console.log('图片识别成功:', response.data)
|
||
return response.data
|
||
}
|
||
|
||
// 上传图片并识别(对外接口,整合两个步骤)
|
||
async uploadImage(filePath: string, type: string = 'word'): Promise<IRecognitionResponse> {
|
||
try {
|
||
// wx.showLoading({ title: '上传中...' })
|
||
|
||
console.log('开始图片上传和识别流程:', { filePath, type })
|
||
|
||
// 第一步:上传文件获取ID
|
||
const fileId = await this.uploadFile(filePath)
|
||
|
||
// 更新加载提示
|
||
// wx.showLoading({ title: '识别中...' })
|
||
|
||
// 第二步:通过文件ID进行图片识别
|
||
const { task_id } = await this.recognizeImageAsync(fileId, type)
|
||
|
||
// 轮询获取识别结果
|
||
let recognitionResult: IRecognitionResponse | null = null
|
||
while (true) {
|
||
try {
|
||
const res = await this.recognizeGetTask(task_id)
|
||
console.log('--lisa-res', res)
|
||
if (res.status === 'completed' && res.result) {
|
||
recognitionResult = res
|
||
break
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 3000)) // 2秒轮询一次
|
||
} catch (error) {
|
||
console.error('轮询获取识别结果失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// wx.hideLoading()
|
||
console.log('图片上传和识别流程完成')
|
||
|
||
return recognitionResult
|
||
|
||
} catch (error) {
|
||
// wx.hideLoading()
|
||
console.error('图片上传和识别流程失败:', error)
|
||
|
||
// 根据错误类型提供更具体的错误信息
|
||
if (error instanceof Error) {
|
||
if (error.message.includes('登录')) {
|
||
// 登录相关错误,不需要额外处理,已经在内部处理了
|
||
throw error
|
||
} else {
|
||
wx.showToast({
|
||
title: error.message || '处理失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} else {
|
||
wx.showToast({
|
||
title: '处理失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取单词详情
|
||
async getWordDetail(word: string): Promise<YdWordDetail> {
|
||
console.log('获取单词详情')
|
||
|
||
// 首先检查本地缓存
|
||
const cacheKey = `word_detail_${word}`
|
||
try {
|
||
const cachedData = wx.getStorageSync(cacheKey)
|
||
if (cachedData) {
|
||
console.log('使用缓存的单词详情:', word)
|
||
return cachedData
|
||
}
|
||
} catch (e) {
|
||
console.warn('读取单词详情缓存失败:', e)
|
||
}
|
||
|
||
// 缓存中没有,从服务器获取
|
||
const response = await this.request<YdWordDetail>(`/api/v1/dict/word/${encodeURIComponent(word)}`)
|
||
const wordDetail = response.data
|
||
|
||
// 将结果缓存到本地存储,设置7天过期时间
|
||
try {
|
||
wx.setStorageSync(cacheKey, wordDetail)
|
||
// 同时存储缓存时间,用于清理时判断
|
||
const cacheTimeKey = `word_detail_time_${word}`
|
||
wx.setStorageSync(cacheTimeKey, Date.now())
|
||
} catch (e) {
|
||
console.warn('存储单词详情缓存失败:', e)
|
||
}
|
||
|
||
console.log('获取单词详情成功:', response)
|
||
return wordDetail
|
||
}
|
||
|
||
// 播放音频文件
|
||
async playAudio(fileName: string): Promise<void> {
|
||
// 检查是否有音频正在播放
|
||
if (this.isAudioPlaying) {
|
||
console.log('音频正在播放中,跳过本次播放请求')
|
||
return Promise.reject(new Error('音频正在播放中,请等待播放完成'))
|
||
}
|
||
|
||
return new Promise((resolve: () => void, reject: (reason: any) => void) => {
|
||
console.log('播放音频文件:', fileName)
|
||
|
||
// 设置音频播放状态为true
|
||
this.isAudioPlaying = true
|
||
|
||
// 首先检查缓存中是否已有该音频文件
|
||
if (audioCache.has(fileName)) {
|
||
const cachedFilePath = audioCache.get(fileName)!
|
||
console.log('使用缓存的音频文件:', cachedFilePath)
|
||
|
||
// 获取缓存文件大小并更新统计(如果还没有统计信息)
|
||
if (!cacheStats.has(`audio_${fileName}`)) {
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath: cachedFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`缓存音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取缓存音频文件大小失败:', err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 直接播放缓存的音频文件
|
||
this.playAudioFromFile(cachedFilePath)
|
||
.then(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
resolve()
|
||
})
|
||
.catch((error) => {
|
||
this.isAudioPlaying = false // 播放失败后重置状态
|
||
reject(error)
|
||
})
|
||
return
|
||
}
|
||
|
||
// 构建音频文件的完整URL
|
||
const audioUrl = `${BASE_URL}/api/v1/dict/audio/${encodeURIComponent(fileName)}`
|
||
|
||
// 获取认证头
|
||
const headers = this.getHeaders()
|
||
|
||
// 先下载音频文件到本地
|
||
wx.downloadFile({
|
||
url: audioUrl,
|
||
header: headers,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
// 确保音频缓存目录存在
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
try {
|
||
// 检查目录是否存在,不存在则创建
|
||
fs.accessSync(audioCacheDir)
|
||
} catch (dirError) {
|
||
// 目录不存在,创建目录(包括父级目录)
|
||
try {
|
||
fs.mkdirSync(audioCacheDir, true)
|
||
console.log('音频缓存目录创建成功:', audioCacheDir)
|
||
} catch (mkdirError) {
|
||
console.error('创建音频缓存目录失败:', mkdirError)
|
||
// 如果目录创建失败,仍然使用临时文件
|
||
this.handleAudioFileSaveFailure(res.tempFilePath, fileName, resolve, reject)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 构建目标文件路径
|
||
const persistentFilePath = `${audioCacheDir}/${fileName}`
|
||
|
||
// 确保目标文件的父目录也存在(处理嵌套目录的情况)
|
||
const lastSlashIndex = fileName.lastIndexOf('/')
|
||
if (lastSlashIndex > 0) {
|
||
const subDir = fileName.substring(0, lastSlashIndex)
|
||
const fullSubDir = `${audioCacheDir}/${subDir}`
|
||
|
||
try {
|
||
fs.accessSync(fullSubDir)
|
||
} catch (subDirError) {
|
||
// 子目录不存在,创建子目录
|
||
try {
|
||
fs.mkdirSync(fullSubDir, true)
|
||
console.log('音频缓存子目录创建成功:', fullSubDir)
|
||
} catch (subMkdirError) {
|
||
console.error('创建音频缓存子目录失败:', subMkdirError)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将临时文件移动到持久化存储目录
|
||
fs.saveFile({
|
||
tempFilePath: res.tempFilePath,
|
||
filePath: persistentFilePath,
|
||
success: (saveRes: any) => {
|
||
// 将文件路径缓存起来
|
||
audioCache.set(fileName, persistentFilePath)
|
||
console.log('音频文件保存成功并已缓存:', persistentFilePath)
|
||
|
||
// 获取文件大小并更新统计
|
||
fs.getFileInfo({
|
||
filePath: persistentFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取音频文件大小失败:', err)
|
||
}
|
||
})
|
||
|
||
// 播放音频
|
||
this.playAudioFromFile(persistentFilePath)
|
||
.then(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
resolve()
|
||
})
|
||
.catch((error) => {
|
||
this.isAudioPlaying = false // 播放失败后重置状态
|
||
reject(error)
|
||
})
|
||
},
|
||
fail: (saveError) => {
|
||
console.error('保存音频文件失败:', saveError)
|
||
this.handleAudioFileSaveFailure(res.tempFilePath, fileName, resolve, reject)
|
||
}
|
||
})
|
||
} else {
|
||
console.error('下载音频文件失败,状态码:', res.statusCode)
|
||
this.isAudioPlaying = false // 下载失败后重置状态
|
||
reject(new Error(`下载音频文件失败,状态码: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('下载音频文件失败:', error)
|
||
this.isAudioPlaying = false // 下载失败后重置状态
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 从本地文件播放音频
|
||
private playAudioFromFile(filePath: string): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// 使用微信的音频播放API
|
||
const innerAudioContext = wx.createInnerAudioContext()
|
||
|
||
// 设置音频源
|
||
innerAudioContext.src = filePath
|
||
|
||
// 监听播放事件
|
||
innerAudioContext.onPlay(() => {
|
||
console.log('开始播放音频')
|
||
resolve()
|
||
})
|
||
|
||
innerAudioContext.onError((res) => {
|
||
console.error('音频播放错误:', res.errMsg)
|
||
console.error('错误码:', res.errCode)
|
||
this.isAudioPlaying = false // 播放错误后重置状态
|
||
reject(new Error(res.errMsg))
|
||
})
|
||
|
||
// 播放完成后清理资源并重置状态
|
||
innerAudioContext.onEnded(() => {
|
||
this.isAudioPlaying = false // 播放完成后重置状态
|
||
innerAudioContext.destroy()
|
||
})
|
||
|
||
// 开始播放
|
||
innerAudioContext.play()
|
||
})
|
||
}
|
||
|
||
// 处理音频文件保存失败的情况
|
||
private handleAudioFileSaveFailure(tempFilePath: string, fileName: string, resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void) {
|
||
console.log('使用临时文件播放音频')
|
||
|
||
// 将临时文件路径缓存起来
|
||
audioCache.set(fileName, tempFilePath)
|
||
|
||
// 获取文件大小并更新统计
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath: tempFilePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size
|
||
cacheStats.set(`audio_${fileName}`, fileSize)
|
||
console.log(`临时音频文件大小: ${fileSize} bytes`)
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取临时音频文件大小失败:', err)
|
||
}
|
||
})
|
||
|
||
// 播放音频
|
||
this.playAudioFromFile(tempFilePath)
|
||
.then(() => resolve())
|
||
.catch((error) => reject(error))
|
||
}
|
||
|
||
// 清除音频缓存
|
||
clearAudioCache(): void {
|
||
console.log('清除音频缓存,缓存数量:', audioCache.size)
|
||
|
||
// 删除持久化存储的音频文件
|
||
const fs = wx.getFileSystemManager()
|
||
const audioCacheDir = `${wx.env.USER_DATA_PATH}/audio_cache`
|
||
|
||
try {
|
||
// 检查目录是否存在
|
||
fs.accessSync(audioCacheDir)
|
||
// 删除整个音频缓存目录
|
||
fs.rmdirSync(audioCacheDir, true)
|
||
console.log('音频缓存目录已删除:', audioCacheDir)
|
||
} catch (e) {
|
||
// 如果目录不存在或删除失败,记录日志但不中断
|
||
console.log('音频缓存目录不存在或删除失败:', e)
|
||
}
|
||
|
||
// 清空内存中的缓存映射
|
||
audioCache.clear()
|
||
|
||
// 清除音频相关的缓存统计
|
||
const keysToDelete: string[] = []
|
||
cacheStats.forEach((value, key) => {
|
||
if (key.startsWith('audio_')) {
|
||
keysToDelete.push(key)
|
||
}
|
||
})
|
||
keysToDelete.forEach(key => cacheStats.delete(key))
|
||
}
|
||
|
||
// 清除单词详情缓存
|
||
clearWordDetailCache(): void {
|
||
console.log('清除单词详情缓存')
|
||
|
||
try {
|
||
const storageInfo = wx.getStorageInfoSync()
|
||
|
||
// 查找所有单词详情相关的缓存键
|
||
const wordDetailKeys: string[] = []
|
||
storageInfo.keys.forEach(key => {
|
||
if (key.startsWith('word_detail_')) {
|
||
wordDetailKeys.push(key)
|
||
}
|
||
})
|
||
|
||
// 删除所有单词详情缓存
|
||
wordDetailKeys.forEach(key => {
|
||
wx.removeStorageSync(key)
|
||
})
|
||
|
||
console.log('已清除单词详情缓存,数量:', wordDetailKeys.length)
|
||
} catch (e) {
|
||
console.error('清除单词详情缓存失败:', e)
|
||
}
|
||
}
|
||
|
||
// 获取缓存统计信息
|
||
getCacheStats(): { audioCount: number; imageCount: number; thumbnailCount: number; totalSize: number; sizeByType: Record<string, number> } {
|
||
console.log('开始获取缓存统计信息')
|
||
const startTime = Date.now()
|
||
|
||
let audioCount = 0
|
||
let imageCount = 0
|
||
let thumbnailCount = 0
|
||
let totalSize = 0
|
||
const sizeByType: Record<string, number> = { audio: 0, image: 0, dict: 0, thumbnail: 0 }
|
||
|
||
// 遍历缓存统计信息
|
||
cacheStats.forEach((size, key) => {
|
||
totalSize += size
|
||
if (key.startsWith('audio_')) {
|
||
audioCount++
|
||
sizeByType.audio += size
|
||
} else if (key.startsWith('image_')) {
|
||
imageCount++
|
||
sizeByType.image += size
|
||
}
|
||
})
|
||
|
||
// 计算缩略图缓存大小
|
||
try {
|
||
const storageInfo = wx.getStorageInfoSync()
|
||
let thumbnailSize = 0
|
||
let thumbnailEntries = 0
|
||
storageInfo.keys.forEach(key => {
|
||
if (key.startsWith('thumbnail_cache_')) {
|
||
try {
|
||
const item = wx.getStorageSync(key)
|
||
if (item && item.size) {
|
||
thumbnailSize += item.size
|
||
thumbnailEntries++
|
||
}
|
||
} catch (e) {
|
||
console.warn('读取缩略图缓存大小失败:', key, e)
|
||
}
|
||
}
|
||
})
|
||
sizeByType.thumbnail = thumbnailSize
|
||
totalSize += thumbnailSize
|
||
thumbnailCount = thumbnailEntries
|
||
} catch (e) {
|
||
console.error('获取缩略图缓存统计失败:', e)
|
||
}
|
||
|
||
// 计算词典缓存大小
|
||
try {
|
||
const storageInfo = wx.getStorageInfoSync()
|
||
let dictSize = 0
|
||
storageInfo.keys.forEach(key => {
|
||
if (key.startsWith('word_detail_')) {
|
||
try {
|
||
const item = wx.getStorageSync(key)
|
||
if (item) {
|
||
// 估算 JSON 大小
|
||
const jsonString = JSON.stringify(item)
|
||
dictSize += jsonString.length
|
||
}
|
||
} catch (e) {
|
||
console.warn('读取词典缓存大小失败:', key, e)
|
||
}
|
||
}
|
||
})
|
||
sizeByType.dict = dictSize
|
||
totalSize += dictSize
|
||
} catch (e) {
|
||
console.error('获取词典缓存统计失败:', e)
|
||
}
|
||
|
||
const endTime = Date.now()
|
||
console.log('缓存统计信息获取完成,耗时:', endTime - startTime, 'ms', {
|
||
audioCount,
|
||
imageCount,
|
||
thumbnailCount,
|
||
totalSize,
|
||
sizeByType
|
||
})
|
||
|
||
return { audioCount, imageCount, thumbnailCount, totalSize, sizeByType }
|
||
}
|
||
|
||
// 格式化文件大小显示
|
||
formatFileSize(bytes: number): string {
|
||
if (bytes < 1024) {
|
||
return `${bytes}B`
|
||
} else if (bytes < 1024 * 1024) {
|
||
return `${(bytes / 1024).toFixed(1)}KB`
|
||
} else if (bytes < 1024 * 1024 * 1024) {
|
||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
|
||
} else {
|
||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`
|
||
}
|
||
}
|
||
|
||
// 获取用户信息
|
||
async getUserInfo(): Promise<IUserInfo> {
|
||
const response = await this.request<IUserInfo>('/api/v1/user/info')
|
||
return response.data
|
||
}
|
||
|
||
// 检查登录状态
|
||
async checkLoginStatus(): Promise<boolean> {
|
||
try {
|
||
const app = getApp<IAppOption>()
|
||
if (!app.globalData.token) {
|
||
return false
|
||
}
|
||
|
||
// 向服务器验证 token 是否有效
|
||
await this.request('/api/v1/auth/verify', 'GET', undefined, false)
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查本地 token 是否过期
|
||
isTokenExpired(): boolean {
|
||
try {
|
||
const tokenExpiry = wx.getStorageSync('tokenExpiry')
|
||
if (!tokenExpiry) {
|
||
return true
|
||
}
|
||
|
||
const now = Date.now()
|
||
const isExpired = now >= tokenExpiry
|
||
|
||
console.log('Token 过期检查:', {
|
||
current: new Date(now).toLocaleString(),
|
||
expiry: new Date(tokenExpiry).toLocaleString(),
|
||
isExpired
|
||
})
|
||
|
||
return isExpired
|
||
} catch (error) {
|
||
console.error('检查 token 过期时间失败:', error)
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 获取存储的认证信息
|
||
getStoredAuthInfo() {
|
||
try {
|
||
return {
|
||
token: wx.getStorageSync('token'),
|
||
tokenExpiry: wx.getStorageSync('tokenExpiry'),
|
||
sessionUuid: wx.getStorageSync('sessionUuid'),
|
||
userInfo: wx.getStorageSync('userInfo'),
|
||
dictLevel: wx.getStorageSync('dictLevel') // 新增词典等级
|
||
}
|
||
} catch (error) {
|
||
console.error('获取存储的认证信息失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 修改用户设置
|
||
// async updateUserSettings(settings: { dict_level: string }): Promise<boolean> {
|
||
// try {
|
||
// const response = await this.request<null>('/api/v1/wx/settings', 'PUT', settings)
|
||
|
||
// if (response.code === 0 || response.code === 200) {
|
||
// // 更新本地缓存和全局状态
|
||
// const app = getApp<IAppOption>()
|
||
// app.globalData.dictLevel = settings.dict_level
|
||
// wx.setStorageSync('dictLevel', settings.dict_level)
|
||
|
||
// console.log('用户设置更新成功:', settings)
|
||
// return true
|
||
// } else {
|
||
// throw new Error(response.message || response.msg || '更新失败')
|
||
// }
|
||
// } catch (error) {
|
||
// console.error('更新用户设置失败:', error)
|
||
// throw error
|
||
// }
|
||
// }
|
||
|
||
// 获取词典等级选项
|
||
getDictLevelOptions() {
|
||
return DICT_LEVEL_OPTIONS
|
||
}
|
||
|
||
// 测试401自动重试机制(仅用于开发调试)
|
||
async test401AutoRetry(): Promise<any> {
|
||
console.log('开始测试401自动重试机制')
|
||
try {
|
||
// 请求一个可能返回401的接口
|
||
const response = await this.request('/api/v1/auth/verify', 'GET', undefined, false)
|
||
console.log('401测试成功,响应:', response)
|
||
return response
|
||
} catch (error) {
|
||
console.log('401测试结果:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取审核统计数据
|
||
async getAuditStatistics(): Promise<{ total_count: number; today_count: number; image_count: number }> {
|
||
try {
|
||
console.log('开始获取审核统计数据')
|
||
const response = await this.request<{ total_count: number; today_count: number; image_count: number }>('/api/v1/audit/statistics')
|
||
console.log('获取审核统计数据成功:', response.data)
|
||
return response.data
|
||
} catch (error) {
|
||
console.error('获取审核统计数据失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取历史记录数据
|
||
async getAuditHistory(page: number = 1, size: number = 15): Promise<IAuditHistoryResponse> {
|
||
try {
|
||
console.log('开始获取审核历史记录数据', { page, size });
|
||
const response = await this.request<IAuditHistoryResponse>(`/api/v1/audit/history?page=${page}&size=${size}`);
|
||
console.log('获取审核历史记录数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取审核历史记录数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async getDailySummary(page: number = 1, size: number = 15): Promise<IDailySummaryResponse> {
|
||
try {
|
||
console.log('开始获取历史记录数据', { page, size });
|
||
const response = await this.request<IDailySummaryResponse>(`/api/v1/audit/summary?page=${page}&size=${size}`);
|
||
console.log('获取历史记录数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取历史记录数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async getTodaySummary(page: number = 1, size: number = 15): Promise<IDailySummaryResponse> {
|
||
try {
|
||
console.log('开始获取今日总结数据', { page, size });
|
||
const response = await this.request<IDailySummaryResponse>(`/api/v1/audit/today_summary?page=${page}&size=${size}`);
|
||
console.log('获取今日总结数据成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取今日总结数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取口语评估结果
|
||
async getAssessmentResult(fileId: string, imageTextId: string): Promise<any> {
|
||
try {
|
||
console.log('开始获取口语评估结果', { fileId });
|
||
const response = await this.request<any>(`/api/v1/recording/assessment`, 'POST', {
|
||
file_id: fileId,
|
||
image_text_id: imageTextId
|
||
})
|
||
|
||
console.log('获取口语评估结果成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取口语评估结果失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 安全下载文件接口
|
||
async downloadFile(fileId: string): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
// console.log('开始安全下载文件:', fileId);
|
||
|
||
// 构建文件下载URL
|
||
const fileUrl = `${BASE_URL}/api/v1/file/${fileId}`;
|
||
|
||
// 获取认证头
|
||
const headers = this.getHeaders();
|
||
|
||
// 使用微信的下载文件API
|
||
wx.downloadFile({
|
||
url: fileUrl,
|
||
header: headers,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
// 将文件保存到本地并返回本地路径
|
||
const fs = wx.getFileSystemManager();
|
||
const filePath = `${wx.env.USER_DATA_PATH}/${fileId}`;
|
||
|
||
fs.saveFile({
|
||
tempFilePath: res.tempFilePath,
|
||
filePath: filePath,
|
||
success: () => {
|
||
// console.log('文件保存成功:', filePath);
|
||
resolve(filePath);
|
||
},
|
||
fail: (error) => {
|
||
console.error('文件保存失败:', error);
|
||
reject(new Error('文件保存失败'));
|
||
}
|
||
});
|
||
} else if (res.statusCode === 401) {
|
||
// 处理认证失败
|
||
console.error('文件下载认证失败');
|
||
this.handleTokenExpired();
|
||
reject(new Error('认证失败,请重新登录'));
|
||
} else {
|
||
console.error('文件下载失败,状态码:', res.statusCode);
|
||
reject(new Error(`文件下载失败,状态码: ${res.statusCode}`));
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('文件下载请求失败:', error);
|
||
reject(new Error('文件下载失败'));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 将文件ID转换为可显示的图片URL
|
||
async getFileDisplayUrl(fileId: string): Promise<string> {
|
||
try {
|
||
// 首先检查图片缓存中是否已有该文件
|
||
if (imageCache.has(fileId)) {
|
||
const cachedFilePath = imageCache.get(fileId)!;
|
||
console.log('使用缓存的图片文件:', cachedFilePath);
|
||
return cachedFilePath;
|
||
}
|
||
|
||
// 下载文件
|
||
const filePath = await this.downloadFile(fileId);
|
||
|
||
// 将文件路径缓存起来
|
||
imageCache.set(fileId, filePath);
|
||
|
||
// 获取文件大小并更新统计
|
||
const fs = wx.getFileSystemManager();
|
||
fs.getFileInfo({
|
||
filePath: filePath,
|
||
success: (infoRes) => {
|
||
const fileSize = infoRes.size;
|
||
cacheStats.set(`image_${fileId}`, fileSize);
|
||
// console.log(`图片文件大小: ${fileSize} bytes`);
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取图片文件大小失败:', err);
|
||
}
|
||
});
|
||
|
||
return filePath;
|
||
} catch (error) {
|
||
console.error('获取文件显示URL失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 清除图片缓存
|
||
clearImageCache(): void {
|
||
console.log('清除图片缓存,缓存数量:', imageCache.size);
|
||
|
||
// 删除持久化存储的图片文件
|
||
const fs = wx.getFileSystemManager();
|
||
|
||
// 清空内存中的缓存映射
|
||
imageCache.clear();
|
||
|
||
// 清除图片相关的缓存统计
|
||
const keysToDelete: string[] = [];
|
||
cacheStats.forEach((value, key) => {
|
||
if (key.startsWith('image_')) {
|
||
keysToDelete.push(key);
|
||
}
|
||
});
|
||
keysToDelete.forEach(key => cacheStats.delete(key));
|
||
}
|
||
|
||
// 清除缩略图缓存
|
||
clearThumbnailCache(): void {
|
||
console.log('清除缩略图缓存');
|
||
|
||
try {
|
||
const storageInfo = wx.getStorageInfoSync();
|
||
|
||
// 查找所有缩略图缓存键
|
||
const thumbnailKeys: string[] = [];
|
||
storageInfo.keys.forEach(key => {
|
||
if (key.startsWith('thumbnail_cache_')) {
|
||
thumbnailKeys.push(key);
|
||
}
|
||
});
|
||
|
||
// 删除所有缩略图缓存
|
||
thumbnailKeys.forEach(key => {
|
||
try {
|
||
const cacheEntry = wx.getStorageSync(key);
|
||
if (cacheEntry && cacheEntry.filePath) {
|
||
// 删除缓存的文件
|
||
const fs = wx.getFileSystemManager();
|
||
fs.unlinkSync(cacheEntry.filePath);
|
||
}
|
||
wx.removeStorageSync(key);
|
||
} catch (e) {
|
||
console.warn('删除缩略图缓存失败:', key, e);
|
||
}
|
||
});
|
||
|
||
console.log('已清除缩略图缓存,数量:', thumbnailKeys.length);
|
||
} catch (e) {
|
||
console.error('清除缩略图缓存失败:', e);
|
||
}
|
||
}
|
||
|
||
// 获取图片文本和评分信息
|
||
async getImageTextInit(imageId: string): Promise<{
|
||
image_file_id: string,
|
||
assessments: Array<{
|
||
id: string
|
||
content: string,
|
||
ipa: string,
|
||
file_id: string,
|
||
details: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number,
|
||
Words: {
|
||
Word: string
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
}[]
|
||
}
|
||
}
|
||
} | null
|
||
}>
|
||
}> {
|
||
try {
|
||
console.log('开始获取图片文本和评分信息', { imageId });
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
|
||
const response = await this.request<{
|
||
image_file_id: string,
|
||
assessments: Array<{
|
||
id: string
|
||
content: string,
|
||
ipa: string,
|
||
file_id: string,
|
||
details: {
|
||
assessment: {
|
||
code: number
|
||
final: number
|
||
message: string
|
||
voice_id: string
|
||
result: {
|
||
RefTextId: number,
|
||
SentenceId: number,
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
PronCompletion: number,
|
||
SuggestedScore: number,
|
||
Words: {
|
||
Word: string
|
||
PronAccuracy: number,
|
||
PronFluency: number,
|
||
}[]
|
||
}
|
||
}
|
||
} | null
|
||
}>
|
||
}>(`/api/v1/image_text/init`, 'POST', {
|
||
dict_level: dictLevel,
|
||
image_id: imageId
|
||
})
|
||
|
||
console.log('获取图片文本和评分信息成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取图片文本和评分信息失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async getStandardVoice(text_id: string | number): Promise<{audio_id: number | string}> {
|
||
try {
|
||
console.log('开始获取标准音频');
|
||
const response = await this.request<{audio_id: number | string}>(`/api/v1/image_text/standard/${text_id}`);
|
||
console.log('获取标准音频成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取标准音频失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 获取积分数据
|
||
async getPointsData(): Promise<import("../types/app").IPointsData> {
|
||
try {
|
||
console.log('开始获取积分数据');
|
||
const response = await this.request<import("../types/app").IPointsData>('/api/v1/points/self');
|
||
console.log('获取积分数据成功:', response.data);
|
||
|
||
// 如果返回的数据为null,则返回默认值
|
||
if (!response.data) {
|
||
return { balance: 0, expired_time: '' };
|
||
}
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('获取积分数据失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 兑换码兑换积分
|
||
async redeemCoupon(code: string): Promise<any> {
|
||
try {
|
||
console.log('开始兑换码兑换:', code);
|
||
const response = await this.request<any>('/api/v1/coupon/redeem', 'POST', {
|
||
code: code
|
||
});
|
||
console.log('兑换成功:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('兑换失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 导出单例
|
||
const apiManager = new ApiManager()
|
||
export default apiManager
|
||
export { ApiManager }
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|