HarmonyOS 7 + ArkTS/Hvigor:上架审核前的隐私声明一致性扫描【鸿蒙心迹】
审核被打回时,真正费时间的往往不是改一行配置,而是确认代码、权限、SDK、隐私政策和后台声明到底哪一处没有对齐。

一、审核意见只有一句,排查却横跨四个地方
这次遇到的审核意见并不复杂:“应用实际申请的权限与隐私政策说明不一致,请核对后重新提交。”团队第一反应是检查 module.json5,确认相机和相册权限都写了用途说明。配置看起来没问题,隐私政策里也出现了“图片处理”几个字,于是大家怀疑是审核误判。
真正把代码、构建产物和隐私政策放在一起看,问题才显出来:项目曾经接入一个图片统计 SDK,后来业务入口删除了,依赖包仍然保留;隐私政策列出了相机和相册,却没有逐项说明这个 SDK 可能处理的设备信息;测试包里还存在一条只在调试页面触发的位置权限请求。每个单点看起来都“差不多正确”,组合起来却无法形成一致的证据链。
我没有继续人工翻文件,而是给项目补了一个 PrivacyGate 检查脚本。它不替代人工合规判断,也不保证通过审核,它只解决一个工程问题:在打包前,把声明权限、代码调用、三方依赖、隐私政策关键词和上架后台清单整理成同一份报告,让不一致尽早暴露。
Demo 工程叫 QuietGallery,构建版本为 2.3.0(20300),检查批次为 PG-20260930-04。扫描结果一共发现 3 个阻断项:未声明的 SDK 数据类型、无业务入口的位置权限、隐私政策缺少撤回授权路径。
二、上架审核不是最后一步,而是构建链的一部分
官方上架指引明确提醒,HarmonyOS 应用发布前需要完成漏洞、隐私、兼容性、稳定性和性能等测试;如果集成第三方 SDK,还要在隐私政策中逐一明示其收集个人信息的目的、方式和范围。换句话说,隐私文本不是运营同事在后台补的一段介绍,它应当与代码和依赖一起被版本管理。
我把审核相关信息分成五份清单:
declared-permissions.json:模块清单中声明的权限与用途;runtime-requests.json:代码中可能触发的权限请求;third-party-sdks.json:OHPM 与本地 HAR/HSP 依赖;privacy-policy.json:隐私政策中结构化的数据类型和处理目的;release-profile.json:版本号、包名、上架区域与后台配置快照。

