// src/ads.rs use serde::{Deserialize, Serialize}; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use tracing::{info, error}; // 原始 ADS API 返回的数据文档结构 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdsPaperDoc { pub bibcode: String, pub title: Option>, pub author: Option>, pub year: Option, #[serde(rename = "pub")] pub pub_journal: Option, pub keyword: Option>, pub abstract_text: Option, pub doi: Option>, pub citation_count: Option, pub reference_count: Option, pub reference: Option>, pub citation: Option>, pub identifier: Option>, } #[derive(Debug, Deserialize)] pub struct AdsResponseDocs { pub docs: Vec, } #[derive(Debug, Deserialize)] pub struct AdsSearchResponse { pub response: AdsResponseDocs, } #[derive(Debug, Deserialize)] pub struct AdsExportResponse { pub export: String, } // ADS API 服务客户端 #[derive(Clone)] pub struct AdsClient { api_key: String, client: reqwest::Client, } impl AdsClient { pub fn new(api_key: String) -> Self { AdsClient { api_key, client: reqwest::Client::new(), } } // 拼装鉴权 Header fn headers(&self) -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}", self.api_key)).unwrap_or_else(|_| HeaderValue::from_static("")), ); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers } // 调用 ADS 检索接口获取文献元数据列表,支持分页与排序 pub async fn search(&self, query: &str, start: i32, rows: i32, sort: &str) -> anyhow::Result> { let url = "https://api.adsabs.harvard.edu/v1/search/query"; let translated = crate::services::query_parser::to_ads_query(query); // fl 声明返回字段,包括 reference 和 citation 引用关系数组及 identifier let fl = "bibcode,title,author,year,pub,keyword,abstract,doi,citation_count,reference_count,reference,citation,identifier"; let ads_sort = match sort { "date_desc" => "date desc", "date_asc" => "date asc", "citations_desc" => "citation_count desc", _ => "score desc", }; info!("正在发送检索请求到 ADS 平台: 原始词='{}', 翻译词='{}', 起始={}, 数量={}, 排序='{}'", query, translated, start, rows, ads_sort); let start_str = start.to_string(); let rows_str = rows.to_string(); let response = self.client .get(url) .headers(self.headers()) .query(&[ ("q", translated.as_str()), ("start", start_str.as_str()), ("rows", rows_str.as_str()), ("fl", fl), ("sort", ads_sort), ]) .send() .await?; if !response.status().is_success() { let status = response.status(); let err_body = response.text().await.unwrap_or_default(); error!("ADS 检索请求失败: 状态码={}, 返回错误={}", status, err_body); return Err(anyhow::anyhow!("ADS API 接口返回错误码: {}", status)); } let raw_res: RawSearchResponse = response.json().await?; let docs = raw_res.response.docs.into_iter().map(|d| { AdsPaperDoc { bibcode: d.bibcode, title: d.title, author: d.author, year: d.year, pub_journal: d.pub_journal, keyword: d.keyword, abstract_text: d.abstract_field, doi: d.doi, citation_count: d.citation_count, reference_count: d.reference_count, reference: d.reference, citation: d.citation, identifier: d.identifier, } }).collect(); Ok(docs) } // 调用 ADS Export 接口导出 BibTeX 文本内容 pub async fn export_bibtex(&self, bibcodes: Vec) -> anyhow::Result { let url = "https://api.adsabs.harvard.edu/v1/export/bibtex"; info!("正在向 ADS 请求导出 {} 篇文献的 BibTeX 数据", bibcodes.len()); let payload = serde_json::json!({ "bibcode": bibcodes }); let response = self.client .post(url) .headers(self.headers()) .json(&payload) .send() .await?; if !response.status().is_success() { let status = response.status(); let err_body = response.text().await.unwrap_or_default(); error!("ADS 导出 BibTeX 失败: 状态码={}, 返回信息={}", status, err_body); return Err(anyhow::anyhow!("ADS 导出接口返回错误码: {}", status)); } let res_data: AdsExportResponse = response.json().await?; Ok(res_data.export) } // 获取某个查询词在 ADS 的匹配文献总量 pub async fn get_total_count(&self, query: &str) -> anyhow::Result { let url = "https://api.adsabs.harvard.edu/v1/search/query"; let translated = crate::services::query_parser::to_ads_query(query); info!("正在向 ADS 查询匹配的总文献数, 原始词: '{}', 翻译词: '{}'", query, translated); let response = self.client .get(url) .headers(self.headers()) .query(&[("q", translated.as_str()), ("rows", "0")]) .send() .await?; if !response.status().is_success() { let status = response.status(); return Err(anyhow::anyhow!("ADS API 接口返回错误码: {}", status)); } #[derive(Deserialize)] struct SimpleResponse { response: SimpleDocs, } #[derive(Deserialize)] struct SimpleDocs { #[serde(rename = "numFound")] num_found: i32, } let raw: SimpleResponse = response.json().await?; Ok(raw.response.num_found) } } // 内部反序列化辅助结构,防止由于 abstract/pub 关键字冲突导致编译失败 #[derive(Debug, Deserialize)] struct RawDoc { bibcode: String, title: Option>, author: Option>, year: Option, #[serde(rename = "pub")] pub_journal: Option, keyword: Option>, #[serde(rename = "abstract")] abstract_field: Option, doi: Option>, citation_count: Option, reference_count: Option, reference: Option>, citation: Option>, identifier: Option>, } #[derive(Debug, Deserialize)] struct RawSearchResponse { response: RawDocs, } #[derive(Debug, Deserialize)] struct RawDocs { docs: Vec, }