1use ek_core::{Field, SectionMap};
11
12#[derive(Debug, Clone)]
14pub struct AcctSpec {
15 pub broker_id: String,
16 pub user: String,
17 pub td_addr: String,
18 pub md_addr: String,
19 pub app_id: String,
20 pub auth_code: String,
21 pub classes: String,
23 pub md_user: String,
24 pub md_password: Option<String>,
26}
27
28pub fn derived_name(spec: &AcctSpec) -> Result<String, String> {
30 ek_ctp::derived_gateway_name(&spec.broker_id, &spec.user).ok_or_else(|| {
31 format!(
32 "派生注册名失败(broker={:?} 账号={:?}):字符不合名纪律",
33 spec.broker_id, spec.user
34 )
35 })
36}
37
38pub fn unique_gw_name(
53 taken: &[(String, String, String, String)],
54 broker: &str,
55 user: &str,
56 td_addr: &str,
57) -> Result<String, String> {
58 let broker = broker.trim();
59 let user = user.trim();
60 let td = td_addr.trim();
61 let base = ek_ctp::derived_gateway_name(broker, user).ok_or_else(|| {
62 format!("派生注册名失败(broker={broker:?} 账号={user:?}):字符不合名纪律")
63 })?;
64 for (gw, b, u, t) in taken {
65 if b.trim() == broker && u.trim() == user && t.trim() == td {
66 return Err(format!(
67 "该 broker+账号+td 前置已在册(行 {gw},前置 {td});同前置不重复登记"
68 ));
69 }
70 }
71 if !taken.iter().any(|(gw, _, _, _)| gw == &base) {
72 return Ok(base);
73 }
74 for n in 2..=99u32 {
75 let cand = format!("{base}_{n}");
76 if cand.len() > 32 {
77 return Err(format!(
78 "同 broker+账号 的第 {n} 套前置超派生名 32 字符上限({cand})"
79 ));
80 }
81 if !taken.iter().any(|(gw, _, _, _)| gw == &cand) {
82 return Ok(cand);
83 }
84 }
85 Err("同 broker+账号 的前置套数已超 98 条(找不着后缀空位)".to_string())
86}
87
88pub fn taken_accounts(store: &ek_cfg::Store) -> Result<Vec<(String, String, String, String)>, String> {
93 let mut out = Vec::new();
94 for ns in store.list_namespaces().map_err(|e| format!("档案目录列举失败: {e:?}"))? {
95 let Some(gw) = ns.strip_prefix("ctp.") else { continue };
96 if let Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) = store.load(&ns) {
97 out.push((
98 gw.to_string(),
99 map.get_plain("broker_id").unwrap_or("").to_string(),
100 map.get_plain("td_user").unwrap_or("").to_string(),
101 map.get_plain("td_addr").unwrap_or("").to_string(),
102 ));
103 }
104 }
105 Ok(out)
106}
107
108pub fn write_account(
116 store: &ek_cfg::Store,
117 spec: &AcctSpec,
118 password: &str,
119) -> Result<String, String> {
120 if spec.broker_id.is_empty()
121 || spec.user.is_empty()
122 || spec.td_addr.is_empty()
123 || spec.app_id.is_empty()
124 || spec.auth_code.is_empty()
125 {
126 return Err("缺必填要素(broker/user/td 前置/app_id/auth_code)".to_string());
127 }
128 if password.trim().is_empty() {
129 return Err("密码为空:档案不接受无密码账户(受理校验同款)".to_string());
130 }
131 let gw = unique_gw_name(&taken_accounts(store)?, &spec.broker_id, &spec.user, &spec.td_addr)?;
132 let ns = format!("ctp.{gw}");
133 ek_cfg::secret::register(
134 &ns,
135 "gw_td_password",
136 ek_cfg::SecretValue::from_bytes(password.trim().to_owned()),
137 )
138 .map_err(|e| format!("密码登记密库失败: {e:?}"))?;
139 let mut map = SectionMap::new(1)
140 .set("broker_id", Field::plain(spec.broker_id.trim()))
141 .set("td_addr", Field::plain(spec.td_addr.trim()))
142 .set("td_user", Field::plain(spec.user.trim()))
143 .set("td_app_id", Field::plain(spec.app_id.trim()))
144 .set("td_auth_code", Field::plain(spec.auth_code.trim()))
145 .set("td_password", Field::secret("gw_td_password"));
146 if !spec.md_addr.trim().is_empty() {
147 map = map.set("md_addr", Field::plain(spec.md_addr.trim()));
148 if !spec.md_user.trim().is_empty() {
149 map = map.set("md_user", Field::plain(spec.md_user.trim()));
150 }
151 if let Some(mp) = spec.md_password.as_deref() {
152 let mp = mp.trim();
153 if !mp.is_empty() {
154 ek_cfg::secret::register(&ns, "gw_md_password", ek_cfg::SecretValue::from_bytes(mp))
155 .map_err(|e| format!("行情密码登记密库失败: {e:?}"))?;
156 map = map.set("md_password", Field::secret("gw_md_password"));
157 }
158 }
159 }
160 let classes = spec.classes.trim();
161 if !classes.is_empty() {
162 for c in classes.split(',').map(str::trim) {
163 if c.chars().count() != 1 {
164 return Err(format!(
165 "product_classes 项 {c:?} 非单字符 CTP 码(D-PC-FILTER)"
166 ));
167 }
168 }
169 map = map.set("product_classes", Field::plain(classes));
170 }
171 store
172 .save(&ns, &map)
173 .map_err(|e| format!("档案落盘失败: {e:?}"))?;
174 Ok(gw)
175}
176
177pub fn purge_account_archive(store: &ek_cfg::Store, gw: &str) -> Result<(), String> {
189 purge_ns_direct(store, &format!("ctp.{gw}"))
190}
191
192pub fn purge_ns_direct(store: &ek_cfg::Store, ns: &str) -> Result<(), String> {
195 match store.load(ns) {
196 Ok(ek_cfg::LoadOutcome::Loaded { .. }) => {}
197 Ok(ek_cfg::LoadOutcome::Fresh) => return Ok(()),
198 Err(e) => return Err(format!("档案 {ns} 读不动: {e}")),
199 }
200 store.purge(ns).map_err(|e| format!("档案 {ns} 清段失败: {e}"))?;
203 ek_cfg::secret::forget_ns(ns).map_err(|e| format!("秘库 {ns} 清理失败: {e}"))?;
204 Ok(())
205}