HarmonyOS 7 Hvigor:上架前权限证据与版本核验【鸿蒙心迹】
这次写的不是界面开发,而是临近发版才会让人紧张的一类工作:功能已经跑通,Debug 包也能安装,提交应用市场之前却发现权限说明、应用身份、证书与最终构建产物没有形成同一份证据。
做项目时最怕的是“每个人都检查过一点,但没有人能说清最后交出去的到底是哪一包”。运营拿着一张隐私说明截图,开发者手里是另一套权限配置,测试同学装的是昨天的 HAP,打包脚本又从旧目录找到了一个同名产物。每一环似乎都做了检查,最后还是会在审核阶段反复返工。
所以我做了一个很小的工程侧工具 ReleaseGate,对应的示例应用叫 晨间清单。本轮演示的身份固定为 com.example.morninglist,版本名 1.6.2,版本号 10602。它不替代应用市场审核,也不试图预测通过率,而是只做一件事:在提交之前,把本地能够核对的证据收集到一份可以复验的结果里。

一、真正危险的是“检查对象不是同一份产物”
一开始我也想把预检工具做得很简单:读一下配置文件,确认权限字段不为空;构建目录里有 HAP 就算通过。这种方式写出来很快,却容易带来虚假的安全感。因为配置文件正确,不代表当前产物就是用它构建的;产物存在,不代表签名 Profile 正确;页面里的“审核准备完成”,也不代表 AppGallery Connect 已经接受该版本。
我后来给检查项分了三个层级。配置层看 AppScope/app.json5、entry/src/main/module.json5 及资源文件;构建层看 release 模式、构建输出、包名版本与签名信息;交付层看最终要上传的那个包的摘要、文件尺寸、生成时间和人工复核证据。三层检查必须明确指向同一轮构建,而不是从不同目录拼凑一个“全通过”。
示例的初次检查故意保留一个真实工程里很常见的问题:CAMERA 权限用途说明有中文文案,却缺少英文环境下的对应资源。结果应该是 NEED_REVIEW,12 个本地检查项通过 11 项;修复后再运行才变成 READY_FOR_REVIEW,12/12 通过。这里两个状态都是 ReleaseGate 自己定义的本地状态,绝不能被解读为“官方审核结果”。
二、先锁定包名、版本和本轮构建身份
最容易被忽略的细节,是团队把版本名与版本号口头说成一回事。实际上这两个字段承担不同职责,包名则是另一个身份维度。演示应用的 app.json5 里使用如下配置片段:
{
"app": {
"bundleName": "com.example.morninglist",
"versionCode": 10602,
"versionName": "1.6.2",
"label": "$string:app_name"
}
}
这段示例不是一份完整可直接替换的 app.json5,仅突出检查字段。实际工程仍应保留厂商模板、SDK 版本与其他必要项。关键是把这三项作为构建清单的不可变输入,而不是每次发布时临时手填到后台。发布前一旦发现配置变动,就应作废上一轮产物和证据,重新构建。
我的习惯是在 preflight 报告里同时写入 bundleName、versionCode、versionName、构建模式、目标产物相对路径、摘要与检查时间。所谓“版本一致”必须是多方比对:源配置与实际打包结果一致,发布后台登记的应用身份也对应。如果只看工程目录名叫 MorningList 就认为包名正确,迟早会碰到麻烦。
还有一个实际问题是多模块工程。项目里可能不止一个 entry,还含有 feature 或 shared 模块。配置检查不应凭路径里恰好有一个 module.json5 就得出全局结论,要先从工程的模块定义获得需要构建和交付的模块清单,再按目标产品配置逐项关联。对于 release 产物,应以本次构建目标为准,避免把开发环境中的 debug 结果混入报告。
三、权限审查不要只匹配一个字段,要匹配“申请理由—使用场景”
华为的权限声明文档明确强调,涉及需要用户授权或手动设置的权限,配置中还要关注说明文案与 usedScene。这种检查最怕使用一个非常浅的规则,比如“只要 requestPermissions 非空就是合规”。真正影响审核的是业务为什么申请、用户在什么功能点能感知到、语言资源是否完整、权限级别及受限权限资格是否匹配。
示例在 entry/src/main/module.json5 里描述相机调用场景:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
这段代码解决的是声明结构的可追踪性,不等于配置一写就具备该权限。产品里还要在恰当交互节点执行相应授权流程,拒绝授权时提供可用的降级路径。演示场景中,相机用于“拍摄任务附件”,所以理由不能写成“为了提升体验”,更不能把相机写成蓝牙、定位等完全不同的用途。
本次预检失败的点非常具体:$string:camera_permission_reason 在中文资源中可解析,却在项目声称支持的英文资源包里不存在。ReleaseGate 不应该自动把中文文案复制过去冒充英文,也不应该悄悄标记为通过;它应该输出哪个权限、哪个 resource key、哪个语言环境缺失,交由产品确认文案含义,再由本地化流程修复。
权限的声明、弹窗与隐私政策仍需要人工联动审查。比如配置允许相机,但首次启动就弹出无上下文的权限框,虽然预检脚本可以证明字段存在,体验和审核风险依旧没有消失。工具要把“机器检查能确定的事实”和“需人工判断的合规性”分栏表达,而不是用一个绿色勾掩盖边界。
四、脚本规则要可复验,而不是临时搜索字符串
ReleaseGate 的脚本放在 scripts/preflight.mjs。我不愿意把检查写成十几条 grep,因为 JSON5 包含注释和尾逗号,用原始字符串检索既会产生误报,也很难分析字段层级。下面示例使用 Node.js 与项目显式安装的 json5 包读取配置;注意 json5 是脚本依赖,不是 Node 自带模块。
import fs from 'node:fs/promises'
import JSON5 from 'json5'
async function readJson5(path) {
const raw = await fs.readFile(path, 'utf8')
return JSON5.parse(raw)
}
async function verifyIdentity(expected) {
const cfg = await readJson5('AppScope/app.json5')
const actual = cfg.app ?? {}
return actual.bundleName === expected.bundleName &&
actual.versionCode === expected.versionCode &&
actual.versionName === expected.versionName
}
const identityOk = await verifyIdentity({
bundleName: 'com.example.morninglist',
versionCode: 10602,
versionName: '1.6.2'
})
console.info(`[PREFLIGHT] bundle identity ${identityOk ? 'PASS' : 'FAIL'}`)
这段脚本只完成配置层身份检查。最重要的限制要说清楚:它没有从 HAP 内部读取实际 manifest,也没有校验证书。 如果把这段代码的 PASS 用作最终发布许可,就是把范围说大了。完整工具应继续解析本轮构建产物的元数据,并与配置层形成关联;不同工具链版本的包检查命令及字段位置,应以当前 DevEco Studio/Hvigor 版本为准。
下一段代码解决更实际的权限语言资源核对。为了保持演示清晰,示例的资源定位由 loadResourceKeys(locale) 这个业务函数完成;它应解析工程实际使用的资源目录及回退策略,不可把“文件不存在”粗暴等同于“权限一定不合法”。检查输出区分机器发现和审核解释。
function auditCameraReason(permission, zhKeys, enKeys) {
if (permission.name !== 'ohos.permission.CAMERA') return null
const reason = permission.reason ?? ''
const key = reason.startsWith('$string:')
? reason.slice('$string:'.length) : ''
const zhOk = key.length > 0 && zhKeys.has(key)
const enOk = key.length > 0 && enKeys.has(key)
return {
ruleId: 'PERMISSION_REASON_I18N',
permission: permission.name,
resourceKey: key,
passed: zhOk && enOk,
detail: !enOk ? 'en_US reason missing' : 'OK'
}
}
const finding = auditCameraReason(
{ name: 'ohos.permission.CAMERA',
reason: '$string:camera_permission_reason' },
new Set(['camera_permission_reason']),
new Set([])
)
console.warn(`[PREFLIGHT] ${finding?.detail}`)
执行结果是 en_US reason missing,这正好对应首轮的一个失败项。真实系统的语言资源查找还涉及设备语言匹配、默认资源回退和多区域资源结构,因此发布规则不应止于上面这个简化函数。实际工程需要按已声明的支持语言逐个解析并对照运营文案;对于不支持英文的产品,则应先校验“支持语言列表”本身,而不是强加一个英文规则。

