Skip to main content

futucli/cmd/sys/
quote_rights.rs

1use std::sync::Arc;
2
3use anyhow::{Result, anyhow, bail};
4use futu_net::client::FutuClient;
5use futu_qot::quote_rights::{QuoteRightsProfile, SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE};
6use prost::Message;
7use tabled::{Table, Tabled, settings::Style};
8
9use crate::common::connect_gateway;
10use crate::output::OutputFormat;
11
12#[derive(Tabled)]
13pub(super) struct QuoteRightsInfoRow {
14    #[tabled(rename = "字段")]
15    pub(super) field: String,
16    #[tabled(rename = "值")]
17    pub(super) value: String,
18}
19
20#[derive(Tabled)]
21pub(super) struct QuoteRightsRow {
22    #[tabled(rename = "市场")]
23    pub(super) market: String,
24    #[tabled(rename = "品类")]
25    pub(super) category: String,
26    #[tabled(rename = "权限")]
27    pub(super) value: String,
28}
29
30pub(super) fn quote_right_user_rows(profile: &QuoteRightsProfile) -> Vec<QuoteRightsInfoRow> {
31    let mut rows = Vec::with_capacity(3);
32    rows.push(QuoteRightsInfoRow {
33        field: "昵称".to_string(),
34        value: profile
35            .nick_name
36            .clone()
37            .unwrap_or_else(|| "未知".to_string()),
38    });
39    rows.push(QuoteRightsInfoRow {
40        field: "牛牛号".to_string(),
41        value: profile
42            .user_id
43            .map(|v| v.to_string())
44            .unwrap_or_else(|| "未知".to_string()),
45    });
46    rows.push(QuoteRightsInfoRow {
47        field: "注册归属地".to_string(),
48        value: match (&profile.user_attribution_region, profile.user_attribution) {
49            (Some(region), Some(raw)) => format!("{region} ({raw})"),
50            (Some(region), None) => region.clone(),
51            (None, Some(raw)) => raw.to_string(),
52            (None, None) => "未知".to_string(),
53        },
54    });
55
56    rows
57}
58
59pub(super) fn quote_right_quota_rows(profile: &QuoteRightsProfile) -> Vec<QuoteRightsInfoRow> {
60    let mut rows = Vec::with_capacity(3);
61    rows.push(QuoteRightsInfoRow {
62        field: "已用订阅额度/总额".to_string(),
63        value: profile
64            .quota
65            .subscribe_total
66            .map(|v| format!("0/{v}"))
67            .unwrap_or_else(|| "未知".to_string()),
68    });
69    rows.push(QuoteRightsInfoRow {
70        field: "已用历史K线额度/总额".to_string(),
71        value: profile
72            .quota
73            .history_kl_total
74            .map(|v| format!("0/{v}"))
75            .unwrap_or_else(|| "未知".to_string()),
76    });
77    if let Some(msg) = &profile.ret_msg {
78        rows.push(QuoteRightsInfoRow {
79            field: "刷新状态".to_string(),
80            value: msg.clone(),
81        });
82    }
83
84    rows
85}
86
87pub(super) fn quote_right_rows(profile: &QuoteRightsProfile) -> Vec<QuoteRightsRow> {
88    profile
89        .items
90        .iter()
91        .map(|item| QuoteRightsRow {
92            market: item.market.clone(),
93            category: item.category.clone(),
94            value: match item.raw {
95                Some(raw) => format!("{} ({raw})", item.label),
96                None => item.label.clone(),
97            },
98        })
99        .collect()
100}
101
102fn print_quote_rights_table(profile: &QuoteRightsProfile) {
103    println!("用户");
104    let user_rows = quote_right_user_rows(profile);
105    let mut user_table = Table::new(user_rows);
106    user_table.with(Style::rounded());
107    println!("{user_table}");
108
109    println!();
110    println!("额度");
111    let quota_rows = quote_right_quota_rows(profile);
112    let mut quota_table = Table::new(quota_rows);
113    quota_table.with(Style::rounded());
114    println!("{quota_table}");
115
116    println!();
117    println!("权限");
118    let rows = quote_right_rows(profile);
119    let mut table = Table::new(rows);
120    table.with(Style::rounded());
121    println!("{table}");
122}
123
124async fn refresh_quote_rights(client: &Arc<FutuClient>) -> Result<()> {
125    let req = futu_proto::test_cmd::Request {
126        c2s: futu_proto::test_cmd::C2s {
127            cmd: "request_highest_quote_right".to_string(),
128            param_str: None,
129            param_bytes: None,
130        },
131    };
132    let frame = client
133        .request(futu_core::proto_id::TEST_CMD, req.encode_to_vec())
134        .await?;
135    let resp = futu_proto::test_cmd::Response::decode(frame.body.as_ref())
136        .map_err(|e| anyhow!("decode request_highest_quote_right: {e}"))?;
137    if resp.ret_type != 0 {
138        bail!(
139            "request_highest_quote_right ret_type={} msg={:?}",
140            resp.ret_type,
141            resp.ret_msg
142        );
143    }
144    Ok(())
145}
146
147async fn fetch_quote_rights_profile(client: &Arc<FutuClient>) -> Result<QuoteRightsProfile> {
148    let req = futu_proto::test_cmd::Request {
149        c2s: futu_proto::test_cmd::C2s {
150            cmd: SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE.to_string(),
151            param_str: None,
152            param_bytes: None,
153        },
154    };
155    let frame = client
156        .request(futu_core::proto_id::TEST_CMD, req.encode_to_vec())
157        .await?;
158    let resp = futu_proto::test_cmd::Response::decode(frame.body.as_ref())
159        .map_err(|e| anyhow!("decode {SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE}: {e}"))?;
160    if resp.ret_type != 0 {
161        bail!(
162            "{SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE} ret_type={} msg={:?}",
163            resp.ret_type,
164            resp.ret_msg
165        );
166    }
167    let json = resp
168        .s2c
169        .and_then(|s| s.result_str)
170        .ok_or_else(|| anyhow!("{SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE}: missing result_str"))?;
171    serde_json::from_str(&json)
172        .map_err(|e| anyhow!("parse {SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE} profile: {e}"))
173}
174
175pub async fn run_quote_rights(gateway: &str, refresh: bool, format: OutputFormat) -> Result<()> {
176    let (client, _rx) = connect_gateway(gateway, "futucli-quote-rights").await?;
177    if refresh {
178        refresh_quote_rights(&client).await?;
179    }
180    let profile = fetch_quote_rights_profile(&client).await?;
181    match format {
182        OutputFormat::Table => print_quote_rights_table(&profile),
183        OutputFormat::Json | OutputFormat::Jsonl => {
184            let rows = quote_right_rows(&profile);
185            format.print_rows(&rows, &[profile])?;
186        }
187    }
188    Ok(())
189}