refactor: 观测层双轴正交重构——spectra→observation、工具/API 收敛、安全韧性加固
将"以光谱为中心"的观测数据架构升级为 (Source × ProductType) 双轴正交模型,
光谱降级为与光变/测光/图像平级的产品类型之一;同步把分散的工具、API、缓存表
收敛为统一入口。新增 Gaia 光变曲线(EPOCH_PHOTOMETRY)支持。
【架构】services/spectra 整体替换为 services/observation(双轴正交)
- Source(LAMOST/Gaia/SDSS/DESI)× ProductType(Spectrum/LightCurve/Photometry/Image)
正交组合,新增源/产品类型为纯加法(OCP)
- ObservationFetcher trait + ObservationRegistry:每个有效组合实现一个 fetcher,
启动时注册;SDSS specobj/APOGEE 共用 key 按 subtype 二级路由
- cone 缓存逻辑模板方法化(trait 默认方法),消除各源 4 份重复代码
- 多文件 Artifact 模型:一个逻辑产物可含多文件(如 Gaia 光变 G/BP/RP 三波段各一 FITS)
- 统一编排 dispatch.rs:search(仅检索)/ download(检索+下载),支持坐标模式
(cone→选源→下载)与标识符模式(直按 ID 下载)双输入
【Agent 工具整合】26 → 24
- 新增 find_observation:跨源×跨产品×双模式统一观测下载,取代 find_spectrum
- catalog_operation 升级为 6 合 1(search/describe/query/cone/export/lookup),
取代独立的 query_vizier + cone_search
- citation_network + library_search 合并为 library.rs
【API 路由】
- 新增 /observation/{search,download,capabilities,list} 命名空间
- 移除 /catalog/{crossmatch,spectrum/download,spectrum/list}
- GET /observation/capabilities 暴露 registry 能力清单,前端动态渲染源/产品/版本
下拉(不再硬编码各源支持矩阵)
【数据库迁移】
- 新表 observation_cache:新增 product 列 + artifacts_json(多文件产物),无 TTL
(观测数据不可变,区别于 vizier_query_cache 的 7 天 TTL)
- 20260705140001:spectrum_cache 旧数据迁入 observation_cache,单文件→单元素 artifacts
【前端】
- 新 ObservationPanel(988 行):双视图(检索下载 / 缓存库),选项由 capabilities 动态生成
- 新 useObservation hook、ObservationResultCard、observation/constants、utils/apiError
【安全与韧性加固】
- sessions 锁 Mutex → RwLock(读多写少,降低争用)
- 新增 upload_rate_limiter;login_rate_limiter 容量保护(10000 上限,超限清最旧一半)
- bookmarklet API 密钥 SHA-1 → SHA-256;ADMIN_PASSWORD 长度上限 128
- *_TIMEOUT_SECS / EMBEDDING_DIM 非法值告警并回退默认;DB_POOL_SIZE 可配置(原硬编码 5)
- sqlite-vec 注册逻辑下沉至 utils::register_sqlite_vec_extension
This commit is contained in:
+5
-162
@@ -37,22 +37,14 @@ pub struct ConeSearchParams {
|
||||
pub ra: f64,
|
||||
/// Dec 坐标(度)
|
||||
pub dec: f64,
|
||||
/// 检索半径(度)
|
||||
/// 检索半径(度,0~20)
|
||||
pub radius: Option<f64>,
|
||||
/// 目标星表(必填,如 "I/355/gaiadr3")
|
||||
pub table: String,
|
||||
/// 最大返回行数(默认 50,上限 2000)
|
||||
pub max_records: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CrossMatchParams {
|
||||
pub ra: f64,
|
||||
pub dec: f64,
|
||||
pub radius: Option<f64>,
|
||||
/// 目标星表(必填,如 "I/355/gaiadr3")
|
||||
pub table: String,
|
||||
pub max_records: Option<i64>,
|
||||
/// 返回策略:"nearest"(按角距离排序,返回最近的)或 "all"(无序返回全部),默认 nearest
|
||||
pub strategy: Option<String>,
|
||||
}
|
||||
|
||||
// ── 响应封装 ──
|
||||
@@ -134,6 +126,7 @@ pub async fn cone_search(
|
||||
) -> ApiResult<Json<VizierQueryResult>> {
|
||||
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,
|
||||
@@ -143,6 +136,7 @@ pub async fn cone_search(
|
||||
radius,
|
||||
¶ms.table,
|
||||
max_records,
|
||||
nearest,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -156,154 +150,3 @@ pub async fn cone_search(
|
||||
})?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/catalog/crossmatch —— 交叉证认
|
||||
pub async fn cross_match(
|
||||
State(state): State<std::sync::Arc<AppState>>,
|
||||
Query(params): Query<CrossMatchParams>,
|
||||
) -> ApiResult<Json<VizierQueryResult>> {
|
||||
let radius = params.radius.unwrap_or(0.05);
|
||||
let max_records = clamp_max(params.max_records);
|
||||
|
||||
let result = crate::services::cds::vizier::cross_match(
|
||||
&state.db,
|
||||
&state.vizier,
|
||||
params.ra,
|
||||
params.dec,
|
||||
radius,
|
||||
¶ms.table,
|
||||
max_records,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("交叉证认失败: {}", e);
|
||||
let msg = e.to_string();
|
||||
if msg.contains("半径") || msg.contains("坐标") || msg.contains("非法字符") {
|
||||
AppError::bad_request(msg)
|
||||
} else {
|
||||
AppError::internal(format!("交叉证认失败: {}", msg))
|
||||
}
|
||||
})?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// 统一光谱下载(跨 LAMOST/Gaia/SDSS)
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SpectrumDownloadParams {
|
||||
/// 数据源:lamost / gaia / sdss
|
||||
pub survey: String,
|
||||
/// 坐标模式:ra/dec/radius + strategy
|
||||
pub ra: Option<f64>,
|
||||
pub dec: Option<f64>,
|
||||
pub radius: Option<f64>,
|
||||
/// 选源策略:nearest(默认)/ all
|
||||
pub strategy: Option<String>,
|
||||
/// 标识符模式:逗号分隔的源标识列表
|
||||
pub source_ids: Option<String>,
|
||||
/// 数据发布版本(可选):lamost=dr5..dr11, gaia=dr3, sdss=dr16..dr19
|
||||
pub release: Option<String>,
|
||||
/// 数据类型(可选):lamost=lrs/mrs, gaia=xp_continuous/xp_sampled/epoch_photometry/rvs
|
||||
pub data_type: Option<String>,
|
||||
pub force: Option<bool>,
|
||||
}
|
||||
|
||||
/// GET /api/catalog/spectrum/download —— 统一光谱下载
|
||||
///
|
||||
/// 两种模式(二选一):
|
||||
/// - 坐标模式:提供 ra + dec(可选 radius/strategy),自动 cone 检索并下载
|
||||
/// - 标识符模式:提供 source_ids(逗号分隔),直接按标识下载
|
||||
pub async fn spectrum_download(
|
||||
State(state): State<std::sync::Arc<AppState>>,
|
||||
Query(params): Query<SpectrumDownloadParams>,
|
||||
) -> ApiResult<Json<crate::services::spectra::DownloadBatch>> {
|
||||
use crate::services::spectra::{FindStrategy, SpectrumRequest, SpectrumSurvey};
|
||||
|
||||
let survey = match params.survey.to_lowercase().as_str() {
|
||||
"lamost" => SpectrumSurvey::Lamost,
|
||||
"gaia" => SpectrumSurvey::Gaia,
|
||||
"sdss" => SpectrumSurvey::Sdss,
|
||||
"desi" => SpectrumSurvey::Desi,
|
||||
other => {
|
||||
return Err(AppError::bad_request(format!(
|
||||
"不支持的 survey '{}',可选: lamost / gaia / sdss / desi",
|
||||
other
|
||||
)))
|
||||
}
|
||||
};
|
||||
let force = params.force.unwrap_or(false);
|
||||
|
||||
let request = if let Some(ids_str) = params.source_ids.as_deref() {
|
||||
// 标识符模式
|
||||
let source_ids: Vec<String> = ids_str
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if source_ids.is_empty() {
|
||||
return Err(AppError::bad_request("source_ids 不能为空"));
|
||||
}
|
||||
SpectrumRequest::ByIdentifier {
|
||||
survey,
|
||||
source_ids,
|
||||
release: params.release.clone(),
|
||||
data_type: params.data_type.clone(),
|
||||
}
|
||||
} else {
|
||||
// 坐标模式
|
||||
let ra = params.ra.ok_or_else(|| {
|
||||
AppError::bad_request("坐标模式缺少 ra(或改用 source_ids 标识符模式)")
|
||||
})?;
|
||||
let dec = params.dec.ok_or_else(|| {
|
||||
AppError::bad_request("坐标模式缺少 dec(或改用 source_ids 标识符模式)")
|
||||
})?;
|
||||
let radius = params.radius.unwrap_or(0.1);
|
||||
let strategy = match params.strategy.as_deref().unwrap_or("nearest") {
|
||||
"nearest" => FindStrategy::Nearest,
|
||||
"all" => FindStrategy::All,
|
||||
other => {
|
||||
return Err(AppError::bad_request(format!(
|
||||
"不支持的 strategy '{}',可选: nearest / all",
|
||||
other
|
||||
)))
|
||||
}
|
||||
};
|
||||
SpectrumRequest::ByCoordinates {
|
||||
survey,
|
||||
ra,
|
||||
dec,
|
||||
radius_deg: radius,
|
||||
strategy,
|
||||
release: params.release.clone(),
|
||||
data_type: params.data_type.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
let result = crate::services::spectra::download_spectrum(&state, &request, force)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("光谱下载失败: {}", e);
|
||||
let msg = e.to_string();
|
||||
if msg.contains("半径") || msg.contains("坐标") || msg.contains("格式") {
|
||||
AppError::bad_request(msg)
|
||||
} else {
|
||||
AppError::internal(format!("光谱下载失败: {}", msg))
|
||||
}
|
||||
})?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/catalog/spectrum/list —— 列出全部数据源的已缓存光谱
|
||||
pub async fn spectrum_list(
|
||||
State(state): State<std::sync::Arc<AppState>>,
|
||||
) -> ApiResult<Json<Vec<crate::services::spectra::common::SpectrumCacheRow>>> {
|
||||
let rows = crate::services::spectra::common::list_all_cached(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("光谱列表查询失败: {}", e);
|
||||
AppError::internal(format!("光谱列表查询失败: {}", e))
|
||||
})?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user