Skip to content

Commit 2f07fc2

Browse files
Merge pull request #75 from JYinherit/main
FIX:FSRS算法无限循环复习
2 parents 2b697d2 + ede8134 commit 2f07fc2

4 files changed

Lines changed: 236 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## DEVELOPING - ????-??-?? - (??????)
4+
5+
### Added
6+
7+
- 复习模块添加了强化学习选项
8+
9+
### Improvement
10+
11+
- 优化了FSRS学习机制
12+
13+
### Fix
14+
15+
- 修复了可能的复习列表重复问题
16+
317
## v1.0.0 - 2026-3-18 - (100000)
418

519
### Added

lib/funcs/fsrs_func.dart

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,28 @@ class FSRS {
2929
} else {
3030
config = FSRSConfig.buildFromMap(jsonDecode(appData.storage.getString("fsrsData")!));
3131
logger.info("FSRS配置加载完成");
32+
33+
// 清洗潜在的重复脏数据 (Deduplication)
34+
final Set<int> seenIds = {};
35+
final List<Card> uniqueCards = [];
36+
final List<ReviewLog> uniqueLogs = [];
37+
38+
for(int i = 0; i < config.cards.length; i++) {
39+
final currentCardId = config.cards[i].cardId;
40+
if(!seenIds.contains(currentCardId)) {
41+
seenIds.add(currentCardId);
42+
uniqueCards.add(config.cards[i]);
43+
if(i < config.reviewLogs.length) {
44+
uniqueLogs.add(config.reviewLogs[i]);
45+
}
46+
}
47+
}
48+
49+
if(uniqueCards.length < config.cards.length) {
50+
logger.warning("发现并清理了 ${config.cards.length - uniqueCards.length} 条重复复习记录");
51+
config = config.copyWith(cards: uniqueCards, reviewLogs: uniqueLogs);
52+
save();
53+
}
3254
}
3355

3456
if(config.enabled) return true;
@@ -50,58 +72,66 @@ class FSRS {
5072
save();
5173
}
5274

53-
int willDueIn(int index) {
54-
return config.cards[index].due.toLocal().difference(DateTime.now()).inDays;
75+
int willDueIn(Card card) {
76+
return card.due.toLocal().difference(DateTime.now()).inDays;
5577
}
5678

57-
void reviewCard(int wordId, int duration, bool isCorrect, {Rating? forceRate}) {
58-
logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect");
59-
int index = config.cards.indexWhere((Card card) => card.cardId == wordId); // 避免有时候cardId != wordId
60-
logger.fine("定位复习卡片地址: $index, 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}");
61-
final (:card, :reviewLog) = config.scheduler!.reviewCard(config.cards[index], forceRate ?? calculate(duration, isCorrect), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration);
62-
config.cards[index] = card;
63-
config.reviewLogs[index] = reviewLog;
64-
logger.fine("卡片 $index 复习后: 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}");
79+
void produceCard(int wordId, {int? duration, bool? isCorrect, Rating? forceRate}) {
80+
logger.fine("记录复习卡片: Id: $wordId; duration: $duration; isCorrect: $isCorrect; forceRate: $forceRate");
81+
final int index = config.cards.indexWhere((Card card) => card.cardId == wordId);
82+
if(index == -1) {
83+
// 卡片不存在 进行添加
84+
logger.fine("添加复习卡片: Id: $wordId");
85+
if(config.cards.isEmpty) {
86+
config = config.copyWith(
87+
cards: [],
88+
reviewLogs: []
89+
);
90+
}
91+
config.cards.add(Card(cardId: wordId, state: State.learning));
92+
config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now()));
93+
} else {
94+
// 卡片存在 进行复习
95+
if((duration == null || isCorrect == null) && forceRate == null) {
96+
logger.shout("传入信息缺失: wordId: $wordId; duration: $duration; isCorrect: $isCorrect; forceRate: $forceRate");
97+
return; // 避免错误信息导入
98+
}
99+
logger.fine("定位复习卡片地址: $index, 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}");
100+
final (:card, :reviewLog) = config.scheduler!.reviewCard(config.cards[index], forceRate ?? calculate(duration!, isCorrect!), reviewDateTime: DateTime.now().toUtc(), reviewDuration: duration);
101+
config.cards[index] = card;
102+
config.reviewLogs[index] = reviewLog;
103+
logger.fine("卡片 $index 复习后: 目前阶段: ${config.cards[index].step}, 难度: ${config.cards[index].difficulty}, 稳定: ${config.cards[index].stability}, 过期时间(+8): ${config.cards[index].due.toLocal()}");
104+
}
65105
save();
66106
}
67107

