第4篇时间轨迹的隐私安全与沙盒保存让数据留得住也收得稳如果前几篇更多是在讲“怎么用”“怎么搭”“怎么更耐看”那第四篇我想补上一个经常被忽略、但非常关键的问题数据到底怎么存才既安全又稳定。时间轨迹这类应用天然会碰到几类敏感信息时间位置照片账号自定义水印内容工作记录历史地址这些内容里有些是用户愿意展示出来的有些只是为了完成记录流程必须临时使用有些则是明确不应该“随手放出去”的。所以这一篇不讲“再加一个功能”而是讲隐私分级、沙盒保存、账号隔离、退出清理这四件事。先把结论说清楚我对这类应用的保存原则只有一句话能公开的放在业务数据里需要长期保留的放进应用沙盒临时用的放进缓存敏感的按账号拆开。如果把所有内容都塞进同一份 JSON前期看起来省事后面会很快变成账号切换后数据串了退出登录后历史还在乱跳图片缓存越来越多位置历史和个人资料混在一起用户一旦担心隐私就不敢继续用这不是“功能问题”而是“数据治理问题”。一张图先看清数据流是否拍照 / 导入图片权限检查是否允许访问生成时间轨迹内容降级提示 / 仅浏览脱敏处理写入应用沙盒按 openId 分目录保存历史记录 / 预览 / 恢复缓存目录保存临时文件使用后清理这张图表达的不是“流程很多”而是不同类型的数据应该走不同的保存路径。数据分层比“统一保存”更重要在时间轨迹里我建议把数据分成四层。数据类型示例保存方式生命周期用户偏好自动水印、显示时间、显示地址、主题模式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{constrootthis.getAccountRoot(openId);this.ensureDirectory(root);this.writeJson(this.getUserDataPath(openId),data);}privateloadUserData(openId:string):UserData{constpaththis.getUserDataPath(openId);returnthis.readJsonUserData(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);constdatathis.loadUserData(openId);this.restoreRecords(data);}这样一来退出时只处理“会话”恢复时只处理“内容”。边界清楚了体验就稳了。日志也要脱敏隐私文章里经常被忽略的一层是日志本身。真正上线时下面这些字段都不建议原样打印openId、session token、账号手机号头像本地路径、导出文件路径、缓存目录位置历史、工作记录明细、图片原始路径任何能直接关联到用户身份的调试参数更稳妥的做法是只保留必要的状态名账号 ID 做掩码展示路径只打印目录级别不打印完整文件名线上错误上报前先做脱敏处理privatemaskId(id:string):string{if(!id)return;if(id.length6)return***;returnid.slice(0,2)***id.slice(-2);}logger.info(save user data for this.maskId(openId));这一篇最想留下的结论时间轨迹做到第四篇其实已经能看出一个很明确的产品脉络第一篇讲的是入口和闭环第二篇讲的是架构和技术栈第三篇讲的是视觉和交互体验第四篇讲的是隐私、安全和保存策略前面三篇让它“能用、好用、耐看”这一篇让它“可信、可控、可恢复”。我觉得一个产品真正成熟不是把按钮做多而是把数据边界想清楚。该长期保存的有去处该临时保存的会自动清理该敏感的能隔离该恢复的能恢复。这就是时间轨迹这类应用最值得被认真对待的地方。参考HarmonyOS app data persistenceHarmonyOS HUKS encryption/decryptionentry/src/main/ets/utils/AccountService.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/ets/pages/LoginPage.etsentry/src/main/ets/pages/ProfilePage.ets
第4篇|时间轨迹的隐私安全与沙盒保存:让数据留得住,也收得稳
发布时间:2026/6/3 10:35:53
第4篇时间轨迹的隐私安全与沙盒保存让数据留得住也收得稳如果前几篇更多是在讲“怎么用”“怎么搭”“怎么更耐看”那第四篇我想补上一个经常被忽略、但非常关键的问题数据到底怎么存才既安全又稳定。时间轨迹这类应用天然会碰到几类敏感信息时间位置照片账号自定义水印内容工作记录历史地址这些内容里有些是用户愿意展示出来的有些只是为了完成记录流程必须临时使用有些则是明确不应该“随手放出去”的。所以这一篇不讲“再加一个功能”而是讲隐私分级、沙盒保存、账号隔离、退出清理这四件事。先把结论说清楚我对这类应用的保存原则只有一句话能公开的放在业务数据里需要长期保留的放进应用沙盒临时用的放进缓存敏感的按账号拆开。如果把所有内容都塞进同一份 JSON前期看起来省事后面会很快变成账号切换后数据串了退出登录后历史还在乱跳图片缓存越来越多位置历史和个人资料混在一起用户一旦担心隐私就不敢继续用这不是“功能问题”而是“数据治理问题”。一张图先看清数据流是否拍照 / 导入图片权限检查是否允许访问生成时间轨迹内容降级提示 / 仅浏览脱敏处理写入应用沙盒按 openId 分目录保存历史记录 / 预览 / 恢复缓存目录保存临时文件使用后清理这张图表达的不是“流程很多”而是不同类型的数据应该走不同的保存路径。数据分层比“统一保存”更重要在时间轨迹里我建议把数据分成四层。数据类型示例保存方式生命周期用户偏好自动水印、显示时间、显示地址、主题模式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{constrootthis.getAccountRoot(openId);this.ensureDirectory(root);this.writeJson(this.getUserDataPath(openId),data);}privateloadUserData(openId:string):UserData{constpaththis.getUserDataPath(openId);returnthis.readJsonUserData(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);constdatathis.loadUserData(openId);this.restoreRecords(data);}这样一来退出时只处理“会话”恢复时只处理“内容”。边界清楚了体验就稳了。日志也要脱敏隐私文章里经常被忽略的一层是日志本身。真正上线时下面这些字段都不建议原样打印openId、session token、账号手机号头像本地路径、导出文件路径、缓存目录位置历史、工作记录明细、图片原始路径任何能直接关联到用户身份的调试参数更稳妥的做法是只保留必要的状态名账号 ID 做掩码展示路径只打印目录级别不打印完整文件名线上错误上报前先做脱敏处理privatemaskId(id:string):string{if(!id)return;if(id.length6)return***;returnid.slice(0,2)***id.slice(-2);}logger.info(save user data for this.maskId(openId));这一篇最想留下的结论时间轨迹做到第四篇其实已经能看出一个很明确的产品脉络第一篇讲的是入口和闭环第二篇讲的是架构和技术栈第三篇讲的是视觉和交互体验第四篇讲的是隐私、安全和保存策略前面三篇让它“能用、好用、耐看”这一篇让它“可信、可控、可恢复”。我觉得一个产品真正成熟不是把按钮做多而是把数据边界想清楚。该长期保存的有去处该临时保存的会自动清理该敏感的能隔离该恢复的能恢复。这就是时间轨迹这类应用最值得被认真对待的地方。参考HarmonyOS app data persistenceHarmonyOS HUKS encryption/decryptionentry/src/main/ets/utils/AccountService.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/ets/pages/LoginPage.etsentry/src/main/ets/pages/ProfilePage.ets