恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
HarmonyOS应用实战-启示散页-96-空答案别混进抽取池:保存前校验 answer normalized text
首页
资讯中心
/
HarmonyOS应用实战-启示散页-96-空答案别混进抽取池:保存前校验 answer normalized text
HarmonyOS应用实战-启示散页-96-空答案别混进抽取池:保存前校验 answer normalized text
发布时间:2026/8/14 11:00:18
HarmonyOS 应用实战 96空答案别混进抽取池保存前统一校验 answer normalized text实际项目里题库编辑页最容易留下一个小坑页面上看着有两行答案服务层保存时却把空白、超长文本过滤掉最后抛出“题库至少需要 2 条答案”。用户会觉得按钮明明能点保存却失败开发者排查时又容易只改页面按钮状态没有把服务层清洗规则同步出去。“答案之书”当前工程已经有一条可靠底线DeckService.save()会 trim 答案过滤空文本和超长文本再按有效答案数量决定是否保存。第 96 篇要解决的不是“再加一个 Toast”而是把页面预提示、服务校验、导入解析和回归样本统一到同一套 normalized text 规则上。先看现状页面和服务各算了一次有效答案当前DeckCreatePage的提交按钮只按“trim 后非空”统计有效答案privatecanSubmit():boolean{if(this.submitting){returnfalse;}if(!this.deckName.trim()){returnfalse;}constvalid:numberthis.answers.filter((a:EditableAnswer):booleana.text.trim().length0).length;returnvalidMIN_ANSWERS_PER_DECK;}DeckEditPage也有类似判断privatecanSave():boolean{if(this.saving){returnfalse;}if(this.builtIn){returnfalse;}if(!this.deckName.trim()){returnfalse;}if(this.validCount()MIN_ANSWERS_PER_DECK){returnfalse;}returnthis.isDirty();}这两段代码能挡住纯空输入但它们没有执行服务层的完整规则。比如页面只看非空服务层还会过滤长度超过MAX_ANSWER_LEN的答案。两边规则一旦不一致按钮状态和保存结果就会对不上。服务层已经有最终防线DeckService.save()是当前工程真正的保存入口。它先处理题库名再把答案统一清洗constcleaned:string[]payload.answers.map((a:string):stringa.trim()).filter((a:string):booleana.length0a.lengthMAX_ANSWER_LEN);if(cleaned.lengthMIN_ANSWERS_PER_DECK){thrownewError(题库至少需要${MIN_ANSWERS_PER_DECK}条答案);}if(cleaned.lengthMAX_ANSWERS_PER_DECK){thrownewError(题库最多${MAX_ANSWERS_PER_DECK}条答案);}这段代码是必须保留的底线。页面提示可以更友好但不能替代服务层校验。原因很简单保存入口不只来自手动输入还可能来自导入、编辑、恢复、后续扩展的批量写入。只要绕过页面最终仍然要由DeckService保证空答案不会进仓储。失败链路按钮能点但保存失败这个问题可以用一组具体输入复现输入样本页面当前判断服务层结果用户看到的问题[是, ]只有 1 条有效按钮禁用拒绝正常[是, 超过 60 字的长文本...]2 条非空可能允许提交超长被过滤只剩 1 条点击后才失败[是, 是]2 条非空当前服务会保存两条同文本抽取池可能出现重复体验[ 是 , 否 ]2 条非空保存为是/否正常第 96 篇应该放大的就是第二类问题页面预提示没有使用服务层 normalized 规则导致用户在页面上得到一个过于乐观的按钮状态。把清洗规则抽成可复用报告建议把“清洗 原因”从save()里拆成一个可复用函数。下面是建议补强代码不代表当前工程已经存在同名接口exportinterfaceAnswerValidationReport{cleaned:string[];blankCount:number;tooLongCount:number;duplicateCount:number;canSave:boolean;reason:string;}exportfunctionvalidateAnswers(input:string[]):AnswerValidationReport{constcleaned:string[][];constseen:SetstringnewSetstring();letblankCount0;lettooLongCount0;letduplicateCount0;input.forEach((raw:string){consttext:stringraw.trim();if(!text){blankCount;return;}if(text.lengthMAX_ANSWER_LEN){tooLongCount;return;}if(seen.has(text)){duplicateCount;return;}seen.add(text);cleaned.push(text);});constcanSave:booleancleaned.lengthMIN_ANSWERS_PER_DECKcleaned.lengthMAX_ANSWERS_PER_DECK;constreason:stringcanSave?:有效答案不足${MIN_ANSWERS_PER_DECK}条;return{cleaned,blankCount,tooLongCount,duplicateCount,canSave,reason};}这个报告有两个用途页面用它显示预提示服务层用它做最终拦截。字段不是为了堆概念而是为了让用户知道“为什么明明输入了几行最后只有 1 条有效”。DeckService 仍然是最终入口拆出报告后DeckService.save()不应该降低防线而是改成调用同一套规则asyncsave(payload:SaveDeckPayload):PromiseDeckSummary{consttrimmedName:stringpayload.name.trim();if(!trimmedName){thrownewError(题库名称不能为空);}if(trimmedName.lengthMAX_DECK_NAME_LEN){thrownewError(题库名称不能超过${MAX_DECK_NAME_LEN}字);}constreport:AnswerValidationReportvalidateAnswers(payload.answers);if(!report.canSave){thrownewError(report.reason);}constnow:numberDate.now();constanswers:Answer[]report.cleaned.map((text:string):Answer{return{id:newId(a),text,createdAt:now};});// 后续仍然执行 repo.save 和 AppStorage.setOrCreate}这样调整以后服务层仍然掌握写入权页面只是提前展示同一份报告。保存失败时错误原因也能和页面预提示对上。页面提示不要只显示 validCount当前页面显示的是答案列表 (${this.validCount()}/${MAX_ANSWERS_PER_DECK})。更稳的做法是显示有效数和丢弃原因privateanswerReport():AnswerValidationReport{returnvalidateAnswers(this.answers.map((a:EditableAnswer):stringa.text));}privatecanSubmit():boolean{if(this.submitting||!this.deckName.trim()){returnfalse;}returnthis.answerReport().canSave;}页面文案可以继续克制不需要把每个规则都写成一大段说明。关键是让用户知道“空白 2 条、超长 1 条、重复 1 条”这类具体原因而不是只在保存失败后弹一个服务层错误。导入解析也要用同一套规则工程里parseDeckImport()已经会 trim、去空、过滤超长、去重constseen:SetstringnewSetstring();constanswers:string[][];for(leti1;iparts.length;i){consta:stringparts[i];if(!a){continue;}if(a.lengthMAX_ANSWER_LEN){continue;}if(seen.has(a)){continue;}seen.add(a);answers.push(a);}这说明导入链路已经比页面手输更接近服务层规则。后续如果抽出validateAnswers()导入解析也可以复用它避免出现“导入能去重手输保存不去重”的体验差异。验证路径用负向样本逼出边界静态确认先看这些点rg-nconst cleaned|validateAnswers|AnswerValidationReportD:\ProgramData\huawei\lesson\The_Book_of_Answers\libraryHSP\src\main\ets\services\DeckService.etsrg-ncanSubmit|canSave|validCount|parseDeckImportD:\ProgramData\huawei\lesson\The_Book_of_Answers\libraryHSP\src\main\ets再跑交互样本样本预期页面预期服务层两条空白按钮不可提交提示有效答案不足拒绝一条正常一条超长页面显示超长被丢弃拒绝两条正常一条空白可保存提示空白不会入库保存 2 条重复答案页面提示重复被合并或保留规则与服务层一致导入含空段导入预览显示有效数保存结果一致如果只是本地写文章不要把这些交互写成已经在设备上完成。真正落地工程后还需要跑hvigorw assembleHap --no-daemon和页面手工回归。常见问题现象优先排查修复方向按钮可点但保存失败页面是否只按非空统计页面复用validateAnswers()服务保存后答案数变少是否有空白或超长被过滤显示丢弃原因导入和手输规则不同parseDeckImport()与DeckService.save()是否各写一套抽公共报告抽取池出现重复体验是否允许同文本重复保存在报告里明确重复策略收口空答案问题的处理方法很明确页面可以预提示但DeckService.save()必须是最终防线页面、导入、服务层要共用同一套 normalized text 规则。这样用户在编辑时看到的有效数和保存后真正进入抽取池的答案数才会一致。落地时不要只改按钮颜色。先把validateAnswers()这种纯规则放到服务可复用的位置再让创建页、编辑页、导入解析和保存入口都调用它。页面负责把空白、超长、重复这些原因讲清楚服务负责保证任何入口写入前都得到同样的 cleaned 结果。这样后续再加批量导入、备份恢复或云同步也不会重新引入一套不一致的答案规则。