use axum::{ extract::{Path, Query}, http::StatusCode, response::Json, routing::{get, post}, Router, }; use serde::{Deserialize, Serialize}; use crate::core::db::{Database, PostgresDb}; use crate::core::person_identity::{ BindIdentityRequest, Identity, MergeIdentitiesRequest, UnbindIdentityRequest, }; #[derive(Debug, Clone, Serialize)] pub struct ApiResponse { pub success: bool, pub message: String, pub data: Option, } // ============================================================================ // API Handlers // ============================================================================ async fn get_db() -> Result)> { PostgresDb::init().await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("DB init failed: {}", e) })), ) }) } /// 獲取 Identity (人物) 列表 pub async fn list_identities( Query(params): Query, ) -> Result>>, (StatusCode, Json)> { let db = get_db().await?; let limit = params.limit.unwrap_or(100); let offset = params.offset.unwrap_or(0); let search = params.search.unwrap_or_default(); let identities = db .list_identities(&search, limit, offset) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!("Found {} identities", identities.len()), data: Some(identities), })) } /// V4.0 直接綁定:face_detections.identity_id = identities.id pub async fn bind_identity( Path(identity_uuid): Path, Json(req): Json, ) -> Result>, (StatusCode, Json)> { let table = crate::core::db::schema::table_name("face_detections"); let id_table = crate::core::db::schema::table_name("identities"); let db = sqlx::PgPool::connect(&crate::core::config::DATABASE_URL) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; // Get identity_id from identity_uuid let identity_row: Option<(i32, String)> = sqlx::query_as(&format!( "SELECT id, name FROM {} WHERE uuid = $1::uuid", id_table )) .bind(&identity_uuid) .fetch_optional(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; let (identity_id, name) = identity_row.ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": format!("Identity not found: {}", identity_uuid)})), ) })?; // Direct UPDATE face_detections.identity_id let result = sqlx::query(&format!( "UPDATE {} SET identity_id = $1 WHERE file_uuid = $2 AND face_id = $3", table )) .bind(identity_id) .bind(&req.file_uuid) .bind(&req.face_id) .execute(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; Ok(Json(ApiResponse { success: true, message: format!( "Bound face {} of {} to {}", req.face_id, req.file_uuid, name ), data: Some(serde_json::json!({"rows_affected": result.rows_affected()})), })) } /// V4.0 直接解綁:SET face_detections.identity_id = NULL pub async fn unbind_identity( Json(req): Json, ) -> Result>, (StatusCode, Json)> { let table = crate::core::db::schema::table_name("face_detections"); let db = sqlx::PgPool::connect(&crate::core::config::DATABASE_URL) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; let result = sqlx::query(&format!( "UPDATE {} SET identity_id = NULL WHERE file_uuid = $1 AND face_id = $2", table )) .bind(&req.file_uuid) .bind(&req.face_id) .execute(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; Ok(Json(ApiResponse { success: true, message: format!("Unbound face {} from {}", req.face_id, req.file_uuid), data: Some(serde_json::json!({"rows_affected": result.rows_affected()})), })) } /// V4.0 合併:將 identity A 合併入 identity B,A 被刪除 pub async fn merge_identities( Path(from_uuid): Path, Json(req): Json, ) -> Result>, (StatusCode, Json)> { let face_table = crate::core::db::schema::table_name("face_detections"); let id_table = crate::core::db::schema::table_name("identities"); let db = sqlx::PgPool::connect(&crate::core::config::DATABASE_URL) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; // Get IDs for both identities let from_row: Option<(i32, String)> = sqlx::query_as(&format!( "SELECT id, name FROM {} WHERE uuid = $1::uuid", id_table )) .bind(&from_uuid) .fetch_optional(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; let (from_id, from_name) = from_row.ok_or(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Source identity not found"})), ))?; let into_row: Option<(i32, String)> = sqlx::query_as(&format!( "SELECT id, name FROM {} WHERE uuid = $1::uuid", id_table )) .bind(&req.into_uuid) .fetch_optional(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; let (into_id, into_name) = into_row.ok_or(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Target identity not found"})), ))?; // Transfer all face bindings from source → target let updated = sqlx::query(&format!( "UPDATE {} SET identity_id = $1 WHERE identity_id = $2", face_table )) .bind(into_id) .bind(from_id) .execute(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; let keep = req.keep_history.unwrap_or(true); if keep { // Mark as merged, keep record sqlx::query(&format!( "UPDATE {} SET status = 'merged', metadata = COALESCE(metadata, '{{}}'::jsonb) || jsonb_build_object('merged_into', $1) WHERE id = $2", id_table )) .bind(&req.into_uuid).bind(from_id) .execute(&db).await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?; } else { // Delete source identity sqlx::query(&format!("DELETE FROM {} WHERE id = $1", id_table)) .bind(from_id) .execute(&db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})), ) })?; } Ok(Json(ApiResponse { success: true, message: format!( "Merged '{}' into '{}' ({} faces transferred, {})", from_name, into_name, updated.rows_affected(), if keep { "history kept" } else { "source deleted" } ), data: Some(serde_json::json!({"faces_transferred": updated.rows_affected()})), })) } // ============================================================================ // Router Setup // ============================================================================ // Router Setup // ============================================================================ #[derive(Debug, Deserialize)] pub struct ListIdentitiesParams { pub search: Option, pub limit: Option, pub offset: Option, } pub fn identity_binding_routes() -> Router { Router::new() .route("/api/v1/identity/:identity_uuid/bind", post(bind_identity)) .route( "/api/v1/identity/:identity_uuid/unbind", post(unbind_identity), ) .route( "/api/v1/identity/:from_uuid/mergeinto", post(merge_identities), ) }