fsrs使用错误指南
2026/6/29 20:53:18 网站建设 项目流程

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 // 然后再赋值

或者更简洁:

go

newCard := schedulingInfo.Card newCard.ID = card.ID card.Card = newCard

问题3:ReviewLog应使用调度返回的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返回的不完全一致,至少保证记录了StabilityDifficulty,它们是参数优化的核心数据。


问题4:ReviewCount计算错误

card.Card.Reps已经是复习总次数(包含本次),你加1会导致多算一次。

go

// 错误 ❌ ReviewCount: int(card.Card.Reps) + 1, // 正确 ✅ ReviewCount: int(card.Card.Reps),

修复后的完整代码

go

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]("单词不存在") } // 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) }

额外建议

  1. 参数优化:积累一定量(建议1000条以上)的复习日志后,可以调用fsrs.OptimizeParameters()为用户生成个性化参数。

  2. 状态迁移:当卡片状态发生变化(如从NewLearningReview)时,确保你的UI能正确反映。

  3. 时区处理time.Now()建议统一使用time.Now().UTC(),避免时区问题影响间隔计算。

如果还有其他疑问,比如如何做参数优化或批量处理,随时告诉我。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询