AstroResearch/src/clients/ads.rs
Asfmq e13fa2ad40 refactor!: 模块化拆分 src 结构,新增批量同步服务、查询解析器及前端分页/高级检索功能
- src/ 按 clients/services/api 分层,Config 提升至 crate 根
- 新增 batch_sync.rs(双源并行收割)、query_parser.rs(多平台检索式转换)
- build.rs 自动触发前端 npm install & build
- SearchPanel 支持分页/排序/每页条数/高级检索构建器,前端加入搜索缓存
- 新增 SyncPanel 替换 SettingsPanel;新增 live_search 集成测试
2026-06-09 10:29:24 +08:00

218 lines
7.0 KiB
Rust

// 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<Vec<String>>,
pub author: Option<Vec<String>>,
pub year: Option<String>,
#[serde(rename = "pub")]
pub pub_journal: Option<String>,
pub keyword: Option<Vec<String>>,
pub abstract_text: Option<String>,
pub doi: Option<Vec<String>>,
pub citation_count: Option<i32>,
pub reference_count: Option<i32>,
pub reference: Option<Vec<String>>,
pub citation: Option<Vec<String>>,
pub identifier: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct AdsResponseDocs {
pub docs: Vec<AdsPaperDoc>,
}
#[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<Vec<AdsPaperDoc>> {
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<String>) -> anyhow::Result<String> {
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<i32> {
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<Vec<String>>,
author: Option<Vec<String>>,
year: Option<String>,
#[serde(rename = "pub")]
pub_journal: Option<String>,
keyword: Option<Vec<String>>,
#[serde(rename = "abstract")]
abstract_field: Option<String>,
doi: Option<Vec<String>>,
citation_count: Option<i32>,
reference_count: Option<i32>,
reference: Option<Vec<String>>,
citation: Option<Vec<String>>,
identifier: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct RawSearchResponse {
response: RawDocs,
}
#[derive(Debug, Deserialize)]
struct RawDocs {
docs: Vec<RawDoc>,
}