图 02 用的是 DevEco Studio 白色主题,左侧目录能看到 AppScope/app.json5、entry/src/main/module.json5、resources/base/element/string.json、scripts/preflight.mjs;中间是检查规则;右侧本地自检页显示 11/12,底部 Build 控制台记录 permission reason en_US missing。需要提醒的是,脚本日志与模拟器画面是为文章编排的拟真示意,并非实际执行工具的自动截图。
五、把“12 项中的 1 项失败”具体说清楚
我希望失败结果不是一个红叉,而是一张能够驱动修复的工单。首次检测的状态是 NEED_REVIEW,检测总数 12、通过 11、待修复 1;出错对象是 ohos.permission.CAMERA,字段是用途说明的英文资源。其它项目包括应用身份、签名配置、资源文件、产物是否存在等,但每一项的执行证据都应有独立链接,不能只是静态显示“PASS”。

这个页面刻意不显示“审核失败”,而是写“上架预检”。因为本地脚本检测到了明确缺失,但它没有资格替官方审核系统作决定。页面上的“查看待修复项”应能打开资源文件和规则说明,而不是把开发者导向一个含糊的 FAQ 页面。日志最好保留完整 resource key 和语言环境,减少跨团队反复问“缺的是哪一句话”。
我还把严重程度拆成三档:硬性阻断是产物缺失、包身份不符、已知关键资源缺失等;需要人工复核是权限用途、隐私描述、核心功能访问路径等;提醒是包体增长、非关键资源缺少说明等。前两档不能随便自动变绿,最后一档也不意味着可以完全忽略。这样拆分后,项目经理看到报告就知道该找开发、测试、产品还是合规同学。
六、修好资源以后,为什么还要重新构建、重新算摘要
很多团队在改完 string.json 后只跑一遍脚本,就认为证据已经齐了。我的想法正好相反:任何进入安装包的资源变动,都会使旧构建产物的证据失效。 即使改的只是几行文案,也应该生成新的 release 产物,再对它执行核对,而不是拿“新配置的检查报告”加上“旧 HAP”一起提交。
为了保证报告绑定到最终交付文件,我把 SHA-256 作为产物指纹。下面是可以单独运行的 Node.js 示例:
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
async function sha256OfFile(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash('sha256')
const stream = createReadStream(filePath)
stream.on('data', (chunk) => hash.update(chunk))
stream.on('error', reject)
stream.on('end', () => resolve(hash.digest('hex')))
})
}
const releaseFile = process.argv[2]
if (!releaseFile) throw new Error('Usage: node hash.mjs <release-file>')
console.info(`[PREFLIGHT] artifact SHA-256 ${await sha256OfFile(releaseFile)}`)
此处计算的是字节摘要,不是签名可信性校验。它的好处是可以让两个人确认是否拿着同一个文件,但无法单凭哈希判断签名证书合法、Profile 未过期、受限权限已经获批。签名信息还应由构建和签名工具链的产物检查环节验证,并把证书标识、Profile 期限、目标包名与批准的权限清单记录到本次报告。
同样不能只凭“HAP 构建成功”就推出“审核一定通过”。构建工具主要证明能按当前配置形成产物,应用市场还会检查业务功能、隐私说明、内容合规与其他发布要求。这里把不同责任边界留清楚,恰恰是 ReleaseGate 的价值。
七、前端展示的状态必须和后台报告同源
预检工具还有一个不起眼的问题:如果页面自己管理检查项数量,脚本也独立管理一套,两者就会出现“控制台报 11/12,页面却显示 12/12”。我不希望 ReleaseGate 变成另一个状态不同步的工具,因此页面只渲染最后一份已提交的 PreflightReport,由检查执行器产生报告,UI 不自己猜。
这段 ArkTS 是页面消费报告模型的最小示意。它不负责运行 Node 脚本;脚本执行应在开发机或 CI 中进行,报告再通过工程约定的导入/同步方式进入应用演示界面。这样可以避免把“IDE 端 Node 脚本”假装成手机端可直接调用的系统接口。
interface PreflightReport {
appName: string
bundleName: string
versionName: string
versionCode: number
total: number
passed: number
status: 'NEED_REVIEW' | 'READY_FOR_REVIEW'
}
@Component
struct PreflightSummary {
@Prop report: PreflightReport
build(): void {
Column({ space: 12 }) {
Text(this.report.appName).fontSize(22).fontWeight(FontWeight.Bold)
Text(`${this.report.bundleName} · ${this.report.versionName}`)
Text(`${this.report.passed}/${this.report.total} 本地检查通过`)
Text(this.report.status)
.fontColor(this.report.status === 'READY_FOR_REVIEW'
? Color.Green : Color.Orange)
}.width('100%')
}
}
我在报告协议里还会加入规则版本 ruleSetVersion、源代码提交标识、构建产物摘要、生成时间和是否完成必要人工复核的标记。这些字段不要求都显示在首屏,但不能从报告里省略。否则一年之后再看到“12/12”,没有人知道当年执行的是哪十二条规则。
八、复检通过之后,交付的是可追踪证据,不是一张绿色截图
演示案例在 14:26 看到 NEED_REVIEW,随后补齐 CAMERA 说明并重新产出包。14:31 的复检页显示 READY_FOR_REVIEW、12/12;包名仍为 com.example.morninglist,版本仍为 1.6.2 (10602)。为了便于对照,示意报告中还展示了产物摘要片段 8b1f4e2c9d6a7e33…。这个摘要只是示例值,不能当成文中任何真实 HAP 的校验结果。

