很多 HarmonyOS 应用第一次提审,功能没崩、页面也挺漂亮,最后却卡在了“隐私合规”。

更让人摸不着头脑的是:隐私政策明明写了,首次启动也弹窗了,为什么还是被驳回?

因为隐私合规从来不只是一个弹窗。页面上怎么说、代码里怎么做、第三方 SDK 实际收集了什么,三者必须对得上。

这篇文章不照着条款念,也不堆法律术语。我们用开发者最熟悉的方式,从一次权限申请开始,把隐私声明、授权时机、SDK 初始化、日志脱敏、数据删除和提审自查完整走一遍。

文中的示例采用 ArkTS 和 Stage 模型。不同 HarmonyOS SDK/API 版本的接口可能略有变化,请以当前工程的类型提示和最新开发文档为准。


一、先记住这 5 句话,能避开大部分坑

如果暂时没时间读全文,先记住下面五点:

  1. **声明了权限,不代表可以随时申请。**用户真正使用功能时再申请。
  2. 用户点同意之前,不要初始化非必要的统计、广告、画像等 SDK。
  3. 隐私政策写了什么,代码就只能做什么;代码做了什么,隐私政策就必须如实说明。
  4. 用户拒绝非必要权限后,应用不能闪退、卡死或无限弹窗。
  5. 第三方 SDK 的行为,也算应用的行为。“这是 SDK 自动采集的”通常不是合格解释。

可以把隐私合规理解成一个等式:

隐私合规 = 清楚告知 + 用户选择 + 最小收集 + 安全处理 + 可以撤回

少一项,整个链路都可能出问题。


二、隐私弹窗不是“免责按钮”

先看一个非常典型的错误流程:

应用启动
  ↓
初始化统计 SDK、广告 SDK、推送 SDK
  ↓
读取设备信息或生成标识
  ↓
弹出隐私政策
  ↓
用户点击同意

表面上看,应用确实弹了隐私政策;但在用户同意之前,SDK 已经运行,甚至可能已经发出了网络请求。弹窗出现得再漂亮,也改变不了“先处理、后告知”的事实。

正确顺序应该是:

应用启动
  ↓
读取本地的隐私同意状态
  ├─ 已同意:进入应用,初始化已披露的服务
  └─ 未同意:展示隐私说明
                 ├─ 同意:记录状态,再初始化相关服务
                 └─ 不同意:退出或进入不采集非必要信息的基础模式

这里有三个容易忽略的细节:

  • 弹窗中的“隐私政策”和“用户协议”应该可以点击并完整阅读;
  • 同意按钮不能默认勾选,也不能用几乎看不见的颜色弱化“不同意”;
  • 用户同意后才初始化的逻辑,不能因为页面重构又偷偷挪回启动阶段。

所以,与其把隐私合规散落在十几个页面里,不如做一个统一的“隐私启动闸门”。


三、权限申请:不是能申请,而是该不该申请

HarmonyOS 提供了系统权限机制,但系统允许申请某项权限,不代表你的业务就有理由申请。

一个天气应用在用户查看“当前位置天气”时申请定位权限,很合理;一个壁纸应用刚启动就申请麦克风、通讯录和位置,审核员和用户都会问一句:你到底想干什么?

1. 先做一张权限盘点表

写代码前,先把权限和功能的关系列出来:

权限或能力使用场景是否核心功能能否替代申请时机
相机扫描二维码可以手动输入编号点击“扫一扫”后
定位查询附近门店可以手动选择城市点击“附近门店”后
麦克风语音输入可以键盘输入点击麦克风按钮后
通知到期提醒应用内消息中心用户主动开启提醒时

如果“使用场景”这一栏写不清楚,这个权限很可能不该申请。

如果存在不需要敏感权限的系统能力,也应该优先考虑。例如,用户主动选择一张图片时,优先研究系统选择器能否满足需求,不要一上来就申请整个媒体库的访问权限。

2. 在 module.json5 中正确声明

以扫码功能需要相机权限为例:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

