so使用原则(要么不支持,要么全部支持) 固定的运行平台 指定目录:System.load("/data/data/<package-name>/mydir/libmath.so");

加载.so文件实践
本文介绍如何指定目录加载.so文件,并强调了为不同ABI提供优化过的.so文件的重要性。

指定目录
System.load("/data/data/<package-name>/mydir/libmath.so");

你应该尽可能的提供专为每个ABI优化过的.so文件,但要么都不支持, 要么全部支持
<think> 根据错误日志,问题出现在应用启动时无法加载页面文件 `__uniappview.html`,这通常是 UniApp 资源文件缺失或路径错误导致的。同时,您的错误处理逻辑检测到 `window` 对象不可用(在 App 环境中正常),并回退使用了 `uni.onError`。 ### 解决方案 #### 1. 修复页面加载失败问题(核心问题) **检查资源文件完整性:** ```bash # 清除缓存并重新构建 npm run clean npm install npm run build ``` **修复文件路径引用:** ```javascript // main.js 或 App.vue import Vue from 'vue' import App from './App' // 确保所有资源使用相对路径 Vue.config.base = './' // 设置基础路径 // 修复图片等静态资源引用 Vue.prototype.$static = function(path) { if (process.env.NODE_ENV === 'development') { return require(`@/static/${path}`) } else { return `./static/${path}` } } // 手动检查页面是否可访问 if (typeof plus !== 'undefined') { plus.io.resolveLocalFileSystemURL('_www/__uniappview.html', () => console.log('页面文件存在'), (e) => { console.error('页面文件缺失', e) // 重新加载应用 plus.runtime.restart() } ) } ``` #### 2. 增强错误处理逻辑 ```javascript // utils/errorHandler.js export default { install() { const handleError = (error, context = '') => { console.error(`[${context}]`, error) // 记录错误日志 const log = { time: new Date().toISOString(), platform: uni.getSystemInfoSync().platform, error: { message: error.message, stack: error.stack } } // 存储错误日志(使用本地存储) try { const logs = uni.getStorageSync('errorLogs') || [] logs.push(log) uni.setStorageSync('errorLogs', logs) } catch (e) {} // 显示用户友好的提示 uni.showModal({ title: '应用遇到问题', content: '抱歉,应用运行遇到问题,请尝试重启', showCancel: false }) } // 全局错误处理 if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('error', (e) => handleError(e.error, 'Global')) window.addEventListener('unhandledrejection', (e) => handleError(e.reason, 'Promise')) } else { // App 环境处理 uni.onError((error) => handleError(error, 'UniApp')) if (typeof plus !== 'undefined') { plus.globalEvent.addEventListener('error', (e) => { handleError(new Error(e.message), '5+ Runtime') }) } } } } // main.js 中注册 import errorHandler from '@/utils/errorHandler' Vue.use(errorHandler) ``` #### 3. 添加页面加载失败专用处理 ```javascript // App.vue export default { onLaunch() { // 监听页面加载失败事件 if (typeof plus !== 'undefined') { plus.webview.currentWebview().addEventListener('error', (e) => { console.error('页面加载失败', e) this.handlePageLoadError(e) }) } }, methods: { handlePageLoadError(error) { // 显示自定义错误页面 uni.redirectTo({ url: '/pages/error/load-fail' }) // 上报错误 uni.request({ url: 'https://your-api.com/error-report', method: 'POST', data: { type: 'page_load_fail', url: error.url, message: error.message } }) } } } ``` #### 4. 创建错误页面 ```vue <!-- /pages/error/load-fail.vue --> <template> <view class="error-container"> <image src="/static/error-icon.png" class="error-icon" /> <text class="error-title">页面加载失败</text> <text class="error-desc">抱歉,应用启动时遇到问题</text> <button class="retry-btn" @click="reloadApp">重试</button> <button class="retry-btn" @click="clearCache">清除缓存并重试</button> <text class="error-code">错误代码: {{ errorCode }}</text> </view> </template> <script> export default { data() { return { errorCode: 'ERR_PAGE_LOAD' } }, methods: { reloadApp() { if (typeof plus !== 'undefined') { plus.runtime.restart() } else { location.reload() } }, async clearCache() { try { // 清除缓存 await uni.clearStorage() await uni.removeStorage({ key: 'cache_data' }) // 删除文件缓存 if (typeof plus !== 'undefined') { plus.cache.clear() } this.reloadApp() } catch (e) { uni.showToast({ title: '清除缓存失败', icon: 'none' }) } } } } </script> ``` ### 关键问题分析 1. **页面加载失败原因**: - 资源文件未正确打包到应用中 - 文件路径引用错误(绝对路径 vs 相对路径) - 应用缓存损坏 - 设备存储空间不足 2. **环境检测优化**: ```javascript // 更可靠的环境检测 const isH5 = typeof window !== 'undefined' && window.document const isApp = typeof plus !== 'undefined' && plus.os.name const isWeapp = typeof wx !== 'undefined' && wx.getSystemInfoSync console.log(`当前环境: H5=${isH5}, App=${isApp}, 小程序=${isWeapp}`) ``` 3. **资源完整性检查**: ```javascript // 在应用启动时检查关键资源 const checkResources = async () => { const requiredFiles = [ '__uniappview.html', 'static/js/vendor.js', 'static/css/app.css' ] for (const file of requiredFiles) { try { await uni.getFileInfo({ filePath: file }) } catch (e) { console.error(`文件缺失: ${file}`, e) uni.redirectTo({ url: '/pages/error/file-missing' }) return false } } return true } // App.vue 的 onLaunch 中调用 onLaunch() { checkResources().then(valid => { if (!valid) return // 继续应用初始化 }) } ``` ### 调试建议 1. **真机调试工具**: ```javascript // 添加调试菜单 uni.onWindowResize(() => { if (uni.getSystemInfoSync().windowHeight < 50) { // 顶部下滑触发调试菜单 uni.navigateTo({ url: '/pages/debug/console' }) } }) ``` 2. **日志增强**: ```javascript // 重写 console 方法 const originalConsole = { ...console } console.error = (...args) => { originalConsole.error(...args) uni.sendNativeEvent('LOG_ERROR', { message: args.join(' ') }) } ``` 3. **资源监控**: ```javascript // 监控文件系统变化 if (typeof plus !== 'undefined') { plus.io.addEventListener('fschange', (e) => { console.log('文件系统变化:', e) if (e.path.includes('__uniappview.html')) { console.warn('关键文件被修改') } }) } ``` §§相关问题§§: 1. 在 UniApp 中如何正确管理静态资源路径以适应不同平台? 2. 如何实现 UniApp 应用的自动更新和热修复机制? 3. UniApp 在 App 环境中如何处理文件系统权限和存储空间不足的问题? 4. 如何设计一个完整的 UniApp 错误监控和上报系统? </think> 我注意到错误信息显示页面加载失败,并且应用无法跳转页面。这通常是由于核心资源缺失或路由系统故障导致的。以下是一个完整的解决方案,专门处理 UniApp 应用启动时页面加载失败和路由失效的问题: ### 核心解决方案:页面加载失败与路由修复 #### 1. 路由系统增强与容错处理 ```javascript // utils/router.js let isRouterReady = false; let pendingRoutes = []; // 增强路由方法 const enhanceRouter = () => { const originalNavigateTo = uni.navigateTo; const originalRedirectTo = uni.redirectTo; const originalReLaunch = uni.reLaunch; const originalSwitchTab = uni.switchTab; const routerWrapper = (method) => { return (options) => { return new Promise((resolve, reject) => { if (!isRouterReady) { console.warn('路由未就绪,加入待处理队列'); pendingRoutes.push({ method, options, resolve, reject }); return; } // 实际执行路由跳转 method.call(uni, { ...options, success: (res) => { resolve(res); }, fail: (err) => { console.error(`路由跳转失败: ${method.name}`, err); handleRouteFailure(options.url, err).then(resolve).catch(reject); } }); }); }; }; uni.navigateTo = routerWrapper(originalNavigateTo); uni.redirectTo = routerWrapper(originalRedirectTo); uni.reLaunch = routerWrapper(originalReLaunch); uni.switchTab = routerWrapper(originalSwitchTab); }; // 处理路由失败 const handleRouteFailure = (url, error) => { return new Promise((resolve) => { console.error(`跳转页面失败: ${url}`, error); // 尝试修复路由 setTimeout(async () => { try { // 1. 检查页面是否注册 const pages = getCurrentPages(); const pageExists = pages.some(page => page.route === url); if (!pageExists) { // 尝试动态注册页面 await registerPage(url); } // 2. 重新尝试跳转 uni.reLaunch({ url }); resolve(); } catch (e) { console.error('路由修复失败', e); // 显示错误页面 uni.reLaunch({ url: '/pages/error/route-fail?url=' + encodeURIComponent(url) }); resolve(); } }, 500); }); }; // 动态注册页面 const registerPage = async (path) => { console.log(`尝试动态注册页面: ${path}`); // 获取页面组件路径 const pagePath = path.startsWith('/') ? path : `/${path}`; // 动态加载页面组件 try { const component = await import(`@/pages${pagePath}`); // 注册页面 const pageConfig = { path: pagePath, component: component.default || component }; // 添加到路由配置 const appConfig = getApp().$router; appConfig.routes.push(pageConfig); console.log(`页面动态注册成功: ${pagePath}`); return true; } catch (e) { console.error(`动态注册页面失败: ${pagePath}`, e); throw new Error('PAGE_NOT_FOUND'); } }; // 初始化路由系统 export const initRouter = () => { enhanceRouter(); // 标记路由就绪 setTimeout(() => { isRouterReady = true; // 处理待处理的路由 pendingRoutes.forEach(({ method, options, resolve }) => { method(options).then(resolve); }); pendingRoutes = []; }, 1000); }; // main.js中初始化 import { initRouter } from '@/utils/router'; initRouter(); ``` #### 2. 核心文件完整性检查与修复 ```javascript // utils/fileIntegrity.js export const checkCoreFiles = async () => { const requiredFiles = [ '__uniappview.html', 'static/js/manifest.js', 'static/js/vendor.js', 'static/js/app.js' ]; const results = await Promise.allSettled( requiredFiles.map(file => checkFile(file)) ); return results.every(result => result.status === 'fulfilled' && result.value); }; const checkFile = (filePath) => { return new Promise((resolve, reject) => { plus.io.resolveLocalFileSystemURL( `_www/${filePath}`, (entry) => { if (entry.isFile) { entry.file( (file) => { if (file.size > 100) { resolve(true); } else { console.error(`文件大小异常: ${filePath} (${file.size} bytes)`); reject(new Error('INVALID_FILE_SIZE')); } }, (err) => { console.error(`读取文件失败: ${filePath}`, err); reject(err); } ); } else { console.error(`路径不是文件: ${filePath}`); reject(new Error('NOT_A_FILE')); } }, (err) => { console.error(`文件不存在: ${filePath}`, err); reject(new Error('FILE_NOT_FOUND')); } ); }); }; export const repairCoreFiles = async () => { console.log('开始修复核心文件...'); const filesToRepair = [ { path: '__uniappview.html', url: 'https://your-cdn.com/app/__uniappview.html' }, { path: 'static/js/manifest.js', url: 'https://your-cdn.com/app/static/js/manifest.js' }, { path: 'static/js/vendor.js', url: 'https://your-cdn.com/app/static/js/vendor.js' }, { path: 'static/js/app.js', url: 'https://your-cdn.com/app/static/js/app.js' } ]; for (const file of filesToRepair) { try { await downloadAndSaveFile(file.url, file.path); console.log(`文件修复成功: ${file.path}`); } catch (e) { console.error(`文件修复失败: ${file.path}`, e); return false; } } return true; }; const downloadAndSaveFile = (url, path) => { return new Promise((resolve, reject) => { const downloadTask = uni.downloadFile({ url, success: (downloadResult) => { if (downloadResult.statusCode === 200) { plus.io.resolveLocalFileSystemURL( '_www', (rootDir) => { rootDir.getFile( path, { create: true, exclusive: false }, (fileEntry) => { fileEntry.createWriter( (fileWriter) => { fileWriter.write(downloadResult.tempFilePath); fileWriter.onwriteend = () => resolve(); fileWriter.onerror = (e) => { console.error('文件写入失败', e); reject(e); }; }, (err) => { console.error('创建文件写入器失败', err); reject(err); } ); }, (err) => { console.error('获取文件失败', err); reject(err); } ); }, (err) => { console.error('解析根目录失败', err); reject(err); } ); } else { reject(new Error(`下载失败: ${downloadResult.statusCode}`)); } }, fail: (err) => { console.error('下载文件失败', err); reject(err); } }); // 监控下载进度 downloadTask.onProgressUpdate((res) => { uni.$emit('file_download_progress', { path, progress: res.progress }); }); }); }; // App.vue中调用 onLaunch() { this.checkAndRepairFiles(); }, methods: { async checkAndRepairFiles() { try { const isFilesValid = await checkCoreFiles(); if (!isFilesValid) { uni.showLoading({ title: '正在修复应用文件...', mask: true }); const repairSuccess = await repairCoreFiles(); uni.hideLoading(); if (repairSuccess) { uni.showToast({ title: '修复完成,即将重启', icon: 'success', duration: 1500 }); setTimeout(() => plus.runtime.restart(), 2000); } else { uni.showModal({ title: '修复失败', content: '无法修复应用文件,请重新安装应用', showCancel: false }); } } } catch (e) { console.error('文件检查失败', e); } } } ``` #### 3. 应用状态监控与自愈系统 ```javascript // utils/appMonitor.js let appHealthTimer = null; const HEALTH_CHECK_INTERVAL = 30000; // 30秒 export const startAppMonitoring = () => { // 停止之前的监控 if (appHealthTimer) clearInterval(appHealthTimer); appHealthTimer = setInterval(() => { checkAppHealth().then(isHealthy => { if (!isHealthy) { handleAppUnhealthy(); } }); }, HEALTH_CHECK_INTERVAL); // 添加事件监听 uni.$on('app_freeze_detected', handleAppFreeze); uni.$on('memory_overflow', handleMemoryOverflow); }; const checkAppHealth = async () => { try { // 1. 检查页面堆栈 const pages = getCurrentPages(); if (pages.length === 0) { console.error('页面堆栈为空'); return false; } // 2. 检查路由状态 const currentRoute = pages[pages.length - 1].route; if (!currentRoute) { console.error('当前路由状态异常'); return false; } // 3. 检查内存使用 if (performance && performance.memory) { const usedMB = performance.memory.usedJSHeapSize / 1024 / 1024; const totalMB = performance.memory.jsHeapSizeLimit / 1024 / 1024; if (usedMB > totalMB * 0.85) { uni.$emit('memory_overflow', { usedMB, totalMB }); return false; } } // 4. 检查文件系统 const fsStatus = await checkFileSystem(); if (!fsStatus.healthy) { console.error('文件系统异常', fsStatus.error); return false; } return true; } catch (e) { console.error('健康检查失败', e); return false; } }; const checkFileSystem = () => { return new Promise((resolve) => { plus.io.requestFileSystem(plus.io.PRIVATE_WWW, (fs) => { try { // 尝试创建测试文件 fs.root.getFile( '__healthcheck.tmp', { create: true, exclusive: true }, (fileEntry) => { // 删除测试文件 fileEntry.remove(() => { resolve({ healthy: true }); }, (err) => { resolve({ healthy: false, error: 'DELETE_FAIL' }); }); }, (err) => { resolve({ healthy: false, error: 'CREATE_FAIL' }); } ); } catch (e) { resolve({ healthy: false, error: 'FS_ERROR' }); } }, (err) => { resolve({ healthy: false, error: 'FS_UNAVAILABLE' }); }); }); }; const handleAppUnhealthy = () => { console.warn('应用状态异常,尝试恢复'); // 1. 清除内存缓存 try { uni.clearStorageSync(); if (typeof plus !== 'undefined') { plus.cache.clear(); } } catch (e) {} // 2. 重启Webview const currentPage = getCurrentPages().pop(); if (currentPage) { uni.reLaunch({ url: `/${currentPage.route}` }); } else { uni.reLaunch({ url: '/pages/index/index' }); } }; const handleAppFreeze = () => { console.error('检测到应用卡死,重启应用'); plus.runtime.restart(); }; const handleMemoryOverflow = (data) => { console.warn(`内存使用过高: ${data.usedMB.toFixed(1)}MB/${data.totalMB.toFixed(1)}MB`); // 释放内存 uni.$emit('release_memory'); }; // 主应用启动时调用 onLaunch() { startAppMonitoring(); } ``` #### 4. 错误页面增强版 ```vue <!-- /pages/error/universal-fail.vue --> <template> <view class="error-container"> <!-- 顶部状态栏 --> <view class="status-bar"> <text class="status-text">应用状态异常</text> </view> <!-- 错误内容 --> <view class="error-content"> <image src="/static/error-icon.png" class="error-icon" /> <text class="error-title">页面加载失败</text> <text class="error-desc">抱歉,应用遇到问题无法继续</text> <view class="error-details"> <text class="detail-item">错误代码: {{ errorCode }}</text> <text class="detail-item">页面路径: {{ targetPath }}</text> <text class="detail-item">时间: {{ errorTime }}</text> <text class="detail-item" v-if="storageInfo">存储: {{ storageInfo }}</text> </view> </view> <!-- 操作按钮 --> <view class="action-buttons"> <button class="action-btn primary" @click="reloadApp"> <image src="/static/refresh-icon.png" class="btn-icon" /> <text>重新加载</text> </button> <button class="action-btn" @click="clearCache"> <image src="/static/clear-icon.png" class="btn-icon" /> <text>清除缓存</text> </button> <button class="action-btn" @click="reportProblem"> <image src="/static/report-icon.png" class="btn-icon" /> <text>报告问题</text> </button> <button class="action-btn" @click="goHome"> <image src="/static/home-icon.png" class="btn-icon" /> <text>返回首页</text> </button> </view> <!-- 调试信息 --> <view class="debug-section" @click="toggleDebug"> <text>调试信息 {{ showDebug ? '▲' : '▼' }}</text> </view> <view class="debug-info" v-if="showDebug"> <text v-for="(item, index) in debugInfo" :key="index" class="debug-item"> {{ item }} </text> </view> </view> </template> <script> export default { data() { return { errorCode: 'ERR_ROUTE_FAIL', targetPath: '', errorTime: '', storageInfo: '', showDebug: false, debugInfo: [] }; }, onLoad(options) { this.targetPath = options.url || '未知'; this.errorTime = new Date().toLocaleString(); // 收集调试信息 this.collectDebugInfo(); // 获取存储信息 this.getStorageInfo(); }, methods: { collectDebugInfo() { this.debugInfo = [ `平台: ${uni.getSystemInfoSync().platform}`, `版本: ${plus.runtime.version}`, `设备ID: ${plus.device.uuid || '未知'}`, `网络: ${uni.getNetworkTypeSync().networkType}`, `Webview: ${plus.webview.currentWebview().id}`, `路由栈: ${getCurrentPages().map(p => p.route).join(' > ')}`, `内存: ${performance.memory ? (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1) + 'MB' : '未知'}`, `UA: ${navigator.userAgent}` ]; }, async getStorageInfo() { try { const info = await uni.getStorageInfo(); this.storageInfo = `${(info.currentSize / 1024).toFixed(1)}KB/${(info.limitSize / 1024).toFixed(1)}KB`; } catch (e) { this.storageInfo = '获取失败'; } }, reloadApp() { const pages = getCurrentPages(); if (pages.length > 0) { uni.reLaunch({ url: `/${pages[pages.length - 1].route}` }); } else { plus.runtime.restart(); } }, async clearCache() { uni.showLoading({ title: '清除中...', mask: true }); try { // 清除缓存 await uni.clearStorage(); // 清除文件缓存 if (typeof plus !== 'undefined') { await new Promise((resolve) => plus.cache.clear(resolve)); } uni.showToast({ title: '清除成功', icon: 'success' }); setTimeout(this.reloadApp, 1500); } catch (e) { uni.showToast({ title: '清除失败', icon: 'none' }); } finally { uni.hideLoading(); } }, reportProblem() { const errorData = { type: 'page_load_fail', path: this.targetPath, code: this.errorCode, time: this.errorTime, device: uni.getSystemInfoSync(), debug: this.debugInfo.join('\n') }; uni.navigateTo({ url: '/pages/error/report?data=' + encodeURIComponent(JSON.stringify(errorData)) }); }, goHome() { uni.reLaunch({ url: '/pages/index/index' }); }, toggleDebug() { this.showDebug = !this.showDebug; } } }; </script> <style scoped> .error-container { display: flex; flex-direction: column; height: 100vh; background-color: #f8f8f8; } .status-bar { background-color: #e74c3c; padding: 10px; text-align: center; } .status-text { color: white; font-weight: bold; } .error-content { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .error-icon { width: 80px; height: 80px; margin-bottom: 20px; } .error-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; } .error-desc { color: #666; margin-bottom: 20px; } .error-details { background-color: #fff; border-radius: 8px; padding: 15px; width: 90%; margin-top: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .detail-item { display: block; margin: 5px 0; color: #555; } .action-buttons { display: flex; flex-wrap: wrap; justify-content: center; padding: 15px; } .action-btn { margin: 8px; padding: 10px 15px; border-radius: 20px; background-color: white; display: flex; align-items: center; box-shadow: 0 2px 6px rgba(0,0,0,0.1); } .action-btn.primary { background-color: #3498db; } .action-btn.primary text { color: white; } .btn-icon { width: 20px; height: 20px; margin-right: 8px; } .debug-section { padding: 10px; text-align: center; background-color: #eee; border-top: 1px solid #ddd; } .debug-info { padding: 15px; background-color: #fff; max-height: 200px; overflow-y: auto; } .debug-item { display: block; font-size: 12px; color: #666; margin: 3px 0; word-break: break-all; } </style> ``` ### 关键问题分析 1. **路由系统故障原因**: - 页面组件未正确注册 - 路由配置损坏或丢失 - Webview 实例创建失败 - 内存不足导致路由跳转失败 2. **核心文件加载失败原因**: - 应用更新过程中断 - 存储空间不足导致文件写入失败 - 设备文件系统损坏 - 应用签名变更导致文件访问权限问题 3. **自愈系统设计要点**: - **实时监控**:应用状态、内存、文件系统 - **自动修复**:核心文件、路由配置 - **优雅降级**:在无法恢复时提供用户友好的错误界面 - **数据收集**:自动收集错误信息用于后续分析 ### 优化建议 1. **应用启动加速**: ```javascript // 预加载关键资源 plus.webview.preload({ url: '/pages/index/index', styles: { render: 'always' } }); // 预加载公共组件 const preloadComponents = ['/components/Header.vue', '/components/Footer.vue']; preloadComponents.forEach(path => { import(`@${path}`); }); ``` 2. **内存优化策略**: ```javascript // 释放内存事件监听 uni.$on('release_memory', () => { console.log('释放内存...'); // 1. 清除缓存数据 uni.clearStorageSync(); // 2. 清理未使用的Webview const allWebviews = plus.webview.all(); const currentId = plus.webview.currentWebview().id; allWebviews.forEach(webview => { if (webview.id !== currentId && !webview.opened()) { webview.close('none'); } }); // 3. 触发垃圾回收 if (typeof plus.android !== 'undefined') { plus.android.invoke('java/lang/System', 'gc'); } }); ``` 3. **离线资源包管理**: ```javascript // 检查并应用离线包 const applyOfflinePackage = async () => { const packageInfo = uni.getStorageSync('offline_package'); if (packageInfo && packageInfo.version > currentVersion) { uni.showLoading({ title: '应用更新...' }); try { // 应用离线包中的文件 for (const file of packageInfo.files) { await saveOfflineFile(file.path, file.content); } // 更新应用版本 uni.setStorageSync('app_version', packageInfo.version); uni.showToast({ title: '更新完成,即将重启' }); setTimeout(() => plus.runtime.restart(), 2000); } catch (e) { console.error('应用离线包失败', e); uni.showToast({ title: '更新失败', icon: 'none' }); } } }; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值