小程序隐私弹窗上线后投诉归零:wx.getPrivacySetting 与接口授权时序

2026-10-03 01:22:36 0 次浏览
微信小程序隐私协议wx.getPrivacySetting合规开发前端开发

适用读者:负责微信小程序迭代的前端开发、被平台合规抽查搞得焦头烂额的小程序负责人、以及正在接手别人老项目、发现 chooseImage 一调用就 fail 的接盘侠。

上个月我们一个跑了三年的工具类小程序收到平台合规整改通知,随后一周里 chooseImage、getLocation 这类接口开始直接返回 fail,用户投诉从每天三五条涨到近五十条。查了两天日志才发现,问题根本不在代码逻辑,而是老项目从来没接过隐私协议授权体系。把 wx.getPrivacySetting 那套时序补上之后,接口恢复正常,投诉第二天就归零。这篇文章把整个改造过程完整拆开讲。

出事之前:接口为什么突然 fail 了

微信在基础库 2.32.3 之后逐步强制执行隐私协议机制。平台会做合规抽查,抽查到你后台没配置「用户隐私保护指引」,或者代码里没实现授权弹窗,就会触发整改期。整改期内,所有被标记为隐私接口的 API 直接调用会失败,err_msg 里带着 privacy permission is not authorized 之类的字样。

小程序隐私协议授权时序

我们的日志里是这个样子:

wx.chooseImage fail: chooseImage:fail api scope is not declared in the privacy agreement

受影响的不止一个接口。我们项目里中招的有:

接口 使用的场景 涉及的隐私类型
wx.chooseImage 用户上传头像和凭证截图 选中的文件
wx.getLocation 订单页展示附近门店 位置信息
wx.chooseAddress 收货地址快捷填充 位置信息
wx.getUserProfile 会员页昵称头像 用户信息

以前这些接口只需要配合 scope 授权(比如 scope.userLocation)就能跑。现在多了一层:得先让用户同意隐私协议,然后才轮到 scope 授权这一步。两层授权顺序不能反,反了就 fail。

机制剖析:平台为什么要这样设计

这一节讲原理,搞清楚运作机制,后面排错会省事很多。

微信小程序生态里用户隐私数据的实际持有方是小程序开发者,平台侧能做的监管手段有限。所以微信把合规动作拆成了三层,用技术手段把「开发者承诺」和「用户知情」绑死在调用链上:

  1. 后台配置层:开发者在小程序管理后台填写「用户隐私保护指引」,声明你会收集哪些类型的隐私数据、用来干什么。这一步是开发者对平台的承诺,不配等于没承诺。
  2. 运行时告知层:代码里通过 wx.getPrivacySetting 查询当前是否需要弹窗(needAuthorization 字段),需要的话由开发者自己的 UI 把协议内容(通过 wx.getPrivacyContract 拿到真实文本链接)展示给用户。这一步保证用户是在知情的前提下点的同意。
  3. 接口拦截层:用户同意之前,隐私接口调用会被基础库直接拦截。拦截是按接口清单逐个来的,清单以官方文档为准,且会随版本调整。

这三层缺一层都过不了关。只配了后台不写代码,接口照样 fail;只写了弹窗但后台指引没声明对应类型,也会被判定不合规。我们第一次整改就栽在第二点:代码接好了,但后台指引里漏了「选中的照片或视频信息」这一项,审核没过。

三层之间的关系用一张图看更直观:

flowchart LR
    A[后台配置用户隐私保护指引] --> B[代码实现弹窗告知]
    B --> C[基础库拦截未授权调用]
    C -->|未完成前两层| D[隐私接口直接 fail]
    C -->|三层齐备| E[接口正常返回]

完整实现:弹窗组件加调用封装

下面是可落地的方案,环境要求:基础库 ≥ 2.32.3,开发者工具 ≥ 1.06.240,原生小程序(uni-app 思路相同,API 名一致)。

