futucli/cmd/sys/
quote_rights.rs1use 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}