学习计划管理——LearningPlanManager 的目标设定与追踪
引言
学习计划是英语学习 App 中驱动用户持续学习的关键机制。一个好的学习计划系统不仅要支持用户设定每日目标,还需要追踪完成进度、维护连续学习天数、通过成就徽章激励用户。LearningPlanManager就是这样一个集目标设定、进度追踪、连续天数维护、成就系统于一体的 Manager 模块。本文将深入分析它的设计思路与实现细节。
一、数据模型设计
LearningPlan接口包含了学习计划所需的全部状态字段:
exportinterfaceLearningPlan{// 每日目标dailyWordGoal:number// 每日单词目标dailyListeningGoal:number// 每日听力目标(分钟)dailyReadingGoal:number// 每日阅读目标(分钟)// 连续学习continuousDays:number// 连续学习天数lastStudyDate:number// 最后学习日期(时间戳)// 今日进度todayWordCount:number// 今日已学单词数todayListeningDuration:number// 今日听力时长todayReadingDuration:number// 今日阅读时长// 提醒设置reminderEnabled:boolean// 学习提醒是否开启reminderTime:string// 提醒时间(如 "08:00")reminderRepeatType:number// 重复周期 0:每天 1:工作日 2:自定义// 累计数据totalWordCount:number// 总学习单词数totalStudyDuration:number// 总学习时长(分钟)// 成就徽章badges:string[]// 已解锁徽章列表}默认值通过常量DEFAULT_LEARNING_PLAN提供:
exportconstDEFAULT_LEARNING_PLAN:LearningPlan={dailyWordGoal:20,// 每日 20 词dailyListeningGoal:15,// 听力 15 分钟dailyReadingGoal:10,// 阅读 10 分钟continuousDays:0,lastStudyDate:0,todayWordCount:0,todayListeningDuration:0,todayReadingDuration:0,reminderEnabled:false,reminderTime:'08:00',reminderRepeatType:0,totalWordCount:0,totalStudyDuration:0,badges:[]};二、读写与部分更新
LearningPlanManager以单例模式提供读写接口:
exportclassLearningPlanManager{privatestaticinstance?:LearningPlanManager;privateLEARNING_PLAN_KEY='LEARNING_PLAN';publicstaticgetInstance():LearningPlanManager{if(!LearningPlanManager.instance){LearningPlanManager.instance=newLearningPlanManager();}returnLearningPlanManager.instance;}// 读取publicgetLearningPlan():LearningPlan{try{letplan=PreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY,undefined)asLearningPlan;if(!plan){plan=this.cloneLearningPlan(DEFAULT_LEARNING_PLAN);this.saveLearningPlan(plan);}returnplan;}catch(e){Logger.error('LearningPlanManager',`获取学习计划失败:${JSON.stringify(e)}`);returnthis.cloneLearningPlan(DEFAULT_LEARNING_PLAN);}}// 保存publicsaveLearningPlan(plan:LearningPlan):void{try{PreferenceUtil.getInstance().put(this.LEARNING_PLAN_KEY,plan);}catch(e){Logger.error('LearningPlanManager',`保存学习计划失败:${JSON.stringify(e)}`);}}// 部分更新publicupdateLearningPlan(plan:Partial<LearningPlan>):LearningPlan{try{letcurrentPlan=this.getLearningPlan();letnewPlan=this.mergeLearningPlans(currentPlan,plan);this.saveLearningPlan(newPlan);returnnewPlan;}catch(e){Logger.error('LearningPlanManager',`更新学习计划失败:${JSON.stringify(e)}`);returnthis.getLearningPlan();}}}部分更新的关键在于mergeLearningPlans方法中的??空值合并运算符:
privatemergeLearningPlans(currentPlan:LearningPlan,plan:Partial<LearningPlan>):LearningPlan{return{dailyWordGoal:plan.dailyWordGoal??currentPlan.dailyWordGoal,dailyListeningGoal:plan.dailyListeningGoal??currentPlan.dailyListeningGoal,// ... 每个字段都有 ?? 兜底badges:plan.badges?plan.badges.concat():currentPlan.badges.concat()};}这样调用方可以只传入需要修改的字段:
LearningPlanManager.getInstance().updateLearningPlan({dailyWordGoal:30,// 只修改单词目标reminderEnabled:true});三、连续学习天数维护
连续学习天数是激励用户每日打卡的重要机制:
publiccheckAndUpdateContinuousDays():LearningPlan{try{letplan=this.getLearningPlan();lettoday=newDate();today.setHours(0,0,0,0);lettodayTimestamp=today.getTime();if(plan.lastStudyDate===0){// 首次学习plan.continuousDays=1;}else{letlastDate=newDate(plan.lastStudyDate);lastDate.setHours(0,0,0,0);letlastTimestamp=lastDate.getTime();letdiffDays=Math.floor((todayTimestamp-lastTimestamp)/(1000*60*60*24));if(diffDays===0){// 同一天学习,不更新}elseif(diffDays===1){// 连续学习,天数 +1plan.continuousDays++;}else{// 中断,重置为 1plan.continuousDays=1;}}plan.lastStudyDate=todayTimestamp;this.saveLearningPlan(plan);returnplan;}catch(e){Logger.error('LearningPlanManager',`更新连续学习天数失败:${JSON.stringify(e)}`);returnthis.getLearningPlan();}}核心逻辑:
- 将
today和lastStudyDate都归零到当天 00:00:00,消除时间影响。 - 计算天数差
diffDays:- 0:同一天多次学习,连续天数不变。
- 1:昨天也学习了,连续天数 +1。
- >=2:中断了,重置为 1。
lastStudyDate使用Date.getTime()的时间戳,方便存储和比较。
四、学习记录与进度查询
// 记录单词学习publicrecordWordStudy(count:number):LearningPlan{letplan=this.getLearningPlan();plan.todayWordCount+=count;plan.totalWordCount+=count;plan=this.checkAndUpdateContinuousDays();plan=this.checkAndUnlockBadges(plan);this.saveLearningPlan(plan);returnplan;}// 获取今日进度(百分比)publicgetTodayProgress():TodayProgress{letplan=this.getLearningPlan();return{wordProgress:plan.dailyWordGoal>0?Math.min(100,Math.floor(plan.todayWordCount/plan.dailyWordGoal*100)):0,listeningProgress:plan.dailyListeningGoal>0?Math.min(100,Math.floor(plan.todayListeningDuration/plan.dailyListeningGoal*100)):0,readingProgress:plan.dailyReadingGoal>0?Math.min(100,Math.floor(plan.todayReadingDuration/plan.dailyReadingGoal*100)):0};}getTodayProgress返回三个维度的百分比(0-100),使用Math.min(100, ...)防止分母为 0 或进度超过 100% 的场景。
五、成就徽章系统
徽章系统是 gamification 的典型应用,用户达成特定里程碑后自动解锁徽章:
// 徽章常量exportconstBADGE_DAYS_3='连续学习3天'exportconstBADGE_DAYS_7='连续学习7天'exportconstBADGE_DAYS_30='连续学习30天'exportconstBADGE_WORDS_100='词汇达人(100词)'exportconstBADGE_WORDS_500='词汇大师(500词)'exportconstBADGE_WORDS_1000='词汇王者(1000词)'exportconstBADGE_LISTENING_10='听力新手(10分钟)'exportconstBADGE_LISTENING_60='听力高手(60分钟)'privatecheckAndUnlockBadges(plan:LearningPlan):LearningPlan{letbadges=plan.badges.concat();// 连续学习徽章(阶梯式解锁)if(plan.continuousDays>=30&&!badges.includes(BADGE_DAYS_30)){badges.push(BADGE_DAYS_30);}elseif(plan.continuousDays>=7&&!badges.includes(BADGE_DAYS_7)){badges.push(BADGE_DAYS_7);}elseif(plan.continuousDays>=3&&!badges.includes(BADGE_DAYS_3)){badges.push(BADGE_DAYS_3);}// 词汇徽章if(plan.totalWordCount>=1000&&!badges.includes(BADGE_WORDS_1000)){badges.push(BADGE_WORDS_1000);}elseif(plan.totalWordCount>=500&&!badges.includes(BADGE_WORDS_500)){badges.push(BADGE_WORDS_500);}elseif(plan.totalWordCount>=100&&!badges.includes(BADGE_WORDS_100)){badges.push(BADGE_WORDS_100);}// 听力徽章if(plan.totalStudyDuration>=60&&!badges.includes(BADGE_LISTENING_60)){badges.push(BADGE_LISTENING_60);}elseif(plan.totalStudyDuration>=10&&!badges.includes(BADGE_LISTENING_10)){badges.push(BADGE_LISTENING_10);}plan.badges=badges;returnplan;}设计亮点:
- 阶梯判断:从高到低判断,确保用户达到 30 天时不会重复获得 3 天和 7 天徽章。
- 幂等性:每次检查
badges.includes(),防止徽章被重复添加。 - 可扩展:新增徽章只需添加常量+判断逻辑,不影响现有代码。
六、最佳实践
6.1 使用concat()确保不可变
在cloneLearningPlan和mergeLearningPlans中,badges数组使用concat()复制而非直接赋值,避免多个LearningPlan对象共享同一个数组引用导致意外修改。
6.2 进度百分比上限保护
getTodayProgress使用Math.min(100, ...)确保进度不会超过 100%,避免 Progress 组件显示异常。
6.3 每日数据重置
resetDailyData方法在新的一天开始时调用,将todayWordCount、todayListeningDuration、todayReadingDuration重置为 0。根据业务需求,可以在应用启动时或每次学习记录前判断是否需要重置。
七、总结
LearningPlanManager是一个功能完备的学习计划管理模块,它覆盖了目标设定、进度追踪、连续天数维护、成就徽章等完整的学习激励闭环。通过Partial<T>参数实现优雅的部分更新,通过时间戳比较实现准确的连续天数计算,通过阶梯式徽章检测实现渐进式的用户激励。这种设计模式可以轻松复用到其他需要目标追踪和成就系统的场景中。