这张图的信息结构与图 03 不同。它不是再把失败项列表重复一遍,而是展示修复前后 11/12 → 12/12、具体变更点、bundleName 一致性、签名检查、产物指纹和本地检查日志。最重要的一条提示也放在页面里:本地自检通过 ≠ 应用市场审核通过。用户可据此进入提交步骤,但最终结论仍要以官方平台实际审核结果为准。
复检时还应该做一次反向测试:故意把英文资源 key 改错,看脚本能否重新报错;把 versionCode 改成与预期不同的值,确认身份检查会阻断;替换产物文件,看摘要能否变化。只有正向通过而没有负向验证的工具,特别容易被小范围静态演示误导。
如果涉及受限权限,人工环节还必须确认申请资格、ACL、Profile 和业务使用场景。自动化脚本不应把“工程里声明了一个权限”解释为“平台已经批准使用该权限”。在审核涉及动态政策变化的地方,我宁愿把检测结果写成“需要人工核实”,也不愿输出不可靠的“合规通过”。
九、把发版前的检查变成固定工作,而不是最后一天的突击
我后来把 ReleaseGate 组织成三个阶段。开发日常分支只检查配置结构和资源完整性,以便尽早发现简单错误;发布候选版本要求跑 release 构建、签名与产物信息核对;最终准备上架时,由测试、产品和发布负责人复核功能与说明,再把最终文件摘要锁进交付记录。每一阶段都有范围,不跨级代替另一阶段的职责。
这套做法最大的变化不是减少多少点击,而是让沟通从“我记得我检查过”变成“这份报告对应哪个提交、哪个包、哪组配置”。有人问 CAMERA 为什么申请,就能顺着规则定位到业务场景、用途资源和界面触发点;有人问上传的包是不是最终版,就能以摘要确认,不用在聊天群里翻几十个“final_final”文件。
如果要在真实项目里进一步落地,我会补充 CI 门禁、发布资产的只读存档、配置与最终产物的双向解析、检查规则的版本管理,以及人工复核签名。工具也要允许“检查无法完成”的第三种结果,不应把网络故障、命令缺失、解析失败默认算作通过或不通过。明确未知,比伪造确定性更有价值。
最后回到最初的问题:HarmonyOS 应用上架前的质量准备,不是把所有检查项打勾,而是让权限说明、版本身份、签名配置、构建产物和人工审核证据指向同一次真实交付。这样即使需要返工,团队也知道问题出在哪一层,不会每次都从零开始排查。
十、把预检报告接进 CI 时,我会坚持这几条交付约束
如果 ReleaseGate 真要成为团队工作流,而不只是开发者电脑上的一个小工具,我会先定清报告的可重复性。相同代码提交、相同依赖锁文件、相同签名配置与相同构建参数,应该尽量产生可比较的检查输入。脚本依赖用锁文件锁定;检查规则按版本发布;构建参数作为报告的一部分入库。这样即使 DevEco Studio 或 Hvigor 升级,团队也能知道是工程变化还是工具版本变化引起的检查差异。
CI 流程我会拆为“读取配置—静态规则—release 构建—产物元数据—签名检查—计算摘要—生成报告”几个节点。前面任何节点没有成功完成,都不能让后面的状态默认显示 PASS。特别是证书校验工具不可用时,应输出 NOT_VERIFIED,而不是因为没有检测到错误就认为签名有效。这个区分看似啰嗦,却直接决定报告是否值得被其他人信任。
对于本例的英文 CAMERA 权限说明,我还会加一份小型回归样例:保留一个缺失 en_US 资源的输入,期望命中 PERMISSION_REASON_I18N;再保留一个已经补齐资源的输入,期望规则通过。下一次改动脚本时,这两份输入可以立即告诉我们有没有把旧问题重新引入。与其依赖开发者记住每一条规则的细枝末节,不如把规则行为固定成可执行用例。
最后是交付权限。摘要应在最终 release 包产生后计算,并存到不可随意覆盖的发布记录中。产品同学拿到的是报告与审核材料,发布负责人拿到的是摘要匹配的最终产物,而不是一个大家都能重新打包的目录。即使应用市场要求补材料、需要重新上传,团队也能明确区分“材料补充”与“安装包变更”。这套边界一旦形成,预检才真正从一张检查页面变成了可追溯的工程过程。
参考资料与实施说明
- 华为开发者文档:声明权限
- 华为开发者文档:工程目录结构
- 华为开发者文档:签名配置与 ACL
- 华为开发者文档:Hvigor 构建模式
- 华为开发者文档:发布运行要求
文章所列 12 项为自定义本地检查模型,并非华为官方固定检查项。图片均为拟真演示,结果数值与摘要片段是样例数据,不能用于替代真实签名验证、HAP 检查、正式发布测试和 AppGallery Connect 审核。
更多推荐



所有评论(0)