68108
int getWillDueCount() {
69109
int dueCards = 0;
70-
for(int i = 0; i < config.cards.length; i++) {
71-
if(willDueIn(i) < 1) {
110+
for(Card card in config.cards) {
111+
if(willDueIn(card) < 1) {
72112
dueCards++;
73113
}
74114
}
75115
return dueCards;
76116
}
77117

78118
int getLeastDueCard() {
79-
if (config.cards.isEmpty) return -1;
80-
int leastDueIndex = 0;
81-
for(int i = 1; i < config.cards.length; i++) {
82-
if(config.cards[i].due.toLocal().isBefore(config.cards[leastDueIndex].due.toLocal()) && config.cards[i].due.toLocal().difference(DateTime.now()) < Duration(days: 1)) {
83-
leastDueIndex = i;
119+
Card? leastDueCard;
120+
for(Card card in config.cards) {
121+
if(willDueIn(card) < 1) {
122+
if(leastDueCard == null || card.due.toLocal().isBefore(leastDueCard.due.toLocal())) {
123+
leastDueCard = card;
124+
}
84125
}
85126
}
86-
if(config.cards[leastDueIndex].due.difference(DateTime.now()) > Duration(days: 1)) return -1;
87-
return config.cards[leastDueIndex].cardId;
127+
if (leastDueCard == null) return -1;
128+
return leastDueCard.cardId;
88129
}
89130

90131
bool isContained(int wordId) {
91132
return config.cards.any((Card card) => card.cardId == wordId);
92133
}
93134

94-
void addWordCard(int wordId) {
95-
logger.fine("添加复习卡片: Id: $wordId");
96-
if (config.cards.isEmpty) {
97-
config = config.copyWith(cards: [], reviewLogs: []);
98-
}
99-
// os the wordID == cardID
100-
config.cards.add(Card(cardId: wordId, state: State.learning));
101-
config.reviewLogs.add(ReviewLog(cardId: wordId, rating: Rating.good, reviewDateTime: DateTime.now()));
102-
save();
103-
}
104-
105135
Rating calculate(int duration, bool isCorrect) {
106136
// duration in milliseconds
107137
if (!isCorrect) {
@@ -133,6 +163,7 @@ class FSRSConfig {
133163
final bool preferSimilar;
134164
final bool selfEvaluate;
135165
final int pushAmount;
166+
final bool reinforceMemory;
136167

137168
const FSRSConfig({
138169
bool? enabled,
@@ -144,7 +175,8 @@ class FSRSConfig {
144175
int? goodDuration,
145176
bool? preferSimilar,
146177
bool? selfEvaluate,
147-
int? pushAmount
178+
int? pushAmount,
179+
bool? reinforceMemory
148180
}) :
149181
enabled = enabled??false,
150182
cards = cards??const [],
@@ -154,7 +186,8 @@ class FSRSConfig {
154186
goodDuration = goodDuration??6000,
155187
preferSimilar = preferSimilar??false,
156188
selfEvaluate = selfEvaluate??false,
157-
pushAmount = pushAmount??0;
189+
pushAmount = pushAmount??0,
190+
reinforceMemory = reinforceMemory??false;
158191

159192
Map<String, dynamic> toMap(){
160193
return {
@@ -167,7 +200,8 @@ class FSRSConfig {
167200
"goodDuration": goodDuration,
168201
"preferSimilar": preferSimilar,
169202
"selfEvaluate": selfEvaluate,
170-
"pushAmount": pushAmount
203+
"pushAmount": pushAmount,
204+
"reinforceMemory": reinforceMemory
171205
};
172206
}
173207

@@ -181,7 +215,8 @@ class FSRSConfig {
181215
int? goodDuration,
182216
bool? preferSimilar,
183217
bool? selfEvaluate,
184-
int? pushAmount
218+
int? pushAmount,
219+
bool? reinforceMemory
185220
}) {
186221
return FSRSConfig(
187222
enabled: enabled??this.enabled,
@@ -193,7 +228,8 @@ class FSRSConfig {
193228
goodDuration: goodDuration??this.goodDuration,
194229
preferSimilar: preferSimilar??this.preferSimilar,
195230
selfEvaluate: selfEvaluate??this.selfEvaluate,
196-
pushAmount: pushAmount??this.pushAmount
231+
pushAmount: pushAmount??this.pushAmount,
232+
reinforceMemory: reinforceMemory??this.reinforceMemory
197233
);
198234
}
199235

@@ -209,7 +245,8 @@ class FSRSConfig {
209245
goodDuration: configData["goodDuration"],
210246
preferSimilar: configData["preferSimilar"],
211247
selfEvaluate: configData["selfEvaluate"],
212-
pushAmount: configData["pushAmount"]
248+
pushAmount: configData["pushAmount"],
249+
reinforceMemory: configData["reinforceMemory"]
213250
);
214251
}
215252
return FSRSConfig(enabled: false);

0 commit comments

Comments
 (0)