// src/handlers.rs use axum::{ extract::{Query, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::fs; use tracing::{info, warn, error}; use sqlx::{SqlitePool, Row}; use crate::config::Config; use crate::translation::Dictionary; use crate::qiniu::QiniuClient; use crate::ads::{AdsClient, AdsPaperDoc}; use crate::arxiv::{ArxivClient, ArxivPaper}; use crate::download::Downloader; // 全局共享的 Axum 应用上下文状态 pub struct AppState { pub config: Config, pub db: SqlitePool, pub dict: Dictionary, pub qiniu: QiniuClient, pub ads: AdsClient, pub arxiv: ArxivClient, pub downloader: Downloader, pub harvest_status: Arc>, pub process_status: Arc>, } // 检索请求参数 #[derive(Debug, Deserialize)] pub struct SearchParams { pub q: String, pub source: Option, // "all" | "ads" | "arxiv" pub rows: Option, pub start: Option, // 分页起始偏移量 pub sort: Option, // 排序字段,例如 "date_desc", "citations_desc", "relevance" } // 统一标准化的文献格式,用于向前端传输 #[derive(Debug, Serialize, Deserialize, Clone)] pub struct StandardPaper { pub bibcode: String, pub title: String, pub authors: Vec, let doi = doc.doi.as_ref() .and_then(|v: &Vec| v.first()) .cloned() .unwrap_or_default(); let mut arxiv_id = String::new(); if let Some(identifiers) = &doc.identifier { for id in identifiers { if id.starts_with("arXiv:") { arxiv_id = id.replace("arXiv:", "").trim().to_string(); break; } } } if arxiv_id.is_empty() { if doc.bibcode.starts_with("arXiv") { arxiv_id = doc.bibcode.replace("arXiv", "").trim().to_string(); } } StandardPaper { bibcode: doc.bibcode.clone(), title, authors, year: doc.year.clone().unwrap_or_default(), pub_journal: doc.pub_journal.clone().unwrap_or_default(), keywords, abstract_text: doc.abstract_text.clone().unwrap_or_default(), doi, arxiv_id, citation_count: doc.citation_count.unwrap_or(0), reference_count: doc.reference_count.unwrap_or(0), is_downloaded: false, has_markdown: false, has_translation: false, } } pub(crate) fn convert_arxiv_to_standard(doc: &ArxivPaper) -> StandardPaper { StandardPaper { bibcode: doc.id.clone(), title: doc.title.clone(), authors: doc.authors.clone(), year: doc.year.clone(), pub_journal: "arXiv Preprint".to_string(), keywords: Vec::new(), abstract_text: doc.abstract_text.clone(), doi: doc.doi.clone().unwrap_or_default(), arxiv_id: doc.id.clone(), citation_count: 0, reference_count: 0, is_downloaded: false, has_markdown: false, has_translation: false, } } pub(crate) async fn save_paper_to_db(db: &SqlitePool, p: &StandardPaper) -> anyhow::Result<()> { let authors_json = serde_json::to_string(&p.authors)?; let keywords_json = serde_json::to_string(&p.keywords)?; // 1. 如果存在 arxiv_id,检查是否有已存在的相同 arxiv_id 记录以防 duplicate if !p.arxiv_id.is_empty() { let existing_opt: Option<(String, Option, Option, Option, Option)> = sqlx::query_as( "SELECT bibcode, pdf_path, html_path, markdown_path, translation_path FROM papers WHERE arxiv_id = ?" ) .bind(&p.arxiv_id) .fetch_optional(db) .await?; if let Some((existing_bibcode, _pdf, _html, _md, _tr)) = existing_opt { if existing_bibcode != p.bibcode { // 发现不同 bibcode 标识的同一篇文献记录,需要进行合并合并 // 如果已存在的记录使用的是临时 arXiv ID 作为 bibcode,且新记录使用的是正式 ADS bibcode,我们升级 bibcode 主键 let is_existing_temp = existing_bibcode == p.arxiv_id; let is_new_formal = p.bibcode != p.arxiv_id; if is_existing_temp && is_new_formal { info!("发现相同 arXiv ID 的 .await?; // 运行迁移 sqlx::migrate!("./migrations") .run(&pool) .await?; let paper = StandardPaper { bibcode: "2026A&A...123..456X".to_string(), title: "A Test Title".to_string(), authors: vec!["Author A".to_string()], year: "2026".to_string(), pub_journal: "Astronomy & Astrophysics".to_string(), keywords: vec!["Keyword 1".to_string()], abstract_text: "This is abstract".to_string(), doi: "10.1000/test.doi".to_string(), arxiv_id: "".to_string(), citation_count: 5, reference_count: 10, is_downloaded: false, has_markdown: false, has_translation: false, }; // 保存 save_paper_to_db(&pool, &paper).await?; // 读取 let retrieved = get_paper_from_db(&pool, std::path::Path::new(""), "2026A&A...123..456X").await?; assert_eq!(retrieved.title, paper.title); assert_eq!(retrieved.authors, paper.authors); assert_eq!(retrieved.keywords, paper.keywords); // 检查路径状态(初始为 None) let paths = check_paper_paths_in_db(&pool, std::path::Path::new(""), "2026A&A...123..456X").await?; assert!(paths.is_some()); let (pdf, html, md, tr) = paths.unwrap(); assert!(pdf.is_none()); assert!(html.is_none()); assert!(md.is_none()); assert!(tr.is_none()); Ok(()) } }