diff --git a/backend/deploy/envs/asset.env b/backend/deploy/envs/asset.env index 0ec3d90..f3ff483 100644 --- a/backend/deploy/envs/asset.env +++ b/backend/deploy/envs/asset.env @@ -13,4 +13,4 @@ OSS_ROLE_ARN=acs:ram::1387642798143585:role/top-fans-oss-user # H5 落地页 Base URL (生产环境) # 分享服务生成的落地页链接前缀,生产域名 -LANDING_BASE_URL=https://api.topfans.online +LANDING_BASE_URL=http://topfans.online diff --git a/docker/.env.prod b/docker/.env.prod index 8339a70..ded3463 100644 --- a/docker/.env.prod +++ b/docker/.env.prod @@ -34,7 +34,7 @@ REDIS_DB=0 # ==================== H5 落地页 Base URL ==================== # 分享服务生成的落地页链接前缀 (landingBase + asset_id + from=sharer + s=system_type) # 与 frontend/.env.production 的 VITE_LANDING_BASE_URL 保持一致 -LANDING_BASE_URL=https://api.topfans.online +LANDING_BASE_URL=http://topfans.online # ==================== 镭射卡生成器 ==================== # LASER_GEN_PROVIDER: diff --git a/frontend/App.vue b/frontend/App.vue index 9cf2747..252ed9a 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -108,6 +108,9 @@ export default { }) // #endif + // ★ 新增:清理历史下载的更新包(见 upgrade-popup.vue:_downloads/upgrade_*.wgt 累积) + this.cleanupUpgradePackages(); + this.setPermissions(); // #ifdef APP-PLUS @@ -184,6 +187,62 @@ export default { const globalSocket = getGlobalSocket(); globalSocket.closeAll(); }, + /** + * 清理历史下载的更新包(_downloads/upgrade_*.{wgt,apk,ipa}) + * - uni-upgrade-center-app 插件的 plus.downloader 默认把包放在 _downloads/upgrade_${ts}.{wgt|apk} + * - 升级弹窗关闭/下载中断/未点击安装时,这些临时包不会自动清理,会持续累积 + * - 已通过 saveFile() 持久化的包在 _doc/ 目录下(路径不同),由升级弹窗 checkLocalStoragePackage 控制 + * + * 各平台实际会产生哪些文件: + * - Android 整包更新 → .apk + * - Android wgt 热更新 → .wgt + * - iOS wgt 热更新 → .wgt (iOS 整包 (ipa) 走 App Store,不下载本地) + * - ipa 加入正则仅作防御:防止后端配置错误/silent update 误推到本地 + * + * 启动期清理一次即可(App 正常运行期间只要不离 _downloads/ 目录就不会有新遗留) + * 仅 APP-PLUS 有效 + */ + cleanupUpgradePackages() { + // #ifdef APP-PLUS + try { + const dirUrl = plus.io.convertLocalFileSystemURL("_downloads/"); + plus.io.resolveLocalFileSystemURL( + dirUrl, + (dirEntry) => { + const reader = dirEntry.createReader(); + reader.readEntries( + (entries) => { + entries.forEach((entry) => { + // 命名规则:见 uni-upgrade-center-app/pages/upgrade-popup.vue:_downloads/upgrade_${Date.now()}.{wgt|apk} + // ipa 在标准流程不会出现,但加入正则作防御 + if (!/^upgrade_\d+\.(wgt|apk|ipa)$/i.test(entry.name)) return; + entry.remove( + () => {}, + (e) => + console.warn( + "[UPGRADE-CLEANUP] 删包失败:", + entry.fullPath, + e?.message, + ), + ); + }); + }, + (e) => + console.warn( + "[UPGRADE-CLEANUP] 枚举 _downloads 失败:", + e?.message, + ), + ); + }, + () => { + // _downloads 目录不存在是正常情况(从未下过更新包),静默忽略 + }, + ); + } catch (e) { + console.warn("[UPGRADE-CLEANUP] 初始化失败:", e?.message); + } + // #endif + }, /** * 处理"从后台切回前台"事件 * 通过 storage 中的上次隐藏时间判断本次 onShow 是否由后台返回触发 diff --git a/frontend/composables/useShare.js b/frontend/composables/useShare.js index 3e8e8f9..b142549 100644 --- a/frontend/composables/useShare.js +++ b/frontend/composables/useShare.js @@ -262,6 +262,43 @@ export function useShare(props) { return true; } + // iOS 相册权限预检:仅在用户触发保存动作时调用,不在 App 启动时弹 + // iOS PHPhotoLibrary 授权状态: + // 0=NotDetermined(系统未问) 1=Restricted(家长控制/MDM,用户不能改) + // 2=Denied(用户拒绝) 3=Authorized(允许全部) 4=Limited(iOS14+ 部分照片) + // 返回:true=可以继续保存,false=被拒绝/受限,本次保存终止 + async function ensureIosPhotoAlbumPermission() { + if (typeof plus === 'undefined' || plus.os.name !== 'iOS') return true; + const status = plus.ios.invoke('PHPhotoLibrary', 'authorizationStatus'); + if (status === 3 || status === 4) return true; // Authorized / Limited + if (status === 0) return true; // NotDetermined:由 uni.saveImageToPhotosAlbum 首次调用时系统自动弹 + if (status === 1) { + // Restricted — 引导去设置页也无效,直接 toast 提示 + uni.showToast({ title: '相册权限被限制,无法保存到相册', icon: 'none' }); + return false; + } + // status === 2: Denied — 引导用户去设置页开启 + return new Promise((resolve) => { + uni.showModal({ + title: '相册权限开启提醒', + content: '您还没有开启相册权限,无法保存分享的卡片到相册,请前往设置开启!', + showCancel: false, + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + const app = plus.ios.invoke('UIApplication', 'sharedApplication'); + const setting = plus.ios.invoke('NSURL', 'URLWithString:', 'app-settings:'); + plus.ios.invoke(app, 'openURL:', setting); + plus.ios.deleteObject(setting); + plus.ios.deleteObject(app); + } + // 本次保存终止;用户从设置页回来后需重新触发保存动作 + resolve(false); + }, + }); + }); + } + function startShareTimeout(action) { const ms = SHARE_TIMEOUT[action] || 5000; shareTimeoutTimer.value = setTimeout(() => { @@ -279,6 +316,26 @@ export function useShare(props) { async function pick(action) { if (!(await ensureLogin())) return; + // ★ iOS 相册权限预检:仅在用户触发保存动作时检查,不在 App 启动时弹 + if (action === 'save_image') { + const canProceed = await ensureIosPhotoAlbumPermission(); + if (!canProceed) { + // 跟踪为权限失败(与 fail_app_missing 同类语义) + const userId = getCurrentUser()?.uid; + if (userId) { + await trackShare({ + asset_id: toInt64(typeof props.getAssetId === 'function' ? props.getAssetId() : props.assetId), + sharer_user_id: toInt64(userId), + system_type: systemType.value, + share_target: action, + result: 'fail_permission_denied', + client_ts: Date.now() + }); + } + return; + } + } + state.value = 'composing'; errorMsg.value = ''; diff --git a/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue b/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue index d82129a..075cfe3 100644 --- a/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue +++ b/frontend/uni_modules/uni-upgrade-center-app/pages/upgrade-popup.vue @@ -604,6 +604,10 @@ export default { this.installing = false; this.installed = true; + // ★ 安装成功后立即删除本地包文件,避免 _doc/_downloads 累积 + // (App.vue 启动时也会兜底清理 _downloads/upgrade_*.wgt,但已 save 的 _doc/ 包只能在这里清) + this.cleanupAfterInstall(this.tempFilePath); + // wgt包,安装后会提示 安装成功,是否重启 if (this.isWGT) { // 强制更新安装完成重启 @@ -694,6 +698,44 @@ export default { filePath, }); }, + /** + * 安装成功后清理本地包文件 + * 文件可能位于: + * - _doc/... (用户点过返回/手动 saveFile 后) + * - _downloads/upgrade_*.{wgt|apk} (裸下载,未走 saveFile;Android apk 或 iOS/Android wgt) + * 两种位置都通过 plus.io.resolveLocalFileSystemURL 适配删除 + * 仅 APP-PLUS 有效;Harmony/其他平台跳过(由 App.vue 启动清理兜底) + * + * 注:ipa 整包更新走 isApplicationStore 路径(跳转 App Store),不走 installPackage, + * 不会有 ipa 文件落到本地,这里不需要单独处理 ipa + */ + cleanupAfterInstall(filePath) { + if (!filePath) return; + // #ifdef APP-PLUS + try { + const fileUrl = plus.io.convertLocalFileSystemURL(filePath); + plus.io.resolveLocalFileSystemURL( + fileUrl, + (entry) => { + entry.remove( + () => {}, + (e) => + console.warn( + "[UPGRADE-CLEANUP] 删除已安装包失败:", + filePath, + e?.message, + ), + ); + }, + () => { + // 文件已被系统清理或不存在,正常情况(例如用户在 _downloads/ 启动清理后) + }, + ); + } catch (e) { + console.warn("[UPGRADE-CLEANUP] 清理已安装包异常:", e?.message); + } + // #endif + }, copyDebugInfo() { uni.setClipboardData({ data: this.debugInfo, diff --git a/frontend/utils/constants.js b/frontend/utils/constants.js index 6d09a6a..e8480a8 100644 --- a/frontend/utils/constants.js +++ b/frontend/utils/constants.js @@ -12,7 +12,7 @@ * 一旦两侧不一致,扫描二维码 / 复制链接就会跳错地址 —— 改 env 务必同步后端 env。 */ export const LANDING_BASE = String( - import.meta.env.VITE_API_BASE_URL || 'https://api.topfans.online' + import.meta.env.VITE_API_BASE_URL || 'http://topfans.online' ).replace(/\/+$/, ''); /**