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; - 上架前用脚本扫声明、触发和兜底是否对齐。
这样处理以后,权限审核不再是最后一天临时补材料,而是开发过程中就能持续验证的一条工程规则。
更多推荐



所有评论(0)