HarmonyOS应用实战-启示散页-22-历史记录别变成日志仓库:给问题、答案和再次提问划清边界

📅 2026/7/23 19:56:30
HarmonyOS应用实战-启示散页-22-历史记录别变成日志仓库:给问题、答案和再次提问划清边界
HarmonyOS应用实战-启示散页-22-历史记录别变成日志仓库给问题、答案和再次提问划清边界这一组文章继续围绕The_Book_of_Answers展开。它不是一次性页面 Demo而是一个小型 HarmonyOS 离线应用默认题库从 rawfile 播种题库、收藏、历史写进 Preferences抽取流程经过 DrawingPage发布前还要说明隐私、备份、包体和日志口径。轻量应用最容易被误判为“页面写完就结束”。实际交付时真正拖慢排障的通常不是某个 ArkUI 组件而是状态从哪里来、数据由谁写、页面靠什么刷新、异常能不能恢复。下面每篇只拆一个问题并尽量把它落到答案之书已有的页面、Service、Repository、AppStorage、资源或发布配置边界上。这篇文章解决四件事复盘历史记录别变成日志仓库在答案之书项目里怎么出现。把给问题、答案和再次提问划清边界落到页面、Service、Repository 和 AppStorage 的具体边界。给出能迁移到 HarmonyOS/ArkTS 项目的代码形态并指出反例。用验证清单和排障表收尾避免只看源码不看实际入口。1. 从故障链路看历史记录别变成日志仓库历史功能如果追求完整很快会把每一次抽取都存起来。答案之书更需要的是最近可用上下文用户问过什么、当时看到什么、是否要基于同一个问题再抽一次。 这个问题如果只在当前页面补一个判断短期可能能跑但下一次从历史、收藏、分享或恢复入口进入时同样的问题还会出现。先把故障链路写出来才能知道该修页面、服务还是持久层。触发点历史记录别变成日志仓库 错误写法页面直接判断或直接读写持久化数据 放大后果返回重进、前后台切换、删除/恢复、发布排查都会看到旧状态 收口位置QuestionHistoryService 处理业务规则HistorySheet 只消费结果态 刷新方式业务动作成功后写 AppStorageKey.LastHistoryUpdateAt2. 先把边界表写清楚历史记录不是抽取缓存。查看历史可以展示旧答案再次提问必须重新走AnswerDrawService这样才不会把旧结果伪装成新抽取。 这张表的作用是防止后面写代码时顺手越界。尤其是答案之书这种本地应用很多问题看起来都能在页面里临时解决但页面一旦知道太多存储细节发布后的排障成本会明显升高。层级在这篇里的职责不应该做的事HistorySheet展示、点击、跳转、订阅刷新信号直接拼 Preferences key 或修复脏数据QuestionHistoryService校验输入、组装QuestionHistoryItem、决定空态和恢复路径持有 ArkUI 组件状态HistoryRepository稳定读写本地实体和索引判断页面怎么展示AppStorage传递AppStorageKey.LastHistoryUpdateAt这类轻量刷新信号保存完整业务对象3.QuestionHistoryItem只表达页面结果不照搬存储实体QuestionHistoryItem是给HistorySheet消费的结果模型不是 Preferences 里的原始结构。这样做的好处是Repository 可以继续按本地存储优化字段页面仍然拿到稳定、可渲染、可判断动作的结果。interfaceQuestionHistoryItem{id:string;question:string;answerText:string;deckId:string;createdAt:number;}4.QuestionHistoryService才是规则 ownerQuestionHistoryService负责把HistoryRepository读出的数据转成页面能用的结果。空值、损坏、回退和默认值都应该在这一层处理页面不需要知道底层为什么缺字段。classQuestionHistoryService{asyncload(deckId:string):PromiseQuestionHistoryItem{constrecord:QuestionHistoryItemHistoryRecordFactory.fromDrawResult(result);constold:QuestionHistoryItem[]awaitHistoryRepository.loadAll();constnormalized:stringnormalizeQuestion(record.question);constnext:QuestionHistoryItem[][record].concat(old.filter((item)normalizeQuestion(item.question)!normalized)).slice(0,50);awaitHistoryRepository.saveAll(next);AppStorage.setOrCreate(AppStorageKey.LastHistoryUpdateAt,Date.now());}}5.HistorySheet不直接碰持久化HistorySheet的职责应该保持轻进入时加载收到信号时重载用户点击时发起明确动作。这样页面不会同时背上 Preferences、业务规则、错误恢复和发布排查四种职责。Componentstruct HistorySheet{StateprivatequestionHistoryItem:QuestionHistoryItem|nullnull;StorageLink(lastHistoryUpdateAt)Watch(reload)privatechangedAt:number0;privatecurrentDeckId:string;asyncaboutToAppear():Promisevoid{awaitthis.reload();}privateasyncreload():Promisevoid{constdeckId:stringBookRouteGuard.requireDeckParam({deckId:this.currentDeckId});this.questionHistoryItemawaitnewQuestionHistoryService().load(deckId);}}6. 反例把所有逻辑塞回页面会怎样这个反例在第一版开发时很常见因为写起来快但它把题库读取、空态判断、展示模型转换和刷新信号都揉在组件里。后续一旦增加 Sheet、深链、恢复页或平板布局就会出现多个入口行为不一致。// 反例页面同时知道数据结构、业务规则和刷新方式constdeckawaitHistoryRepository.loadDeck(this.currentDeckId);if(decknull){promptAction.showToast({message:暂无数据});return;}this.questionHistoryItemdeckasunknownasQuestionHistoryItem;AppStorage.setOrCreate(AppStorageKey.LastHistoryUpdateAt,Date.now());7. 刷新信号只写AppStorageKey.LastHistoryUpdateAt不要广播完整对象AppStorageKey.LastHistoryUpdateAt是运行期联动不是数据仓库。业务动作成功后只写一个时间戳页面收到后再按自己的 owner 重拉数据可以避免跨页面共享可变对象。classBookRefreshCenter{staticnotifyQuestionHistoryItemChanged():void{AppStorage.setOrCreate(AppStorageKey.LastHistoryUpdateAt,Date.now());}staticasyncafterBusinessAction(action:()Promisevoid):Promisevoid{awaitaction();this.notifyQuestionHistoryItemChanged();}}8. 入口参数要先校验再进入业务答案之书的入口不只首页一个历史再次提问、收藏再次提问、分享导入、恢复页都可能进入RouteName.Drawing。目标页先校验参数错误进入可恢复路径不要让空 deckId 流到 Service 深处才爆出难懂异常。classBookRouteGuard{staticrequireDeckParam(param:object|undefined):string{constdeckId(paramasRecordstring,string|undefined)?.deckId??;if(!deckId){thrownewError(缺少题库 id不能进入 RouteName.Drawing);}returndeckId;}}9. 排查命令围绕 owner 搜不围绕页面猜排查时不要只盯着出问题的 UI。先搜 Service、模型、刷新信号再看页面是否绕过了 owner。这样可以分清是数据没写、结果没组装还是页面没订阅刷新。rg-nQuestionHistoryService|QuestionHistoryItem|AppStorageKey.LastHistoryUpdateAtD:\ProgramData\huawei\lesson\The_Book_of_Answers rg-nRouteName.Drawing|HistorySheetD:\ProgramData\huawei\lesson\The_Book_of_Answers rg-nHistoryRepository|AppStorage.setOrCreateD:\ProgramData\huawei\lesson\The_Book_of_Answers10. 验证要覆盖正常路径和损坏路径连续问同一个问题、空问题、删除来源题库后打开历史、从历史再次提问确认历史列表清爽且再次提问生成新结果。建议至少按这四组走清应用数据后的首次进入。有自建题库、收藏和历史后的返回重进。手工制造空值、重复值或损坏数据后的恢复路径。发布态检查日志和截图确认没有把用户问题、答案全文或题库全文暴露出去。验收口径 1. 正常入口可用。 2. 异常入口有提示或恢复页。 3. 返回重进不显示旧数据。 4. AppStorageKey.LastHistoryUpdateAt 变化后只刷新对应 owner。 5. 发布态不输出敏感明文。11. 落地时的取舍这里没有把给问题、答案和再次提问划清边界做成很重的框架能力是因为答案之书的核心仍然是离线、轻量、可维护。过度抽象会让一个小功能穿过太多层完全写在页面里又会让数据修复、备份恢复、发布排障没有稳定入口。比较合适的取舍是用户内容、持久化结构、跨页面刷新和发布自查进入 Service 或 Repository只影响当前视觉节奏的内容留在页面。适合抽出去 - QuestionHistoryItem - QuestionHistoryService - AppStorageKey.LastHistoryUpdateAt 不急着抽出去 - 当前页面的一次性动画状态 - 只影响局部样式的临时 UI 变量 - 不跨页面复用的按钮交互12. 常见问题与处理现象先看哪里处理历史无限增长是否 slice 上限保存时截断最近 50 条再次提问展示旧答案是否直接用 answerText重新进入 DrawingPage空问题污染列表normalizeQuestion 是否过滤空问题不进入历史或标注随便问问复查顺序 1. HistoryRepository 是否返回可信数据。 2. QuestionHistoryService 是否统一处理空值和异常。 3. HistorySheet 是否绕过 Service。 4. AppStorageKey.LastHistoryUpdateAt 是否在业务动作成功后写入。 5. 发布态日志是否隐藏用户输入和答案全文。验证清单清应用数据后进入HistorySheet确认默认题库、页面状态和刷新信号都能闭环。从首页、Sheet、历史、收藏、分享或恢复入口触发一次确认RouteName.Drawing的参数校验稳定。手工制造空值、重复数据或损坏数据确认错误停在QuestionHistoryService或恢复页而不是让页面崩掉。触发业务动作后观察AppStorageKey.LastHistoryUpdateAt确认只有相关页面重拉数据没有全局乱刷新。如果涉及主题、资源、布局、隐私或发布态必须用真机截图、构建产物或发布清单补充确认。小结历史记录别变成日志仓库给问题、答案和再次提问划清边界不是一个孤立 API 问题而是答案之书这种离线应用在长期维护里一定会遇到的边界问题。把QuestionHistoryItem、QuestionHistoryService、HistoryRepository、HistorySheet和AppStorageKey.LastHistoryUpdateAt分清以后项目继续扩展题库、收藏、历史、分享、恢复和发布诊断时才不会把每个入口都写成一次性的临时逻辑。是一个孤立 API 问题而是答案之书这种离线应用在长期维护里一定会遇到的边界问题。把QuestionHistoryItem、QuestionHistoryService、HistoryRepository、HistorySheet和AppStorageKey.LastHistoryUpdateAt分清以后项目继续扩展题库、收藏、历史、分享、恢复和发布诊断时才不会把每个入口都写成一次性的临时逻辑。