ArkUI+AGC:隐私政策双入口版本指纹与失败降级【鸿蒙心迹】
上架前核对隐私政策,很容易只检查“设置页有一个入口”。可审核人员或普通用户进入应用的路径并不一样:有人在首次引导页点进政策,有人在设置中心里寻找,有人从应用市场的详情页打开链接。页面里都写着“隐私政策”,并不表示它们指向同一版本,更不表示远端页面一定能打开。这篇以 PolicyBridge 演示一个很克制的策略:先把本地入口一致性做成确定性检查,再把真正需要联网和人工确认的事项明确留在待验区。

一、别把“按钮存在”理解成“政策可达”
华为 AppGallery 的公开审核指南要求应用内提供容易找到的隐私政策入口,也需要在 AppGallery Connect 提交相应政策链接。政策应说明个人信息处理的目的、方式和范围,并覆盖相关第三方处理场景;应用内和提交到平台的信息还需要保持一致。这不是“在页面上写一句我们重视隐私”就可以替代的工作。本文讨论工程检查,不对某个真实应用作出通过审核的判断。
演示工程叫 PolicyBridge,页面是 PolicyCheckPage,任务编号 POL-614。样例政策版本定为 v6,使用两个本地入口:welcome 对应首次引导,settings 对应设置中心。两个入口都映射到同一份本地政策快照;屏幕展示的 2/2 只代表这两个本地配置项都匹配。线上地址是否可达、AppGallery Connect 后台实际填写了什么、站点内容是否符合合规要求,在这个数字之外。
这层区分非常重要。如果把“本地路径都匹配”直接写成“审核通过”,之后一旦出现线上 404、重定向到旧版、不同地区语言不一致,就会找不到真正的责任边界。
二、给政策版本一个可复算的指纹
只记录版本号 v6 还不够。不同构建分支都可能声称自己属于 v6,却包含不同段落。因此在演示中,版本号用于人看,短指纹用于快速发现构建产物是否混用。指纹依据一个固定的、按约定顺序拼接的样例字符串生成:PolicyBridge|v6|目的:查看任务数据|方式:本地展示|范围:任务标识。这不是具有法律效力的隐私政策全文,也没有覆盖实际 SDK 清单;它只服务于本文的校验逻辑。
下面这段属于开发阶段的 Node.js TypeScript/JavaScript 工具代码,不是 ArkTS 系统 API。它解决的问题是:使不同构建人员能够对同一演示快照算出相同标识,而不是手工填一个看起来随机的哈希值。
import { createHash } from 'node:crypto'
const policySnapshot: string =
'PolicyBridge|v6|目的:查看任务数据|方式:本地展示|范围:任务标识'
const fingerprint: string = createHash('sha256')
.update(policySnapshot, 'utf8')
.digest('hex').slice(0, 8)
console.info(`PolicyBridge version=v6 fingerprint=${fingerprint}`)
// 对该固定演示字符串,结果为 c275357f
八位十六进制串适合作为可读的短校验标记,但不能作为安全签名,也无法替代全量内容校验。正式项目应对规范化后的完整政策正文生成足够长度的摘要,保留完整值与签名来源,并把多语言版本纳入计算。如果源文件中混入动态时间戳、编辑器换行风格或不可见字符,也会影响哈希,必须先明确规范化规则,而不是为了让值相同随意去掉正文。
三、把两个入口的配置收敛为一份数据
做完指纹,我首先取消两处页面各写一遍 URL 的方式。把版本、指纹和入口名放进一份应用层映射,页面只传 source;检查逻辑按入口键读取目标版本。这里只是应用内的配置结构,不涉及读取 AppGallery Connect 私有接口,更没有调用所谓“审核结果 API”。
为了让文章可复现,下面给出最小 PolicyUtils.ets。其中 getOnlineVersion() 这个方法名是 Demo 中沿用的历史叫法,实际只读取本地的候选发布快照;生产版本应改名为 getReleaseCandidateVersion(),并由独立检查任务验证线上页面。不能把这个演示返回值理解为真的访问过网络。
export class PolicyUtils {
static readonly VERSION: string = 'v6'
static readonly FINGERPRINT: string = 'c275357f'
private static readonly routes: Record<string, string> = {
welcome: 'v6',
settings: 'v6'
}
static getLocalVersion(): Promise<string> {
return Promise.resolve(PolicyUtils.VERSION)
}
static getOnlineVersion(): Promise<string> {
// 演示:返回待发布配置快照,不执行联网请求
return Promise.resolve('v6')
}
static countMatchingRoutes(): number {
return Object.values(PolicyUtils.routes)
.filter((v: string) => v === PolicyUtils.VERSION).length
}
}
业务端只接受 welcome 和 settings 两个来源。如果来源无效,应该报出缺失入口,而不是默默替用户跳转到首页。新增“关于我们”“游客模式”之类入口时,不应直接把分母仍写成 2;应该更新声明列表和回归用例。Promise.resolve 在这里刻意表示本地静态样例,不模拟网络成功率,也不会生产任何真实 AppGallery 审核数据。
四、页面只负责展示已知事实
PolicyBridge 的 PolicyCheckPage 保留三个容易理解的状态:当前来源、是否已进入政策流程、最近一次检查摘要。首次引导和设置中心两个按钮调用相同的 openPolicy,只在参数上区别入口。这样可以排除“两个按钮调用两份过时页面逻辑”的风险。
这里的 ArkTS 代码解决两个问题:单一跳转函数和明确的失败降级。PrivacyPage 应在真实工程的页面配置中注册,并通过路由参数显示完整、可访问的政策正文;示例仅展示主页面逻辑,并不附带一个冒充真实政策的 HTML 页面。
import { router } from '@kit.ArkUI'
import { PolicyUtils } from '../utils/PolicyUtils'
@Entry
@Component
struct PolicyCheckPage {
@State policyVersion: string = 'v6'
@State fingerprint: string = 'c275357f'
@State source: string = 'settings'
@State resultText: string = '未检测'
@State visible: boolean = false
openPolicy(source: string): void {
if (source !== 'welcome' && source !== 'settings') return
this.source = source
this.visible = true
console.info(`[PolicyBridge] source=${source} version=v6 fp=c275357f`)
router.pushUrl({ url: 'pages/PrivacyPage', params: { source: source } })
.catch(() => { this.resultText = '打开失败:请使用内置政策入口' })
}
async checkPolicy(): Promise<void> {
try {
const local: string = await PolicyUtils.getLocalVersion()
const candidate: string = await PolicyUtils.getOnlineVersion()
const count: number = PolicyUtils.countMatchingRoutes()
this.resultText = local === candidate && count === 2 ? '本地一致性 2/2' : '本地配置不一致'
console.info(`[PolicyBridge] result=LOCAL_MATCH_2_OF_2 agc=UNVERIFIED`)
} catch (e) {
this.resultText = '配置读取失败,线上状态未核验'
}
}
build() {
Column({ space: 16 }) {
Text('隐私政策入口核验').fontSize(24)
Text(`政策版本 ${this.policyVersion} / 指纹 ${this.fingerprint}`)
Button('首次引导').onClick(() => this.openPolicy('welcome'))
Button('设置中心').onClick(() => this.openPolicy('settings'))
Button('检查入口').onClick(() => { this.checkPolicy() })
Text(this.resultText)
Text('线上地址待核验')
}.padding(20).width('100%')
}
}
visible 用来记录当前页面是否已发起入口动作,在扩展页面里可配合返回处理恢复界面状态;它不是路由成功的判据。路由失败时只展示降级文案还不够,生产应用最好保留一个真正可打开的内置政策页面,记录失败来源与时间,不应在未经测试的情况下直接跳过用户知情环节。checkPolicy 中的 candidate 依旧是本地候选版本,故日志明确写 agc=UNVERIFIED。如果后续增加异步网络校验,还应处理超时、重定向、证书和页面销毁后的过期回调。

