学习计划管理------LearningPlanManager 的目标设定与追踪

引言
学习计划是英语学习 App 中驱动用户持续学习的关键机制。一个好的学习计划系统不仅要支持用户设定每日目标,还需要追踪完成进度、维护连续学习天数、通过成就徽章激励用户。LearningPlanManager 就是这样一个集目标设定、进度追踪、连续天数维护、成就系统于一体的 Manager 模块。本文将深入分析它的设计思路与实现细节。
一、数据模型设计
LearningPlan 接口包含了学习计划所需的全部状态字段:
typescript
export interface LearningPlan {
// 每日目标
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 提供:
typescript
export const DEFAULT_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 以单例模式提供读写接口:
typescript
export class LearningPlanManager {
private static instance?: LearningPlanManager;
private LEARNING_PLAN_KEY = 'LEARNING_PLAN';
public static getInstance(): LearningPlanManager {
if (!LearningPlanManager.instance) {
LearningPlanManager.instance = new LearningPlanManager();
}
return LearningPlanManager.instance;
}
// 读取
public getLearningPlan(): LearningPlan {
try {
let plan = PreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY, undefined) as LearningPlan;
if (!plan) {
plan = this.cloneLearningPlan(DEFAULT_LEARNING_PLAN);
this.saveLearningPlan(plan);
}
return plan;
} catch (e) {
Logger.error('LearningPlanManager', `获取学习计划失败: ${JSON.stringify(e)}`);
return this.cloneLearningPlan(DEFAULT_LEARNING_PLAN);
}
}
// 保存
public saveLearningPlan(plan: LearningPlan): void {
try {
PreferenceUtil.getInstance().put(this.LEARNING_PLAN_KEY, plan);
} catch (e) {
Logger.error('LearningPlanManager', `保存学习计划失败: ${JSON.stringify(e)}`);
}
}
// 部分更新
public updateLearningPlan(plan: Partial<LearningPlan>): LearningPlan {
try {
let currentPlan = this.getLearningPlan();
let newPlan = this.mergeLearningPlans(currentPlan, plan);
this.saveLearningPlan(newPlan);
return newPlan;
} catch (e) {
Logger.error('LearningPlanManager', `更新学习计划失败: ${JSON.stringify(e)}`);
return this.getLearningPlan();
}
}
}
部分更新的关键 在于 mergeLearningPlans 方法中的 ?? 空值合并运算符:
typescript
private mergeLearningPlans(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()
};
}
这样调用方可以只传入需要修改的字段:
typescript
LearningPlanManager.getInstance().updateLearningPlan({
dailyWordGoal: 30, // 只修改单词目标
reminderEnabled: true
});
三、连续学习天数维护
连续学习天数是激励用户每日打卡的重要机制:
typescript
public checkAndUpdateContinuousDays(): LearningPlan {
try {
let plan = this.getLearningPlan();
let today = new Date();
today.setHours(0, 0, 0, 0);
let todayTimestamp = today.getTime();
if (plan.lastStudyDate === 0) {
// 首次学习
plan.continuousDays = 1;
} else {
let lastDate = new Date(plan.lastStudyDate);
lastDate.setHours(0, 0, 0, 0);
let lastTimestamp = lastDate.getTime();
let diffDays = Math.floor((todayTimestamp - lastTimestamp) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
// 同一天学习,不更新
} else if (diffDays === 1) {
// 连续学习,天数 +1
plan.continuousDays++;
} else {
// 中断,重置为 1
plan.continuousDays = 1;
}
}
plan.lastStudyDate = todayTimestamp;
this.saveLearningPlan(plan);
return plan;
} catch (e) {
Logger.error('LearningPlanManager', `更新连续学习天数失败: ${JSON.stringify(e)}`);
return this.getLearningPlan();
}
}
核心逻辑:
- 将
today和lastStudyDate都归零到当天 00:00:00,消除时间影响。 - 计算天数差
diffDays:- 0:同一天多次学习,连续天数不变。
- 1:昨天也学习了,连续天数 +1。
- >=2:中断了,重置为 1。
lastStudyDate使用Date.getTime()的时间戳,方便存储和比较。
四、学习记录与进度查询
typescript
// 记录单词学习
public recordWordStudy(count: number): LearningPlan {
let plan = this.getLearningPlan();
plan.todayWordCount += count;
plan.totalWordCount += count;
plan = this.checkAndUpdateContinuousDays();
plan = this.checkAndUnlockBadges(plan);
this.saveLearningPlan(plan);
return plan;
}
// 获取今日进度(百分比)
public getTodayProgress(): TodayProgress {
let plan = 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 的典型应用,用户达成特定里程碑后自动解锁徽章:
typescript
// 徽章常量
export const BADGE_DAYS_3 = '连续学习3天'
export const BADGE_DAYS_7 = '连续学习7天'
export const BADGE_DAYS_30 = '连续学习30天'
export const BADGE_WORDS_100 = '词汇达人(100词)'
export const BADGE_WORDS_500 = '词汇大师(500词)'
export const BADGE_WORDS_1000 = '词汇王者(1000词)'
export const BADGE_LISTENING_10 = '听力新手(10分钟)'
export const BADGE_LISTENING_60 = '听力高手(60分钟)'
private checkAndUnlockBadges(plan: LearningPlan): LearningPlan {
let badges = plan.badges.concat();
// 连续学习徽章(阶梯式解锁)
if (plan.continuousDays >= 30 && !badges.includes(BADGE_DAYS_30)) {
badges.push(BADGE_DAYS_30);
} else if (plan.continuousDays >= 7 && !badges.includes(BADGE_DAYS_7)) {
badges.push(BADGE_DAYS_7);
} else if (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);
} else if (plan.totalWordCount >= 500 && !badges.includes(BADGE_WORDS_500)) {
badges.push(BADGE_WORDS_500);
} else if (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);
} else if (plan.totalStudyDuration >= 10 && !badges.includes(BADGE_LISTENING_10)) {
badges.push(BADGE_LISTENING_10);
}
plan.badges = badges;
return plan;
}
设计亮点:
- 阶梯判断:从高到低判断,确保用户达到 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> 参数实现优雅的部分更新,通过时间戳比较实现准确的连续天数计算,通过阶梯式徽章检测实现渐进式的用户激励。这种设计模式可以轻松复用到其他需要目标追踪和成就系统的场景中。