use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct UserTodo { pub id: i64, pub project_id: String, pub content: String, pub done: bool, pub sort_order: i64, pub created_at: String, } // ── 业务函数(依赖注入,可单元测试)──────────────────────────────────────────── pub fn get_todos(conn: &Connection, project_id: &str) -> Vec { let mut stmt = match conn.prepare( "SELECT id, project_id, content, done, sort_order, created_at FROM user_project_todos WHERE project_id = ?1 ORDER BY sort_order ASC, id ASC", ) { Ok(s) => s, Err(_) => return vec![], }; stmt.query_map(params![project_id], |row| { Ok(UserTodo { id: row.get(0)?, project_id: row.get(1)?, content: row.get(2)?, done: row.get::<_, i64>(3)? != 0, sort_order: row.get(4)?, created_at: row.get(5)?, }) }) .map(|iter| iter.filter_map(|r| r.ok()).collect()) .unwrap_or_default() } pub fn add_todo(conn: &Connection, project_id: &str, content: &str) -> Result { let content = content.trim(); if content.is_empty() { return Err("内容不能为空".to_string()); } // sort_order = 当前最大值 + 1 let max_order: i64 = conn .query_row( "SELECT COALESCE(MAX(sort_order), -1) FROM user_project_todos WHERE project_id = ?1", params![project_id], |row| row.get(0), ) .unwrap_or(-1); conn.execute( "INSERT INTO user_project_todos (project_id, content, sort_order) VALUES (?1, ?2, ?3)", params![project_id, content, max_order + 1], ) .map_err(|e| e.to_string())?; let id = conn.last_insert_rowid(); conn.query_row( "SELECT id, project_id, content, done, sort_order, created_at FROM user_project_todos WHERE id = ?1", params![id], |row| { Ok(UserTodo { id: row.get(0)?, project_id: row.get(1)?, content: row.get(2)?, done: row.get::<_, i64>(3)? != 0, sort_order: row.get(4)?, created_at: row.get(5)?, }) }, ) .map_err(|e| e.to_string()) } pub fn toggle_todo(conn: &Connection, id: i64) -> Result<(), String> { let rows = conn .execute( "UPDATE user_project_todos SET done = CASE WHEN done = 0 THEN 1 ELSE 0 END WHERE id = ?1", params![id], ) .map_err(|e| e.to_string())?; if rows == 0 { return Err(format!("todo {} 不存在", id)); } Ok(()) } pub fn delete_todo(conn: &Connection, id: i64) -> Result<(), String> { conn.execute("DELETE FROM user_project_todos WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } // ── Tauri command 薄包装层(仅负责获取连接)──────────────────────────────────── #[tauri::command] pub fn get_user_todos(project_id: String) -> Result, String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; Ok(get_todos(&conn, &project_id)) } #[tauri::command] pub fn add_user_todo(project_id: String, content: String) -> Result { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; add_todo(&conn, &project_id, &content) } #[tauri::command] pub fn toggle_user_todo(id: i64) -> Result<(), String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; toggle_todo(&conn, id) } #[tauri::command] pub fn delete_user_todo(id: i64) -> Result<(), String> { let conn = crate::db::pool().get().map_err(|e| e.to_string())?; delete_todo(&conn, id) } // ── 单元测试 ─────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use rusqlite::Connection; fn setup() -> Connection { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch( "CREATE TABLE user_project_todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, content TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (datetime('now', 'localtime')) );", ) .unwrap(); conn } #[test] fn test_add_and_get() { let conn = setup(); add_todo(&conn, "proj-1", "买牛奶").unwrap(); add_todo(&conn, "proj-1", "写测试").unwrap(); let todos = get_todos(&conn, "proj-1"); assert_eq!(todos.len(), 2); assert_eq!(todos[0].content, "买牛奶"); assert!(!todos[0].done); } #[test] fn test_empty_content_rejected() { let conn = setup(); assert!(add_todo(&conn, "proj-1", " ").is_err()); } #[test] fn test_toggle() { let conn = setup(); let todo = add_todo(&conn, "proj-1", "测试条目").unwrap(); toggle_todo(&conn, todo.id).unwrap(); let todos = get_todos(&conn, "proj-1"); assert!(todos[0].done); toggle_todo(&conn, todo.id).unwrap(); let todos = get_todos(&conn, "proj-1"); assert!(!todos[0].done); } #[test] fn test_delete() { let conn = setup(); let todo = add_todo(&conn, "proj-1", "待删除").unwrap(); delete_todo(&conn, todo.id).unwrap(); assert!(get_todos(&conn, "proj-1").is_empty()); } #[test] fn test_get_empty_returns_empty() { let conn = setup(); assert!(get_todos(&conn, "不存在的项目").is_empty()); } #[test] fn test_sort_order_increments() { let conn = setup(); add_todo(&conn, "proj-1", "第一").unwrap(); add_todo(&conn, "proj-1", "第二").unwrap(); let todos = get_todos(&conn, "proj-1"); assert!(todos[0].sort_order < todos[1].sort_order); } }