HarmonyOS 应用开发《掌上英语》第27篇:学习统计持久化——StatisticsManager 的数据聚合方案

HarmonyOS 应用开发《掌上英语》第27篇:学习统计持久化——StatisticsManager 的数据聚合方案
学习统计持久化——StatisticsManager 的数据聚合方案引言学习数据统计是教育类 App 的核心功能之一。用户完成一次练习后系统需要快速计算出正确率、各题型得分率、平均答题时长等指标并将这些数据持久化以便用户在学习报告等页面查看历史趋势。本文将以StatisticsManager的实现为例分析如何设计一套可扩展、可聚合的学习统计方案。一、数据模型设计统计数据的核心是PracticeStatistics接口它包含了全局统计和分题型统计两个维度exportinterfacePracticeStatistics{// 全局统计数据totalQuestions:number// 总答题数correctQuestions:number// 正确题数wrongQuestions:number// 错误题数correctRate:number// 正确率百分比vocabularyMasteryRate:number// 词汇掌握率listeningCorrectRate:number// 听力正确率readingCorrectRate:number// 阅读正确率clozeCorrectRate:number// 完形填空正确率avgAnswerTime:number// 平均答题时长秒// 分题型统计typeStatistics:TypeStat[]}exportinterfaceTypeStat{type:string// 题型编号1词汇 2听力 3阅读 4完形 5语法 6口语total:number// 该题型总题数correct:number// 正确数wrong:number// 错误数}设计亮点typeStatistics数组而非固定字段。这样可以支持未来新增题型而无需修改数据模型——符合开闭原则。二、单次练习统计计算calculateStatistics方法接收一次练习的题目列表和总时长实时计算各项指标publiccalculateStatistics(ques:TopicItemType[],duration:number):PracticeStatistics{try{lettotalQuestionsques.length;letcorrectQuestions0;letwrongQuestions0;lettypeStats:TypeStat[][];ques.forEach(item{if(item.isAnswer){// 判断答案是否正确letisCorrectthis.checkAnswerCorrect(item.selectQues,item.rightQues);if(isCorrect){correctQuestions;}else{wrongQuestions;}// 按题型累加统计lettypeKeyitem.type||其他;letfoundfalse;for(leti0;itypeStats.length;i){if(typeStats[i].typetypeKey){typeStats[i].total;if(isCorrect)typeStats[i].correct;elsetypeStats[i].wrong;foundtrue;break;}}if(!found){typeStats.push({type:typeKey,total:1,correct:isCorrect?1:0,wrong:isCorrect?0:1});}}});letcorrectRatetotalQuestions0?(correctQuestions/totalQuestions*100):0;// 各题型专项正确率letlisteningCorrectRatethis.getTypeCorrectRate(typeStats,2);letreadingCorrectRatethis.getTypeCorrectRate(typeStats,3);letclozeCorrectRatethis.getTypeCorrectRate(typeStats,4);letvocabularyMasteryRatethis.getTypeCorrectRate(typeStats,1);letavgAnswerTimetotalQuestions0?Math.floor(duration/totalQuestions):0;letstatistics:PracticeStatistics{totalQuestions,correctQuestions,wrongQuestions,correctRate:Math.round(correctRate*100)/100,vocabularyMasteryRate:Math.round(vocabularyMasteryRate*100)/100,listeningCorrectRate:Math.round(listeningCorrectRate*100)/100,readingCorrectRate:Math.round(readingCorrectRate*100)/100,clozeCorrectRate:Math.round(clozeCorrectRate*100)/100,avgAnswerTime,typeStatistics:typeStats};// 合并到历史数据this.saveStatistics(statistics);returnstatistics;}catch(e){Logger.error(StatisticsManager,计算统计数据失败:${JSON.stringify(e)});returnthis.getEmptyStatistics();}}核心逻辑遍历题目逐题判断是否正确同时按题型分类统计。答案比较checkAnswerCorrect比较selectQues和rightQues两个数组排序后是否完全一致。四舍五入使用Math.round(x * 100) / 100保留两位小数。降级处理异常时返回空统计数据不影响用户使用。三、历史数据聚合单次练习的数据需要与历史数据合并才能反映用户的长期学习趋势privatesaveStatistics(statistics:PracticeStatistics):void{try{lethistoryStatsthis.getHistoryStatistics();letmergedStats:PracticeStatistics{totalQuestions:historyStats.totalQuestionsstatistics.totalQuestions,correctQuestions:historyStats.correctQuestionsstatistics.correctQuestions,wrongQuestions:historyStats.wrongQuestionsstatistics.wrongQuestions,correctRate:statistics.totalQuestions0?statistics.correctRate:historyStats.correctRate,vocabularyMasteryRate:statistics.vocabularyMasteryRate,listeningCorrectRate:statistics.listeningCorrectRate,readingCorrectRate:statistics.readingCorrectRate,clozeCorrectRate:statistics.clozeCorrectRate,avgAnswerTime:statistics.avgAnswerTime,typeStatistics:this.mergeTypeStatistics(historyStats.typeStatistics,statistics.typeStatistics)};PreferenceUtil.getInstance().put(this.STATISTICS_KEY,mergedStats);}catch(e){Logger.error(StatisticsManager,保存统计数据失败:${JSON.stringify(e)});}}聚合策略累加型字段totalQuestions、correctQuestions、wrongQuestions历史值 新值直接相加。覆盖型字段correctRate、各专项正确率以最新一次计算为准。分题型统计通过mergeTypeStatistics方法进行深度合并。privatemergeTypeStatistics(history:TypeStat[],current:TypeStat[]):TypeStat[]{// 先深度复制历史数据letresult:TypeStat[][];for(leti0;ihistory.length;i){result.push({type:history[i].type,total:history[i].total,correct:history[i].correct,wrong:history[i].wrong});}// 合并当前数据for(leti0;icurrent.length;i){letfoundfalse;for(letj0;jresult.length;j){if(result[j].typecurrent[i].type){result[j].totalcurrent[i].total;result[j].correctcurrent[i].correct;result[j].wrongcurrent[i].wrong;foundtrue;break;}}if(!found){result.push({type:current[i].type,total:current[i].total,correct:current[i].correct,wrong:current[i].wrong});}}returnresult;}这里手动深度复制而非直接赋值是因为TypeStat是引用类型直接复制会共享底层对象导致历史数据被意外修改。四、历史数据读取与展示publicgetHistoryStatistics():PracticeStatistics{try{letstatsPreferenceUtil.getInstance().get(this.STATISTICS_KEY,undefined)asPracticeStatistics;if(!stats){returnthis.getEmptyStatistics();}returnstats;}catch(e){Logger.error(StatisticsManager,获取历史统计数据失败:${JSON.stringify(e)});returnthis.getEmptyStatistics();}}publicgetQuestionTypeStats():QuestionTypeStat[]{try{letstatsthis.getHistoryStatistics();letresult:QuestionTypeStat[][];for(leti0;istats.typeStatistics.length;i){lettypeStatstats.typeStatistics[i];result.push({typeName:this.getTypeName(typeStat.type),total:typeStat.total,correct:typeStat.correct,wrong:typeStat.wrong,correctRate:typeStat.total0?Math.round(typeStat.correct/typeStat.total*10000)/100:0});}returnresult.sort((a,b)b.total-a.total);}catch(e){Logger.error(StatisticsManager,获取题型统计失败:${JSON.stringify(e)});return[];}}getQuestionTypeStats将内部的数字题型编号转换为可读的中文名称并按总题数降序排列方便 UI 层展示。五、最佳实践5.1 空值安全首次使用或数据被清空时getHistoryStatistics返回空统计数据而非null避免调用方做空值检查privategetEmptyStatistics():PracticeStatistics{return{totalQuestions:0,correctQuestions:0,wrongQuestions:0,correctRate:0,vocabularyMasteryRate:0,listeningCorrectRate:0,readingCorrectRate:0,clozeCorrectRate:0,avgAnswerTime:0,typeStatistics:[]};}5.2 精度控制所有百分比值使用Math.round(x * 100) / 100确保保留两位小数避免浮点数精度问题导致 UI 上显示一长串小数。5.3 题型映射集中管理题型编号到中文名称的映射集中在getTypeName方法中避免在 UI 层散落 switch-caseprivategetTypeName(type:string):string{switch(type){case1:return单词拼写case2:return听力理解case3:return阅读理解case4:return完形填空case5:return语法练习case6:return口语练习default:return题型${type}}}六、总结StatisticsManager展现了一个典型的数据聚合场景从原始数据题目列表到计算指标正确率再到历史数据合并累加 覆盖最后到 UI 友好的展示格式题型名称转换 排序。这种分层设计使得统计逻辑高度内聚、易于测试同时在需要新增题型或统计维度时只需扩展TypeStat数组即可无需修改核心聚合逻辑。

最新新闻

日新闻

周新闻

月新闻