Skip to content

Commit 290a085

Browse files
committed
fix(groups): 改判清空系列键与结算性能修复
1 parent cb27eb5 commit 290a085

4 files changed

Lines changed: 51 additions & 4 deletions

File tree

‎app/src-tauri/src/commands_settlement.rs‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,14 @@ pub fn execute_settlement(
9696
}
9797
}
9898
}
99-
// 核心提炼:组核心笔记(本地规则——笔记目录 + 碎片摘要,落组内)
100-
let core_note_id = create_core_note(&state_clone, group_id)?;
99+
// 核心提炼:组核心笔记(本地规则——笔记目录 + 碎片摘要,落组内)。
100+
// 审查修复(2026-08-22):无合并无归档的空转结算不产核心笔记——
101+
// 防反复点击结算刷出重复笔记(仪式留痕靠 settlements 记录即可)。
102+
let core_note_id = if merged > 0 || archived > 0 {
103+
create_core_note(&state_clone, group_id)?
104+
} else {
105+
None
106+
};
101107
// 留痕:结算记录 + 北极星埋点(组成③经历过结算)
102108
let stats = serde_json::json!({
103109
"merged": merged, "archived": archived, "coreNoteId": core_note_id,
@@ -144,9 +150,11 @@ fn settlement_plan_inner(state: &AppState, group_id: i64) -> Result<SettlementPl
144150
})
145151
.collect();
146152
let age_cutoff = now_secs - ARCHIVE_AGE_DAYS * 86_400;
153+
// 审查修复(2026-08-22):有卡绑定判定改单条 SQL 全集(替代逐碎片 N+1)
154+
let card_bound = state.db.fragment_ids_with_cards().map_err(|e| e.to_string())?;
147155
let mut archive_candidates = Vec::new();
148156
for f in &fragments {
149-
if f.created_at < age_cutoff && !state.db.fragment_has_card(f.id).map_err(|e| e.to_string())? {
157+
if f.created_at < age_cutoff && !card_bound.contains(&f.id) {
150158
archive_candidates.push(ArchiveCandidateView { id: f.id, text: f.text.clone() });
151159
}
152160
}

‎app/src-tauri/src/db_note_groups.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ impl Db {
144144

145145
/// 路由改判(REQ-198 修改即记忆):覆盖 kind/domain_tag,reason 追加来源,
146146
/// route_overridden 置 1——后续自动路由不得覆盖用户裁决。
147+
///
148+
/// @ai-context: 审查修复(2026-08-22):改判为非课程组时同步清空 series_key——
149+
/// 残留的系列键会让后续同系列会话经 find_group_by_series_key 误归入
150+
/// 已被改判的组(路由误判 ★★★★ 死法的改判侧漏洞)。
147151
pub fn override_group_route(
148152
&self,
149153
id: i64,
@@ -154,7 +158,9 @@ impl Db {
154158
self.with_conn(|conn| {
155159
let affected = conn.execute(
156160
"UPDATE note_groups SET kind = ?1, domain_tag = ?2, route_reason = ?3,
157-
route_overridden = 1, updated_at = ?4 WHERE id = ?5",
161+
route_overridden = 1,
162+
series_key = CASE WHEN ?1 = 'course' THEN series_key ELSE NULL END,
163+
updated_at = ?4 WHERE id = ?5",
158164
params![kind, domain_tag, reason, unix_seconds(), id],
159165
)?;
160166
Ok(affected > 0)

‎app/src-tauri/src/db_note_groups_tests.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ fn override_route_marks_and_updates() {
131131
assert!(fetched.route_reason.unwrap().contains("用户改判"));
132132
}
133133

134+
#[test]
135+
fn override_course_group_clears_series_key() {
136+
// Arrange:课程组带系列键(审查修复回归:改判后系列键不得残留,
137+
// 否则后续同系列会话经 find_group_by_series_key 误归入已改判的组)
138+
let db = mem_db();
139+
let mut course = group("零基础化妆");
140+
course.kind = "course".to_string();
141+
course.source = "series".to_string();
142+
course.series_key = Some("零基础化妆".to_string());
143+
let g = db.create_group(&course).expect("create");
144+
// Act:改判为主题组
145+
db.override_group_route(g.id, "topic", Some("beauty"), "用户改判").expect("override");
146+
// Assert:系列键清空,系列查找不再命中
147+
let fetched = db.get_group(g.id).expect("get").expect("exists");
148+
assert_eq!(fetched.series_key, None);
149+
assert!(db.find_group_by_series_key("零基础化妆").expect("find").is_none());
150+
}
151+
134152
#[test]
135153
fn move_note_between_groups() {
136154
// Arrange

‎app/src-tauri/src/db_settlements.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ impl Db {
3838
}
3939

4040
/// 碎片是否有闪卡绑定(归档候选判据:有卡碎片不进归档——学习循环资产)。
41+
/// 登记豁免 dead_code:计划构建已改走 fragment_ids_with_cards 批量口径,
42+
/// 单查接口留给后续单碎片操作面(单测消费中)。
43+
#[allow(dead_code)]
4144
pub fn fragment_has_card(&self, fragment_id: i64) -> Result<bool> {
4245
self.with_conn(|conn| {
4346
let count: i64 = conn.query_row(
@@ -48,6 +51,18 @@ impl Db {
4851
Ok(count > 0)
4952
})
5053
}
54+
55+
/// 有卡绑定的碎片 id 集合(审查修复 2026-08-22:结算计划批量判定替代
56+
/// 逐碎片 fragment_has_card 的 N+1 查询——单条 SQL 全集)。
57+
pub fn fragment_ids_with_cards(&self) -> Result<std::collections::HashSet<i64>> {
58+
self.with_conn(|conn| {
59+
let mut stmt =
60+
conn.prepare("SELECT DISTINCT fragment_id FROM flashcards WHERE fragment_id IS NOT NULL")?;
61+
let rows = stmt.query_map([], |r| r.get::<_, i64>(0))?;
62+
let set: std::collections::HashSet<i64> = rows.collect::<rusqlite::Result<Vec<_>>>()?.into_iter().collect();
63+
Ok(set)
64+
})
65+
}
5166
}
5267

5368
/// 单测独立文件。

0 commit comments

Comments
 (0)