HarmonyOS 7 API 26 权限审核自查流程

HarmonyOS 应用做上架审核时,权限问题很容易被低估。很多人以为只要在 module.json5 里声明了权限,页面里能弹出授权框,就算处理完了。实际审核和线上体验不会这么看。审核更关心三件事:你为什么要这个权限、你什么时候向用户要、用户拒绝以后应用还能不能正常走下去。

我现在检查权限弹窗,不会只看“有没有弹窗”。我会把它拆成一条链路:配置声明、触发入口、业务解释、授权结果、拒绝兜底、日志记录、上架材料说明。只要其中一段断了,最后都可能变成审核风险或者线上投诉。

先把问题拆清楚

权限弹窗常见的问题有四类:

  • module.json5 里声明了权限,但页面实际触发场景说不清楚;
  • 页面一打开就弹权限,用户还不知道为什么要授权;
  • 用户拒绝以后,页面还是继续调用能力,最后只剩一个失败提示;
  • 多个入口都要同一个权限,但解释文案和兜底逻辑各写各的,后期很难查。

更稳的做法不是把授权代码到处复制,而是把权限申请收口成一个小模块。页面只说明“我要做什么”,模块负责判断权限、触发弹窗、记录结果和返回降级状态。

案例一:相机权限不能一进页面就要

假设页面里有一个扫码入口。很多实现会在页面进入时立刻申请相机权限:

aboutToAppear() {
  this.permissionService.requestCamera()
}

这个写法最大的问题是时机太早。用户还没点扫码,也没看到权限用途,系统弹窗突然出现,审核时也不好解释为什么页面初始化就要相机。

我更倾向于把触发点放到用户主动点击以后:

@Entry
@Component
struct ScanEntryPage {
  @State tip: string = '点击扫码后再申请相机权限'

  async onScanClick() {
    const result = await PermissionGuard.request({
      scene: 'scan_qr_code',
      permission: 'ohos.permission.CAMERA',
      reason: '用于扫描二维码,不会在后台使用相机'
    })

    if (result.granted) {
      this.tip = '权限已通过,进入扫码页'
      router.pushUrl({ url: 'pages/ScanPage' })
      return
    }

    this.tip = '没有相机权限,暂时不能扫码。你也可以手动输入编号继续操作。'
  }

  build() {
    Column({ space: 16 }) {
      Text(this.tip).fontSize(16)
      Button('扫码')
        .onClick(() => this.onScanClick())
      Button('手动输入编号')
        .onClick(() => router.pushUrl({ url: 'pages/ManualInputPage' }))
    }
    .padding(20)
  }
}

这里的重点不是代码多复杂,而是顺序清楚:用户先点扫码,应用再解释用途,再申请权限;用户拒绝以后,还有手动输入入口。审核看到这条链路时,更容易判断这个权限是按需使用的。

权限收口模块怎么写

下面这个封装只保留必要结构,真实项目里可以再接日志、埋点和多语言文案:

type PermissionScene = 'scan_qr_code' | 'save_image' | 'pick_file'

type PermissionRequest = {
  scene: PermissionScene
  permission: string
  reason: string
}

type PermissionResult = {
  granted: boolean
  scene: PermissionScene
  permission: string
  fallback: 'manual_input' | 'settings' | 'readonly'
}

class PermissionGuard {
  static async request(req: PermissionRequest): Promise<PermissionResult> {
    const supported = PermissionPolicy.checkScene(req.scene, req.permission)
    if (!supported) {
      return {
        granted: false,
        scene: req.scene,
        permission: req.permission,
        fallback: 'readonly'
      }
    }

    // 这里接入 abilityAccessCtrl.requestPermissionsFromUser。
    // 示例里保留抽象调用,重点是把授权结果统一收口。
    const granted = await PermissionRuntime.requestFromUser(req.permission, req.reason)

    if (granted) {
      return {
        granted: true,
        scene: req.scene,
        permission: req.permission,
        fallback: 'settings'
      }
    }

    return {
      granted: false,
      scene: req.scene,
      permission: req.permission,
      fallback: PermissionPolicy.fallbackOf(req.scene)
    }
  }
}

再加一层场景规则:

class PermissionPolicy {
  private static scenePermissions: Record<PermissionScene, string[]> = {
    scan_qr_code: ['ohos.permission.CAMERA'],
    save_image: ['ohos.permission.WRITE_IMAGEVIDEO'],
    pick_file: []
  }

  static checkScene(scene: PermissionScene, permission: string): boolean {
    return this.scenePermissions[scene].includes(permission)
  }

