use ek_core::{Field, SectionMap};
#[derive(Debug, Clone)]
pub struct AcctSpec {
pub broker_id: String,
pub user: String,
pub td_addr: String,
pub md_addr: String,
pub app_id: String,
pub auth_code: String,
pub classes: String,
pub md_user: String,
pub md_password: Option<String>,
}
pub fn derived_name(spec: &AcctSpec) -> Result<String, String> {
ek_ctp::derived_gateway_name(&spec.broker_id, &spec.user).ok_or_else(|| {
format!(
"派生注册名失败(broker={:?} 账号={:?}):字符不合名纪律",
spec.broker_id, spec.user
)
})
}
pub fn unique_gw_name(
taken: &[(String, String, String, String)],
broker: &str,
user: &str,
td_addr: &str,
) -> Result<String, String> {
let broker = broker.trim();
let user = user.trim();
let td = td_addr.trim();
let base = ek_ctp::derived_gateway_name(broker, user)
.ok_or_else(|| format!("派生注册名失败(broker={broker:?} 账号={user:?}):字符不合名纪律"))?;
for (gw, b, u, t) in taken {
if b.trim() == broker && u.trim() == user && t.trim() == td {
return Err(format!(
"该 broker+账号+td 前置已在册(行 {gw},前置 {td});同前置不重复登记"
));
}
}
if !taken.iter().any(|(gw, _, _, _)| gw == &base) {
return Ok(base);
}
for n in 2..=99u32 {
let cand = format!("{base}_{n}");
if cand.len() > 32 {
return Err(format!(
"同 broker+账号 的第 {n} 套前置超派生名 32 字符上限({cand})"
));
}
if !taken.iter().any(|(gw, _, _, _)| gw == &cand) {
return Ok(cand);
}
}
Err("同 broker+账号 的前置套数已超 98 条(找不着后缀空位)".to_string())
}
pub fn taken_accounts(
store: &ek_cfg::Store,
) -> Result<Vec<(String, String, String, String)>, String> {
let mut out = Vec::new();
for ns in store
.list_namespaces()
.map_err(|e| format!("档案目录列举失败: {e:?}"))?
{
let Some(gw) = ns.strip_prefix("ctp.") else {
continue;
};
if let Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) = store.load(&ns) {
out.push((
gw.to_string(),
map.get_plain("broker_id").unwrap_or("").to_string(),
map.get_plain("td_user").unwrap_or("").to_string(),
map.get_plain("td_addr").unwrap_or("").to_string(),
));
}
}
Ok(out)
}
pub fn write_account(
store: &ek_cfg::Store,
spec: &AcctSpec,
password: &str,
) -> Result<String, String> {
if spec.broker_id.is_empty()
|| spec.user.is_empty()
|| spec.td_addr.is_empty()
|| spec.app_id.is_empty()
|| spec.auth_code.is_empty()
{
return Err("缺必填要素(broker/user/td 前置/app_id/auth_code)".to_string());
}
if password.trim().is_empty() {
return Err("密码为空:档案不接受无密码账户(受理校验同款)".to_string());
}
let gw = unique_gw_name(
&taken_accounts(store)?,
&spec.broker_id,
&spec.user,
&spec.td_addr,
)?;
let ns = format!("ctp.{gw}");
ek_cfg::secret::register(
&ns,
"gw_td_password",
ek_cfg::SecretValue::from_bytes(password.trim().to_owned()),
)
.map_err(|e| format!("密码登记密库失败: {e:?}"))?;
let mut map = SectionMap::new(1)
.set("broker_id", Field::plain(spec.broker_id.trim()))
.set("td_addr", Field::plain(spec.td_addr.trim()))
.set("td_user", Field::plain(spec.user.trim()))
.set("td_app_id", Field::plain(spec.app_id.trim()))
.set("td_auth_code", Field::plain(spec.auth_code.trim()))
.set("td_password", Field::secret("gw_td_password"));
if !spec.md_addr.trim().is_empty() {
map = map.set("md_addr", Field::plain(spec.md_addr.trim()));
if !spec.md_user.trim().is_empty() {
map = map.set("md_user", Field::plain(spec.md_user.trim()));
}
if let Some(mp) = spec.md_password.as_deref() {
let mp = mp.trim();
if !mp.is_empty() {
ek_cfg::secret::register(
&ns,
"gw_md_password",
ek_cfg::SecretValue::from_bytes(mp),
)
.map_err(|e| format!("行情密码登记密库失败: {e:?}"))?;
map = map.set("md_password", Field::secret("gw_md_password"));
}
}
}
let classes = spec.classes.trim();
if !classes.is_empty() {
for c in classes.split(',').map(str::trim) {
if c.chars().count() != 1 {
return Err(format!(
"product_classes 项 {c:?} 非单字符 CTP 码(D-PC-FILTER)"
));
}
}
map = map.set("product_classes", Field::plain(classes));
}
store
.save(&ns, &map)
.map_err(|e| format!("档案落盘失败: {e:?}"))?;
Ok(gw)
}
pub fn purge_account_archive(store: &ek_cfg::Store, gw: &str) -> Result<(), String> {
purge_ns_direct(store, &format!("ctp.{gw}"))
}
pub fn purge_ns_direct(store: &ek_cfg::Store, ns: &str) -> Result<(), String> {
match store.load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { .. }) => {}
Ok(ek_cfg::LoadOutcome::Fresh) => return Ok(()),
Err(e) => return Err(format!("档案 {ns} 读不动: {e}")),
}
store
.purge(ns)
.map_err(|e| format!("档案 {ns} 清段失败: {e}"))?;
ek_cfg::secret::forget_ns(ns).map_err(|e| format!("秘库 {ns} 清理失败: {e}"))?;
Ok(())
}