199 lines
6.3 KiB
Rust
199 lines
6.3 KiB
Rust
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<UserTodo> {
|
|
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<UserTodo, String> {
|
|
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<Vec<UserTodo>, 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<UserTodo, String> {
|
|
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);
|
|
}
|
|
}
|