  static fallbackOf(scene: PermissionScene): PermissionResult['fallback'] {
    if (scene === 'scan_qr_code') {
      return 'manual_input'
    }
    if (scene === 'save_image') {
      return 'settings'
    }
    return 'readonly'
  }
}

这层规则有两个作用。第一,页面不会随便申请一个不属于当前场景的权限。第二,拒绝以后的兜底不是临时写出来的,而是跟场景绑定。

案例二:保存图片权限要有可替代路径

保存图片也很常见。用户点“保存图片”,应用可能需要写入相册。如果用户拒绝授权,页面不能只弹一个“保存失败”。更好的处理是给出替代路径,比如复制链接、分享图片、或者引导到设置页。

async function savePoster() {
  const result = await PermissionGuard.request({
    scene: 'save_image',
    permission: 'ohos.permission.WRITE_IMAGEVIDEO',
    reason: '用于把海报保存到相册'
  })

  if (result.granted) {
    await PosterService.saveToAlbum()
    Toast.show('已保存到相册')
    return
  }

  if (result.fallback === 'settings') {
    ActionSheet.show({
      title: '没有相册写入权限',
      actions: [
        { text: '复制图片链接', value: 'copy_link' },
        { text: '去系统设置开启', value: 'open_settings' }
      ]
    })
  }
}

这个例子里,用户拒绝后仍然有路可走。审核材料里也能说明:权限只在用户主动保存时触发,拒绝后不会强制中断主流程。

用脚本提前扫一遍配置

权限问题最好不要靠人工逐页点。至少要有一个脚本先扫出明显问题:

const modulePermissions = [
  'ohos.permission.CAMERA',
  'ohos.permission.WRITE_IMAGEVIDEO'
]

const scenePermissionMap = [
  { scene: 'scan_qr_code', permission: 'ohos.permission.CAMERA', fallback: 'manual_input' },
  { scene: 'save_image', permission: 'ohos.permission.WRITE_IMAGEVIDEO', fallback: 'settings' },
  { scene: 'pick_file', permission: '', fallback: 'readonly' }
]

function inspectPermissionScene(item) {
  const errors = []

  if (item.permission && !modulePermissions.includes(item.permission)) {
    errors.push('module.json5 里缺少权限声明')
  }

  if (item.permission && !item.fallback) {
    errors.push('缺少拒绝后的兜底路径')
  }

  if (item.scene === 'scan_qr_code' && item.fallback !== 'manual_input') {
    errors.push('扫码拒绝后应该保留手动输入路径')
  }

  return {
    scene: item.scene,
    passed: errors.length === 0,
    errors
  }
}

const report = scenePermissionMap.map(inspectPermissionScene)
console.log(JSON.stringify({
  total: report.length,
  failed: report.filter(item => !item.passed).length,
  report
}, null, 2))

正常输出应该能看到每个场景是否通过:

{
  "total": 3,
  "failed": 0
}

如果以后新增一个录音、定位或读取图片的入口,只要忘了声明权限或兜底路径,脚本就能先拦一下。它不能替代审核,但能减少明显低级错误。

几种处理方式对比

处理方式 优点 风险 适合场景
页面里直接申请权限 写起来快 时机、文案、兜底分散 临时 Demo
每个页面各自封装 页面更灵活 后期规则不一致 小项目
统一 PermissionGuard 审核说明清楚,复用稳定 前期要建规则表 正式上架应用

我更推荐第三种。权限不是一个弹窗,而是一套用户知情和能力降级机制。把它收口以后,页面代码反而更清楚,审核材料也更好写。

上架前我会查什么

检查项 合格标准
权限声明 module.json5 中声明与实际触发能力一致
触发时机 用户主动点击相关能力后再申请
用途解释 页面文案能说明为什么需要这个权限
拒绝兜底 用户拒绝后仍有替代路径或只读路径
日志记录 能区分未申请、已拒绝、已授权、系统异常
审核材料 权限用途、截图、触发路径能互相对应

后面怎么避免

权限弹窗的问题,不能等到上架被拒以后再补。我会把它前移到开发阶段:

  • 新增需要权限的能力时,先写场景和兜底,再写页面;
  • 权限声明和页面触发点要成对检查;
  • 用户拒绝不是异常,而是正常分支;
  • 所有权限入口都走同一个 PermissionGuard
  • 上架前用脚本扫声明、触发和兜底是否对齐。

这样处理以后,权限审核不再是最后一天临时补材料,而是开发过程中就能持续验证的一条工程规则。

Logo

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

更多推荐