这五份数据不是让开发者维护五遍,而是由扫描器从不同来源提取后生成。人工只维护一份规则映射,例如“相机权限对应拍摄头像功能,触发点在 AvatarEditor,隐私政策条目为 camera_capture”。
三、先从权限清单里找到静态事实
第一步扫描 module.json5 中的 requestPermissions。这里只能证明应用声明了什么,不能证明运行时一定会请求,也不能证明说明文案符合实际业务。扫描器把权限名称、reason 资源、使用场景和模块名称统一输出,作为后续比对的基线。
**这段代码解决什么问题:**解析模块权限配置,识别重复声明、缺少用途资源和没有配置使用场景的权限。
// 文件:tools/privacy-gate/src/permission-scanner.ts
// 用途:读取 module.json5 并输出标准权限记录
export interface PermissionRecord {
moduleName: string
name: string
reason: string
abilities: string[]
}
export function scanPermissions(moduleConfig: Record<string, Object>): PermissionRecord[] {
const moduleInfo = moduleConfig['module'] as Record<string, Object>
const moduleName = moduleInfo['name'] as string
const requests = moduleInfo['requestPermissions'] as Array<Record<string, Object>> ?? []
return requests.map((item: Record<string, Object>) => {
const usedScene = item['usedScene'] as Record<string, Object> | undefined
return {
moduleName,
name: item['name'] as string,
reason: item['reason'] as string ?? '',
abilities: usedScene?.['abilities'] as string[] ?? []
}
})
}
export function validatePermission(record: PermissionRecord): string[] {
const errors: string[] = []
if (record.reason.length === 0) {
errors.push(`PERMISSION_REASON_MISSING:${record.name}`)
}
if (record.abilities.length === 0) {
errors.push(`PERMISSION_SCENE_EMPTY:${record.name}`)
}
return errors
}
实际项目中要注意 JSON5 不是严格 JSON,不能简单删除注释后直接解析。脚本应使用与构建环境兼容的解析器,并且同时扫描主模块和动态特性模块。只看 entry 目录,会漏掉其他模块带入的权限。
四、代码里出现权限名,不等于一定会申请
第二步是扫描运行时请求。最粗糙的方式是全文搜索权限字符串,但它会把注释、测试代码和常量定义都算进去。PrivacyGate 采用两级策略:先通过文本搜索找到候选文件,再识别权限请求 API 的调用上下文,记录调用方法、页面和构建条件。
这里不追求做一个完整 ArkTS 编译器,而是找出“值得人工确认”的位置。脚本报告中把结果分成三类:生产路径、调试路径和无法判断。无法判断不等于违规,只是提醒发布前补一次人工确认。
**这段代码解决什么问题:**把代码中的权限请求映射到业务入口,并识别没有对应配置声明的调用。
// 文件:tools/privacy-gate/src/runtime-request-scanner.ts
// 用途:提取 requestPermissionsFromUser 调用附近的权限常量
export interface RuntimeRequest {
file: string
line: number
permission: string
buildScope: 'production' | 'debug' | 'unknown'
}
export function findRuntimeRequests(file: string, source: string): RuntimeRequest[] {
const rows = source.split('\n')
const result: RuntimeRequest[] = []
rows.forEach((row: string, index: number) => {
if (!row.includes('requestPermissionsFromUser')) {
return
}
const context = rows.slice(Math.max(0, index - 8), index + 8).join('\n')
const matches = context.match(/ohos\.permission\.[A-Z_]+/g) ?? []
const scope = context.includes('BuildProfile.DEBUG') ? 'debug' : 'unknown'
matches.forEach((permission: string) => {
result.push({ file, line: index + 1, permission, buildScope: scope })
})
})
return result
}
扫描结果发现 LocationDebugPage.ets:86 请求了位置权限。页面已经没有菜单入口,但仍会被打包进生产模块。我的处理不是在隐私政策里补一条位置说明,而是把调试页面从 release 构建中排除,同时删除对应权限。合规不是“代码申请什么就都写进政策”,没有实际业务需要的权限应该从产物里拿掉。
五、三方 SDK 是最容易漏掉的一层
应用自身没有读取设备信息,不代表依赖库不会处理。依赖扫描不能只看包名,还要记录版本、来源、能力说明、数据类型、目的、方式和政策链接。升级依赖版本后,这些字段也要重新确认。
PrivacyGate 会读取 oh-package-lock.json5 和约定目录中的 sdk-privacy.json。如果某个生产依赖没有隐私说明文件,报告直接标成阻断项;开发依赖则标成提醒项,并检查它是否意外进入 release 产物。
**这段代码解决什么问题:**将依赖锁文件与项目维护的 SDK 隐私清单比对,找到“代码里有、政策里没有”的三方组件。
// 文件:tools/privacy-gate/src/sdk-policy-checker.ts
// 用途:核对三方依赖版本与隐私政策条目
export interface SdkPrivacyItem {
packageName: string
version: string
dataTypes: string[]
purpose: string
policyKey: string
}
export function checkSdkDisclosure(
releasePackages: Map<string, string>,
disclosures: SdkPrivacyItem[]): string[] {
const errors: string[] = []
releasePackages.forEach((version: string, packageName: string) => {
const item = disclosures.find((value: SdkPrivacyItem) =>
value.packageName === packageName && value.version === version)
if (!item) {
errors.push(`SDK_DISCLOSURE_MISSING:${packageName}@${version}`)
return
}
if (item.dataTypes.length === 0 || item.purpose.length === 0) {
errors.push(`SDK_DISCLOSURE_INCOMPLETE:${packageName}@${version}`)
}
})
return errors
}
这次被找出来的是 @quiet/analytics@1.6.2。依赖没有被调用,但锁文件和 release 依赖图里仍然存在。删除依赖后重新构建,产物大小减少 412 KB,对应隐私条目也不再需要。这个结果说明,依赖清理既是合规工作,也是包体积治理的一部分。
六、隐私政策要能被机器粗检,也要能被人读懂
为了便于检查,我们在仓库中保留一份结构化政策源文件,再生成用户阅读的 HTML。结构化字段包括数据类型、处理目的、处理方式、触发场景、保存期限、三方接收方、撤回方式和删除路径。
机器检查能发现字段缺失,却不能判断一句话是否足够清楚。例如“为了改善体验,我们可能收集必要信息”从字段上看并不为空,但用户无法知道是什么信息、何时收集、用在哪里。脚本只能做下限保护,最终文本仍要经过产品、法务和开发共同确认。
运行页显示本次扫描批次 PG-20260930-04,版本 2.3.0(20300),五项检查完成三项,当前发现 3 个阻断项。每个阻断项都能跳到来源文件,而不是只显示一个红色数字。

