From 1e56646f54ddfb7c63fb93120324b825d17812f6 Mon Sep 17 00:00:00 2001 From: argszero Date: Tue, 18 Aug 2026 00:12:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20Config::validate=20rules=20?= =?UTF-8?q?=E2=80=94=20points=5Fper=5Funit>0,=20plan=E2=86=92provider=20ex?= =?UTF-8?q?ists,=20endpoints=E2=89=A51,=20protocol=20enum=20(+4=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/config.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0f4498..a9fe3f4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke ## 状态 -- ✅ **P0-A(v0.2.0,2026-08-17)**:后端骨架 + 配置加载(`config/config.example.toml`)+ SQLite 数据层(幂等迁移 + demo 种子)+ 认证(argon2 + Bearer API Key)+ API Key 端点。`cargo run` 后: +- ✅ **P0-A(v0.2.0,2026-08-17)**:后端骨架 + 配置加载(`config/config.example.toml`,含 `Config::validate` 校验:points_per_unit>0 / plan→provider 存在 / endpoints≥1 / protocol 枚举)+ SQLite 数据层(幂等迁移 + demo 种子)+ 认证(argon2 + Bearer API Key)+ API Key 端点。`cargo run` 后: - `GET /healthz` → `{"status":"ok","version":"0.2.0"}` - `POST /api/auth/login`(demo@aitokenpool.local / demo1234)→ `{api_key}` - `POST|GET /api/api-keys`(Bearer 认证,key 脱敏 `atk_live_****xxxx`) diff --git a/src/config.rs b/src/config.rs index ccad88f..a687037 100644 --- a/src/config.rs +++ b/src/config.rs @@ -114,8 +114,48 @@ impl Config { pub fn load(path: &str) -> anyhow::Result { let s = std::fs::read_to_string(path)?; let cfg: Config = toml::from_str(&s)?; + cfg.validate()?; Ok(cfg) } + + /// 校验规则(issue #6:points_per_unit > 0、plan 引用的 provider 必须存在、 + /// endpoints 至少 1 个、protocol 枚举合法) + pub fn validate(&self) -> anyhow::Result<()> { + use anyhow::anyhow; + + if self.points.points_per_unit == 0 { + return Err(anyhow!("[points] points_per_unit 必须 > 0,当前为 0")); + } + if self.providers.is_empty() { + return Err(anyhow!("providers 不能为空")); + } + let ids: std::collections::HashSet<&str> = + self.providers.iter().map(|p| p.id.as_str()).collect(); + for plan in &self.plans { + if !ids.contains(plan.provider.as_str()) { + return Err(anyhow!( + "plan[{}] 引用了不存在的 provider: {}", + plan.id, + plan.provider + )); + } + if plan.endpoints.is_empty() { + return Err(anyhow!("plan[{}] endpoints 至少 1 个", plan.id)); + } + for ep in &plan.endpoints { + match ep.protocol.as_str() { + "openai_chat" | "anthropic" | "responses" => {} + other => { + return Err(anyhow!( + "plan[{}] 非法 protocol: {}(允许 openai_chat | anthropic | responses)", + plan.id, other + )); + } + } + } + } + Ok(()) + } } #[cfg(test)] @@ -126,6 +166,8 @@ mod tests { fn parse_config_example_ok() { let cfg = Config::load("config/config.example.toml").expect("解析 config.example.toml 应成功"); + // 校验规则也应通过 + cfg.validate().expect("example 配置应通过校验"); // 点数 assert_eq!(cfg.points.anchor_currency, "USD"); assert_eq!(cfg.points.points_per_unit, 1000); @@ -173,4 +215,36 @@ mod tests { assert_eq!(cfg.server.addr, "0.0.0.0:8080"); assert_eq!(cfg.server.db_path, "data/aitokenpool.db"); } + + #[test] + fn validate_rejects_zero_points_per_unit() { + let mut cfg = Config::load("config/config.example.toml").unwrap(); + cfg.points.points_per_unit = 0; + let err = cfg.validate().unwrap_err().to_string(); + assert!(err.contains("points_per_unit"), "err: {err}"); + } + + #[test] + fn validate_rejects_missing_provider_ref() { + let mut cfg = Config::load("config/config.example.toml").unwrap(); + cfg.plans[0].provider = "nonexistent".to_string(); + let err = cfg.validate().unwrap_err().to_string(); + assert!(err.contains("不存在的 provider"), "err: {err}"); + } + + #[test] + fn validate_rejects_empty_endpoints() { + let mut cfg = Config::load("config/config.example.toml").unwrap(); + cfg.plans[0].endpoints.clear(); + let err = cfg.validate().unwrap_err().to_string(); + assert!(err.contains("endpoints 至少 1 个"), "err: {err}"); + } + + #[test] + fn validate_rejects_illegal_protocol() { + let mut cfg = Config::load("config/config.example.toml").unwrap(); + cfg.plans[0].endpoints[0].protocol = "grpc".to_string(); + let err = cfg.validate().unwrap_err().to_string(); + assert!(err.contains("非法 protocol"), "err: {err}"); + } }