futu_rest/routes/qot/
snapshot.rs1use axum::extract::{Json, State};
6use axum::http::StatusCode;
7use serde_json::Value;
8
9use futu_core::proto_id;
10
11use super::*;
12
13pub async fn get_snapshot(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
14 let snapshot_resp = adapter::proto_request::<
18 qot_get_security_snapshot::Request,
19 qot_get_security_snapshot::Response,
20 >(
21 &state,
22 proto_id::QOT_GET_SECURITY_SNAPSHOT,
23 Some(body.clone()),
24 )
25 .await?;
26
27 let mut json_rsp = snapshot_resp.0;
32 if let Ok(static_resp) = adapter::proto_request::<
33 qot_get_static_info::Request,
34 qot_get_static_info::Response,
35 >(&state, proto_id::QOT_GET_STATIC_INFO, Some(body))
36 .await
37 {
38 augment_snapshot_with_exchange_code(&mut json_rsp, &static_resp.0);
39 }
40 Ok(Json(json_rsp))
41}
42
43pub async fn get_static_info(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
58 if let Some(rejection) = check_static_info_input(&body) {
61 return Err(rejection);
62 }
63
64 let resp =
65 adapter::proto_request::<qot_get_static_info::Request, qot_get_static_info::Response>(
66 &state,
67 proto_id::QOT_GET_STATIC_INFO,
68 Some(body),
69 )
70 .await?;
71
72 let mut json_rsp = resp.0;
73 augment_static_info_with_exchange_code(&mut json_rsp);
74 Ok(Json(json_rsp))
75}
76
77pub(super) fn check_static_info_input(body: &Value) -> Option<(StatusCode, Json<Value>)> {
93 let c2s = body.get("c2s")?;
94 let candidates = ["code_list", "security_list", "securityList", "codeList"];
95 for key in candidates {
96 if let Some(v) = c2s.get(key)
97 && v.is_array()
98 && v.as_array().is_some_and(|a| a.is_empty())
99 {
100 return Some((
101 StatusCode::BAD_REQUEST,
102 Json(serde_json::json!({
103 "error": "/api/static-info: c2s.code_list 不能为空 ([]). \
104 必须显式传至少 1 个 (market, code), 或缺省该字段走 \
105 market / sec_type filter. 空 list fall through 到 \
106 backend 会返全 universe (~28MB),违反客户端预期 \
107 (eli FINAL-BUG-REPORT-v5 #5 P1).",
108 "field": key,
109 })),
110 ));
111 }
112 }
113 None
114}
115
116pub(super) fn augment_static_info_with_exchange_code(json_rsp: &mut Value) {
122 let Some(s2c) = json_rsp.get_mut("s2c") else {
123 return;
124 };
125 let Some(list) = s2c
126 .get_mut("static_info_list")
127 .and_then(|v| v.as_array_mut())
128 else {
129 return;
130 };
131 for entry in list {
132 let Some(basic) = entry.get_mut("basic") else {
133 continue;
134 };
135 let exch_type_i32 = basic.get("exch_type").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
136 if let Some(s) = futu_core::exch_type::exch_type_to_string(exch_type_i32)
137 && let Some(obj) = basic.as_object_mut()
138 {
139 obj.insert("exchange_code".to_string(), Value::String(s.to_string()));
140 }
141 }
142}
143
144pub(super) fn augment_snapshot_with_exchange_code(snapshot_rsp: &mut Value, static_rsp: &Value) {
148 use std::collections::HashMap;
149
150 let mut exch_code_by_key: HashMap<(i64, String), String> = HashMap::new();
152 if let Some(list) = static_rsp
153 .get("s2c")
154 .and_then(|v| v.get("static_info_list"))
155 .and_then(|v| v.as_array())
156 {
157 for entry in list {
158 let Some(basic) = entry.get("basic") else {
159 continue;
160 };
161 let market = basic
162 .get("security")
163 .and_then(|v| v.get("market"))
164 .and_then(|v| v.as_i64())
165 .unwrap_or(0);
166 let code = basic
167 .get("security")
168 .and_then(|v| v.get("code"))
169 .and_then(|v| v.as_str())
170 .unwrap_or("")
171 .to_string();
172 let exch_type_i32 = basic.get("exch_type").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
173 if let Some(s) = futu_core::exch_type::exch_type_to_string(exch_type_i32) {
174 exch_code_by_key.insert((market, code), s.to_string());
175 }
176 }
177 }
178
179 let Some(list) = snapshot_rsp
181 .get_mut("s2c")
182 .and_then(|v| v.get_mut("snapshot_list"))
183 .and_then(|v| v.as_array_mut())
184 else {
185 return;
186 };
187 for entry in list {
188 let Some(basic) = entry.get_mut("basic") else {
189 continue;
190 };
191 let market = basic
192 .get("security")
193 .and_then(|v| v.get("market"))
194 .and_then(|v| v.as_i64())
195 .unwrap_or(0);
196 let code = basic
197 .get("security")
198 .and_then(|v| v.get("code"))
199 .and_then(|v| v.as_str())
200 .unwrap_or("")
201 .to_string();
202 if let Some(exch_code) = exch_code_by_key.get(&(market, code))
203 && let Some(obj) = basic.as_object_mut()
204 {
205 obj.insert(
206 "exchange_code".to_string(),
207 Value::String(exch_code.clone()),
208 );
209 }
210 }
211}