对应的权限说明不要写成一句没有信息量的“用于相机功能”,可以这样表达:

{
  "string": [
    {
      "name": "camera_permission_reason",
      "value": "用于扫描设备二维码,仅在你主动使用扫一扫功能时调用相机"
    }
  ]
}

对比一下:

不推荐:需要相机权限,请允许。

推荐:用于扫描设备二维码,仅在你点击“扫一扫”后调用相机。

第二句话回答了三个问题:**做什么、什么时候用、为什么需要。**这才是有效说明。

3. 在真正需要时动态申请

不要在 aboutToAppear() 中一股脑申请所有权限。用户点击“扫一扫”时,再请求相机权限:

import { abilityAccessCtrl, common } from '@kit.AbilityKit'

const CAMERA_PERMISSION = 'ohos.permission.CAMERA'

async function requestCameraPermission(
  context: common.UIAbilityContext
): Promise<boolean> {
  const accessManager = abilityAccessCtrl.createAtManager()

  try {
    const result = await accessManager.requestPermissionsFromUser(
      context,
      [CAMERA_PERMISSION]
    )

    // 当前常用 API 中,0 表示授权成功。
    // 正式项目建议结合当前 SDK 的 GrantStatus 枚举判断。
    return result.authResults.length > 0 && result.authResults[0] === 0
  } catch (error) {
    console.error(`request camera permission failed: ${String(error)}`)
    return false
  }
}

在页面中调用:

import { common } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'

@Component
struct ScannerEntry {
  private async openScanner(): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext
    const granted = await requestCameraPermission(context)

    if (granted) {
      // router.pushUrl({ url: 'pages/Scanner' })
      return
    }

    promptAction.showToast({
      message: '没有相机权限,可返回后手动输入设备编号'
    })
  }

  build() {
    Column({ space: 16 }) {
      Button('扫一扫')
        .onClick(() => this.openScanner())

      Button('手动输入设备编号')
        .onClick(() => {
          // 进入不依赖相机权限的替代流程
        })
    }
  }
}

这段代码最重要的不是 API,而是产品逻辑:

  • 用户点了相关功能,才看到权限请求;
  • 授权成功,进入扫码页面;
  • 用户拒绝,不崩溃,也不反复弹窗;
  • 提供“手动输入”作为替代方案。

这就是隐私合规里经常提到的“最小必要”和“可选择”。

4. 权限被拒绝后,千万别死循环

下面这种写法看似执着,实际上很容易让用户崩溃:

// 错误示意:拒绝后立即再次申请
while (!granted) {
  granted = await requestCameraPermission(context)
}

正确思路是:

  • 第一次拒绝:解释影响,提供替代操作;
  • 用户以后再次点击相关功能:可以重新触发合理的申请流程;
  • 如果系统不再弹出授权框:引导用户前往系统设置,但不要强迫;
  • 与当前功能无关的页面:不要反复提醒。

用户拒绝的是一项权限,不是拒绝使用你的整个应用。


四、把“同意隐私政策”真正落到代码里

隐私同意状态通常需要持久化,否则用户每次打开应用都要重新选择。可以使用 Preferences 保存状态。

1. 封装同意状态存储

import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

const PRIVACY_STORE = 'privacy_store'
const PRIVACY_AGREED = 'privacy_agreed'
const PRIVACY_VERSION = 'privacy_version'
const CURRENT_PRIVACY_VERSION = '2026.09'

export class PrivacyStore {
  static async hasAgreed(context: common.Context): Promise<boolean> {
    const store = await preferences.getPreferences(context, {
      name: PRIVACY_STORE
    })

    const agreed = await store.get(PRIVACY_AGREED, false) as boolean
    const version = await store.get(PRIVACY_VERSION, '') as string

    // 隐私政策发生重要变化时,可以根据版本重新提示用户
    return agreed && version === CURRENT_PRIVACY_VERSION
  }