隐私弹窗组件 privacy-popup

先写一个全屏弹窗组件,负责展示协议链接和处理同意动作:

<!-- components/privacy-popup/privacy-popup.wxml -->
<!-- 全屏遮罩层,wx:if 控制显隐避免常驻渲染开销 -->
<view wx:if="{{show}}" class="mask">
  <view class="panel">
    <!-- 标题按微信官方弹窗样式来,别自创文案 -->
    <view class="title">隐私保护提示</view>
    <view class="content">
      <text>在使用本小程序前,请阅读并同意</text>
      <!-- contractUrl 由 wx.getPrivacyContract 返回,点击可查看协议全文 -->
      <text class="link" bindtap="openContract">《用户隐私保护指引》</text>
      <text>。如你同意,请点击“同意”开始使用。</text>
    </view>
    <view class="btns">
      <!-- 拒绝按钮只关弹窗,不触发任何隐私授权 -->
      <!-- 按钮顺序有讲究:拒绝放左侧弱化视觉权重,别放右侧 -->
      <button class="btn plain" bindtap="onReject">拒绝</button>
      <!-- open-type 必须是 agreePrivacyAuthorization,普通按钮无效 -->
      <!-- 同意按钮是整个时序的起点,基础库会记录这次同意行为 -->
      <button class="btn primary" id="agree-btn" open-type="agreePrivacyAuthorization"
              bindagreeprivacyauthorization="onAgree">同意</button>
    </view>
  </view>
</view>

关键在同意按钮的 open-type="agreePrivacyAuthorization",这是官方规定的同意通道,基础库会记录这次同意行为,自己写个普通按钮调 API 是不算数的。

组件逻辑:

// components/privacy-popup/privacy-popup.js
// 组件职责:检查授权状态 + 展示弹窗 + 上报同意/拒绝事件
Component({
  data: {
    // 弹窗显隐,由 check 方法的查询结果驱动
    show: false,
    // 协议全文链接,来自 wx.getPrivacyContract
    contractUrl: ''
  },
  methods: {
    // 外部调用入口:先查隐私设置,需要授权就弹窗
    check() {
      wx.getPrivacySetting({
        success: (res) => {
          // needAuthorization 为 true 表示有协议未同意,必须先弹窗
          if (res.needAuthorization) {
            this.loadContract();
            this.setData({ show: true });
          } else {
            // 已同意过,直接放行后续业务,不再打扰用户
            this.triggerEvent('privacyready');
          }
        },
        fail: () => {
          // 查询失败时降级放行,别把用户卡死在入口
          this.triggerEvent('privacyready');
        }
      });
    },
    // 拉取协议真实链接,这个链接指向后台配置的指引文本
    loadContract() {
      wx.getPrivacyContract({
        success: (res) => {
          this.setData({ contractUrl: res.url });
        }
      });
    },
    // 用户点同意后基础库已记录授权状态,这里通知业务层继续
    onAgree() {
      this.setData({ show: false });
      this.triggerEvent('privacyready');
    },
    // 拒绝:只关弹窗。隐私接口此时会被拦截,业务层要兜底
    onReject() {
      this.setData({ show: false });
      this.triggerEvent('privacyreject');
    },
    // 跳转协议全文,用内置 web-view 能力即可
    openContract() {
      if (this.data.contractUrl) {
        wx.navigateTo({
          // 协议链接要做 encodeURIComponent,防止 & 截断参数
          url: '/pages/webview/webview?url=' + encodeURIComponent(this.data.contractUrl)
        });
      }
    }
  }
});

业务调用封装

弹窗组件解决了「要不要弹」,业务层还需要一个封装来保证时序正确。我们把它做成了一个 promise 风格的工具函数:

// utils/privacy.js
// 保证「先隐私协议、后 scope 授权、再调接口」的时序
// 依赖:基础库 >= 2.32.3;页面需挂载 privacy-popup 组件

