第78篇 | HarmonyOS 隐私失败态:认证失败、未解锁和导出限制如何提示
第78篇 | HarmonyOS 隐私失败态:认证失败、未解锁和导出限制如何提示
第 78 篇收束第 16 天:隐私能力不仅要有成功路径,还要有失败态。保险箱、系统导出、防窥、分享这些能力都和系统权限、用户确认、设备支持有关。只写成功路径的 Demo 很容易看起来漂亮,但用户一旦取消授权或设备不支持,就会卡住。
双镜记忆相机在多个位置处理失败态:认证结果映射成可读文案,敏感操作前要求再次本地认证,导出到系统相册时处理空文件、取消保存和保存失败,防窥能力把权限缺失、设备不支持、系统未开启区分开。本文用这些真实代码整理一套隐私失败态写法。
本篇目标
- 理解隐私失败态为什么要区分权限、认证、设备和用户取消。
- 掌握敏感操作前复用本地认证的写法。
- 理解导出系统相册时如何处理空文件、取消和异常。
- 学会把错误结果写回对应作用域的状态文案。
对应源码位置
superImage/entry/src/main/ets/pages/Index.ets
失败态决定隐私功能是否可信
保险箱页面看起来只有一个解锁按钮,但它背后有很多可能失败的点:权限被拒绝、没有录入生物特征、认证超时、设备不支持、导出时用户取消、源文件还没恢复。每一种失败都需要让用户知道当前发生了什么。
高质量文章不能只截图成功页面,还要把失败路径写出来。真实项目中,用户遇到最多的往往不是“代码完全不可用”,而是某个系统条件没满足。把这些条件处理好,功能才像一个完整应用,而不是一次性演示。

保险箱隐私操作需要明确失败提示和状态恢复
认证结果先映射成用户能懂的话
getUserAuthResultMessage 把 UserAuth 的结果码映射成具体文案。失败、取消、超时、服务忙、认证能力锁定、未录入、类型不支持、信任等级不支持都分别处理。这样页面不需要直接展示数字错误码。
这类映射函数建议集中维护。后续如果要把文案换成更产品化的表达,或者对某些错误码增加引导,只需要改一个函数。训练营项目写到这里时,可以把它作为“错误码到用户语言”的示例。