  static async agree(context: common.Context): Promise<void> {
    const store = await preferences.getPreferences(context, {
      name: PRIVACY_STORE
    })

    await store.put(PRIVACY_AGREED, true)
    await store.put(PRIVACY_VERSION, CURRENT_PRIVACY_VERSION)
    await store.flush()
  }

  static async withdraw(context: common.Context): Promise<void> {
    const store = await preferences.getPreferences(context, {
      name: PRIVACY_STORE
    })

    await store.put(PRIVACY_AGREED, false)
    await store.flush()
  }
}

为什么还要保存一个 privacy_version

因为隐私政策不是永远不变。比如后续版本新增了位置功能,或者接入了新的第三方 SDK,旧版本的同意未必能覆盖新的处理目的。是否需要重新征得同意,应结合具体变化和最新要求判断;从工程角度看,至少要具备版本管理能力。

注意,不能每次普通文案改了一个标点,就强迫用户重新同意。版本机制是为了处理实质变化,不是为了制造弹窗。

2. 建立统一的初始化闸门

import { common } from '@kit.AbilityKit'

export class AppBootstrap {
  private static optionalServicesStarted: boolean = false

  static startEssentialServices(): void {
    // 只启动应用运行必需、且不进行非必要个人信息处理的能力
    // 这里依然要结合项目实际情况评估,不能仅靠函数名判断合规
  }

  static startServicesAfterConsent(): void {
    if (this.optionalServicesStarted) {
      return
    }

    this.optionalServicesStarted = true

    // 示例:只有确认用户同意,且隐私政策已准确披露后再初始化
    // AnalyticsService.init()
    // PushService.init()
    // AdService.init()
  }

  static stopOptionalServices(): void {
    // 如果相关 SDK 支持停止、注销或清除标识,可在这里统一处理
    this.optionalServicesStarted = false
  }

  static async start(context: common.Context): Promise<boolean> {
    this.startEssentialServices()

    const agreed = await PrivacyStore.hasAgreed(context)
    if (agreed) {
      this.startServicesAfterConsent()
    }

    return agreed
  }
}

以后新增 SDK 时,团队不应该随手在 EntryAbility、首页和某个工具类里各初始化一次,而是先回答四个问题:

  1. 这个 SDK 做什么?
  2. 它会处理哪些信息?
  3. 隐私政策是否已经披露?
  4. 它应该在用户同意前还是同意后初始化?

回答清楚,再把初始化放进对应阶段。

3. 首次启动页面怎么组织?

下面是一个简化的页面思路:

import { common } from '@kit.AbilityKit'

@Entry
@Component
struct Index {
  @State private privacyChecked: boolean = false
  @State private privacyAgreed: boolean = false

  async aboutToAppear(): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext
    this.privacyAgreed = await AppBootstrap.start(context)
    this.privacyChecked = true
  }

  private async agreeAndContinue(): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext
    await PrivacyStore.agree(context)
    AppBootstrap.startServicesAfterConsent()
    this.privacyAgreed = true
  }

  build() {
    Column() {
      if (!this.privacyChecked) {
        LoadingProgress()
      } else if (!this.privacyAgreed) {
        PrivacyConsentView({
          onAgree: () => this.agreeAndContinue(),
          onReject: () => {
            // 根据业务选择退出,或进入不处理非必要信息的基础模式
          }
        })
      } else {
        HomePage()
      }
    }
    .width('100%')
    .height('100%')
  }
}

为了突出流程,示例省略了异常处理、并发保护和具体组件实现。实际项目还要处理 Preferences 读取失败、快速重复点击、应用恢复等情况。

还有一个很重要的点:**不要把隐私同意状态上传成一个到处可查的用户标签,除非确实有跨端同步等合理需要。**能在本地完成的判断,就不要为了“方便”额外收集。


五、隐私声明到底该写什么?别再复制模板了

不少团队的隐私政策是从网上找一份模板,把公司名替换掉就上线。这样做最大的问题不是文案难看,而是内容和真实应用完全对不上。

一份能落地的隐私声明,至少应该回答下面这些问题:

1. 我收集什么?

不要只写“可能收集设备信息”,要尽量说清具体类型。例如:

为了完成账号登录,我们会处理你主动填写的手机号。

当你使用“扫一扫”功能时,我们会申请相机权限,用于识别二维码。
相机画面仅用于本地识别;如业务确需上传,应另外准确说明上传目的和范围。

2. 为什么收集?

同一类信息在不同场景中,目的可能完全不同。

例如位置数据:

  • 用于展示附近门店;
  • 用于配送地址定位;
  • 用于个性化广告。

这三个目的不能用一句“改善用户体验”全部带过。目的越模糊,越难证明收集是必要的。

3. 在什么时候收集?

隐私政策中写“使用扫码功能时申请相机”,代码却在首页启动时就申请,这就是声明与行为不一致。

建议产品、开发和合规人员共用一张数据清单:

信息/权限处理目的触发页面是否上传保存期限第三方接收方
手机号登录和账号安全登录页账号存续期间或依法处理短信服务商
相机扫描二维码扫码页视实际业务填写视实际业务填写无或如实填写
崩溃日志定位应用异常用户同意后的运行阶段按实际策略填写崩溃分析服务商

这里不要照抄表格里的示例答案。比如你的二维码图片会上传服务器,就不能写“仅本地处理”;你的崩溃分析 SDK 会读取设备标识,也不能只写“收集崩溃时间”。

4. 数据会保存多久?

“我们会永久保存”通常不是一个让人安心的答案,“在实现目的所需的最短时间内保存”如果没有实际删除机制,也可能只是口号。

开发侧至少要能回答:

  • 数据存在哪里;
  • 依靠什么字段找到它;
  • 到期后由谁删除;
  • 用户注销时是否同步删除;
  • 备份和日志中的数据怎么处理;
  • 法律法规要求保留的数据如何隔离。

5. 用户怎么控制自己的数据?

用户应该能够知道如何:

  • 查看、更正个人信息;
  • 关闭相关权限;
  • 撤回隐私同意;
  • 删除部分数据;
  • 注销账号;
  • 联系开发者或投诉反馈。

入口不要藏在连续五层页面之后。常见做法是放在:

我的 → 设置 → 隐私设置
我的 → 设置 → 账号与安全 → 注销账号

只写“如需注销请联系客服”,但客服永远没人回复,这种设计显然经不起实际验证。


六、第三方 SDK:最容易被漏掉的隐私风险

你的代码可能一行设备信息都没读,但只要接入的 SDK 在用户同意前发起了采集,应用仍然可能出现合规问题。

1. 建立 SDK 台账

建议每次发版都维护下面这张表:

SDK 名称用途可能处理的信息初始化时机是否必要隐私政策是否披露
崩溃分析 SDK定位闪退设备信息、崩溃堆栈等用户同意后可评估是/否
推送 SDK消息通知推送标识等按实际能力评估非全部场景必要是/否
广告 SDK展示广告设备及广告相关信息等用户同意后是/否

表中的“可能处理的信息”不能靠猜。应该查看 SDK 的官方隐私说明、版本更新记录和实际运行行为。

2. 升级 SDK 后要重新检查

SDK 从 2.1 升级到 2.2,看起来只是一个小版本,但它可能:

  • 新增权限;
  • 新增采集字段;
  • 改变初始化时机;
  • 增加新的子处理方;
  • 修改数据保存策略。

所以依赖升级不能只做“编译通过”和“功能正常”测试,还要重新核对隐私说明。

3. 删除 SDK 要删干净

常见情况是:产品说“不接广告了”,开发删除了广告页面,却留下了依赖、初始化代码和配置;或者代码删了,隐私政策仍然写着相关 SDK。

正确的下线动作包括:

删除依赖 → 删除初始化 → 删除相关权限 → 删除配置
→ 检查构建产物 → 更新隐私政策 → 回归网络请求

“应用不再展示广告”和“广告 SDK 已经完全停止工作”是两件不同的事。


七、数据处理:别让日志和缓存毁掉前面的努力

