隐私不是最后补一份政策
很多应用的隐私工作从“准备上架”才开始:先把功能做完,再统计用了哪些 SDK、申请了哪些权限,最后补一份隐私政策。
对于日记类产品,这个顺序风险很高。因为一旦技术架构默认依赖账号、分析 SDK、云端接口和宽泛权限,后面很难只靠文案把它变成“隐私优先”。
开发“心晴手记”时,我先确定了几条产品约束:
- 无账号;
- 无服务端;
- 无广告;
- 无第三方统计 SDK;
- 心情、日记、标签、习惯仅保存在本机;
- 设备认证仅在用户主动开启应用锁后使用;
- 用户可以导出和删除数据;
- 统计不做医疗诊断。
这些约束共同决定了项目结构,而不是发布页上的一句宣传语。
一、先画数据流,再写隐私政策
当前应用的数据流非常短:
用户输入
↓
ArkUI 页面状态
↓
AppRepository
↓
Preferences 本地 JSON
只有两个主动触发的系统出口:
用户点击导出 → 系统文件保存器
用户点击隐私/支持 → 系统浏览器或邮件应用
项目没有网络层、登录层和远程数据库,因此不存在“先上传再在云端删除”的隐藏链路。
画这张图的价值在于:隐私政策中的每一项数据处理,都应该能映射到真实代码;真实代码中的每一个外部数据流,也应该能在政策和商店隐私标签中找到说明。
二、首次同意之前不初始化用户内容
首次启动页面不应该一边展示隐私弹窗,一边已经在后台创建默认习惯、读取敏感数据或请求认证。
项目通过 privacyAcceptedVersion 建立隐私门禁:
export const CURRENT_PRIVACY_VERSION: number = 1;
export interface AppSettings {
privacyAcceptedVersion: number;
onboardingCompleted: boolean;
appLockEnabled: boolean;
// 其他设置
}
用户同意时才记录当前版本:
private acceptPrivacy(): void {
const next = copySettings(this.settings);
next.privacyAcceptedVersion = CURRENT_PRIVACY_VERSION;
this.settings = next;
this.persist();
}
页面入口根据版本判断:
if (
this.settings.privacyAcceptedVersion <
CURRENT_PRIVACY_VERSION
) {
this.PrivacyGate();
} else {
// 引导或主界面
}
默认习惯也不是在 Repository 初始化时偷偷创建,而是在用户同意并完成引导后生成。
当隐私政策的数据处理方式发生实质变化时,可以提升 CURRENT_PRIVACY_VERSION,让旧用户重新确认,而不是假设一次同意永久覆盖所有未来行为。
三、拒绝必须是一个真实选择
隐私告知至少应提供明确的同意和不同意入口。
“不同意”不应该被设计成颜色几乎看不见、点击后又反复弹回的假按钮。对于必须处理日记数据才能工作的应用,用户不同意后退出应用,是比强迫同意更清晰的边界。
同意之后,也要在设置页保留撤回入口:
private async withdrawConsent(): Promise<void> {
const next = copySettings(this.settings);
next.privacyAcceptedVersion = 0;
next.appLockEnabled = false;
this.settings = next;
this.locked = false;
await this.persist();
}
撤回同意不等于偷偷删除全部日记。当前实现会重新回到隐私门禁,并关闭依赖同意状态的可选应用锁;数据删除由单独的明确操作完成。
四、权限最小化要落实到 module.json5
当前模块只声明:
ohos.permission.ACCESS_BIOMETRIC
使用场景是用户主动开启应用锁。
应用没有因为“以后也许会用”就预先申请相机、麦克风、定位、通讯录、相册或网络权限。系统文件 Picker 可以让用户主动选择导出位置,也不需要应用扫描全部存储空间。
每增加一个权限,都应该回答:
- 哪个用户动作会触发?
- 不授权是否仍能使用核心功能?
- 权限文案能否说清具体用途?
- 是否存在更窄的系统 Picker 或临时授权方案?
- 隐私政策和商店标签是否同步更新?
如果答案不完整,先不要把权限写进配置。
五、应用锁保护的是显示入口,不是生物特征存储
应用锁调用 UserAuthenticationKit,由系统处理 PIN、人脸或指纹认证。
应用保存的只有:
appLockEnabled: boolean
不会保存锁屏密码、人脸图像或指纹模板。认证凭据在系统安全环境中处理,业务页面只接收认证成功或失败结果。
这条边界必须在产品文案里表达准确:
- 可以说“支持系统设备认证保护本机记录”;
- 不应该说“应用加密保存了你的指纹”;
- 也不要把应用锁夸大成对所有物理攻击都有效的保险箱。
六、用户要能查看、带走和删除数据
本地保存不代表应用可以永久控制数据。
项目提供:
- 完整 JSON 导出;
- 心情 CSV 导出;
- 历史记录编辑;
- 单个习惯删除;
- 全部本地数据删除。
删除全部数据时,心情、习惯和打卡数组一起清空:
private deleteAllData(): void {
this.moodEntries = [];
this.habits = [];
this.completions = [];
const next = copySettings(this.settings);
next.seededStarterHabits = true;
this.settings = next;
this.persist();
}
这里把 seededStarterHabits 保持为 true,是为了避免用户刚删除全部数据,应用重启后又自动创建六个默认习惯,让用户误以为删除失败。
删除习惯时还要同步删除关联打卡,避免产生无法在 UI 中管理的孤儿数据。
七、日志、崩溃和分析也属于数据流
即使没有业务服务器,开发者仍需检查:
- 是否在日志中打印了日记正文;
- 是否把完整 JSON 放进错误堆栈;
- 是否接入会收集设备标识的第三方崩溃 SDK;
- 是否把用户标签当作分析事件参数;
- 截图或演示数据是否包含真实用户记录。
“没有主动写网络请求”不自动等于“没有外部数据流”。依赖库、诊断工具和发布平台配置都应纳入数据清单。
当前项目通过验证脚本检查源码中是否意外加入网络权限或常见网络调用:
for (const forbidden of [
'ohos.permission.INTERNET',
'http.request(',
'@ohos/axios'
]) {
if (indexSource.includes(forbidden)) {
fail(`unexpected network capability: ${forbidden}`);
}
}
这不是完整的安全扫描,但可以防止最明显的架构回退。
八、心情统计必须避免医疗化表达
心情记录涉及敏感感受,产品很容易为了营销写出:
- “检测抑郁风险”;
- “治愈焦虑”;
- “改善失眠”;
- “AI 心理诊断”。
如果应用实际上只是统计用户主动保存的心情和标签,这些表述既不准确,也可能带来错误期待和额外合规要求。
当前统计只做描述性回顾:
- 最近 7/30 天记录了多少天;
- 哪种心情记录次数最多;
- 哪个标签出现较多;
- 习惯完成率是多少。
并明确说明:这些数字不构成医疗建议、诊断或治疗。
九、发布前做一次“三方一致”检查
提交 AppGallery 前,把下面三份内容放在一起逐项核对:
| 检查对象 | 应回答的问题 |
|---|---|
| 实际代码 | 真的申请了什么权限、读写了什么数据 |
| 隐私政策 | 是否准确说明处理目的、方式和用户权利 |
| 商店隐私标签 | 是否与代码和政策一致 |
最危险的不是某一份材料写得少,而是三者互相矛盾。
还要确认隐私政策页面已公开可访问,主体名称、支持邮箱、备案信息与开发者账号保持一致。
总结
隐私优先的工程路径可以概括为:
- 先确定不做什么;
- 画清楚所有数据流;
- 同意前不处理用户内容;
- 权限按用户动作最小化触发;
- 系统凭据交给系统能力;
- 提供导出、更正、撤回和删除入口;
- 让代码、政策和商店标签保持一致;
- 对敏感场景使用克制、准确的产品文案。
做到这些之后,“本地优先”才不只是营销形容词,而是一组能在代码中被检查的约束。
本文案例来自“心晴手记”HarmonyOS 版。它是一款记录工具,不提供医学诊断或治疗。

459

被折叠的 条评论
为什么被折叠?



