有些上架问题不是"功能坏了",而是工程里的三份事实没有对齐:
module.json5声明了一套权限,ArkTS 实际申请的是另一套,隐私文案又写成第三套。页面自己测没有问题,到了提审阶段才开始逐项找差异,成本会比开发时高很多。

我这次没有再做一篇审核规则清单,而是写了一个很小的工程工具 ReleaseGuard。它不代替 AppGallery 审核,也不声称能保证通过;它只在提交版本前把项目里几类可以机械检查的内容拉到一起,先把明显的不一致找出来。
这次固定的运行结果是 audit_20261001_02,最终状态 PASS,6 / 6 检查通过,Findings = 0。
一、真正麻烦的不是"有没有权限",而是三处事实会漂移
做业务时很常见。
最初扫码页需要相机,于是在 module.json5 里加了 ohos.permission.CAMERA。后来产品调整流程,把扫码放到二级页,开发者也把运行时弹窗改成用户点击"扫描小票"时才出现。
再后来隐私文案还是旧版本,写着"启动应用时需要相机权限"。
单独看三处都不一定报错:
- manifest 能编译;
- ArkTS 能正常弹权限框;
- 隐私政策也有"相机"两个字。
但放到一起,场景已经不一致了。
华为应用市场当前审核指南要求应用信息和包体完整准确、应用本身可正常运行,并符合隐私相关要求。隐私规则里还强调权限使用最小化、清楚说明权限对应功能和场景,同时避免在首次启动时频繁请求敏感权限。
所以我想做的不是替审核员判断合规,而是在代码提交阶段回答几个更具体的问题:
- 声明了哪些权限?
- 哪些权限在 ArkTS 中真的发起运行时申请?
- 隐私映射里有没有对应的用途和触发场景?
- 三方依赖有没有进入项目自己的隐私清单?
- 审核备注是否准备好?
- 当前发布信息是否完整?
二、先建一份"工程内隐私映射",别把真相只留在 Word 里
这段配置解决的问题,是给脚本一个可比较的数据源。
我没有直接让脚本解析线上隐私政策,因为文案格式可能变化,也不适合把网络状态带进本地构建。Demo 里维护一个 scripts/privacy-map.json,它不是官方要求的文件,只是项目自己的"机器可读映射"。
json
{
"permissions": {
"ohos.permission.CAMERA": {
"requestAtRuntime": true,
"purpose": "扫描纸质小票并生成识别图片",
"scene": "用户点击扫描小票按钮后申请"
},
"ohos.permission.INTERNET": {
"requestAtRuntime": false,
"purpose": "上传用户确认后的识别结果",
"scene": "用户主动点击同步后使用"
}
},
"thirdParty": []
}
我特意加了 requestAtRuntime。
因为不是所有 manifest 权限都应该用同一套逻辑判断。如果脚本简单规定"声明的权限必须全部出现在 requestPermissionsFromUser()",很快就会制造大量误报。
这个文件真正有价值的地方,是把"权限名、用途、场景、是否运行时申请"绑定在一起。以后业务改流程时,代码评审不只看 ArkTS,也能顺手看这一份映射有没有一起改。
三、manifest 只保留当前功能需要的声明
当前 Demo 用到相机和网络。
这段配置解决的是声明侧事实,让扫描脚本能拿到"工程准备申请什么"。
json5
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.INTERNET"
}
]
}
}
正式项目里不要看到以前用过的权限就一直留着。
权限越多不等于能力越完整。审核规则本身就强调最小化原则。如果一个旧模块已经下线,声明却没有删,隐私文案也被迫继续解释一个实际上不再存在的场景,后续维护反而更困难。
reason、usedScene 是否需要、怎么配置,要按具体权限类型和当前 HarmonyOS 权限文档处理,不要把一个权限的写法机械复制到另一个权限。
四、运行时授权只放在真正发生功能动作的位置
我这次把相机授权从 aboutToAppear() 移到了"扫描小票"按钮动作里。
这段 ArkTS 解决的问题,是用户没有使用扫码功能时,不提前打断流程。
ts
import {
abilityAccessCtrl,
common,
PermissionRequestResult
} from '@kit.AbilityKit';
async function requestCameraWhenNeeded(
context: common.UIAbilityContext
): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
try {
const result: PermissionRequestResult =
await atManager.requestPermissionsFromUser(
context,
['ohos.permission.CAMERA']
);
return result.authResults[0] ===
abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
console.error(`request camera failed: ${JSON.stringify(error)}`);
return false;
}
}
调用方再决定后续动作:
ts
private async startReceiptScan(): Promise<void> {
const context = this.getUIContext()
.getHostContext() as common.UIAbilityContext;
const granted = await requestCameraWhenNeeded(context);
if (!granted) {
this.scanState = 'PERMISSION_DENIED';
return;
}
this.scanState = 'CAMERA_READY';
await this.openScanner();
}
这个调整以后,权限状态和业务状态就分开了。
用户拒绝相机,不应该导致整个应用不可用;它只影响"扫码小票"这一项功能。后续如果系统策略要求引导用户到设置页重新授权,也应该在用户再次触发相应功能时处理,而不是在首页循环弹窗。
官方当前文档里仍然使用 abilityAccessCtrl.createAtManager() 和 requestPermissionsFromUser() 这条链路做用户授权请求。工程里需要额外关注的,是拒绝以后怎么降级,以及不要把授权弹窗当成业务成功结果。
五、扫描脚本只做"能证明的事"
接下来是这篇里真正的小工具。
我没有让脚本判断"你的隐私政策是否合法",这种结论它做不了。它只比较本地工程里可以确认的集合。
这段 Node.js 脚本解决三个最直接的问题:
- manifest 声明了什么;
- ArkTS 代码里请求了什么;
privacy-map.json覆盖了什么。
js
import fs from 'node:fs';
import path from 'node:path';
import JSON5 from 'json5';
import fg from 'fast-glob';
const moduleJson = JSON5.parse(
fs.readFileSync('entry/src/main/module.json5', 'utf8')
);
const privacyMap = JSON.parse(
fs.readFileSync('scripts/privacy-map.json', 'utf8')
);
const declared = new Set(
(moduleJson.module.requestPermissions ?? []).map(item => item.name)
);
const etsFiles = await fg(['entry/src/main/ets/**/*.ets']);
const source = etsFiles
.map(file => fs.readFileSync(file, 'utf8'))
.join('\n');
const runtimeRequested = new Set(
[...source.matchAll(/ohos\.permission\.[A-Z0-9_]+/g)]
.map(match => match[0])
);
const mapped = new Set(
Object.keys(privacyMap.permissions ?? {})
);
const findings = [];
for (const permission of declared) {
if (!mapped.has(permission)) {
findings.push(`No privacy mapping: ${permission}`);
}
}
for (const [permission, meta] of Object.entries(privacyMap.permissions)) {
if (meta.requestAtRuntime && !runtimeRequested.has(permission)) {
findings.push(`Runtime request missing: ${permission}`);
}
if (!declared.has(permission)) {
findings.push(`Mapped but not declared: ${permission}`);
}
}
console.log({
declared: declared.size,
runtimeRequested: runtimeRequested.size,
mapped: mapped.size,
findings
});
这里的正则故意很保守。它适合抓 Demo 里直接写出的权限常量,不适合覆盖所有复杂封装。
例如权限名来自常量模块、代码生成、字符串拼接时,这种扫描会漏掉。所以正式项目更适合把权限常量集中管理,或者在 AST 层做分析。
我的原则是:脚本宁可明确自己的边界,也不要输出一个看起来很权威的"100% 合规"。
六、我把最终检查拆成 6 项,而不是一个大 PASS
ReleaseGuard 页面最后展示六项:
- Permission declaration
- Runtime request
- Privacy copy
- Third-party SDK
- Review notes
- Release package
前三项是权限与隐私的一致性。
第四项检查 oh-package.json5 里的三方依赖,是否在项目维护的 thirdParty 清单里有记录。它不判断某个 SDK 一定收集什么信息,而是提醒"新增依赖以后,隐私评估别漏掉"。
官方审核相关说明也明确提到,集成第三方 SDK 时,隐私政策需要准确说明相关个人信息处理情况。具体内容仍然要以对应 SDK 的当前隐私声明和实际使用功能为准。
第五项检查审核备注文件是否存在。某些功能依赖账号、硬件、特殊环境时,官方审核指南要求提供有效测试账号以及必要的审核资源。脚本能做的只是提醒文件为空,不能替你生成真实测试账号。
第六项检查 bundleName、版本名、版本号和当前 release 配置是否都能读到。它同样不证明包一定能上架,只是避免"代码是新版本,提交信息还是上一个版本"这种低级错误。