权限和弹窗都做对了,不代表数据处理就安全。手机号、Token、身份证号、定位信息如果被随手打印到日志中,前面的合规工作很可能功亏一篑。

1. 日志要脱敏

错误示例:

console.info(`login success, phone=${phone}, token=${token}`)

手机号和 Token 都没有必要完整出现在日志里。可以封装一个简单的脱敏方法:

function maskPhone(phone: string): string {
  if (!/^\d{11}$/.test(phone)) {
    return '[invalid-phone]'
  }

  return `${phone.slice(0, 3)}****${phone.slice(-4)}`
}

function safeError(error: object | string): string {
  // 正式项目应使用字段白名单,不要直接 JSON.stringify 整个请求对象
  return typeof error === 'string' ? error.slice(0, 200) : '[business-error]'
}

console.info(`login result, phone=${maskPhone(phone)}`)

真正稳妥的思路不是“先把所有信息打出来,再想办法遮住”,而是日志字段白名单:只记录定位问题确实需要的字段。

尤其不要打印:

  • 登录密码和短信验证码;
  • 完整访问 Token、刷新 Token;
  • 身份证号、银行卡号;
  • 完整手机号、精确地址;
  • 请求头中的认证信息;
  • 用户提交的聊天、表单或健康数据全文。

2. 本地存储要分级

Preferences 适合保存普通配置和开关,不代表任何数据都应该往里塞。

可以做一个简单分级:

数据推荐思路
主题颜色、引导页状态普通本地配置
隐私同意状态本地配置,并做好版本管理
登录凭证、关键密钥使用适合敏感数据的安全存储和密钥管理能力
用户业务数据根据必要性、敏感程度、期限设计加密和删除策略

不要在代码里硬编码服务端密钥,也不要认为把密钥拆成三段字符串就安全了。客户端最终需要使用的静态秘密,理论上总有被分析的风险;真正的敏感凭证应该尽量由服务端控制,并结合安全存储、短期令牌等机制降低风险。

3. 网络传输要有失败边界

涉及账号和个人信息的请求应使用安全传输,并正确校验证书。不要为了调试方便,在正式包里保留“忽略所有证书错误”一类逻辑。

同时要处理失败场景:

import { http } from '@kit.NetworkKit'

async function deleteUserProfile(): Promise<boolean> {
  const request = http.createHttp()

  try {
    const response = await request.request(
      'https://example.com/api/profile',
      {
        method: http.RequestMethod.DELETE,
        connectTimeout: 10000,
        readTimeout: 10000
      }
    )

    return response.responseCode >= 200 && response.responseCode < 300
  } catch (error) {
    console.error(`delete profile failed: ${safeError(error as object)}`)
    return false
  } finally {
    request.destroy()
  }
}

删除失败时,不要在客户端直接显示“已删除成功”;应该告诉用户当前状态,并允许重试或查询处理进度。

4. 数据不能“只进不出”

每新增一类数据,开发时就应该同步设计它的退出路径:

采集 → 使用 → 保存 → 到期删除
                  ├─ 用户主动删除
                  ├─ 用户撤回授权
                  └─ 用户注销账号

如果团队只知道数据怎么写进数据库,却没人知道怎样完整删除,这套功能还没有真正做完。


八、账号注销:一个按钮背后是一整条链路

“设置页加个注销按钮”只是界面完成了,真正的注销至少涉及:

  1. 确认当前账号身份;
  2. 清楚说明注销后果;
  3. 处理未完成订单、余额或其他必要事项;
  4. 服务端关闭账号并处理相关数据;
  5. 让现有 Token 和登录状态失效;
  6. 清理本地缓存及用户数据;
  7. 给用户明确结果,而不是一直显示“处理中”。

客户端可以把注销后的本地清理集中处理:

class AccountSession {
  static async clearAfterCancellation(): Promise<void> {
    // AuthTokenStore.clear()
    // UserCache.clear()
    // SearchHistory.clearByUser()
    // AppBootstrap.stopOptionalServices()
    // router.clear()
    // router.replaceUrl({ url: 'pages/Welcome' })
  }
}

