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 (external report FINAL-BUG-REPORT-v5 #5 P1).",
108 "field": key,
109 })),
110 ));
111 }
112 }
113 None
114}
115
116fn json_i32_field(obj: &Value, key: &str) -> Option<i32> {
122 obj.get(key)
123 .and_then(|v| v.as_i64())
124 .and_then(|raw| i32::try_from(raw).ok())
125}
126
127pub(super) fn augment_static_info_with_exchange_code(json_rsp: &mut Value) {
128 let Some(s2c) = json_rsp.get_mut("s2c") else {
129 return;
130 };
131 let Some(list) = s2c
132 .get_mut("static_info_list")
133 .and_then(|v| v.as_array_mut())
134 else {
135 return;
136 };
137 for entry in list {
138 let Some(basic) = entry.get_mut("basic") else {
139 continue;
140 };
141 let exch_type_i32 = json_i32_field(basic, "exch_type").unwrap_or(0);
142 if let Some(s) = futu_core::exch_type::exch_type_to_string(exch_type_i32)
143 && let Some(obj) = basic.as_object_mut()
144 {
145 obj.insert("exchange_code".to_string(), Value::String(s.to_string()));
146 }
147 }
148}
149
150pub(super) fn augment_snapshot_with_exchange_code(snapshot_rsp: &mut Value, static_rsp: &Value) {
154 use std::collections::HashMap;
155
156 let mut exch_code_by_key: HashMap<(i64, String), String> = HashMap::new();
158 if let Some(list) = static_rsp
159 .get("s2c")
160 .and_then(|v| v.get("static_info_list"))
161 .and_then(|v| v.as_array())
162 {
163 for entry in list {
164 let Some(basic) = entry.get("basic") else {
165 continue;
166 };
167 let market = basic
168 .get("security")
169 .and_then(|v| v.get("market"))
170 .and_then(|v| v.as_i64())
171 .unwrap_or(0);
172 let code = basic
173 .get("security")
174 .and_then(|v| v.get("code"))
175 .and_then(|v| v.as_str())
176 .unwrap_or("")
177 .to_string();
178 let exch_type_i32 = json_i32_field(basic, "exch_type").unwrap_or(0);
179 if let Some(s) = futu_core::exch_type::exch_type_to_string(exch_type_i32) {
180 exch_code_by_key.insert((market, code), s.to_string());
181 }
182 }
183 }
184
185 let Some(list) = snapshot_rsp
187 .get_mut("s2c")
188 .and_then(|v| v.get_mut("snapshot_list"))
189 .and_then(|v| v.as_array_mut())
190 else {
191 return;
192 };
193 for entry in list {
194 let Some(basic) = entry.get_mut("basic") else {
195 continue;
196 };
197 let market = basic
198 .get("security")
199 .and_then(|v| v.get("market"))
200 .and_then(|v| v.as_i64())
201 .unwrap_or(0);
202 let code = basic
203 .get("security")
204 .and_then(|v| v.get("code"))
205 .and_then(|v| v.as_str())
206 .unwrap_or("")
207 .to_string();
208 if let Some(exch_code) = exch_code_by_key.get(&(market, code))
209 && let Some(obj) = basic.as_object_mut()
210 {
211 obj.insert(
212 "exchange_code".to_string(),
213 Value::String(exch_code.clone()),
214 );
215 }
216 }
217}