第4篇|时间轨迹的隐私安全与沙盒保存:让数据留得住,也收得稳
如果前几篇更多是在讲“怎么用”“怎么搭”“怎么更耐看”,那第四篇我想补上一个经常被忽略、但非常关键的问题:数据到底怎么存,才既安全又稳定。
时间轨迹这类应用,天然会碰到几类敏感信息:
- 时间
- 位置
- 照片
- 账号
- 自定义水印内容
- 工作记录
- 历史地址
这些内容里,有些是用户愿意展示出来的,有些只是为了完成记录流程必须临时使用,有些则是明确不应该“随手放出去”的。
所以这一篇不讲“再加一个功能”,而是讲隐私分级、沙盒保存、账号隔离、退出清理这四件事。
先把结论说清楚
我对这类应用的保存原则只有一句话:
能公开的,放在业务数据里;需要长期保留的,放进应用沙盒;临时用的,放进缓存;敏感的,按账号拆开。
如果把所有内容都塞进同一份 JSON,前期看起来省事,后面会很快变成:
- 账号切换后数据串了
- 退出登录后历史还在乱跳
- 图片缓存越来越多
- 位置历史和个人资料混在一起
- 用户一旦担心隐私,就不敢继续用
这不是“功能问题”,而是“数据治理问题”。
一张图先看清数据流
这张图表达的不是“流程很多”,而是不同类型的数据,应该走不同的保存路径。
数据分层,比“统一保存”更重要
在时间轨迹里,我建议把数据分成四层。
| 数据类型 | 示例 | 保存方式 | 生命周期 |
|---|---|---|---|
| 用户偏好 | 自动水印、显示时间、显示地址、主题模式 | PersistentStorage/AppStorage | 长期保留 |
| 业务记录 | 工作记录、地点历史、模板配置 | 应用沙盒中的账号目录 | 跟随账号 |
| 临时文件 | 预览图、导出图、缓存缩略图 | cacheDir/ 临时目录 | 用完即清 |
| 敏感会话 | 登录态、账号标识、当前会话 | 单独会话文件 | 退出后可清理 |
这里最容易混淆的是两类:
- 偏好设置
- 账号数据
这两类绝对不能混。
偏好设置是“这个用户喜欢什么样的界面”,可以跟着设备走。
账号数据是“这个账号自己的历史记录”,必须跟着账号走。
为什么要做账号隔离
这一步其实是整个应用里最值得做的设计之一。
如果时间轨迹支持多账号,或者未来准备接云同步,那么数据目录一定不能是一锅端,而应该按openId或者其他稳定账号标识拆开。
一个更合理的沙盒结构可以长这样:
filesDir/ session.json accounts/ openId_001/ userdata.json wm_location_history.json work_records.json templates.json cache/ openId_002/ userdata.json wm_location_history.json work_records.json templates.json cache/ cacheDir/ preview_*.png export_*.jpg这样做有三个好处:
- 账号切换时不会互相覆盖
- 退出登录时可以只清 session,不一定删历史
- 后续做同步、导出、迁移时边界很清楚
这也是为什么我很喜欢把AccountService单独拎出来。
它不只是“读写账号”,更像是数据边界的守门人。
账号数据怎么写,才不会乱
下面这个思路比“所有内容都存一个文件”稳得多。
// 这是示意代码,重点是结构,不是逐行追求 API 细节interfaceUserData{wmLocationHistory:string[];wrRecords:WorkRecord[];templateIds:string[];customAddress:string;}privategetAccountRoot(openId:string):string{return`${this.context.filesDir}/accounts/${openId}`;}privategetUserDataPath(openId:string):string{return`${this.getAccountRoot(openId)}/userdata.json`;}privatesaveUserData(openId:string,data:UserData):void{constroot=this.getAccountRoot(openId);this.ensureDirectory(root);this.writeJson(this.getUserDataPath(openId),data);}privateloadUserData(openId:string):UserData{constpath=this.getUserDataPath(openId);returnthis.readJson<UserData>(path)??{wmLocationHistory:[],wrRecords:[],templateIds:[],customAddress:''};}这段代码真正想表达的不是文件操作本身,而是这几个设计原则:
- 每个账号有自己的根目录
- 数据文件名固定,便于恢复
- 不同类型的业务数据不要挤在一份超大 JSON 里
- 缺省值要能自动回退,避免文件损坏后直接崩
沙盒保存,不只是“能写进去”
很多人会把“沙盒保存”理解成一句话:存到应用自己的目录里就行。
但真正做的时候,还应该再往前走一步:
1. 公开数据和敏感数据分开
比如:
- 模板列表可以保存
- 水印开关可以保存
- 工作记录可以保存
- 地址历史可以保存
- 会话 token 不应该和记录混在一起
2. 临时文件和长期文件分开
导出的图片、缩略图、分享前的预览文件,应该优先放缓存目录。
一旦用户完成保存、分享或退出,就可以清掉。
3. 退出登录时要做“边界动作”
退出登录不是简单把页面跳回去,而是要明确区分:
- 释放当前会话
- 切换当前账号标识
- 是否清理当前账号的临时数据
- 是否保留历史记录供下次恢复
这件事如果不做,用户会感觉“我明明退出了,怎么内容还跟着我跑”。
权限策略也属于隐私设计
时间轨迹会碰到的权限,通常不止一个:
- 相机权限
- 相册权限
- 位置权限
- 存储相关权限
但这里最重要的不是“申请了多少”,而是什么时候申请。
我更建议的方式是:
- 进入拍照页,再申请相机权限
- 进入时间地点页,再申请位置权限
- 进入导入页,再申请相册权限
- 先给用户解释用途,再弹系统授权
这样做的好处很直接:
- 用户知道为什么要授权
- 拒绝后不会把整个应用拖死
- 业务页面可以优雅降级
比如:
if(!awaitthis.permissionUtil.ensure(['ohos.permission.CAMERA'])){this.toast('未授权相机权限,仍可继续浏览模板与记录');return;}真正好的隐私体验不是“强行拿权限”,而是用户知道自己为什么授权,也知道拒绝后会发生什么。
私密信息最好只在本地完成闭环
如果一个功能完全可以在本地完成,就不要早早把数据往外送。
时间轨迹里的很多内容其实都适合本地闭环:
- 自动水印生成
- 地址历史整理
- 记录归档
- 模板切换
- 账号本地恢复
这样做的价值不只是性能好,还包括:
- 断网也能用
- 敏感数据不必经过额外链路
- 用户更容易信任这个产品
如果未来真的要做云同步,我建议也先做两层分级:
- 先同步非敏感配置
- 再同步可脱敏的业务记录
不要一上来就把所有东西都推上云。
退出、切换、恢复,三件事要分开
这三个动作看起来很像,但本质上不同:
- 退出:结束当前会话
- 切换:从账号 A 进入账号 B
- 恢复:重新把历史内容载回当前账号
这也是为什么要把session.json和userdata.json分开。
privateclearSession():void{this.removeFile(`${this.context.filesDir}/session.json`);AppStorage.setOrCreate('currentOpenId','');}privateswitchAccount(openId:string):void{AppStorage.setOrCreate('currentOpenId',openId);constdata=this.loadUserData(openId);this.restoreRecords(data);}这样一来,退出时只处理“会话”,恢复时只处理“内容”。
边界清楚了,体验就稳了。
日志也要脱敏
隐私文章里经常被忽略的一层,是日志本身。真正上线时,下面这些字段都不建议原样打印:
- openId、session token、账号手机号
- 头像本地路径、导出文件路径、缓存目录
- 位置历史、工作记录明细、图片原始路径
- 任何能直接关联到用户身份的调试参数
更稳妥的做法是:
- 只保留必要的状态名
- 账号 ID 做掩码展示
- 路径只打印目录级别,不打印完整文件名
- 线上错误上报前先做脱敏处理
privatemaskId(id:string):string{if(!id)return'';if(id.length<=6)return'***';returnid.slice(0,2)+'***'+id.slice(-2);}logger.info('save user data for '+this.maskId(openId));这一篇最想留下的结论
时间轨迹做到第四篇,其实已经能看出一个很明确的产品脉络:
- 第一篇讲的是入口和闭环
- 第二篇讲的是架构和技术栈
- 第三篇讲的是视觉和交互体验
- 第四篇讲的是隐私、安全和保存策略
前面三篇让它“能用、好用、耐看”,这一篇让它“可信、可控、可恢复”。
我觉得一个产品真正成熟,不是把按钮做多,而是把数据边界想清楚。
该长期保存的有去处,该临时保存的会自动清理,该敏感的能隔离,该恢复的能恢复。
这就是时间轨迹这类应用最值得被认真对待的地方。
参考
- HarmonyOS app data persistence
- HarmonyOS HUKS encryption/decryption
entry/src/main/ets/utils/AccountService.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/ets/pages/LoginPage.etsentry/src/main/ets/pages/ProfilePage.ets