dev-manager-tauri/src-tauri/src/commands/user_todos.rs
2026-09-21 12:52:37 +09:00

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);
}
}