easy-word 的核心功能是按遗忘曲线安排复习。单词学完后不是”学过了”,而是进入一个调度系统,在最佳时间点提醒学生复习。
艾宾浩斯遗忘曲线 德国心理学家赫尔曼·艾宾浩斯发现,记忆遗忘遵循指数衰减规律。复习的最佳时间点是在遗忘即将发生时:
1 2 3 4 5 6 7 8 第 1 次复习:学完后 20 分钟 第 2 次复习:1 小时后 第 3 次复习:9 小时后(通常安排到第二天) 第 4 次复习:1 天后 第 5 次复习:2 天后 第 6 次复习:6 天后 第 7 次复习:15 天后 第 8 次复习:30 天后
graph LR
A["学习新词"] --> B["20min 复习"]
B --> C["1h 复习"]
C --> D["9h 复习"]
D --> E["1d 复习"]
E --> F["2d 复习"]
F --> G["6d 复习"]
G --> H["15d 复习"]
H --> I["30d 复习"]
I --> J["已掌握"]
调度间隔定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const REVIEW_INTERVALS = [ 20 * 60 * 1000 , 60 * 60 * 1000 , 9 * 60 * 60 * 1000 , 24 * 60 * 60 * 1000 , 2 * 24 * 60 * 60 * 1000 , 6 * 24 * 60 * 60 * 1000 , 15 * 24 * 60 * 60 * 1000 , 30 * 24 * 60 * 60 * 1000 , ] function getNextReviewTime (reviewCount : number , lastReviewAt : number ): number { const index = Math .min (reviewCount, REVIEW_INTERVALS .length - 1 ) return lastReviewAt + REVIEW_INTERVALS [index] }
reviewCount 从 0 开始,每次复习 +1。到达第 8 次复习后,间隔固定为 30 天。
调度器实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class ReviewScheduler { constructor (private dao : LearnRecordDao ) {} async getDueWords (userId : string , limit : number = 20 ): Promise <LearnRecord []> { const predicates = new relationalStore.RdbPredicates ('learn_records' ) predicates.equalTo ('user_id' , userId) predicates.lessThanOrEqualTo ('next_review_at' , Date .now ()) predicates.equalTo ('status' , 1 ) predicates.limit (limit) predicates.orderByAsc ('next_review_at' ) return this .dao .query (predicates) } async recordReview (recordId : number , correct : boolean ): Promise <void > { const record = await this .dao .getById (recordId) if (!record) return const newCount = record.review_count + 1 const newCorrect = correct ? record.correct_count + 1 : record.correct_count const nextReview = getNextReviewTime (newCount, Date .now ()) await this .dao .update (recordId, { review_count : newCount, correct_count : newCorrect, last_review_at : Date .now (), next_review_at : nextReview, status : newCount >= REVIEW_INTERVALS .length ? 2 : 1 }) } async learnNewWord (userId : string , wordId : number ): Promise <void > { await this .dao .insert ({ word_id : wordId, user_id : userId, status : 1 , review_count : 0 , correct_count : 0 , next_review_at : getNextReviewTime (0 , Date .now ()), last_review_at : null }) } }
答错惩罚 学生答错时,不推进间隔,而是回到上一级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 async recordReview (recordId : number , correct : boolean ): Promise <void > { const record = await this .dao .getById (recordId) if (!record) return let newCount : number if (correct) { newCount = record.review_count + 1 } else { newCount = Math .max (0 , record.review_count - 1 ) } const nextReview = getNextReviewTime (newCount, Date .now ()) }
回退一级意味着:如果学生在第 5 次复习(2 天间隔)时答错,回到第 4 次(1 天间隔),明天再复习一次。
每日学习流程 1 2 3 4 5 首页 → 显示今日待复习数量 → 闪卡模式:翻卡 → 认识/不认识 → 拼写模式:听发音 → 输入拼写 → 选择题:四选一 → 完成后更新 next_review_at
三种学习模式(闪卡、拼写、选择题)共享同一个调度器。不管用哪种模式学习,调度逻辑统一。
热力图数据 每次学习结束后,更新 daily_logs 表:
1 2 3 4 5 6 7 8 9 10 11 12 13 async updateDailyLog (userId : string , learned : number , reviewed : number , accuracy : number ) { const today = formatDate (Date .now ()) const predicates = new relationalStore.RdbPredicates ('daily_logs' ) predicates.equalTo ('user_id' , userId) predicates.equalTo ('date' , today) const existing = await this .dao .query (predicates) if (existing.length > 0 ) { await this .dao .updateDaily (userId, today, learned, reviewed, accuracy) } else { await this .dao .insertDaily (userId, today, learned, reviewed, accuracy) } }
热力图读取 daily_logs 表,按日期聚合展示学习强度。第四篇会详细讲热力图的实现。
间隔重复的核心不是”多复习”,是”在对的时间复习”。20 分钟到 30 天的 8 级间隔,覆盖了从短期记忆到长期记忆的完整转化过程。