// 内部状态:本次会话内已同意就不再重复检查,减少弹窗打扰
// 会话级变量即可,小程序冷启动会重置,跨启动交给基础库记忆
let privacyResolved = false;
// 等待队列:弹窗未关闭前进来的调用先挂起,同意后统一放行
let pendingResolvers = [];

// 等待隐私授权完成。已同意则立即 resolve,否则挂起等待弹窗结果
function ensurePrivacyAuthorized(page) {
  if (privacyResolved) {
    return Promise.resolve(true);
  }
  return new Promise((resolve, reject) => {
    // 挂起当前调用,等页面在 privacyready 事件里唤醒
    pendingResolvers.push({ resolve, reject });
    // 触发弹窗组件的检查逻辑,只触发一次,避免重复 setData
    const popup = page.selectComponent('#privacyPopup');
    if (popup && !popup.data.show) {
      popup.check();
    }
  });
}

// 由页面在 privacyready 事件里调用,放行所有等待者
// 注意要先把状态置 true 再遍历,避免 resolve 回调里再触发检查造成死循环
function resolveAll() {
  privacyResolved = true;
  // 逐个唤醒挂起中的 Promise,页面里可以有多个接口同时等待
  pendingResolvers.forEach((item) => item.resolve(true));
  pendingResolvers = [];
}

// 拒绝后放行失败,业务层据此走兜底
// 同样要清空队列,防止残留的 resolver 内存泄漏
function rejectAll() {
  pendingResolvers.forEach((item) => item.reject(new Error('privacy_rejected')));
  pendingResolvers = [];
}

// 完整封装:确保授权后调用 wx.requirePrivacyAuthorize,再执行真实接口
// page: 当前页面实例;apiName: 隐私接口名;options: 透传给接口的参数
function callWithPrivacy(page, apiName, options) {
  return ensurePrivacyAuthorized(page)
    // requirePrivacyAuthorize 会触发系统级授权弹窗(scope 层)
    .then(() => wx.requirePrivacyAuthorize({}))
    .then(() => {
      return new Promise((resolve, reject) => {
        // 走到这里说明隐私协议和 scope 都已就绪,正常调接口
        wx[apiName]({
          ...options,
          success: resolve,
          // 失败原样抛给业务层,由业务层决定兜底方式
          fail: (err) => reject(err)
        });
      });
    });
}

module.exports = { ensurePrivacyAuthorized, resolveAll, rejectAll, callWithPrivacy };

页面侧的接线大概三行:

// pages/index/index.js 片段
// 引入封装好的隐私时序工具,页面只需处理两个事件
const privacy = require('../../utils/privacy');

// 页面 wxml 里挂载弹窗组件并接线事件:<privacy-popup id="privacyPopup" bind:privacyready="onPrivacyReady" bind:privacyreject="onPrivacyReject" />
// privacyready:用户同意协议,唤醒所有挂起中的接口调用
onPrivacyReady() {
  privacy.resolveAll();
},
// privacyreject:用户拒绝协议,所有等待调用转入兜底分支
onPrivacyReject() {
  privacy.rejectAll();
},

// 拍照上传按钮的点击处理
onTakePhoto() {
  privacy.callWithPrivacy(this, 'wx.chooseImage', { count: 1 })
    .then((res) => this.upload(res.tempFilePaths[0]))
    // 拒绝协议或接口失败的兜底:提示后改用手动输入,别让流程断掉
    // errMsg 里有 privacy 关键字时可以顺带上报,便于统计拒绝率
    .catch(() => wx.showToast({ title: '未授权,可手动填写', icon: 'none' }));
}

时序图

正确时序长这样,注意两层授权的先后:

