File size: 4,529 Bytes
7c3e988 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | use crate::models::{AppState, Quote};
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse};
use axum::routing::get;
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tower_http::cors::CorsLayer;
#[derive(Serialize)]
struct HealthResponse {
ok: bool,
started_at: String,
watched_codes: usize,
cached_quotes: usize,
}
#[derive(Deserialize)]
struct LatestQuery {
code: Option<String>,
codes: Option<String>,
limit: Option<usize>,
}
#[derive(Deserialize)]
struct BatchQuery {
codes: String,
}
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/", get(index))
.route("/health", get(health))
.route("/codes", get(codes))
.route("/latest", get(latest))
.route("/latest/:code", get(latest_by_code))
.route("/batch", get(batch))
.with_state(state)
.layer(CorsLayer::permissive())
}
async fn index() -> Html<&'static str> {
Html(
r#"<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Sina Real Time API</title></head>
<body style="font-family: system-ui, sans-serif; line-height: 1.6; max-width: 880px; margin: 40px auto; padding: 0 20px;">
<h1>Sina Real Time API</h1>
<p>服务启动后会持续连接新浪财经 WebSocket,并在内存中保留每只股票的最新行情。</p>
<h2>Endpoints</h2>
<ul>
<li><code>GET /health</code>:服务状态</li>
<li><code>GET /codes</code>:当前监听的股票代码</li>
<li><code>GET /latest?limit=100</code>:最新缓存行情列表</li>
<li><code>GET /latest?code=sh600519</code>:单只股票最新行情</li>
<li><code>GET /latest/sh600519</code>:单只股票最新行情</li>
<li><code>GET /batch?codes=sh600519,sz000001</code>:多只股票最新行情</li>
</ul>
</body>
</html>"#,
)
}
async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
let latest = state.latest.read().await;
Json(HealthResponse {
ok: true,
started_at: state.started_at.clone(),
watched_codes: state.codes.len(),
cached_quotes: latest.len(),
})
}
async fn codes(State(state): State<AppState>) -> Json<Vec<String>> {
Json((*state.codes).clone())
}
async fn latest(
State(state): State<AppState>,
Query(params): Query<LatestQuery>,
) -> impl IntoResponse {
if let Some(code) = params.code {
return latest_response_for_codes(&state, vec![code], false).await;
}
if let Some(codes) = params.codes {
let codes = split_codes(&codes);
return latest_response_for_codes(&state, codes, false).await;
}
let limit = params.limit.unwrap_or(100).min(10_000);
let latest = state.latest.read().await;
let mut rows: Vec<Quote> = latest.values().cloned().collect();
rows.sort_by(|a, b| a.code.cmp(&b.code));
rows.truncate(limit);
Json(rows).into_response()
}
async fn latest_by_code(
State(state): State<AppState>,
Path(code): Path<String>,
) -> impl IntoResponse {
latest_response_for_codes(&state, vec![code], false).await
}
async fn batch(State(state): State<AppState>, Query(params): Query<BatchQuery>) -> impl IntoResponse {
latest_response_for_codes(&state, split_codes(¶ms.codes), true).await
}
async fn latest_response_for_codes(
state: &AppState,
codes: Vec<String>,
always_array: bool,
) -> axum::response::Response {
let latest = state.latest.read().await;
let mut found = Vec::new();
let mut missing = Vec::new();
for code in codes {
match latest.get(&code) {
Some(quote) => found.push(quote.clone()),
None => missing.push(code),
}
}
if found.is_empty() {
return (
StatusCode::NOT_FOUND,
Json(json!({
"error": "quote_not_ready",
"message": "还没有收到这些代码的实时行情,可能是非交易时段、代码未监听,或上游暂未推送。",
"missing": missing,
})),
)
.into_response();
}
if always_array || found.len() > 1 {
Json(json!({ "data": found, "missing": missing })).into_response()
} else {
Json(found.remove(0)).into_response()
}
}
fn split_codes(input: &str) -> Vec<String> {
input
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
|