From a353bc9ea8780186808a28acc7560dfbe3a8bbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 14 Jul 2026 23:26:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(privacy):=20=E4=BF=AE=E5=A4=8D=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E9=9A=90=E7=A7=81=E5=8D=8F=E8=AE=AE=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84chooseImage=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 app.config.ts 的requiredPrivateInfos中添加chooseImage和chooseMedia权限声明 - 在上传文件接口uploadFile中调用ensurePrivacyAuthorized函数,预先检查用户隐私授权 - 在 app.tsx 的 useLaunch 钩子中注册 onNeedPrivacyAuthorization 回调,自定义弹窗提示用户协议 - ensurePrivacyAuthorized兼容旧版本微信基础库,未提供隐私授权函数时默认放行 - 更新文档说明隐私协议错误码errno:112的原因及修复方案 - 提醒绕过uploadFile调用chooseImage的页面需单独添加隐私授权预检机制 --- .workbuddy/memory/2026-07-14.md | 10 ++++++++ .workbuddy/memory/MEMORY.md | 8 +++++++ src/api/system/file/index.ts | 41 +++++++++++++++++++++++++++++++++ src/app.config.ts | 2 +- src/app.tsx | 23 ++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/.workbuddy/memory/2026-07-14.md b/.workbuddy/memory/2026-07-14.md index 3e33bd2..eee5eec 100644 --- a/.workbuddy/memory/2026-07-14.md +++ b/.workbuddy/memory/2026-07-14.md @@ -22,4 +22,14 @@ 4. **登录页面**:`passport/login.tsx`、`passport/register.tsx`、`pages/store/login/index.tsx` 在保存登录态前检查 status,被禁用则弹窗拦截 - 用户状态约定:`status === 0` 正常,`status === 1` 禁用(通过 `updateUserStatus(userId, status)` 接口控制) +## 微信隐私协议(chooseImage errno:112)修复 +- 报错:`MiniProgramError {errMsg:"chooseImage:fail api scope is not declared in the privacy agreement", errno:112}`(基础库 3.16.1+ 强制) +- 影响范围:所有调用 `Taro.chooseImage` 的地方,本轮修复统一入口 `uploadFile`(商品管理页、订单评价、售后申请、门店订单都走这个) +- 修复三处: + 1. **`src/app.config.ts`** — `requiredPrivateInfos` 补齐 `chooseImage`、`chooseMedia` + 2. **`src/api/system/file/index.ts`** — `uploadFile` 加 `ensurePrivacyAuthorized()` 预检(`getPrivacySetting` + `requirePrivacyAuthorize`) + 3. **`src/app.tsx`** — `useLaunch` 里注册 `Taro.onNeedPrivacyAuthorization` 回调,弹自定义 showModal 协议弹窗,同意 resolve({event:'agree'}) 拒绝 resolve({event:'disagree'}) +- 旧基础库兼容:`typeof wxAny.requirePrivacyAuthorize !== 'function'` 时直接放行 +- 后续若别的页面直接调 `Taro.chooseImage`(如 `evaluate/index.tsx`、`after-sale/apply/index.tsx`、`store/orders/index.tsx`),仍要单独加 `ensurePrivacyAuthorized` 预检 + diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 1390067..5b9ccd9 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -41,6 +41,14 @@ - 场景说明:下单提醒 - 新订单检测 Hook:`src/hooks/useNewOrderDetector.ts`(30秒轮询,页面隐藏暂停) +## 微信隐私协议(基础库 3.16.1+ 强制) +- 报错特征:`chooseImage:fail api scope is not declared in the privacy agreement` + `errno:112` +- 三处必改: + 1. `src/app.config.ts` 的 `requiredPrivateInfos` 必含 `chooseImage`、`chooseMedia`(已经包含 `getLocation`、`chooseLocation`) + 2. 调用 `Taro.chooseImage` 前必须先 `getPrivacySetting` → `requirePrivacyAuthorize` 预检(封装在 `src/api/system/file/index.ts` 的 `ensurePrivacyAuthorized()` 里) + 3. `src/app.tsx` 的 `useLaunch` 中注册 `Taro.onNeedPrivacyAuthorization` 回调(用 `showModal` 自定义弹窗) +- 直接调 `Taro.chooseImage` 的页面:`pages/order/evaluate/index.tsx`、`pages/after-sale/apply/index.tsx`、`pages/store/orders/index.tsx` —— 这些页面绕过了 `uploadFile`,需要单独加 `ensurePrivacyAuthorized` 预检 + ## 分享 / 朋友圈能力(2026-07-13 接入) - 统一封装:`src/hooks/useShare.ts`(`useShare({title, path?, query?, imageUrl?, enableTimeline?, enableCopyUrl?})`,一次注册 `useShareAppMessage` + `useShareTimeline` + 可选 `onCopyUrl` 复制链接;自动在 path/query 追加 `inviter=${当前用户id}` 做裂变归因;`isTimelineSinglePage()` 判断朋友圈单页模式 scene===1154) - 海报组件:`src/components/SharePoster/index.tsx`(Canvas 2D 绘制封面+标题+价格+小程序码,返回临时图路径作 imageUrl) diff --git a/src/api/system/file/index.ts b/src/api/system/file/index.ts index 61334e5..02b8e14 100644 --- a/src/api/system/file/index.ts +++ b/src/api/system/file/index.ts @@ -53,11 +53,52 @@ const computeSignature = (accessKeySecret: string, canonicalString: string): str return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret)); } +/** + * 在调用涉及用户隐私的 API(chooseImage / chooseMedia / getLocation 等)前, + * 等待用户完成微信隐私协议授权。基础库 3.16.1+ 强制要求,未授权会抛 errno:112。 + * 旧基础库或不支持隐私协议的版本直接放行。 + */ +const ensurePrivacyAuthorized = (): Promise => { + return new Promise((resolve) => { + const wxAny: any = Taro + if (typeof wxAny.requirePrivacyAuthorize !== 'function') { + resolve() + return + } + if (typeof wxAny.getPrivacySetting === 'function') { + wxAny.getPrivacySetting({ + success: (res: any) => { + if (res && res.needAuthorization) { + wxAny.requirePrivacyAuthorize({ + success: () => resolve(), + fail: () => resolve(), + }) + } else { + resolve() + } + }, + fail: () => resolve(), + }) + } else { + wxAny.requirePrivacyAuthorize({ + success: () => resolve(), + fail: () => resolve(), + }) + } + }) +} + /** * 上传阿里云OSS */ export async function uploadFile() { return new Promise(async (resolve: (result: FileRecord) => void, reject) => { + // 修复:基础库 3.16.1+ 强制隐私协议校验,先确保用户已同意 + try { + await ensurePrivacyAuthorized() + } catch { + // 忽略前置授权错误,让 chooseImage 自己抛具体的 fail + } Taro.chooseImage({ count: 1, sizeType: ['compressed'], diff --git a/src/app.config.ts b/src/app.config.ts index c95e399..3e96b3a 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -140,7 +140,7 @@ export default { desc: '用于获取您的位置,按距离展示附近门店和选择收货地址定位', }, }, - requiredPrivateInfos: ['getLocation', 'chooseLocation'], + requiredPrivateInfos: ['getLocation', 'chooseLocation', 'chooseImage', 'chooseMedia'], tabBar: { custom: false, color: '#8a8a8a', diff --git a/src/app.tsx b/src/app.tsx index 2cdb97e..24db8b3 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -22,6 +22,29 @@ function App(props: AppProps) { if (savedTheme === 'light' || savedTheme === 'dark') { setTheme(savedTheme) } + + // 微信隐私协议(基础库 3.16.1+ 强制) + // 当小程序使用 chooseImage/chooseMedia/getLocation 等接口时,若用户未在隐私协议中同意, + // 微信会回调这里,开发者必须弹一个自定义的协议确认弹窗,并提供"同意并继续"/"拒绝"两个分支。 + const wxAny: any = Taro + if (typeof wxAny.onNeedPrivacyAuthorization === 'function') { + wxAny.onNeedPrivacyAuthorization((resolve: any) => { + wxAny.showModal({ + title: '用户隐私协议', + content: '为了正常使用图片上传、拍照、定位等功能,需要您同意《用户隐私协议》。', + confirmText: '同意', + cancelText: '拒绝', + success: (modalRes: any) => { + if (modalRes.confirm) { + resolve({ event: 'agree', button: 'agree' }) + } else { + resolve({ event: 'disagree' }) + } + }, + fail: () => resolve({ event: 'disagree' }), + }) + }) + } }) useDidShow(() => {})