Skip to main content

futu_backend/
valid_brokers.rs

1//! CMD 20176 `kCmdFetchValidBrokerList` —— 获取 cid 当前有效的券商列表。
2//!
3//! 对齐 C++ `FTLogin/Src/ftlogin/channel/impl/logger.cpp:1425-1500` 的
4//! `FetchValidBrokerList`。从 2024 起 C++ 用这个命令取代了老的 9419
5//! `kCmdFetchMainBroker`,作为 broker 通道**有效性判定**的权威源。
6//!
7//! ## 我们的用法(v1.4.22)
8//!
9//! Platform TCP login 成功后发一次 CMD 20176,返回的 `broker_ids` 用于
10//! 决定本轮应建立哪些 broker 通道;和 HTTP auth 拿到的 `auth_code_list`
11//! 里的 broker_id 集合对比,只是为了把差异打进日志。
12//!
13//! - 一致 → 正常建通道
14//! - 不一致 → 打 WARN,CMD20176 成功时以 CMD20176 为准
15//! - CMD 20176 失败(网络 / 服务端不支持)→ 不阻塞,调用方回退到
16//!   auth_code_list 旧路径
17//!
18//! C++ 10.6 `FetchValidBrokerList` 成功后直接
19//! `CreateBrokerChannel(cid_valid_broker_list)`;失败才使用本地缓存 /
20//! 全 broker fallback。本模块按同一方向收敛:成功响应是权威过滤源,
21//! 失败响应不改变旧行为。
22
23use futu_core::error::{FutuError, Result};
24use prost::Message;
25
26use crate::conn::BackendConn;
27use crate::proto_internal::ft_conn_bind::{GetValidBrokerListReq, GetValidBrokerListRsp};
28
29/// CMD 号
30pub const CMD_FETCH_VALID_BROKER_LIST: u16 = 20176;
31
32/// 向 Platform 通道发 CMD 20176,返回服务端认为该 cid 当前有效的 broker_id
33/// 列表。失败返回 `Err`(调用方通常 log + ignore)。
34pub async fn fetch_valid_broker_list(backend: &BackendConn, uid: u64) -> Result<Vec<u32>> {
35    let req = GetValidBrokerListReq { uid: Some(uid) };
36    let body = req.encode_to_vec();
37    tracing::debug!(
38        uid,
39        body_len = body.len(),
40        "sending CMD20176 GetValidBrokerListReq"
41    );
42
43    let resp = backend.request(CMD_FETCH_VALID_BROKER_LIST, body).await?;
44
45    let rsp = GetValidBrokerListRsp::decode(resp.body.as_ref())
46        .map_err(|e| FutuError::Codec(format!("CMD20176 decode: {e}")))?;
47
48    let ret_code = rsp.ret_code.unwrap_or(-1);
49    if ret_code != 0 {
50        return Err(FutuError::ServerError {
51            ret_type: ret_code,
52            msg: format!(
53                "CMD20176 ret_code={ret_code} msg={:?}",
54                rsp.ret_msg.as_deref().unwrap_or("")
55            ),
56        });
57    }
58
59    tracing::info!(
60        uid = rsp.uid.unwrap_or(0),
61        count = rsp.broker_ids.len(),
62        broker_ids = ?rsp.broker_ids,
63        "CMD20176 valid broker list received"
64    );
65    Ok(rsp.broker_ids)
66}
67
68/// 把 CMD 20176 返回的 broker_ids 和 HTTP auth 返回的 auth_code_list
69/// 做一致性 diff,不一致时打 WARN,返回 C++ 对齐的权威 broker_id 集。
70///
71/// 调用方只应在 CMD20176 成功时调用本函数;CMD20176 失败时沿用旧
72/// auth_code_list fallback。
73pub fn diff_broker_sources(auth_code_broker_ids: &[u32], cmd20176_broker_ids: &[u32]) -> Vec<u32> {
74    use std::collections::HashSet;
75    let auth_set: HashSet<u32> = auth_code_broker_ids.iter().copied().collect();
76    let cmd_set: HashSet<u32> = cmd20176_broker_ids.iter().copied().collect();
77
78    let only_auth: Vec<u32> = auth_set.difference(&cmd_set).copied().collect();
79    let only_cmd: Vec<u32> = cmd_set.difference(&auth_set).copied().collect();
80
81    if !only_auth.is_empty() || !only_cmd.is_empty() {
82        tracing::warn!(
83            only_in_auth_code_list = ?only_auth,
84            only_in_cmd20176 = ?only_cmd,
85            "broker source mismatch: HTTP auth_code_list vs CMD20176 differ — \
86             using CMD20176 as authority"
87        );
88    } else {
89        tracing::debug!(
90            count = auth_code_broker_ids.len(),
91            "broker source consistent between auth_code_list and CMD20176"
92        );
93    }
94
95    cmd20176_broker_ids.to_vec()
96}
97
98#[cfg(test)]
99mod tests;