2256 lines
77 KiB
TypeScript
Executable File
2256 lines
77 KiB
TypeScript
Executable File
// API 工具类 - 封装网络请求和认证逻辑
|
||
import {
|
||
IAppOption,
|
||
ILoginResponse,
|
||
IApiResponse,
|
||
IRecognitionResponse,
|
||
IAuditHistoryResponse,
|
||
IUserInfo,
|
||
IDailySummaryResponse,
|
||
YdWordDetail,
|
||
IQaExerciseCreateAccepted,
|
||
IQaExerciseTaskStatus,
|
||
IQaExerciseQueryResponse,
|
||
IQaQuestionAttemptAccepted,
|
||
IQaQuestionTaskStatus,
|
||
IQaResult,
|
||
IQaConversationSettingResponse,
|
||
IQaConversationDetail
|
||
} from '../types/app';
|
||
import { BASE_URL, USE_CLOUD } 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: "level1",
|
||
LEVEL2: "level2",
|
||
LEVEL3: "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 (USE_CLOUD){
|
||
return this.wx_request<T>(url, method, data, showLoading) as unknown as IApiResponse<T>
|
||
}
|
||
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
|
||
});
|
||
|
||
if (res.statusCode === 200) {
|
||
// 直接返回响应数据,由调用方处理业务逻辑
|
||
resolve(res.data);
|
||
} 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 {
|
||
const response = res.data as IApiResponse<T>
|
||
const errorMsg = response.msg || '请求失败'
|
||
console.error('微信云托管请求错误:', errorMsg, response)
|
||
reject(new Error(errorMsg))
|
||
}
|
||
},
|
||
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> {
|
||
if (USE_CLOUD) {
|
||
const app = getApp<IAppOption>()
|
||
let dictLevel = wx.getStorageSync('dictLevel') || 'level1'
|
||
try {
|
||
const resp = await this.request<{ user: IUserInfo; dict_level?: string; session_uuid?: string; settings?: { dict_level?: string }; points?: { balance: number; expired_time: string } }>(
|
||
'/api/v1/wx/user',
|
||
'GET',
|
||
undefined,
|
||
false
|
||
)
|
||
const data = (resp as any)?.data || resp
|
||
if (data.user) {
|
||
app.globalData.userInfo = data.user
|
||
wx.setStorageSync('userInfo', data.user)
|
||
}
|
||
const dl = data.dict_level || 'level1'
|
||
if (dl) {
|
||
dictLevel = dl
|
||
wx.setStorageSync('dictLevel', dictLevel)
|
||
}
|
||
if (data.session_uuid) {
|
||
wx.setStorageSync('sessionUuid', data.session_uuid)
|
||
}
|
||
} catch (e) {}
|
||
app.globalData.isLoggedIn = true
|
||
app.globalData.dictLevel = dictLevel
|
||
return {
|
||
access_token: '',
|
||
access_token_expire_time: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
||
session_uuid: wx.getStorageSync('sessionUuid') || '',
|
||
dict_level: dictLevel
|
||
}
|
||
}
|
||
if (this.loginPromise && !forceRefresh) {
|
||
return this.loginPromise
|
||
}
|
||
const app = getApp<IAppOption>()
|
||
if (!forceRefresh) {
|
||
const authInfo = this.getStoredAuthInfo()
|
||
if (authInfo && authInfo.token && authInfo.tokenExpiry) {
|
||
const isExpired = this.isTokenExpired()
|
||
if (!isExpired) {
|
||
app.globalData.isLoggedIn = true
|
||
app.globalData.token = authInfo.token
|
||
app.globalData.dictLevel = authInfo.dictLevel || 'level1'
|
||
return {
|
||
access_token: authInfo.token,
|
||
access_token_expire_time: new Date(authInfo.tokenExpiry).toISOString(),
|
||
session_uuid: authInfo.sessionUuid || '',
|
||
dict_level: authInfo.dictLevel || 'level1'
|
||
}
|
||
} else {
|
||
this.clearAuthData()
|
||
}
|
||
}
|
||
} else {
|
||
this.clearAuthData()
|
||
}
|
||
this.loginPromise = this.login().then(
|
||
(result) => {
|
||
this.loginPromise = null
|
||
return result
|
||
},
|
||
(error) => {
|
||
this.loginPromise = null
|
||
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, referrerId?: string): 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 data: any = { code: loginRes.code }
|
||
if (referrerId) {
|
||
data.referrer_id = referrerId
|
||
}
|
||
const response = await this.loginRequest<ILoginResponse>('/api/v1/wx/login', 'POST', data)
|
||
|
||
// 处理新的数据结构
|
||
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> {
|
||
if (USE_CLOUD) {
|
||
return new Promise((resolve, reject) => {
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath,
|
||
success: async (infoRes) => {
|
||
try {
|
||
const filename = this.extractFileName(filePath)
|
||
const mime = this.getMimeType(filePath)
|
||
const initResp = await this.wx_request<{ file_id: string; cloud_path: string; env: string }>(
|
||
'/api/v1/file/upload/init',
|
||
'POST',
|
||
{ filename, size: infoRes.size, mime, biz_type: 'file' }
|
||
)
|
||
const initData = (initResp as any).data || initResp
|
||
const file_id = initData.file_id
|
||
const cloud_path = initData.cloud_path
|
||
const env = initData.env
|
||
const task = wx.cloud.uploadFile({
|
||
cloudPath: cloud_path,
|
||
filePath,
|
||
config: { env },
|
||
success: async (upRes) => {
|
||
try {
|
||
const completeResp = await this.wx_request<{ id: string; url?: string }>(
|
||
'/api/v1/file/upload/complete',
|
||
'POST',
|
||
{ file_id, cloud_path, fileID: upRes.fileID, size: infoRes.size, mime }
|
||
)
|
||
resolve(file_id)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
},
|
||
fail: (e) => {
|
||
const info = String(e)
|
||
if (info.indexOf('abort') !== -1) {
|
||
reject(new Error('上传中断'))
|
||
} else {
|
||
reject(new Error('上传失败'))
|
||
}
|
||
}
|
||
})
|
||
} catch (err) {
|
||
reject(err)
|
||
}
|
||
},
|
||
fail: (err) => reject(err)
|
||
})
|
||
})
|
||
}
|
||
const maxRetries = 1
|
||
return new Promise(async (resolve, reject) => {
|
||
wx.uploadFile({
|
||
url: `${BASE_URL}/api/v1/file/upload`,
|
||
filePath,
|
||
name: 'file',
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
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) {
|
||
resolve(response.data.id)
|
||
} else {
|
||
reject(new Error('服务器返回数据中缺少文件ID'))
|
||
}
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '文件上传失败'
|
||
wx.showToast({ title: errorMsg, icon: 'none' })
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} catch (error) {
|
||
wx.showToast({ title: '数据解析错误', icon: 'none' })
|
||
reject(error)
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
this.clearAuthData()
|
||
const loginResult = await this.smartLogin(true)
|
||
if (loginResult) {
|
||
try {
|
||
const retryResult = await this.uploadFile(filePath, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} catch (loginError) {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
wx.showToast({ title: '文件上传失败', icon: 'none' })
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
wx.showToast({ title: '文件上传失败', icon: 'none' })
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 上传文件(第一步:上传文件获取ID)
|
||
async uploadImageFile(filePath: string, retryCount: number = 0): Promise<string> {
|
||
if (USE_CLOUD) {
|
||
return new Promise((resolve, reject) => {
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath,
|
||
success: async (infoRes) => {
|
||
try {
|
||
const filename = this.extractFileName(filePath)
|
||
const mime = this.getMimeType(filePath)
|
||
const initResp = await this.wx_request<{ file_id: string; cloud_path: string; env: string }>(
|
||
'/api/v1/file/upload/init',
|
||
'POST',
|
||
{ filename, size: infoRes.size, mime, biz_type: 'image' }
|
||
)
|
||
const initData = (initResp as any).data || initResp
|
||
const file_id = initData.file_id
|
||
const cloud_path = initData.cloud_path
|
||
const env = initData.env
|
||
const task = wx.cloud.uploadFile({
|
||
cloudPath: cloud_path,
|
||
filePath,
|
||
config: { env },
|
||
success: async (upRes) => {
|
||
try {
|
||
const completeResp = await this.wx_request<{ id: string; url?: string }>(
|
||
'/api/v1/file/upload/complete',
|
||
'POST',
|
||
{ file_id, cloud_path, fileID: upRes.fileID, size: infoRes.size, mime }
|
||
)
|
||
resolve(file_id)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
},
|
||
fail: (e) => {
|
||
const info = String(e)
|
||
if (info.indexOf('abort') !== -1) {
|
||
reject(new Error('上传中断'))
|
||
} else {
|
||
reject(new Error('上传失败'))
|
||
}
|
||
}
|
||
})
|
||
} catch (err) {
|
||
reject(err)
|
||
}
|
||
},
|
||
fail: (err) => reject(err)
|
||
})
|
||
})
|
||
}
|
||
const maxRetries = 1
|
||
return new Promise(async (resolve, reject) => {
|
||
wx.uploadFile({
|
||
url: `${BASE_URL}/api/v1/file/upload_image`,
|
||
filePath,
|
||
name: 'file',
|
||
header: this.getHeaders(),
|
||
success: async (res) => {
|
||
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) {
|
||
resolve(response.data.id)
|
||
} else {
|
||
reject(new Error('服务器返回数据中缺少文件ID'))
|
||
}
|
||
} else {
|
||
const errorMsg = response.message || response.msg || '文件上传失败'
|
||
wx.showToast({ title: errorMsg, icon: 'none' })
|
||
reject(new Error(errorMsg))
|
||
}
|
||
} catch (error) {
|
||
wx.showToast({ title: '数据解析错误', icon: 'none' })
|
||
reject(error)
|
||
}
|
||
} else if (res.statusCode === 401) {
|
||
if (retryCount < maxRetries) {
|
||
try {
|
||
this.clearAuthData()
|
||
const loginResult = await this.smartLogin(true)
|
||
if (loginResult) {
|
||
try {
|
||
const retryResult = await this.uploadFile(filePath, retryCount + 1)
|
||
resolve(retryResult)
|
||
} catch (retryError) {
|
||
reject(retryError)
|
||
}
|
||
} else {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} catch (loginError) {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
this.handleTokenExpired()
|
||
reject(new Error('登录已过期'))
|
||
}
|
||
} else {
|
||
wx.showToast({ title: '文件上传失败', icon: 'none' })
|
||
reject(new Error(`HTTP ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
wx.showToast({ title: '文件上传失败', icon: 'none' })
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
private extractFileName(path: string): string {
|
||
const i = path.lastIndexOf('/')
|
||
return i >= 0 ? path.substring(i + 1) : path
|
||
}
|
||
|
||
private getMimeType(path: string): string {
|
||
const i = path.lastIndexOf('.')
|
||
const ext = i >= 0 ? path.substring(i + 1).toLowerCase() : ''
|
||
const map: Record<string, string> = {
|
||
jpg: 'image/jpeg',
|
||
jpeg: 'image/jpeg',
|
||
png: 'image/png',
|
||
gif: 'image/gif',
|
||
webp: 'image/webp',
|
||
mpeg: 'audio/mpeg',
|
||
mp3: 'audio/mp3',
|
||
wav: 'audio/wav',
|
||
bmp: 'image/bmp'
|
||
}
|
||
return map[ext] || 'application/octet-stream'
|
||
}
|
||
|
||
// 图片识别(第二步:通过文件ID进行识别)
|
||
async recognizeImageAsync(fileId: string, type: string = 'word'): Promise<{task_id: string, status: string, message: string}> {
|
||
|
||
// 获取当前的词典等级配置
|
||
const app = getApp<IAppOption>()
|
||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||
|
||
console.log('开始图片识别请求:', { fileId, type, dictLevel })
|
||
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.uploadImageFile(filePath)
|
||
try {
|
||
await this.cacheLocalImage(fileId, filePath)
|
||
} catch (e) {}
|
||
|
||
// 更新加载提示
|
||
// 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') {
|
||
recognitionResult = res
|
||
break
|
||
}
|
||
if (res.status === 'failed') {
|
||
recognitionResult = res
|
||
throw new Error(recognitionResult.error_message || '图片识别失败')
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 3000)) // 2秒轮询一次
|
||
} catch (error) {
|
||
console.error('轮询获取识别结果失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
try { this.refreshImageCache(fileId) } catch (e) {}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
|
||
private async cacheLocalImage(fileId: string, localPath: string): Promise<string> {
|
||
return new Promise((resolve) => {
|
||
try {
|
||
const fs = wx.getFileSystemManager()
|
||
const extMatch = localPath && localPath.match(/\.[a-zA-Z0-9]+$/)
|
||
if (!extMatch) {
|
||
imageCache.set(fileId, localPath)
|
||
resolve(localPath)
|
||
return
|
||
}
|
||
const ext = extMatch[0]
|
||
const destPath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||
try { fs.unlinkSync(destPath) } catch (e) {}
|
||
fs.saveFile({
|
||
tempFilePath: localPath,
|
||
filePath: destPath,
|
||
success: () => {
|
||
imageCache.set(fileId, destPath)
|
||
fs.getFileInfo({
|
||
filePath: destPath,
|
||
success: (infoRes) => {
|
||
cacheStats.set(`image_${fileId}`, infoRes.size)
|
||
resolve(destPath)
|
||
},
|
||
fail: () => resolve(destPath)
|
||
})
|
||
},
|
||
fail: () => {
|
||
imageCache.set(fileId, localPath)
|
||
resolve(localPath)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
imageCache.set(fileId, localPath)
|
||
resolve(localPath)
|
||
}
|
||
})
|
||
}
|
||
|
||
private async refreshImageCache(fileId: string): Promise<void> {
|
||
try {
|
||
const path = await this.downloadFile(fileId)
|
||
imageCache.set(fileId, path)
|
||
const fs = wx.getFileSystemManager()
|
||
fs.getFileInfo({
|
||
filePath: path,
|
||
success: (infoRes) => {
|
||
cacheStats.set(`image_${fileId}`, infoRes.size)
|
||
}
|
||
})
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 获取单词详情
|
||
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 updateUserAvatar(fileId: string): Promise<{ url: string }> {
|
||
const response = await this.request<{ url: string }>('/api/v1/wx/user/avatar', 'PUT', {
|
||
avatar_file_id: fileId
|
||
})
|
||
return response.data
|
||
}
|
||
|
||
// 获取用户头像
|
||
async getUserAvatar(): Promise<{ url: string }> {
|
||
const response = await this.request<{ url: string }>('/api/v1/wx/user/avatar', 'GET')
|
||
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, referrerId?: string): Promise<string> {
|
||
if (USE_CLOUD) {
|
||
try {
|
||
let url = `/api/v1/file/temp_url/${fileId}`
|
||
if (referrerId) {
|
||
url += `?r=${referrerId}`
|
||
}
|
||
const resp = await this.wx_request<{ url: string }>(url, 'GET')
|
||
const tempUrl = (resp as any)?.data?.url || (resp as any)?.url
|
||
if (!tempUrl) {
|
||
throw new Error('无法获取临时下载链接')
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
wx.downloadFile({
|
||
url: tempUrl,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
const fs = wx.getFileSystemManager()
|
||
const tempPath = res.tempFilePath
|
||
const extMatch = tempPath && tempPath.match(/\.[a-zA-Z0-9]+$/)
|
||
if (!extMatch) {
|
||
resolve(tempPath)
|
||
return
|
||
}
|
||
const ext = extMatch[0]
|
||
const filePath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||
try { fs.unlinkSync(filePath) } catch (e) {}
|
||
fs.saveFile({
|
||
tempFilePath: tempPath,
|
||
filePath,
|
||
success: () => resolve(filePath),
|
||
fail: () => resolve(tempPath)
|
||
})
|
||
} else {
|
||
reject(new Error(`文件下载失败,状态码: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => reject(new Error('文件下载失败'))
|
||
})
|
||
})
|
||
} catch (e) {
|
||
throw e
|
||
}
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
let fileUrl = `${BASE_URL}/api/v1/file/${fileId}`
|
||
if (referrerId) {
|
||
fileUrl += `?ReferrerId=${referrerId}`
|
||
}
|
||
const headers = this.getHeaders()
|
||
wx.downloadFile({
|
||
url: fileUrl,
|
||
header: headers,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
const fs = wx.getFileSystemManager()
|
||
const tempPath = res.tempFilePath
|
||
const extMatch = tempPath && tempPath.match(/\.[a-zA-Z0-9]+$/)
|
||
if (!extMatch) {
|
||
resolve(tempPath)
|
||
return
|
||
}
|
||
const ext = extMatch[0]
|
||
const filePath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||
try { fs.unlinkSync(filePath) } catch (e) {}
|
||
fs.saveFile({
|
||
tempFilePath: tempPath,
|
||
filePath,
|
||
success: () => resolve(filePath),
|
||
fail: () => resolve(tempPath)
|
||
})
|
||
} else if (res.statusCode === 401) {
|
||
this.handleTokenExpired()
|
||
reject(new Error('认证失败,请重新登录'))
|
||
} else {
|
||
reject(new Error(`文件下载失败,状态码: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => reject(new Error('文件下载失败'))
|
||
})
|
||
})
|
||
}
|
||
|
||
// 获取图片的 file_id
|
||
async getImageFileId(imageId: string): Promise<string> {
|
||
const resp = await this.request<{ file_id: string }>(`/api/v1/image/${imageId}/file_id`, 'GET')
|
||
return resp.data.file_id
|
||
}
|
||
|
||
// 将文件ID转换为可显示的图片URL
|
||
async getFileDisplayUrl(fileId: string, referrerId?: string): Promise<string> {
|
||
try {
|
||
// 首先检查图片缓存中是否已有该文件
|
||
if (imageCache.has(fileId)) {
|
||
const cachedFilePath = imageCache.get(fileId)!;
|
||
console.log('使用缓存的图片文件:', cachedFilePath);
|
||
return cachedFilePath;
|
||
}
|
||
|
||
// 下载文件
|
||
const filePath = await this.downloadFile(fileId, referrerId);
|
||
|
||
// 将文件路径缓存起来
|
||
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') || 'level1'
|
||
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, available_balance: 0, frozen_balance: 0, total_purchased: 0, total_refunded: 0, expired_time: '', is_subscribed: false, subscription_expires_at: null as any };
|
||
}
|
||
const d: any = response.data as any
|
||
const normalized = {
|
||
balance: Number(d.balance || 0),
|
||
available_balance: Number(d.available_balance || d.balance || 0),
|
||
frozen_balance: Number(d.frozen_balance || 0),
|
||
total_purchased: Number(d.total_purchased || 0),
|
||
total_refunded: Number(d.total_refunded || 0),
|
||
expired_time: String(d.expired_time || ''),
|
||
is_subscribed: !!d.is_subscribed,
|
||
subscription_expires_at: d.subscription_expires_at || null
|
||
}
|
||
return normalized as import("../types/app").IPointsData;
|
||
} 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;
|
||
}
|
||
}
|
||
|
||
async createJsapiOrder(product_id: number): Promise<{
|
||
out_trade_no: string
|
||
prepay_id: string
|
||
trade_state: string
|
||
timeStamp?: string
|
||
nonceStr?: string
|
||
paySign?: string
|
||
signType?: string
|
||
}> {
|
||
const response = await this.request<{
|
||
out_trade_no: string
|
||
prepay_id: string
|
||
trade_state: string
|
||
timeStamp?: string
|
||
nonceStr?: string
|
||
paySign?: string
|
||
signType?: string
|
||
}>('/api/v1/wxpay/order/jsapi', 'POST', {
|
||
product_id
|
||
})
|
||
return response.data
|
||
}
|
||
|
||
async createSubscriptionOrder(product_id: string): Promise<{
|
||
out_trade_no: string
|
||
prepay_id: string
|
||
trade_state: string
|
||
timeStamp?: string
|
||
nonceStr?: string
|
||
paySign?: string
|
||
signType?: string
|
||
}> {
|
||
const response = await this.request<{
|
||
out_trade_no: string
|
||
prepay_id: string
|
||
trade_state: string
|
||
timeStamp?: string
|
||
nonceStr?: string
|
||
paySign?: string
|
||
signType?: string
|
||
}>('/api/v1/wxpay/order/jsapi/subscription', 'POST', {
|
||
product_id
|
||
})
|
||
return response.data
|
||
}
|
||
|
||
async getProductList(): Promise<Array<{
|
||
id: number
|
||
title: string
|
||
description: string
|
||
points: number
|
||
amount_cents: number
|
||
one_time: boolean
|
||
}>> {
|
||
const response = await this.request<Array<{
|
||
id: number
|
||
title: string
|
||
description: string
|
||
points: number
|
||
amount_cents: number
|
||
one_time: boolean
|
||
}>>('/api/v1/product/list', 'GET')
|
||
return response.data
|
||
}
|
||
|
||
async getSubscriptionPlans(): Promise<Array<{
|
||
id: string
|
||
name: string
|
||
price: number
|
||
cycle_type: string
|
||
cycle_length: number
|
||
max_cycle_usage: number
|
||
features?: {
|
||
label?: string
|
||
extra?: string
|
||
}
|
||
}>> {
|
||
const response = await this.request<Array<{
|
||
id: string
|
||
name: string
|
||
price: number
|
||
cycle_type: string
|
||
cycle_length: number
|
||
max_cycle_usage: number
|
||
features?: {
|
||
label?: string
|
||
extra?: string
|
||
}
|
||
}>>('/api/v1/product/plan', 'GET')
|
||
return response.data
|
||
}
|
||
|
||
async getWxpayOrder(out_trade_no: string): Promise<{
|
||
out_trade_no: string
|
||
trade_state: string
|
||
prepay_id?: string
|
||
}> {
|
||
const response = await this.request<{ out_trade_no: string; trade_state: string; prepay_id?: string }>(`/api/v1/wxpay/order/${out_trade_no}`, 'GET')
|
||
return response.data
|
||
}
|
||
|
||
async listOrders(page: number = 1, size: number = 20): Promise<{ items: Array<{ id: string; created_time: string; amount_cents: number; points?: number | null; product_id?: string | null; refund_status?: string | null }>; total: number }>{
|
||
const response = await this.request<{ items: Array<{ id: string; created_time: string; amount_cents: number; points?: number | null; product_id?: string | null; refund_status?: string | null }>; total: number }>(`/api/v1/wxpay/order/list`, 'POST', { page, size })
|
||
return response.data
|
||
}
|
||
|
||
async getOrderDetail(out_trade_no: string): Promise<{ out_trade_no: string; transaction_id?: string | null; trade_state: string; amount_cents: number; description: string; can_refund?: boolean | null; refund_status?: string | null; refundable_amount_cents?: number | null; refundable_amount_points?: number | null }>{
|
||
const response = await this.request<{ out_trade_no: string; transaction_id?: string | null; trade_state: string; amount_cents: number; description: string; can_refund?: boolean | null; refund_status?: string | null; refundable_amount_cents?: number | null; refundable_amount_points?: number | null }>(`/api/v1/wxpay/order/${out_trade_no}/details`, 'GET')
|
||
return response.data
|
||
}
|
||
|
||
async refund(out_trade_no: string, reason: string, amount_cents?: number): Promise<any> {
|
||
const payload: any = { out_trade_no, reason }
|
||
if (typeof amount_cents === 'number') payload.amount_cents = amount_cents
|
||
const response = await this.request<any>(`/api/v1/wxpay/refund`, 'POST', payload)
|
||
return response.data
|
||
}
|
||
|
||
async getRefundStatus(out_refund_no: string): Promise<{ out_refund_no: string; status: string }>{
|
||
const response = await this.request<{ out_refund_no: string; status: string }>(`/api/v1/wxpay/refund/${out_refund_no}`, 'GET')
|
||
return response.data
|
||
}
|
||
|
||
async getSceneSentence(imageId: string): Promise<any> {
|
||
const resp = await this.request<any>(`/api/v1/scene/image/${imageId}/sentence`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async getRecordResult(textId: string | number): Promise<any> {
|
||
const resp = await this.request<any>(`/api/v1/image_text/${textId}/record_result`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async createScene(imageId: string, scene_type: string = 'scene_sentence'): Promise<{ task_id: string; status?: string }> {
|
||
const resp = await this.request<{ task_id: string; status?: string }>(`/api/v1/scene/create`, 'POST', {
|
||
image_id: imageId,
|
||
scene_type
|
||
})
|
||
return resp.data
|
||
}
|
||
|
||
async getSceneTask(taskId: string | number): Promise<{ status: string; message?: string }> {
|
||
const resp = await this.request<{ status: string; message?: string }>(`/api/v1/scene/task/${taskId}`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async createQaExerciseTask(imageId: number | string, type?: string): Promise<IQaExerciseCreateAccepted> {
|
||
const payload: Record<string, any> = { image_id: imageId }
|
||
if (type !== undefined) payload.type = type
|
||
const resp = await this.request<IQaExerciseCreateAccepted>(`/api/v1/qa/exercises/tasks`, 'POST', payload)
|
||
return resp.data
|
||
}
|
||
|
||
async getQaExerciseTaskStatus(taskId: string): Promise<IQaExerciseTaskStatus> {
|
||
const resp = await this.request<IQaExerciseTaskStatus>(`/api/v1/qa/exercises/tasks/${taskId}/status`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async listQaExercisesByImage(imageId: string, type?: string): Promise<IQaExerciseQueryResponse | null> {
|
||
let url = `/api/v1/qa/${imageId}/exercises`
|
||
if (type) {
|
||
url += `?type=${type}`
|
||
}
|
||
const resp = await this.request<IQaExerciseQueryResponse | null>(url, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async getQaConversationSetting(imageId: string): Promise<IQaConversationSettingResponse> {
|
||
const resp = await this.request<IQaConversationSettingResponse>('/api/v1/qa/conversations/setting', 'POST', {
|
||
image_id: imageId
|
||
})
|
||
return resp.data
|
||
}
|
||
|
||
async startQaConversation(payload: {
|
||
image_id: string | number
|
||
scene?: Array<{ en: string; zh: string }>
|
||
event?: Array<{ en: string; zh: string }>
|
||
user_role?: { en: string; zh: string }
|
||
assistant_role?: { en: string; zh: string }
|
||
level?: string
|
||
info?: string
|
||
style?: { en: string; zh: string }
|
||
}): Promise<any> {
|
||
const resp = await this.request<any>('/api/v1/qa/conversations/start', 'POST', payload)
|
||
return resp.data
|
||
}
|
||
|
||
async getQaConversationDetail(sessionId: string | number): Promise<IQaConversationDetail> {
|
||
const resp = await this.request<IQaConversationDetail>(`/api/v1/qa/conversations/${sessionId}`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async getQaConversationLatest(sessionId: string | number): Promise<IQaConversationDetail> {
|
||
const resp = await this.request<IQaConversationDetail>(`/api/v1/qa/conversations/${sessionId}/latest`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async listQaConversations(imageId: string | number, page: number = 1, pageSize: number = 10): Promise<any> {
|
||
const resp = await this.request<any>(`/api/v1/qa/conversations/${imageId}/list?page=${page}&page_size=${pageSize}`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async replyQaConversation(sessionId: string | number, content: string): Promise<any> {
|
||
const resp = await this.request<any>(`/api/v1/qa/conversations/${sessionId}/reply`, 'POST', {
|
||
content
|
||
})
|
||
return resp.data
|
||
}
|
||
|
||
async createQaQuestionAttempt(
|
||
questionId: string,
|
||
payload: {
|
||
exercise_id: string
|
||
mode: 'choice' | 'free_text' | 'cloze'
|
||
selected_options?: string[]
|
||
input_text?: string
|
||
file_id?: string
|
||
is_trial?: boolean
|
||
}
|
||
): Promise<IQaQuestionAttemptAccepted> {
|
||
const resp = await this.request<IQaQuestionAttemptAccepted>(`/api/v1/qa/questions/${questionId}/attempts`, 'POST', payload)
|
||
return resp.data
|
||
}
|
||
|
||
async getQaQuestionTaskStatus(taskId: string): Promise<IQaQuestionTaskStatus> {
|
||
const resp = await this.request<IQaQuestionTaskStatus>(`/api/v1/qa/question-tasks/${taskId}/status`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async getQaResult(questionId: string): Promise<IQaResult> {
|
||
const resp = await this.request<IQaResult>(`/api/v1/qa/questions/${questionId}/result`, 'GET')
|
||
return resp.data
|
||
}
|
||
|
||
async getQaQuestionAudio(questionId: string | number): Promise<{ file_id: string | number }> {
|
||
const resp = await this.request<{ file_id: string | number }>(`/api/v1/qa/questions/${questionId}/audio`, 'GET')
|
||
return resp.data
|
||
}
|
||
}
|
||
|
||
// 导出单例
|
||
const apiManager = new ApiManager()
|
||
export default apiManager
|
||
export { ApiManager }
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|