上面只是结构示意。真实项目中,哪些数据必须删除、哪些依法需要保留、备份何时清除,要由业务、技术和合规共同确定。

还要区分两个容易混淆的动作:

退出登录:以后还能用原账号重新登录,服务端数据仍然存在。
注销账号:账号关系终止,并按照规则处理相关个人信息。

不能把“退出登录”包装成“注销账号”。


九、一个完整案例:给 App 增加“扫码绑定设备”

假设产品提出一个需求:用户扫描设备上的二维码,把设备绑定到账号。

看起来只是一个扫码页面,但要合规上线,至少需要走完下面的过程。

第一步:判断相机权限是否必要

扫码确实需要相机,但可以提供手动输入设备编号的替代方案。因此:

  • 相机权限不是应用启动所必需;
  • 用户不授权,仍然能通过手动输入完成业务;
  • 权限应该在用户点击“扫一扫”后申请。

第二步:更新权限和隐私说明

module.json5 增加相机权限及具体原因;隐私政策增加相机使用场景,并说明画面是否上传、二维码识别结果如何使用。

第三步:检查第三方扫码库

如果接入第三方扫码 SDK,不能只看它“能不能扫出来”,还要确认:

  • SDK 是否发起网络请求;
  • 是否收集设备信息;
  • 是否保存或上传扫码图片;
  • 是否需要在第三方 SDK 清单中披露。

第四步:处理所有授权状态

至少测试:

首次申请并同意
首次申请并拒绝
拒绝后使用手动输入
系统设置中关闭权限后再次扫码
授权后拍摄页面被系统中断
弱网下二维码识别成功但绑定接口失败

第五步:验证日志和服务端

二维码中可能包含设备序列号或绑定密钥,不应该被完整打印到日志。绑定接口也应该校验当前账号是否有权绑定该设备,避免只靠客户端判断。

第六步:写清提审备注

本版本新增“扫码绑定设备”功能。

相机权限仅在用户点击“设备 → 添加设备 → 扫一扫”后申请,
用于识别设备二维码。拒绝权限后,用户可选择“手动输入设备编号”,
不影响应用其他功能。

测试路径:
登录 → 设备 → 添加设备 → 扫一扫

你会发现,一项小功能从需求到上线,隐私合规贯穿了产品、代码、测试、文案和审核说明,而不是上线前让运营临时补一个弹窗。


十、这 8 种做法,看起来合规,其实很危险

1. “隐私政策里都写了,所以权限可以一次性全要”

不对。隐私政策负责告知,系统授权负责让用户在具体场景中选择,两者不能互相替代。

2. “先初始化 SDK,反正用户大概率会同意”

用户还没有选择,“大概率”没有意义。初始化时机应该由实际数据处理行为决定。

3. “权限拒绝后就退出,不然功能不好做”

如果这项权限不是应用基本功能所必需,就应该尽量提供替代方案,而不是把授权做成强制通行证。

4. “我们不保存数据,所以不算收集”

读取、上传、使用和共享都可能属于数据处理行为,不是只有写进数据库才需要关注。

5. “第三方 SDK 有自己的隐私政策,跟我没关系”

SDK 是你选择并集成到应用中的。接入方仍然需要了解它的行为,并完成必要披露和控制。

6. “把权限说明写得越宽泛,以后越省事”

例如“为了向你提供更好的服务,我们可能使用所有设备能力”。这种文案看似覆盖面很广,实际没有清楚解释任何事情。

7. “审核通过了,当前做法肯定永久合规”

应用功能、SDK 和规则都会变化。一次通过只代表那个时间点、那个版本完成了审核,不代表以后可以停止维护。

8. “正式包和测试包代码一样,不用重新测”

Release 构建可能启用不同的服务地址、混淆、签名和资源裁剪。隐私与权限测试一定要基于最终提交的安装包。


十一、如何自己做一次隐私合规测试?

不需要一上来就准备复杂工具,先做一轮“黑盒体验”:

测试一:用户同意前

