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//! **sanity check**:和 HTTP auth 拿到的 `auth_code_list` 里的 broker_id
11//! 集合对比。
12//!
13//! - 一致 → 用 auth_code_list 正常建通道
14//! - 不一致 → 打 WARN,**仍以 auth_code_list 为准**(服务端对同账号有时
15//!   返回子集,保守策略:auth_code_list 更全,用它保底)
16//! - CMD 20176 失败(网络 / 服务端不支持)→ 不阻塞,跳过 sanity check
17//!
18//! 未来如果 auth_code_list 开始滞后于 CMD 20176(C++ 已经这样)可以切到
19//! 20176 为权威源。MVP 保守。
20
21use futu_core::error::{FutuError, Result};
22use prost::Message;
23
24use crate::conn::BackendConn;
25use crate::proto_internal::ft_conn_bind::{GetValidBrokerListReq, GetValidBrokerListRsp};
26
27/// CMD 号
28pub const CMD_FETCH_VALID_BROKER_LIST: u16 = 20176;
29
30/// 向 Platform 通道发 CMD 20176,返回服务端认为该 cid 当前有效的 broker_id
31/// 列表。失败返回 `Err`(调用方通常 log + ignore)。
32pub async fn fetch_valid_broker_list(backend: &BackendConn, uid: u64) -> Result<Vec<u32>> {
33    let req = GetValidBrokerListReq { uid: Some(uid) };
34    let body = req.encode_to_vec();
35    tracing::debug!(
36        uid,
37        body_len = body.len(),
38        "sending CMD20176 GetValidBrokerListReq"
39    );
40
41    let resp = backend.request(CMD_FETCH_VALID_BROKER_LIST, body).await?;
42
43    let rsp = GetValidBrokerListRsp::decode(resp.body.as_ref())
44        .map_err(|e| FutuError::Codec(format!("CMD20176 decode: {e}")))?;
45
46    let ret_code = rsp.ret_code.unwrap_or(-1);
47    if ret_code != 0 {
48        return Err(FutuError::ServerError {
49            ret_type: ret_code,
50            msg: format!(
51                "CMD20176 ret_code={ret_code} msg={:?}",
52                rsp.ret_msg.as_deref().unwrap_or("")
53            ),
54        });
55    }
56
57    tracing::info!(
58        uid = rsp.uid.unwrap_or(0),
59        count = rsp.broker_ids.len(),
60        broker_ids = ?rsp.broker_ids,
61        "CMD20176 valid broker list received"
62    );
63    Ok(rsp.broker_ids)
64}
65
66/// 把 CMD 20176 返回的 broker_ids 和 HTTP auth 返回的 auth_code_list
67/// 做一致性 diff,不一致时打 WARN,返回**建议使用的权威 broker_id 集**
68/// (MVP 以 auth_code_list 为准,未来可切 20176)。
69///
70/// 提出函数方便单测 + 将来切权威源。
71pub fn diff_broker_sources(auth_code_broker_ids: &[u32], cmd20176_broker_ids: &[u32]) -> Vec<u32> {
72    use std::collections::HashSet;
73    let auth_set: HashSet<u32> = auth_code_broker_ids.iter().copied().collect();
74    let cmd_set: HashSet<u32> = cmd20176_broker_ids.iter().copied().collect();
75
76    let only_auth: Vec<u32> = auth_set.difference(&cmd_set).copied().collect();
77    let only_cmd: Vec<u32> = cmd_set.difference(&auth_set).copied().collect();
78
79    if !only_auth.is_empty() || !only_cmd.is_empty() {
80        tracing::warn!(
81            only_in_auth_code_list = ?only_auth,
82            only_in_cmd20176 = ?only_cmd,
83            "broker source mismatch: HTTP auth_code_list vs CMD20176 differ — \
84             using auth_code_list as authority (MVP)"
85        );
86    } else {
87        tracing::debug!(
88            count = auth_code_broker_ids.len(),
89            "broker source consistent between auth_code_list and CMD20176"
90        );
91    }
92
93    // MVP:返回 auth_code_list(= 现有行为),未来可改成 cmd20176_broker_ids
94    auth_code_broker_ids.to_vec()
95}
96
97#[cfg(test)]
98mod tests;