// src/api/targets.rs use axum::{ extract::{Query, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use super::AppState; use crate::services::target::{query_target_cached, TargetInfo}; #[derive(Deserialize)] pub struct TargetQueryParams { pub object_name: String, } #[derive(Deserialize)] pub struct AssociateTargetRequest { pub bibcode: String, pub object_name: String, } #[derive(Deserialize)] pub struct TargetListParams { pub bibcode: String, } #[derive(Serialize)] pub struct AssociateResponse { pub status: String, pub target: TargetInfo, } // ── GET /api/target/query ── pub async fn query_target( State(state): State>, Query(params): Query, ) -> Result, (StatusCode, String)> { let client = reqwest::Client::new(); let info = query_target_cached(&state.db, ¶ms.object_name, None, &client) .await .map_err(|e| { tracing::error!("查询天体信息失败 ({}): {}", params.object_name, e); ( StatusCode::INTERNAL_SERVER_ERROR, format!("查询天体信息失败: {}", e), ) })?; Ok(Json(info)) } // ── POST /api/target/associate ── pub async fn associate_target( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let client = reqwest::Client::new(); // 查询并将结果缓存/关联到对应文献 let info = query_target_cached(&state.db, &req.object_name, Some(&req.bibcode), &client) .await .map_err(|e| { tracing::error!( "手动关联天体失败 ({} -> {}): {}", req.object_name, req.bibcode, e ); ( StatusCode::INTERNAL_SERVER_ERROR, format!("手动关联天体失败: {}", e), ) })?; Ok(Json(AssociateResponse { status: "success".to_string(), target: info, })) } // ── GET /api/target/list ── pub async fn list_targets( State(state): State>, Query(params): Query, ) -> Result>, (StatusCode, String)> { let rows = sqlx::query_as::<_, (String, Option, Option, Option, Option, Option, Option)>( "SELECT target_name, ra, dec, parallax, spectral_type, v_magnitude, aliases FROM paper_targets WHERE bibcode = ? ORDER BY target_name" ) .bind(¶ms.bibcode) .fetch_all(&state.db) .await .map_err(|e| { tracing::error!("获取文献天体关联列表失败 ({}): {}", params.bibcode, e); (StatusCode::INTERNAL_SERVER_ERROR, format!("获取文献天体关联列表失败: {}", e)) })?; let targets: Vec = rows .into_iter() .map( |(name, ra, dec, parallax, spectral_type, v_magnitude, aliases_json)| { let aliases: Vec = aliases_json .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); TargetInfo { target_name: name, ra, dec, parallax, spectral_type, v_magnitude, aliases, } }, ) .collect(); Ok(Json(targets)) } // ── POST /api/target/extract ── #[derive(Deserialize)] pub struct ExtractTargetsRequest { pub bibcode: String, } #[derive(Serialize)] pub struct ExtractTargetsResponse { pub targets: Vec, } pub async fn extract_paper_targets( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { tracing::info!("接收到文献天体提取与识别指令: {}", req.bibcode); let (_, _, md_opt, _) = crate::api::helpers::check_paper_paths_in_db( &state.db, &state.config.library_dir, &req.bibcode, ) .await .map_err(|e| (StatusCode::NOT_FOUND, format!("获取文献路径失败: {}", e)))? .ok_or((StatusCode::NOT_FOUND, "该文献未注册在数据库中".to_string()))?; let md_rel = md_opt.ok_or(( StatusCode::BAD_REQUEST, "文献尚未解析为 Markdown,请先执行解析".to_string(), ))?; let md_abs = state.config.library_dir.join(&md_rel); if !md_abs.exists() { return Err(( StatusCode::NOT_FOUND, "文献 Markdown 文件未找到,请重新解析".to_string(), )); } let markdown_content = std::fs::read_to_string(&md_abs).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, format!("读取 Markdown 文件失败: {}", e), ) })?; // 重新识别前先清除该文献已有的天体关联记录,确保陈旧和错误绑定的天体得到重置与刷新 if let Err(e) = sqlx::query("DELETE FROM paper_targets WHERE bibcode = ?") .bind(&req.bibcode) .execute(&state.db) .await { tracing::error!("清除文献 {} 的旧天体关联失败: {}", req.bibcode, e); } let client = reqwest::Client::new(); let targets = crate::services::target::extract_and_cache_targets( &state.db, &markdown_content, &req.bibcode, &client, ) .await; Ok(Json(ExtractTargetsResponse { targets })) }