页面里把“阻断”和“提醒”分开。阻断表示存在明确的不一致,不建议继续打包;提醒表示脚本无法自动判断,需要人工确认。否则团队为了让报告变绿,可能会把真实风险改成忽略规则。
七、把检查接入 Hvigor,但保留开发节奏
检查如果只靠开发者主动运行,很快会被忘掉。我们把它接到 release 构建前:debug 构建输出报告但不阻断,release 构建遇到 blocker 就失败。这样不会影响日常调试,又能保证正式包有一致性检查记录。
// 文件:hvigorfile.ts
// 用途:在 release 打包前执行 PrivacyGate
import { appTasks } from '@ohos/hvigor-ohos-plugin'
import { runPrivacyGate } from './tools/privacy-gate/src/index'
export default {
system: appTasks,
plugins: [{
pluginId: 'privacy-gate',
apply(node) {
node.registerTask({
name: 'privacyGateRelease',
run: async () => {
const report = await runPrivacyGate({
buildMode: 'release',
version: '2.3.0(20300)',
batchId: 'PG-20260930-04'
})
if (report.blockers.length > 0) {
throw new Error(`PRIVACY_GATE_BLOCKED:${report.blockers.length}`)
}
}
})
}
}]
}
不同 DevEco Studio 和 Hvigor 版本的插件接口可能有差异,接入时要以项目当前版本文档与模板为准。本文代码表达的是接入位置和阻断策略,不建议不经验证直接复制到所有版本。
八、修复完成后,报告应该能证明什么
第一次扫描的三个阻断项是:
PG-201 SDK_DISCLOSURE_MISSING @quiet/analytics@1.6.2
PG-104 UNUSED_RUNTIME_PERMISSION ohos.permission.LOCATION
PG-308 WITHDRAW_PATH_MISSING privacy-policy.json
修复后重新运行,SDK 从 release 依赖中删除,位置权限及调试页面从生产构建中移除,隐私政策增加“设置—隐私管理—撤回授权”的可操作路径。第二次报告显示 blockers=0、warnings=1,唯一提醒是截图素材中的账号信息需要发布前复核。

这张诊断图承担的不是展示一个漂亮的绿色页面,而是把整改前后对应起来:同一个批次规则、同一个版本、三项问题的来源和修复结果都能复查。如果审核再次反馈,团队可以从报告回到具体文件,而不是重新开始猜。
九、自动化检查的边界
PrivacyGate 不能判断业务是否具有合法、正当、必要的处理目的,也不能替代审核指南、法律意见或真实设备测试。它能做的是减少低级不一致:配置声明了但代码不用、代码请求了但政策没写、依赖存在但缺少说明、后台版本与构建版本不同。
还有一些信息不适合完全自动化。例如行业资质、上架区域、付费能力和账号主体,需要结合业务实际核对;隐私政策在在架版本关联或审核中时,后台的编辑与生效流程也有状态约束,不能把“文件已更新”等同于“线上协议已生效”。
我的个人判断是,上架审核最有效的提速方式不是研究“审核喜欢什么文案”,而是让每个声明都有代码证据,每个权限都有业务入口,每个 SDK 都有版本和隐私说明,每次发布都留下可复查报告。这样即使规则变化,团队也能快速知道应该改哪一层。
十、提交前的最终动作
在 QuietGallery 中,发布负责人会拿报告做最后一次人工检查:确认包名与版本、核对 release 产物、走一遍首次启动和权限拒绝路径、验证隐私政策与撤回入口、检查截图素材、确认三方 SDK 清单,然后再把 APP 包提交到 AppGallery Connect。
工具把分散信息放到一起,人负责判断这些信息是否真实、必要和清楚。这个分工比“让脚本保证审核通过”更可靠,也更符合实际项目的责任边界。
十一、参考资料
- 华为开发者联盟:提交 HarmonyOS 应用与鸿蒙 APP
https://developer.huawei.com/consumer/cn/app/submit - HarmonyOS 开发者文档:上架申请与 SDK 隐私声明要求
https://developer.huawei.com/consumer/cn/doc/HMScore-Guides/harmonyos-release-application-0000001181600758 - AppGallery Connect:更新隐私政策协议
https://developer.huawei.com/consumer/cn/doc/doccenter-submission/agc-help-publish-api-update-privacy-agreement-0000002328805169
更多推荐


所有评论(0)