HarmonyOS 7 ArkTS + Node.js:把权限声明、运行时授权与隐私文案做成提审前一致性扫描【鸿蒙心迹】

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

我这次没有再做一篇审核规则清单,而是写了一个很小的工程工具 ReleaseGuard。它不代替 AppGallery 审核,也不声称能保证通过;它只在提交版本前把项目里几类可以机械检查的内容拉到一起,先把明显的不一致找出来。

这次固定的运行结果是 audit_20261001_02,最终状态 PASS,6 / 6 检查通过,Findings = 0。

一、真正麻烦的不是"有没有权限",而是三处事实会漂移

做业务时很常见。

最初扫码页需要相机,于是在 module.json5 里加了 ohos.permission.CAMERA。后来产品调整流程,把扫码放到二级页,开发者也把运行时弹窗改成用户点击"扫描小票"时才出现。

再后来隐私文案还是旧版本,写着"启动应用时需要相机权限"。

单独看三处都不一定报错:

  • manifest 能编译;
  • ArkTS 能正常弹权限框;
  • 隐私政策也有"相机"两个字。

但放到一起,场景已经不一致了。

华为应用市场当前审核指南要求应用信息和包体完整准确、应用本身可正常运行,并符合隐私相关要求。隐私规则里还强调权限使用最小化、清楚说明权限对应功能和场景,同时避免在首次启动时频繁请求敏感权限。

所以我想做的不是替审核员判断合规,而是在代码提交阶段回答几个更具体的问题:

  1. 声明了哪些权限?
  2. 哪些权限在 ArkTS 中真的发起运行时申请?
  3. 隐私映射里有没有对应的用途和触发场景?
  4. 三方依赖有没有进入项目自己的隐私清单?
  5. 审核备注是否准备好?
  6. 当前发布信息是否完整?

二、先建一份"工程内隐私映射",别把真相只留在 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 页面最后展示六项:

  1. Permission declaration
  2. Runtime request
  3. Privacy copy
  4. Third-party SDK
  5. Review notes
  6. 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,先把工程里能确定的差异找出来,再把时间留给真正需要人工判断的部分。

参考资料

相关推荐
fellow992 小时前
把 DeepSeek Harness 搬进鸿蒙的 8 个坑
华为·harmonyos
垆边人似月.2 小时前
华为机考题(一):质数因子
算法·华为
Francek Chen2 小时前
【华为Mate90系列】麒麟9050Pro首秀!Mate90系列及多款新品正式发布
人工智能·华为·harmonyos·鸿蒙7·麒麟芯片
李游Leo2 小时前
HarmonyOS 7 + ArkUI GridRow-ListScroller:折叠切换中的列表视口锚点与布局事务【鸿蒙心迹】
华为·harmonyos
liangshanbo12153 小时前
Webpack的分包策略面试题
前端·webpack·node.js
OH_TPC13 小时前
HarmonyOS APP开发---“智泊“智能停车App,需要用到这个库
华为·harmonyos·鸿蒙
搬砖的小码农_Sky16 小时前
AI Agent:Windows上Node.js安装后 npm -v报错
前端·npm·node.js·人机交互
tsqtsqtsq030917 小时前
鸿蒙应用开发配置文件详解
harmonyos