getUserAuthResultMessage 将认证结果转换为可读状态
private getUserAuthResultMessage(resultCode: number): string {
if (resultCode === userAuth.UserAuthResultCode.SUCCESS) {
return '认证成功';
}
if (resultCode === userAuth.UserAuthResultCode.FAIL) {
return '认证失败,请重试';
}
if (resultCode === userAuth.UserAuthResultCode.CANCELED) {
return '已取消认证';
}
if (resultCode === userAuth.UserAuthResultCode.TIMEOUT) {
return '认证超时';
}
if (resultCode === userAuth.UserAuthResultCode.BUSY) {
return '认证服务正忙,请稍后再试';
}
if (resultCode === userAuth.UserAuthResultCode.LOCKED) {
return '认证能力暂时锁定';
}
if (resultCode === userAuth.UserAuthResultCode.NOT_ENROLLED) {
return '还没有设置可用的本地身份认证';
}
if (resultCode === userAuth.UserAuthResultCode.TYPE_NOT_SUPPORT) {
return '当前设备不支持此认证方式';
}
if (resultCode === userAuth.UserAuthResultCode.TRUST_LEVEL_NOT_SUPPORT) {
return '当前安全等级不支持';
}
return `未知认证结果:${resultCode}`;
解锁流程要处理空保险箱和并发点击
unlockVaultWithLocalAuth 先判断 vaultAuthBusy,避免多次点击并发拉起认证;如果保险箱没有记录,直接返回。进入认证流程后,权限失败、认证不通过和异常都会更新 vaultUnlocked 与 vaultStatusText。
最关键的是 finally 里恢复 vaultAuthBusy。没有这个恢复,用户一次取消认证后按钮可能永远处于“认证中”。失败态不是只写 catch,而是要保证状态能回到下一次可操作。

unlockVaultWithLocalAuth 处理并发点击、权限失败和认证失败
private async unlockVaultWithLocalAuth(
authProvider: () => Promise<userAuth.UserAuthResult>
): Promise<void> {
if (this.vaultAuthBusy) {
return;
}
if (this.getVaultRecords().length === 0) {
this.vaultStatusText = '';
return;
}
this.vaultAuthBusy = true;
this.vaultStatusText = '正在进行身份认证...';
try {
const permissionReady = await this.requestBiometricPermission();
if (!permissionReady) {
this.vaultStatusText = '';
return;
}
this.vaultStatusText = '';
const result = await authProvider();
if (result.result === userAuth.UserAuthResultCode.SUCCESS) {
this.vaultUnlocked = true;
this.vaultStatusText = '';
} else {
this.vaultUnlocked = false;
this.vaultStatusText = `身份认证未通过:${this.getUserAuthResultMessage(result.result)}`;
}
} catch (error) {
this.vaultUnlocked = false;
if (error instanceof Error) {
this.vaultStatusText = `解锁保险箱失败:${error.message}`;
} else {
const err = error as BusinessError;
this.vaultStatusText = `解锁保险箱失败:${err.code ?? -1}`;
}
} finally {
this.vaultAuthBusy = false;
}
}
private async unlockVaultWithFace(): Promise<void> {
await this.unlockVaultWithLocalAuth(() => this.authenticateVault());
}
private async unlockVaultWithFingerprint(): Promise<void> {
await this.unlockVaultWithLocalAuth(() => this.authenticateVaultWithFingerprint());
敏感操作前复用本地认证
查看保险箱是一层保护,导出私密照片又是另一层敏感操作。requireLocalAuthForSensitiveAction 会在执行保存、导出等动作前再次要求本地身份认证,并把状态写回 gallery 或 vault 对应作用域。
这个函数复用了权限申请和 authenticateVault,成功返回 true,失败返回 false。调用方只要在敏感操作前 await 它,就能把“二次确认”做成统一模式,避免每个导出按钮都重新写认证流程。

导出等敏感操作前统一复用本地认证
private async requireLocalAuthForSensitiveAction(
scope: 'gallery' | 'vault',
actionText: string
): Promise<boolean> {
if (this.vaultAuthBusy) {
return false;
}
this.vaultAuthBusy = true;
this.updateRecordExportStatus(scope, '正在验证身份...');
try {
const permissionReady = await this.requestBiometricPermission();
if (!permissionReady) {
this.updateRecordExportStatus(scope, `${actionText}需要本地身份验证`);
return false;
}
const result = await this.authenticateVault();
if (result.result === userAuth.UserAuthResultCode.SUCCESS) {
this.updateRecordExportStatus(scope, '');
return true;
}
this.updateRecordExportStatus(scope, `${actionText}未完成:${this.getUserAuthResultMessage(result.result)}`);
return false;
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
this.updateRecordExportStatus(scope, `${actionText}认证失败:${message}`);
return false;
} finally {
this.vaultAuthBusy = false;
}
}
导出到系统相册也要有失败兜底
导出系统相册前,函数先构造导出项;没有可保存文件时直接提示。对于 vault 作用域,还会先调用敏感操作认证。真正保存时,项目处理了源文件不可用、用户取消、保存失败和 photo helper 释放。
这段代码说明隐私失败态不只发生在认证层。文件、系统相册权限、用户确认、系统 API 都可能失败。把状态写回 updateRecordExportStatus,用户才能知道当前是取消、保存失败还是文件还在恢复。

exportRecordToSystemAlbumWithSaveGrant 处理导出空项、认证和保存异常
private async exportRecordToSystemAlbumWithSaveGrant(
record: GalleryMoment,
scope: 'gallery' | 'vault'
): Promise<void> {
if (this.mediaExportBusy) {
return;
}
const exportItems = this.buildRecordExportItems(record);
if (exportItems.length === 0) {
this.updateRecordExportStatus(scope, '没有可保存的照片');
return;
}
if (scope === 'vault') {
const authReady = await this.requireLocalAuthForSensitiveAction(scope, '保存私密照片');
if (!authReady) {
return;
}
}
this.mediaExportBusy = true;
this.updateRecordExportStatus(scope, `正在保存 ${exportItems.length} 张照片到系统相册...`);
const context = this.getAbilityContext();
let photoHelper: photoAccessHelper.PhotoAccessHelper | undefined = undefined;
try {
photoHelper = photoAccessHelper.getPhotoAccessHelper(context);
let exportedCount = 0;
for (const item of exportItems) {
const sourceUri = this.getRecordExportSandboxUri(item);
if (sourceUri.length === 0) {
console.warn(`Skip non-sandbox export source: ${item.sourceUri}`);
continue;
}
const assetChangeRequest = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, sourceUri);
await photoHelper.applyChanges(assetChangeRequest);
exportedCount++;
}
if (exportedCount === 0) {
this.updateRecordExportStatus(scope, '照片文件还在恢复中,请稍后再保存');
return;
}
this.updateRecordExportStatus(scope, `已保存 ${exportedCount} 张到系统相册`);
} catch (error) {
const err = error as BusinessError;
this.updateRecordExportStatus(scope, `保存失败:${this.getFriendlyMediaExportError(err)}`);
console.error(`Failed to export record with save grant: ${JSON.stringify(error)}`);
} finally {
if (photoHelper) {
try {
await photoHelper.release();
} catch (error) {
console.error(`Failed to release photo helper: ${JSON.stringify(error)}`);
}
}
this.mediaExportBusy = false;
}
}
工程检查清单
- 认证结果不能直接展示数字码,要映射成用户语言。
- 认证、导出、分享都需要 busy 状态和 finally 恢复。
- 保险箱敏感操作前应复用本地认证函数。
- 错误状态要写回对应页面作用域,gallery 和 vault 不要混用。
今日练习
- 模拟用户取消认证,确认
vaultAuthBusy会恢复为 false。 - 删除导出源文件路径,观察导出函数如何提示。
- 给
getUserAuthResultMessage增加一个默认日志,记录未知结果码。
训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。
更多推荐



所有评论(0)