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::auth::redact::uid_log_fingerprint;
27use crate::conn::BackendConn;
28use crate::proto_internal::ft_conn_bind::{GetValidBrokerListReq, GetValidBrokerListRsp};
29
30/// CMD 号
31pub const CMD_FETCH_VALID_BROKER_LIST: u16 = 20176;
32
33/// 向 Platform 通道发 CMD 20176,返回服务端认为该 cid 当前有效的 broker_id
34/// 列表。失败返回 `Err`(调用方通常 log + ignore)。
35pub async fn fetch_valid_broker_list(backend: &BackendConn, uid: u64) -> Result<Vec<u32>> {
36    let req = GetValidBrokerListReq { uid: Some(uid) };
37    let body = req.encode_to_vec();
38    tracing::debug!(
39        uid_fp = %uid_log_fingerprint(uid),
40        body_len = body.len(),
41        "sending CMD20176 GetValidBrokerListReq"
42    );
43
44    let resp = backend.request(CMD_FETCH_VALID_BROKER_LIST, body).await?;
45
46    let rsp = GetValidBrokerListRsp::decode(resp.body.as_ref())
47        .map_err(|e| FutuError::Codec(format!("CMD20176 decode: {e}")))?;
48
49    let ret_code = rsp.ret_code.unwrap_or(-1);
50    if ret_code != 0 {
51        return Err(FutuError::ServerError {
52            ret_type: ret_code,
53            msg: format!(
54                "CMD20176 ret_code={ret_code} msg={:?}",
55                rsp.ret_msg.as_deref().unwrap_or("")
56            ),
57        });
58    }
59
60    tracing::info!(
61        uid_fp = %uid_log_fingerprint(rsp.uid.unwrap_or(0)),
62        count = rsp.broker_ids.len(),
63        broker_ids = ?rsp.broker_ids,
64        "CMD20176 valid broker list received"
65    );
66    Ok(rsp.broker_ids)
67}
68
69/// 把 CMD 20176 返回的 broker_ids 和 HTTP auth 返回的 auth_code_list
70/// 做一致性 diff,不一致时打 WARN,返回 C++ 对齐的权威 broker_id 集。
71///
72/// 调用方只应在 CMD20176 成功时调用本函数;CMD20176 失败时沿用旧
73/// auth_code_list fallback。
74pub fn diff_broker_sources(auth_code_broker_ids: &[u32], cmd20176_broker_ids: &[u32]) -> Vec<u32> {
75    use std::collections::HashSet;
76    let auth_set: HashSet<u32> = auth_code_broker_ids.iter().copied().collect();
77    let cmd_set: HashSet<u32> = cmd20176_broker_ids.iter().copied().collect();
78
79    let only_auth: Vec<u32> = auth_set.difference(&cmd_set).copied().collect();
80    let only_cmd: Vec<u32> = cmd_set.difference(&auth_set).copied().collect();
81
82    if !only_auth.is_empty() || !only_cmd.is_empty() {
83        tracing::warn!(
84            only_in_auth_code_list = ?only_auth,
85            only_in_cmd20176 = ?only_cmd,
86            "broker source mismatch: HTTP auth_code_list vs CMD20176 differ — \
87             using CMD20176 as authority"
88        );
89    } else {
90        tracing::debug!(
91            count = auth_code_broker_ids.len(),
92            "broker source consistent between auth_code_list and CMD20176"
93        );
94    }
95
96    cmd20176_broker_ids.to_vec()
97}
98
99#[cfg(test)]
100mod tests;