欢迎光临
我们一直在努力

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

或者更简洁:

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返回的不完全一致,至少保证记录了 Stability 和 Difficulty,它们是参数优化的核心数据。


问题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)
}


额外建议

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

  • 状态迁移:当卡片状态发生变化(如从 New → Learning → Review)时,确保你的UI能正确反映。

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

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

    赞(0)
    未经允许不得转载:171主机测评 » fsrs使用错误指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址