HarmonyOS应用实战-启示散页-05-随机抽答案要避免连续重复:把算法放进服务层
HarmonyOS应用实战-启示散页-05-随机抽答案要避免连续重复把算法放进服务层答案之书的核心体验是“提问后给一个答案”。如果连续两次抽到同一句用户会立刻觉得应用敷衍如果页面自己写随机算法动画页、快捷入口和未来的小组件入口又可能各抽各的。随机不是 UI 小细节它是产品体验规则应该放在服务层统一处理。这篇文章会把问题拆成四个可落地的点把抽取算法从页面移到 AnswerDrawService。用最近结果窗口降低连续重复概率。区分候选池、运行期状态和展示动画。给出小题库、空题库、过滤后无结果的兜底策略。1. 随机体验不等于 Math.randomMath.random()能给出随机下标但不能保证体验。题库只有十几条时连续重复很容易出现用户提问后看到同一句答案会把算法概率理解成产品问题。答案之书把抽取逻辑放进 Service让所有入口都使用同一套规则。interfaceDrawAnswerOptions{deckId:string;question?:string;excludeRecent?:boolean;}interfaceDrawAnswerResult{deckId:string;answer:Answer;drawnAt:number;question?:string;}抽取结果不是一段文本而是带题库、问题和时间的领域结果。这样后续收藏和历史才能拿到完整来源。2. 候选池先由 Service 准备页面不应该自己加载题库再筛答案。Service 先读取当前题库过滤空答案再决定是否允许抽取。空题库、损坏题库、当前题库不存在都在这里统一兜底。asyncdraw(options:DrawAnswerOptions):PromiseDrawAnswerResult{constdeck:Deck|nullawaitDeckRepository.loadDeck(options.deckId);if(!deck){thrownewError(当前题库不存在请重新选择题库);}constcandidates:Answer[]deck.answers.filter((item:Answer):booleanitem.text.trim().length0);if(candidates.length0){thrownewError(当前题库没有可抽取的答案);}returnthis.pickFromCandidates(deck.id,candidates,options.question);}候选池准备属于业务层。页面只关心成功展示或失败提示不应该处理题库损坏的细节。3. 最近窗口只影响选择不改题库避免连续重复不需要修改题库顺序也不需要给答案加权。答案之书维护一个运行期 recent 窗口抽取时优先排除最近出现过的答案如果题库太小导致排除后为空就退回全量候选。privatereadonlyrecentAnswerIds:string[][];privatereadonlyrecentLimit:number3;privatefilterRecent(candidates:Answer[]):Answer[]{constfiltered:Answer[]candidates.filter((item:Answer):boolean{return!this.recentAnswerIds.includes(item.id);});returnfiltered.length0?filtered:candidates;}recent 是运行期体验状态不是持久化数据。它影响下一次抽取但不改变用户题库内容。4. 选中后再更新 recentrecent 窗口要在抽取成功后更新不能在动画结束后才更新。否则用户快速连续点击时第二次抽取仍可能看到旧窗口。Service 返回结果前完成状态更新页面动画只负责表现。privateremember(answerId:string):void{this.recentAnswerIds.unshift(answerId);constunique:string[][];this.recentAnswerIds.forEach((id:string):void{if(!unique.includes(id)){unique.push(id);}});this.recentAnswerIds.splice(0,this.recentAnswerIds.length,...unique.slice(0,this.recentLimit));}这段代码把 recent 维护成去重队列。它不追求复杂概率只解决连续重复这个最刺眼的体验问题。5. 随机下标封装成可替换方法本地小应用不需要一上来引入复杂随机库但随机方法应该集中。未来如果要做权重、节日题库或测试可复现只改一个方法不需要翻页面代码。privatechooseIndex(total:number):number{returnMath.floor(Math.random()*total);}privatepickFromCandidates(deckId:string,candidates:Answer[],question?:string):DrawAnswerResult{constpool:Answer[]this.filterRecent(candidates);constanswer:Answerpool[this.chooseIndex(pool.length)];this.remember(answer.id);return{deckId,answer,question,drawnAt:Date.now()};}集中封装不是为了过度抽象而是为了让体验规则只有一个出口。6. DrawingPage 只处理状态机抽取页最容易把算法、动画和导航揉在一起。答案之书更稳的写法是页面进入 loading调用 Service 拿结果进入 revealing动画结束后进入 done。答案是什么由 Service 决定页面只负责让结果被看见。typeDrawingStateidle|drawing|revealing|done|error;StateprivatedrawingState:DrawingStateidle;StateprivatecurrentResult:DrawAnswerResult|nullnull;privateasyncstartDraw():Promisevoid{this.drawingStatedrawing;try{this.currentResultawaitAnswerDrawService.draw({deckId:this.deckId,question:this.question});this.drawingStaterevealing;}catch(err){this.drawingStateerror;}}状态机让页面逻辑可读也让失败态有入口。抽取失败不应该停在半段动画里。7. 收藏和历史使用同一个 DrawAnswerResult如果收藏只存文本历史又存 question后续就无法判断同一句答案来自哪个题库。抽取 Service 返回完整结果后收藏和历史都使用同一份来源信息能减少数据对不上的问题。awaitHistoryService.append({deckId:result.deckId,answerId:result.answer.id,answerText:result.answer.text,question:result.question,createdAt:result.drawnAt});抽取结果是后续功能的源头。源头字段完整收藏、历史、分享才不会各自补猜。8. 小题库要接受重复但不连续卡死如果题库只有两条答案强行不重复会很快无候选。Service 的策略应该是尽量避免最近重复但候选池为空时回退全量。这比一直重试随机更稳也不会出现死循环。constpool:Answer[]candidates.lengththis.recentLimit?this.filterRecent(candidates):candidates;constanswer:Answerpool[this.chooseIndex(pool.length)];体验规则要尊重数据规模。小题库允许重复但仍可以避免最常见的连续同一句。9. 验证与排障抽取验证不要只看“点一次有答案”。要连续抽 20 次观察重复分布切换题库后 recent 是否影响新题库空题库是否给出错误提示收藏和历史是否拿到同一个结果来源。验证点 1. 同一题库连续抽取不频繁出现相邻重复 2. 两条答案的小题库不会死循环 3. 当前题库不存在时页面进入 error 状态 4. 收藏记录包含 deckId、answerId、answerText 5. 历史记录 createdAt 使用抽取结果时间如果动画页偶发空白先看 Service 是否抛错后页面没有进入 error而不是先调整动画时间。验证清单清应用数据后从冷启动进入确认默认数据、页面状态和日志分支符合预期。对本文涉及的写路径准备正常、空值、重复、越界四类输入确认错误停在 Service 或 Repository。页面返回、重新进入、切换题库、收藏、历史或删除后确认对应刷新信号触发重新读取。修改资源或模块归属后重新构建确认 HAP、HAR、HSP 的依赖方向没有反转。涉及真机体验、备份恢复、发布素材的内容单独记录是否已经在设备或平台侧验证。常见问题与处理现象先看哪里处理方式连续抽到同一句recentAnswerIds 是否更新抽取成功后立即记入 recent小题库抽取卡住是否在 while 中强行排重排除后为空就回退全量候选收藏来源丢失是否只保存 answerText保存 DrawAnswerResult 中的 deckId 和 answerId切换题库后结果异常recent 是否跨题库污染recent 可按 deckId 分组或切换时清空小结随机抽取是产品规则不是页面按钮里的临时表达式。把候选池、recent 窗口、结果来源都放进服务层答案之书的核心体验才有一致性。