Skip to content

Commit fe8c919

Browse files
committed
fix(fragments): promote_fragment_to_note 改显式事务——修伪事务半态风险(审查)
- 原 with_conn 只提供 &Connection 无法开事务,rusqlite 默认 autocommit: 建笔记/删碎片各自提交,④ 删除失败会留'笔记已建/碎片未删'半态(重试 将重复建笔记);改为直接锁 + conn.transaction()+commit(与 versioned_save 既有模式一致),任一步失败整链回滚 - feature_flags_tests 清理 unused mut(结构体初始化直给值)
1 parent 86b627e commit fe8c919

2 files changed

Lines changed: 70 additions & 63 deletions

File tree

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

Lines changed: 69 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -176,71 +176,78 @@ impl Db {
176176
group_id: Option<i64>,
177177
) -> Result<Note> {
178178
let now = unix_seconds();
179-
self.with_conn(|conn| {
180-
// ① 读碎片(事务内读取——存在性校验与删除同锁,防竞态双升)
181-
let fragment = {
182-
let mut stmt = conn.prepare(&format!(
183-
"SELECT {} FROM fragments WHERE id = ?1",
184-
FRAGMENT_COLUMNS
185-
))?;
186-
let mut rows = stmt.query_map(params![fragment_id], row_to_fragment)?;
187-
match rows.next() {
188-
Some(Ok(f)) => f,
189-
Some(Err(e)) => return Err(e.into()),
190-
None => {
191-
return Err(crate::error::AppError::Db(format!(
192-
"碎片不存在: {}",
193-
fragment_id
194-
)))
195-
}
196-
}
197-
};
198-
// ② 建笔记(正文先落碎片文本;图片在 ③ 补写)
199-
conn.execute(
200-
"INSERT INTO notes (title, content, source, session_id, rule_version, purify_stats, tags, properties, group_id, created_at, updated_at)
201-
VALUES (?1, ?2, 'manual', NULL, NULL, NULL, '[]', NULL, ?3, ?4, ?4)",
202-
params![title, fragment.text, group_id, now],
203-
)?;
204-
let note_id = conn.last_insert_rowid();
205-
// ③ 图片搬运 fragments/ → notes-images/{note_id}/(失败降级纯文本)
206-
let mut content = fragment.text.clone();
207-
if let Some(rel) = fragment.image_path.as_deref() {
208-
match copy_fragment_image(data_dir, note_id, rel) {
209-
Some(img_ref) => {
210-
content = format!("{}\n\n![]({})\n", content, img_ref);
211-
}
212-
None => {
213-
eprintln!(
214-
"[db_fragments] 碎片 {} 图片搬运失败(降级纯文本笔记): {}",
215-
fragment_id, rel
216-
);
217-
}
179+
// 显式事务(审查修复):with_conn 只给 &Connection 无法开事务,而
180+
// rusqlite 默认 autocommit——多语句各自提交,④ 失败会留下"笔记已建/
181+
// 碎片未删"半态。与 versioned_save 同模式:直接锁 + conn.transaction()。
182+
let mut conn = self
183+
.conn
184+
.lock()
185+
.unwrap_or_else(|poisoned| poisoned.into_inner());
186+
let tx = conn.transaction()?;
187+
// ① 读碎片(事务内读取——存在性校验与删除同锁,防竞态双升)
188+
let fragment = {
189+
let mut stmt = tx.prepare(&format!(
190+
"SELECT {} FROM fragments WHERE id = ?1",
191+
FRAGMENT_COLUMNS
192+
))?;
193+
let mut rows = stmt.query_map(params![fragment_id], row_to_fragment)?;
194+
match rows.next() {
195+
Some(Ok(f)) => f,
196+
Some(Err(e)) => return Err(e.into()),
197+
None => {
198+
return Err(crate::error::AppError::Db(format!(
199+
"碎片不存在: {}",
200+
fragment_id
201+
)))
218202
}
219203
}
220-
if content != fragment.text {
221-
conn.execute(
222-
"UPDATE notes SET content = ?1 WHERE id = ?2",
223-
params![content, note_id],
224-
)?;
204+
};
205+
// ② 建笔记(正文先落碎片文本;图片在 ③ 补写)
206+
tx.execute(
207+
"INSERT INTO notes (title, content, source, session_id, rule_version, purify_stats, tags, properties, group_id, created_at, updated_at)
208+
VALUES (?1, ?2, 'manual', NULL, NULL, NULL, '[]', NULL, ?3, ?4, ?4)",
209+
params![title, fragment.text, group_id, now],
210+
)?;
211+
let note_id = tx.last_insert_rowid();
212+
// ③ 图片搬运 fragments/ → notes-images/{note_id}/(失败降级纯文本)
213+
let mut content = fragment.text.clone();
214+
if let Some(rel) = fragment.image_path.as_deref() {
215+
match copy_fragment_image(data_dir, note_id, rel) {
216+
Some(img_ref) => {
217+
content = format!("{}\n\n![]({})\n", content, img_ref);
218+
}
219+
None => {
220+
eprintln!(
221+
"[db_fragments] 碎片 {} 图片搬运失败(降级纯文本笔记): {}",
222+
fragment_id, rel
223+
);
224+
}
225225
}
226-
// ④ 删碎片(绑定卡自动解绑保留)
227-
conn.execute("DELETE FROM fragments WHERE id = ?1", params![fragment_id])?;
228-
// ⑤ 组装返回(与库内一致)
229-
Ok(Note {
230-
id: note_id,
231-
title: title.to_string(),
232-
content,
233-
source: "manual".to_string(),
234-
session_id: None,
235-
rule_version: None,
236-
purify_stats: None,
237-
tags: "[]".to_string(),
238-
properties: None,
239-
pin: 0,
240-
group_id,
241-
created_at: now,
242-
updated_at: now,
243-
})
226+
}
227+
if content != fragment.text {
228+
tx.execute(
229+
"UPDATE notes SET content = ?1 WHERE id = ?2",
230+
params![content, note_id],
231+
)?;
232+
}
233+
// ④ 删碎片(绑定卡自动解绑保留)——与 ② 同事务:任一步失败整链回滚
234+
tx.execute("DELETE FROM fragments WHERE id = ?1", params![fragment_id])?;
235+
tx.commit()?;
236+
// ⑤ 组装返回(与库内一致)
237+
Ok(Note {
238+
id: note_id,
239+
title: title.to_string(),
240+
content,
241+
source: "manual".to_string(),
242+
session_id: None,
243+
rule_version: None,
244+
purify_stats: None,
245+
tags: "[]".to_string(),
246+
properties: None,
247+
pin: 0,
248+
group_id,
249+
created_at: now,
250+
updated_at: now,
244251
})
245252
}
246253
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn save_and_reload_persists() {
4444
let dir = std::env::temp_dir().join("ff_save_test");
4545
let _ = std::fs::create_dir_all(&dir);
4646
let path = dir.join("flags.json");
47-
let mut flags = FeatureFlags { feed_capture: true };
47+
let flags = FeatureFlags { feed_capture: true };
4848
// Act
4949
flags.save(&path).expect("save");
5050
let loaded = FeatureFlags::load(&path);

0 commit comments

Comments
 (0)