@@ -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}
0 commit comments