这张 DevEco 图里,我特意把代码、模拟器和 HiLog 放在一起。
底部日志固定为:
text
Audit: audit_20261001_02
Declared permissions: 2
Runtime requests: 1
Privacy mappings: 2
Checks: 6/6
Findings: 0
Result: PASS
这里的 Runtime requests: 1 和 Declared permissions: 2 不矛盾,因为 INTERNET 在当前映射里不走运行时用户授权。
如果扫描器不理解这种差异,最后只会逼着开发者为了"让脚本变绿"写错误代码。
七、PASS 不是"审核一定通过",而是当前工程没有发现这六类差异
手机端我最终只保留一个很克制的结果页。

audit_20261001_02 在 2026-10-01 10:24 完成,状态 PASS,6 / 6,Findings 为 0。
我在页面里没有写"可以保证上架",而是写"当前版本已通过全部检查项,可以进行提审,正式提交前再次确认版本号、证书和发布信息"。
这个措辞很重要。
因为 AppGallery 审核覆盖的内容远不止本文六项。应用稳定性、内容、资质、账号、隐私政策全文、功能真实性、地区规则等都可能影响结果。一个本地脚本没有资格替平台做最终判断。
它的价值只是在开发团队内部建立一道低成本门槛:能机器比对的内容,不要每次都靠人肉记忆。
八、真正省时间的是让变更在提交前暴露
以前我遇到这类问题,往往是打包以后才开始核对。
manifest 打开一遍,代码搜一遍,隐私政策再找一遍,最后还要问产品"这个功能现在是不是还在用"。
工具化以后,我更希望它进入正常开发链路,例如:
json
{
"scripts": {
"audit:privacy": "node scripts/privacy-audit.mjs",
"release:precheck": "npm run audit:privacy"
}
}
如果后续接 CI,也只需要让脚本发现 findings 时返回非 0 状态码,就能阻止一个明显不一致的版本直接进入发布流程。
这里仍然要保留人工复核。
因为"用途是不是描述准确""某个 SDK 在当前配置下究竟处理哪些信息""权限申请时机是否符合真实用户场景",这些都不能靠字符串集合完成。
我更愿意把 ReleaseGuard 看成一个工程校对员,而不是审核裁判。
九、这次工具最终留下的是一张一致性关系图
写完以后,我把整件事归纳成四个箭头:
Manifest 声明 → ArkTS 实际调用 → 隐私映射 → 提审信息。
四处变化应该尽量同步。
新增功能时,不只是"加权限";删除功能时,也不只是"删页面"。权限声明、运行时申请、隐私说明、三方依赖说明、审核备注都可能需要跟着变。
真正容易出问题的,是代码改完了,另外三处还停在上一个版本。
所以这个小工具最有价值的不是 6 个绿色勾,而是把一个原本依赖经验的检查动作,变成每次发布都可以重复执行的流程。
规则会继续更新,平台能力也会变化。脚本里的检查项应该跟着项目和最新官方要求维护,而不是写完一次就永久不动。
至少下一次提审前,我不用再凭记忆问一句:"相机权限的文案是不是还没改?"
运行一次 release:precheck,先把工程里能确定的差异找出来,再把时间留给真正需要人工判断的部分。
参考资料
- AppGallery Review Guidelines:https://developer.huawei.com/consumer/en/doc/50104-overview
- AppGallery User Privacy Review Guidelines:https://developer.huawei.com/consumer/fr/doc/app/50104-07
- 常见个人信息保护问题:https://developer.huawei.com/consumer/jp/doc/app/FAQ-faq-09
- HarmonyOS Ads Kit 当前权限申请示例:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ads-publisher-service-banner