Fundamental_Analysis/services/api-gateway/src/api.rs
Lv, Qi 5327e76aaa chore: 提交本轮 Rust 架构迁移相关改动
- docker-compose: 下线 Python backend/config-service,切换至 config-service-rs
- archive: 归档 legacy Python 目录至 archive/python/*
- services: 新增/更新 common-contracts、api-gateway、各 provider、report-generator-service、config-service-rs
- data-persistence-service: API/system 模块与模型/DTO 调整
- frontend: 更新 useApi 与 API 路由
- docs: 更新路线图并勾选光荣退役
- cleanup: 移除 data-distance-service 占位测试
2025-11-16 20:55:46 +08:00

148 lines
4.5 KiB
Rust

use crate::error::Result;
use crate::state::AppState;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Json},
routing::{get, post},
Router,
};
use common_contracts::messages::FetchCompanyDataCommand;
use common_contracts::observability::{HealthStatus, ServiceStatus, TaskProgress};
use futures_util::future::join_all;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{info, warn};
use uuid::Uuid;
const DATA_FETCH_QUEUE: &str = "data_fetch_commands";
// --- Request/Response Structs ---
#[derive(Deserialize)]
pub struct DataRequest {
pub symbol: String,
pub market: String,
}
#[derive(Serialize)]
pub struct RequestAcceptedResponse {
pub request_id: Uuid,
}
// --- Router Definition ---
pub fn create_router(app_state: AppState) -> Router {
Router::new()
.route("/health", get(health_check))
.route("/tasks", get(get_current_tasks)) // This is the old, stateless one
.route("/v1/data-requests", post(trigger_data_fetch))
.route("/v1/companies/:symbol/profile", get(get_company_profile))
.route("/v1/tasks/:request_id", get(get_task_progress))
.with_state(app_state)
}
// --- Health & Stateless Tasks ---
async fn health_check(State(state): State<AppState>) -> Json<HealthStatus> {
let mut details = HashMap::new();
// 提供确定性且无副作用的健康详情,避免访问不存在的状态字段
details.insert("message_bus".to_string(), "nats".to_string());
details.insert("nats_addr".to_string(), state.config.nats_addr.clone());
let status = HealthStatus {
module_id: "api-gateway".to_string(),
status: ServiceStatus::Ok,
version: env!("CARGO_PKG_VERSION").to_string(),
details,
};
Json(status)
}
async fn get_current_tasks() -> Json<Vec<TaskProgress>> {
Json(vec![])
}
// --- API Handlers ---
/// [POST /v1/data-requests]
/// Triggers the data fetching process by publishing a command to the message bus.
async fn trigger_data_fetch(
State(state): State<AppState>,
Json(payload): Json<DataRequest>,
) -> Result<impl IntoResponse> {
let request_id = Uuid::new_v4();
let command = FetchCompanyDataCommand {
request_id,
symbol: payload.symbol,
market: payload.market,
};
info!(request_id = %request_id, "Publishing data fetch command");
state
.nats_client
.publish(
DATA_FETCH_QUEUE.to_string(),
serde_json::to_vec(&command).unwrap().into(),
)
.await?;
Ok((
StatusCode::ACCEPTED,
Json(RequestAcceptedResponse { request_id }),
))
}
/// [GET /v1/companies/:symbol/profile]
/// Queries the persisted company profile from the data-persistence-service.
async fn get_company_profile(
State(state): State<AppState>,
Path(symbol): Path<String>,
) -> Result<impl IntoResponse> {
let profile = state.persistence_client.get_company_profile(&symbol).await?;
Ok(Json(profile))
}
/// [GET /v1/tasks/:request_id]
/// Aggregates task progress from all downstream provider services.
async fn get_task_progress(
State(state): State<AppState>,
Path(request_id): Path<Uuid>,
) -> Result<impl IntoResponse> {
let client = reqwest::Client::new();
let fetches = state
.config
.provider_services
.iter()
.map(|service_url| {
let client = client.clone();
let url = format!("{}/tasks", service_url);
async move {
match client.get(&url).send().await {
Ok(resp) => match resp.json::<Vec<TaskProgress>>().await {
Ok(tasks) => Some(tasks),
Err(e) => {
warn!("Failed to decode tasks from {}: {}", url, e);
None
}
},
Err(e) => {
warn!("Failed to fetch tasks from {}: {}", url, e);
None
}
}
}
});
let results = join_all(fetches).await;
let mut merged: Vec<TaskProgress> = Vec::new();
for maybe_tasks in results {
if let Some(tasks) = maybe_tasks {
merged.extend(tasks);
}
}
if let Some(task) = merged.into_iter().find(|t| t.request_id == request_id) {
Ok((StatusCode::OK, Json(task)).into_response())
} else {
Ok((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Task not found"}))).into_response())
}
}