fsrs使用错误指南

📅 2026/6/27 20:17:43
fsrs使用错误指南
func (self *FsrsService) ProcessWordReviewRet(req *fsrsreq.FsrsReq) *pagemodel.IchubResult[*fsrsmodel2.ReviewResult] { if req.WordId 0 || req.StudentId 0 { return pagemodel.ResultFail[*fsrsmodel2.ReviewResult](单词id或学生id不能为空) } golog.Info(ProcessWordReviewRet req:, req) // 执行复习并获取新的状态 now : time.Now() // 从数据库获取卡片 card, err : self.GetByWordID(req.StudentId, req.WordId) if err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } if card.ID 0 { return pagemodel.ResultFail[*fsrsmodel2.ReviewResult](单词不存在) } schedulingInfo : self.fsrs.Next(card.Card, now, req.Rating) // 更新卡片信息 card.Due schedulingInfo.Card.Due card.Stability schedulingInfo.Card.Stability card.Difficulty schedulingInfo.Card.Difficulty card.ElapsedDays schedulingInfo.Card.ElapsedDays card.ScheduledDays schedulingInfo.Card.ScheduledDays card.Reps schedulingInfo.Card.Reps card.Lapses schedulingInfo.Card.Lapses card.State schedulingInfo.Card.State card.LastReview now // 保存更新后的卡片 if err : self.Update(card); err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } // 记录复习日志 reviewLog : fsrsmodel2.ReviewLog{ CardID: card.ID, Grade: int(req.Rating), ReviewTime: now, NextDue: schedulingInfo.Card.Due, } if err : self.SaveReviewLog(reviewLog); err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } // 构建复习结果 reviewRet : fsrsmodel2.ReviewResult{ Card: *card, NextReviewAt: schedulingInfo.Card.Due, ReviewedAt: now, ReviewCount: int(card.Card.Reps) 1, IntervalDays: int(schedulingInfo.Card.Due.Sub(now).Hours() / 24), } return pagemodel.ResultOk(reviewRet, true) }你的代码整体逻辑是对的但存在几个关键问题可能导致算法状态混乱或数据不一致。我来逐一指出并给出修复方案问题1错误使用了Next()方法你用的是self.fsrs.Next(card.Card, now, req.Rating)但Next()是单次评分的简化方法它不会考虑卡片的复习历史每次调用都是基于当前状态独立计算。正确做法应该使用Repeat()方法它会基于卡片的完整历史生成所有评分对应的排程结果。go// 错误 ❌ schedulingInfo : self.fsrs.Next(card.Card, now, req.Rating) // 正确 ✅ schedulingCards : self.fsrs.Repeat(card.Card, now) var schedulingInfo *fsrs.SchedulingInfo switch req.Rating { case fsrs.Again: schedulingInfo schedulingCards[0] case fsrs.Hard: schedulingInfo schedulingCards[1] case fsrs.Good: schedulingInfo schedulingCards[2] case fsrs.Easy: schedulingInfo schedulingCards[3] }问题2忘记更新卡片的ID字段Repeat()返回的Card是一个新对象其ID字段为零值。你用schedulingInfo.Card直接覆盖card.Card时会丢失数据库主键。go// 错误 ❌直接覆盖ID会变成0 card.Card schedulingInfo.Card // 正确 ✅逐字段更新保留ID oldID : card.ID // 先保存旧ID newCard : schedulingInfo.Card newCard.ID oldID // 将ID赋给新卡片 card.Card newCard // 然后再赋值或者更简洁gonewCard : schedulingInfo.Card newCard.ID card.ID card.Card newCard问题3ReviewLog应使用调度返回的ReviewLog你手动构建了ReviewLog但正确的做法是使用Repeat()返回的ReviewLog它包含了算法计算出的复习间隔、难度、稳定性等上下文信息对后续参数优化至关重要。go// 错误 ❌ reviewLog : fsrsmodel2.ReviewLog{ CardID: card.ID, Grade: int(req.Rating), ReviewTime: now, NextDue: schedulingInfo.Card.Due, } // 正确 ✅ reviewLog : fsrsmodel2.ReviewLog{ CardID: card.ID, Grade: int(req.Rating), ReviewTime: now, // 从调度结果中提取 Stability: schedulingInfo.Card.Stability, Difficulty: schedulingInfo.Card.Difficulty, ElapsedDays: schedulingInfo.Card.ElapsedDays, ScheduledDays: schedulingInfo.Card.ScheduledDays, Review: schedulingInfo.ReviewLog, // 如果有这个字段 }如果你的ReviewLog模型与FSRS返回的不完全一致至少保证记录了Stability和Difficulty它们是参数优化的核心数据。问题4ReviewCount计算错误card.Card.Reps已经是复习总次数包含本次你加1会导致多算一次。go// 错误 ❌ ReviewCount: int(card.Card.Reps) 1, // 正确 ✅ ReviewCount: int(card.Card.Reps),修复后的完整代码gofunc (self *FsrsService) ProcessWordReviewRet(req *fsrsreq.FsrsReq) *pagemodel.IchubResult[*fsrsmodel2.ReviewResult] { if req.WordId 0 || req.StudentId 0 { return pagemodel.ResultFail[*fsrsmodel2.ReviewResult](单词id或学生id不能为空) } golog.Info(ProcessWordReviewRet req:, req) now : time.Now() card, err : self.GetByWordID(req.StudentId, req.WordId) if err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } if card.ID 0 { return pagemodel.ResultFail[*fsrsmodel2.ReviewResult](单词不存在) } // 1. 使用 Repeat() 获取所有排程结果 schedulingCards : self.fsrs.Repeat(card.Card, now) // 2. 根据用户评分选择对应的结果 var schedulingInfo *fsrs.SchedulingInfo switch req.Rating { case fsrs.Again: schedulingInfo schedulingCards[0] case fsrs.Hard: schedulingInfo schedulingCards[1] case fsrs.Good: schedulingInfo schedulingCards[2] case fsrs.Easy: schedulingInfo schedulingCards[3] default: return pagemodel.ResultFail[*fsrsmodel2.ReviewResult](无效的评分) } // 3. 更新卡片保留ID newCard : schedulingInfo.Card newCard.ID card.ID card.Card newCard card.LastReview now if err : self.Update(card); err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } // 4. 记录复习日志使用调度返回的数据 reviewLog : fsrsmodel2.ReviewLog{ CardID: card.ID, Grade: int(req.Rating), ReviewTime: now, Due: schedulingInfo.Card.Due, Stability: schedulingInfo.Card.Stability, Difficulty: schedulingInfo.Card.Difficulty, ElapsedDays: schedulingInfo.Card.ElapsedDays, ScheduledDays: schedulingInfo.Card.ScheduledDays, State: schedulingInfo.Card.State, } if err : self.SaveReviewLog(reviewLog); err ! nil { return pagemodel.ResultErr[*fsrsmodel2.ReviewResult](err) } // 5. 构建结果 reviewRet : fsrsmodel2.ReviewResult{ Card: *card, NextReviewAt: schedulingInfo.Card.Due, ReviewedAt: now, ReviewCount: int(card.Card.Reps), // 不再1 IntervalDays: int(schedulingInfo.Card.Due.Sub(now).Hours() / 24), } return pagemodel.ResultOk(reviewRet, true) }额外建议参数优化积累一定量建议1000条以上的复习日志后可以调用fsrs.OptimizeParameters()为用户生成个性化参数。状态迁移当卡片状态发生变化如从New→Learning→Review时确保你的UI能正确反映。时区处理time.Now()建议统一使用time.Now().UTC()避免时区问题影响间隔计算。如果还有其他疑问比如如何做参数优化或批量处理随时告诉我。