Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
74 changes: 74 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,48 @@ impl Config {
pub fn load(path: &str) -> anyhow::Result<Self> {
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)]
Expand All @@ -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);
Expand Down Expand Up @@ -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}");
}
}
Loading