DevEco 配图的目录、代码和 HiLog 均为人工组织的演示画面。底部安排两次入口记录 source=welcome、source=settings,两者统一为 version=v6、fp=c275357f;第三条是 LOCAL_MATCH_2_OF_2 agc=UNVERIFIED。部分代码在演示图中被简化,真正的逻辑以本文三个代码片段为准。它不能用作线上请求成功、实际应用审核通过或 IDE 编译通过的证明。
五、要验收的是三种不同的“相同”
第一种相同是本地路由相同:welcome 和 settings 指向同一候选政策版本。第二种是内容快照相同:编译后的内置正文、版本元数据与所提交的发布包对应;短指纹只能做快筛,正式核验应保存全量摘要。第三种是对外内容相同:AppGallery Connect 后台提交的 URL、用户真实访问的最终页面、应用内展示的政策内容和第三方数据处理说明一致。第三种需要在线访问、必要的地区语言切换及人工检查,前两种的 2/2 无法代替。

手机演示图对应任务 POL-614,日期 2026-10-11,版本 v6,短指纹 c275357f,本地入口一致性 2/2,两个入口均显示“已映射 v6”。下方橙色提示“线上地址待核验”是本篇刻意保留的重要状态,不是界面缺陷。14:40 为固定演示时间;这些数据不表示当日有真实审核记录。
实际验收还应打开三个入口,手动执行返回、断网、政策站点不可访问、地区语言切换及账号未登录等操作。关键不是让所有错误都变成“成功页”,而是失败时用户仍知道隐私政策在哪里、目前是否能读取,以及如何联系开发者。政策中对信息收集目的、方式、范围、权利行使和第三方 SDK 的说明,也应由产品与合规负责人员确认,不能靠本地哈希脚本替代。
六、它能减少发布失误,但不能替代审核
这个小工具最大的价值,是把容易被忽略的配置分叉提前暴露:入口加了却没登记、构建版本变了却没同步短指纹、页面可见但完整正文没有配置。代价也很明确——维护一份可靠的候选发布快照,保证生成环节的内容规范化,避免因为人为修改常量而把 2/2 做成自我证明。
我会把线上 URL 检查放在独立发布流程里:真正读取 AppGallery Connect 提交值后再做重定向、TLS、最终正文和语言版本核对;无法自动读取后台配置的部分就留作人工待验,绝不自动标绿。当前 Demo 只完成本地逻辑说明,不包含真实网页抓取、DevEco 编译结果或平台审核结论。能确认什么就标记什么,不把本地一致性包装成上架通过率,这才是这种小工具可以长期复用的边界。
参考资料:华为开发者 AppGallery Review Guidelines 第 7 节 User Privacy:
https://developer.huawei.com/consumer/en/doc/app/50104-07
华为开发者 App Review 常见问题“True and Valid Information Provided by Developers”:
https://developer.huawei.com/consumer/fr/doc/app/FAQ-faq-09
华为开发者 ArkTS 页面生命周期(含 router.pushUrl 示例):
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/arkts-page-custom-components-lifecycle-V5
更多推荐




所有评论(0)