// src/api/catalog.rs // // 天文星表查询 HTTP 处理器 —— VizieR TAP + Cone Search // 对齐 targets.rs 的 handler 范式:State + Query/Json 参数 → service 调用 → ApiResult> use axum::extract::{Query, State}; use axum::Json; use serde::{Deserialize, Serialize}; use super::error::{ApiResult, AppError}; use super::AppState; use crate::clients::cds::vizier::VizierQueryResult; use crate::services::analysis::hr_diagram::{build_hr_diagram, HrDiagramParams, HrDiagramResponse}; // ── 请求参数 ── #[derive(Debug, Deserialize)] pub struct VizierQueryParams { /// 自由 ADQL 查询语句 pub adql: String, /// 最大返回行数(默认 50,上限 2000) pub max_records: Option, } #[derive(Debug, Deserialize)] pub struct VizierTableParams { /// VizieR 表名,如 "I/355/gaiadr3" pub table: String, /// 列名(逗号分隔,为空时取 *) pub columns: Option, /// 最大返回行数(默认 50,上限 2000) pub limit: Option, } #[derive(Debug, Deserialize)] pub struct ConeSearchParams { /// RA 坐标(度) pub ra: f64, /// Dec 坐标(度) pub dec: f64, /// 检索半径(度,0~20) pub radius: Option, /// 目标星表(必填,如 "I/355/gaiadr3") pub table: String, /// 最大返回行数(默认 50,上限 2000) pub max_records: Option, /// 返回策略:"nearest"(按角距离排序,返回最近的)或 "all"(无序返回全部),默认 nearest pub strategy: Option, } // ── 响应封装 ── #[derive(Debug, Serialize)] pub struct CachedResult { #[serde(flatten)] pub result: VizierQueryResult, /// 是否来自缓存 pub from_cache: Option, } const DEFAULT_MAX: i64 = 50; const MAX_LIMIT: i64 = 2000; fn clamp_max(v: Option) -> i64 { v.unwrap_or(DEFAULT_MAX).clamp(1, MAX_LIMIT) } // ── 处理器 ── /// GET /api/catalog/vizier —— 自由 ADQL 查询 pub async fn vizier_query( State(state): State>, Query(params): Query, ) -> ApiResult> { let max_records = clamp_max(params.max_records); let result = crate::services::cds::vizier::query_adql_cached( &state.db, &state.sources.vizier, ¶ms.adql, max_records, ) .await .map_err(|e| { tracing::error!("VizieR 查询失败: {}", e); AppError::internal(format!("VizieR 查询失败: {}", e)) })?; Ok(Json(result)) } /// GET /api/catalog/vizier/table —— 按表名便捷查询 pub async fn vizier_table( State(state): State>, Query(params): Query, ) -> ApiResult> { let limit = clamp_max(params.limit); let columns: Vec = params .columns .as_deref() .map(|c| c.split(',').map(|s| s.trim().to_string()).collect()) .unwrap_or_default(); let result = crate::services::cds::vizier::query_table( &state.db, &state.sources.vizier, ¶ms.table, &columns, limit, ) .await?; Ok(Json(result)) } /// GET /api/catalog/cone —— 锥形检索 pub async fn cone_search( State(state): State>, Query(params): Query, ) -> ApiResult> { let radius = params.radius.unwrap_or(0.1); let max_records = clamp_max(params.max_records); let nearest = params.strategy.as_deref() != Some("all"); let result = crate::services::cds::vizier::cone_search( &state.db, &state.sources.vizier, params.ra, params.dec, radius, ¶ms.table, max_records, nearest, ) .await?; Ok(Json(result)) } /// GET /api/analysis/hr-diagram —— 赫罗图构建 pub async fn hr_diagram( State(state): State>, Query(params): Query, ) -> ApiResult> { let response = build_hr_diagram(&state.sources.gaia, ¶ms) .await .map_err(|e| AppError::internal(format!("构建赫罗图失败: {}", e)))?; Ok(Json(response)) }