审核被打回时,真正费时间的往往不是改一行配置,而是确认代码、权限、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 并输出标准权限记录exportinterfacePermissionRecord{moduleName:stringname:stringreason:stringabilities:string[]}exportfunctionscanPermissions(moduleConfig:Record<string,Object>):PermissionRecord[]{constmoduleInfo=moduleConfig['module']asRecord<string,Object>constmoduleName=moduleInfo['name']asstringconstrequests=moduleInfo['requestPermissions']asArray<Record<string,Object>>??[]returnrequests.map((item:Record<string,Object>)=>{constusedScene=item['usedScene']asRecord<string,Object>|undefinedreturn{moduleName,name:item['name']asstring,reason:item['reason']asstring??'',abilities:usedScene?.['abilities']asstring[]??[]}})}exportfunctionvalidatePermission(record:PermissionRecord):string[]{consterrors: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}`)}returnerrors}实际项目中要注意 JSON5 不是严格 JSON,不能简单删除注释后直接解析。脚本应使用与构建环境兼容的解析器,并且同时扫描主模块和动态特性模块。只看entry目录,会漏掉其他模块带入的权限。
四、代码里出现权限名,不等于一定会申请
第二步是扫描运行时请求。最粗糙的方式是全文搜索权限字符串,但它会把注释、测试代码和常量定义都算进去。PrivacyGate采用两级策略:先通过文本搜索找到候选文件,再识别权限请求 API 的调用上下文,记录调用方法、页面和构建条件。
这里不追求做一个完整 ArkTS 编译器,而是找出“值得人工确认”的位置。脚本报告中把结果分成三类:生产路径、调试路径和无法判断。无法判断不等于违规,只是提醒发布前补一次人工确认。
**这段代码解决什么问题:**把代码中的权限请求映射到业务入口,并识别没有对应配置声明的调用。
// 文件:tools/privacy-gate/src/runtime-request-scanner.ts// 用途:提取 requestPermissionsFromUser 调用附近的权限常量exportinterfaceRuntimeRequest{file:stringline:numberpermission:stringbuildScope:'production'|'debug'|'unknown'}exportfunctionfindRuntimeRequests(file:string,source:string):RuntimeRequest[]{constrows=source.split('\n')constresult:RuntimeRequest[]=[]rows.forEach((row:string,index:number)=>{if(!row.includes('requestPermissionsFromUser')){return}constcontext=rows.slice(Math.max(0,index-8),index+8).join('\n')constmatches=context.match(/ohos\.permission\.[A-Z_]+/g)??[]constscope=context.includes('BuildProfile.DEBUG')?'debug':'unknown'matches.forEach((permission:string)=>{result.push({file,line:index+1,permission,buildScope:scope})})})returnresult}扫描结果发现LocationDebugPage.ets:86请求了位置权限。页面已经没有菜单入口,但仍会被打包进生产模块。我的处理不是在隐私政策里补一条位置说明,而是把调试页面从 release 构建中排除,同时删除对应权限。合规不是“代码申请什么就都写进政策”,没有实际业务需要的权限应该从产物里拿掉。
五、三方 SDK 是最容易漏掉的一层
应用自身没有读取设备信息,不代表依赖库不会处理。依赖扫描不能只看包名,还要记录版本、来源、能力说明、数据类型、目的、方式和政策链接。升级依赖版本后,这些字段也要重新确认。
PrivacyGate会读取oh-package-lock.json5和约定目录中的sdk-privacy.json。如果某个生产依赖没有隐私说明文件,报告直接标成阻断项;开发依赖则标成提醒项,并检查它是否意外进入 release 产物。
**这段代码解决什么问题:**将依赖锁文件与项目维护的 SDK 隐私清单比对,找到“代码里有、政策里没有”的三方组件。
// 文件:tools/privacy-gate/src/sdk-policy-checker.ts// 用途:核对三方依赖版本与隐私政策条目exportinterfaceSdkPrivacyItem{packageName:stringversion:stringdataTypes:string[]purpose:stringpolicyKey:string}exportfunctioncheckSdkDisclosure(releasePackages:Map<string,string>,disclosures:SdkPrivacyItem[]):string[]{consterrors:string[]=[]releasePackages.forEach((version:string,packageName:string)=>{constitem=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}`)}})returnerrors}这次被找出来的是@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 打包前执行 PrivacyGateimport{appTasks}from'@ohos/hvigor-ohos-plugin'import{runPrivacyGate}from'./tools/privacy-gate/src/index'exportdefault{system:appTasks,plugins:[{pluginId:'privacy-gate',apply(node){node.registerTask({name:'privacyGateRelease',run:async()=>{constreport=awaitrunPrivacyGate({buildMode:'release',version:'2.3.0(20300)',batchId:'PG-20260930-04'})if(report.blockers.length>0){thrownewError(`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