全新安装并启动应用,在隐私弹窗上停住,检查:

  • 是否已经弹出系统权限请求;
  • 是否已经初始化非必要 SDK;
  • 是否出现与核心展示无关的网络请求;
  • 隐私政策链接能否打开;
  • 不同意按钮能否正常点击。

测试二:拒绝所有非必要权限

逐个拒绝权限,检查:

  • 应用是否崩溃或白屏;
  • 页面是否无限加载;
  • 是否反复申请;
  • 是否说明受影响的具体功能;
  • 是否提供合理替代路径。

测试三:权限动态变化

先允许权限,再从系统设置中关闭,然后回到应用。很多应用只处理了“首次申请”,没有处理权限被用户随时收回的情况。

测试四:撤回同意和注销账号

检查入口是否能找到、流程是否真实有效、本地数据是否清理、服务端状态是否更新。

测试五:抓取实际行为进行核对

在具备相应测试条件和授权的环境中,核对应用及第三方 SDK 的网络请求、权限调用和本地存储行为,再与隐私政策逐项对照。

最终应该达到这个状态:

商店隐私信息
       = 应用内隐私政策
       = 权限用途说明
       = 代码真实行为
       = 第三方 SDK 真实行为

任何两项对不上,都值得重新检查。


十二、提审前隐私合规清单

建议把下面这张表放进团队的发版流程,每次提交都重新确认。

检查项通过标准
首次启动同意前未启动非必要个人信息处理
隐私弹窗文案清楚,协议可阅读,同意与不同意均可操作
权限声明只保留当前版本实际需要的权限
申请时机用户主动使用相关功能时再申请
权限文案说明具体功能、目的和使用场景
拒绝处理不崩溃、不死循环,提供提示或替代方案
隐私政策与当前应用名称、主体、功能和权限一致
第三方 SDK名称、用途、信息类型和规则如实披露
SDK 初始化按实际数据行为放在正确的同意阶段
日志不输出密码、验证码、Token 和完整敏感信息
本地存储普通数据与敏感数据分级处理
网络传输使用安全连接,不保留跳过证书校验的调试逻辑
数据期限有明确、可执行的保留与删除策略
撤回同意入口可找到,撤回后停止对应的非必要处理
账号注销流程可完成,Token、本地缓存和服务端状态正确处理
商店资料商店填写的隐私信息与应用实际行为一致
最终安装包使用提交审核的 Release 包完整回归

如果团队人数比较多,可以把责任拆开:

产品:说明功能为什么需要数据,提供替代流程
开发:控制权限和 SDK 时机,做好存储、传输、删除
测试:覆盖拒绝、撤回、弱网和全新安装场景
运营:保证商店资料与当前版本一致
合规/法务:核对隐私政策和具体处理规则

隐私合规不是“最后找一个人签字”,而是每个角色都要把自己的那一段做好。


十三、写在最后

做隐私合规最怕两种极端:

一种是完全不当回事,觉得加个弹窗就能解决;另一种是看到“个人信息”四个字就不敢开发任何功能。

其实核心并不神秘:

需要什么就申请什么,用到时再申请;收集之前说清楚,用户拒绝有退路;数据拿到手后保护好,不需要时及时删除。

把这几件事真正落实到代码和产品流程里,隐私合规就不再是提审前的一次“临时补作业”,而会变成应用质量的一部分。

对用户来说,这意味着他知道自己的数据去了哪里;对开发者来说,这也意味着权限逻辑更清楚、异常分支更完整、SDK 接入更可控,应用反而会更稳定。

最后再提醒一次:HarmonyOS API、应用市场要求和相关规则会持续更新。本文提供的是工程落地思路,正式开发和提交审核时,请结合应用面向的地区、业务类型、目标 API 版本,并以 AppGallery Connect 展示的最新要求及 HarmonyOS 最新开发文档为准。

如果这篇文章帮你理清了权限、隐私政策和数据处理之间的关系,欢迎点赞、收藏。你也可以把遇到的审核意见留在评论区,我们一起把问题翻译成能落地的代码。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