sequenceDiagram
    participant U as 用户
    participant P as 页面
    participant C as 弹窗组件
    participant W as 基础库
    U->>P: 点击"上传图片"
    P->>C: ensurePrivacyAuthorized
    C->>W: wx.getPrivacySetting
    W-->>C: needAuthorization=true
    C-->>U: 展示隐私弹窗
    U->>C: 点击"同意"
    C->>W: agreePrivacyAuthorization
    W-->>C: 隐私授权完成
    P->>W: wx.requirePrivacyAuthorize
    W-->>U: scope 授权弹窗
    U->>W: 允许
    W-->>P: chooseImage success

改造前后的行为对比:

对比项 改造前 改造后
隐私协议弹窗 无 首次触发隐私接口前弹出
接口调用链 直接调 API 检查→弹窗→scope→API
平台合规抽查 整改期接口 fail 全部通过
用户拒绝时 不存在该场景 静默兜底,引导手动输入
用户投诉量 每天 50 条上下 归零

后台「用户隐私保护指引」配置要点

代码只是半件事,后台配置不过审一样白费劲。几个实操要点:

  • 逐项对齐代码实际调用的接口。用了 chooseImage 就声明「选中的照片或视频信息」,用了 getLocation 就声明「位置信息」。多声明问题不大,漏声明必挂。
  • 用途描述写具体。写「用于上传用户头像」能过,写「业务需要」会被打回。
  • 指引文本更新后要重新提交审核,审核期间旧版本照常生效,不会出现真空期。
  • 开发版和体验版也会拦截。别指望上线才暴露问题,本地开发者工具切到 2.32.3 以上基础库就能复现拦截行为。
  • 配置入口在 mp.weixin.qq.com 的「设置-服务内容声明」里,类目不同可选的隐私类型清单也不同,找不到某个类型时先确认类目是否匹配。我们当时找「选中的照片或视频信息」翻了十分钟,后来发现要先把图片处理类的服务类目勾上。

排错与兜底

上线前我们用开发者工具的「清除模拟器缓存」反复验证首次弹窗,发现的坑主要三个:

  • 弹窗弹出时机太晚。一开始把检查放在每个接口调用前,用户点上传才弹窗,体验突兀。后来改成 app 启动后首页 onLoad 就检查一次,进入具体功能时基本不会再弹。
  • scope 授权被永久拒绝。隐私协议同意了但用户之前拒绝过位置授权,getLocation 依然 fail。这种情况要在 fail 回调里调 wx.openSetting 引导用户去设置页打开。
  • 自定义弹窗文案带诱导。有同行把拒绝按钮做成浅灰小字被平台判定为误导,弹窗文案别耍小聪明。

改造落地那周的具体数字:日志里 privacy 相关 fail 从每天 1200 多次降到个位数,剩下的基本是拒绝协议的用户。接口调用失败不可怕,可怕的是失败之后没有任何兜底,用户只能投诉。所以每个 catch 里都要给用户留一条能完成事情的路。

另外分享一个监控上的小动作。我们在接口封装的 catch 里统一埋了上报,带上 apiName 和 err.errMsg,接进现有的日志平台。这样整改期内哪些接口在 fail、fail 的原因归类(协议未同意、scope 被拒、指引未声明),后台一张报表就能看清。第二轮整改前,我们就是靠这张表确认三个高频 fail 接口,逐个补的弹窗场景,没再靠猜。

误区澄清

最后纠正两个常见误解。一是「配了后台指引就不用写代码」,不对,运行时弹窗是独立要求,两层都要做。二是「等用户触发隐私接口时再弹窗也来得及」,机制上确实合规,但体验上突兀,实际项目里建议入口处预检查、功能点静默通过,弹窗次数越少留存越好。

如果你也接到过整改通知,欢迎评论区聊聊你踩的坑。哪个环节最容易漏,大家体感不太一样,有的卡在后台指引,有的卡在基础库版本。

参考与延伸

微信小程序、隐私协议、wx.getPrivacySetting、requirePrivacyAuthorize、接口授权时序、合规开发、隐私弹窗

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。