use crate::db::{log_event, pool}; use crate::mcp_inject; use crate::models::{ProductGroup, ProjectGroupMap}; use rusqlite::params; use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct GroupInput { pub id: Option, pub name: String, pub description: Option, pub space_id: Option, } #[tauri::command] pub fn get_groups(space_id: Option) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let result = match &space_id { Some(sid) => { let mut stmt = conn.prepare( "SELECT * FROM product_groups WHERE space_id = ?1 ORDER BY sort_order ASC, id" ).map_err(|e| e.to_string())?; let x = stmt.query_map(params![sid], ProductGroup::from_row) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; x } None => { let mut stmt = conn.prepare("SELECT * FROM product_groups ORDER BY sort_order ASC, id").map_err(|e| e.to_string())?; let x = stmt.query_map([], ProductGroup::from_row) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; x } }; Ok(result) } #[tauri::command] pub fn create_group(input: GroupInput) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; let id = input.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); conn.execute( "INSERT INTO product_groups (id, name, description, space_id) VALUES (?1, ?2, ?3, ?4)", params![id, input.name, input.description, input.space_id], ) .map_err(|e| { if e.to_string().contains("UNIQUE") { "Group id already exists".into() } else { e.to_string() } })?; Ok(()) } #[tauri::command] pub fn update_group(id: String, input: GroupInput) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "UPDATE product_groups SET name = COALESCE(?1, name), description = COALESCE(?2, description) WHERE id = ?3", params![input.name, input.description, id], ) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_group(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 先清除所有成员的 MCP 配置(DB cascade 删除之前) let member_ids: Vec = conn .prepare("SELECT project_id FROM project_group_map WHERE group_id = ?1") .map_err(|e| e.to_string())? .query_map(params![id], |row| row.get::<_, String>(0)) .map_err(|e| e.to_string())? .filter_map(|r| r.ok()) .collect(); for project_id in &member_ids { if let Err(e) = mcp_inject::remove_project_mcp(project_id, &id) { let ctx = serde_json::json!({"project_id": project_id, "group_id": id}).to_string(); log_event("mcp_inject", "warn", &format!("删除组时清除 MCP 配置失败: {e}"), Some(&ctx)); } } conn.execute("DELETE FROM product_groups WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } /// 按传入的 id 顺序批量更新 sort_order(index 即为新顺序) #[tauri::command] pub fn reorder_groups(ids: Vec) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; for (i, id) in ids.iter().enumerate() { conn.execute( "UPDATE product_groups SET sort_order = ?1 WHERE id = ?2", params![i as i32, id], ) .map_err(|e| e.to_string())?; } Ok(()) } #[tauri::command] pub fn get_group_maps() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn.prepare("SELECT * FROM project_group_map").map_err(|e| e.to_string())?; let result = stmt.query_map([], ProjectGroupMap::from_row) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string()); result } /// 确保独立项目有默认产品组。 /// 若项目已属于某产品组,直接返回;否则创建 "{project_name} (独立项目)" 组,加入成员并注入 MCP。 pub fn ensure_standalone_group(project_id: &str) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 已在某个组内,跳过 let already_in_group: bool = conn .query_row( "SELECT COUNT(*) FROM project_group_map WHERE project_id = ?1", params![project_id], |row| row.get::<_, i64>(0), ) .unwrap_or(0) > 0; if already_in_group { return Ok(()); } // 查项目显示名和所属空间 let (project_name, space_id): (String, Option) = conn .query_row( "SELECT pp.name, pw.space_id FROM project_workspaces pw JOIN project_profiles pp ON pp.id = pw.profile_id WHERE pw.id = ?1", params![project_id], |row| Ok((row.get(0)?, row.get(1)?)), ) .map_err(|_| format!("项目 {project_id} 不存在或未关联 profile"))?; let group_id = uuid::Uuid::new_v4().to_string(); let group_name = format!("{project_name} (独立项目)"); conn.execute( "INSERT INTO product_groups (id, name, space_id) VALUES (?1, ?2, ?3)", params![group_id, group_name, space_id], ) .map_err(|e| e.to_string())?; conn.execute( "INSERT OR REPLACE INTO project_group_map (project_id, group_id) VALUES (?1, ?2)", params![project_id, group_id], ) .map_err(|e| e.to_string())?; if let Err(e) = mcp_inject::inject_project_mcp(project_id, &group_id) { let ctx = serde_json::json!({"project_id": project_id, "group_id": &group_id}).to_string(); log_event("mcp_inject", "warn", &format!("独立项目 MCP 注入失败: {e}"), Some(&ctx)); } Ok(()) } #[tauri::command] pub fn add_group_member(project_id: String, group_id: String, role: Option) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "INSERT OR REPLACE INTO project_group_map (project_id, group_id, role) VALUES (?1, ?2, ?3)", params![project_id, group_id, role], ) .map_err(|e| e.to_string())?; // DB 写入成功后注入 MCP 配置(非致命,注入失败不影响成员关系) let inject_warning = match mcp_inject::inject_project_mcp(&project_id, &group_id) { Ok(()) => None, Err(e) => { let ctx = serde_json::json!({"project_id": &project_id, "group_id": &group_id}).to_string(); log_event("mcp_inject", "error", &format!("注入失败: {e}"), Some(&ctx)); Some(e) } }; Ok(inject_warning) } #[tauri::command] pub fn remove_group_member(project_id: String, group_id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 先清除 MCP 配置(需要在 DB 记录删除前查询项目路径) if let Err(e) = mcp_inject::remove_project_mcp(&project_id, &group_id) { let ctx = serde_json::json!({"project_id": &project_id, "group_id": &group_id}).to_string(); log_event("mcp_inject", "warn", &format!("清除 MCP 配置失败: {e}"), Some(&ctx)); } conn.execute( "DELETE FROM project_group_map WHERE project_id = ?1 AND group_id = ?2", params![project_id, group_id], ) .map_err(|e| e.to_string())?; Ok(()) }