feat(saas): add billing infrastructure — tables, types, service, handlers

B1.1 Billing database:
- 5 tables: billing_plans, billing_subscriptions, billing_invoices,
  billing_payments, billing_usage_quotas
- Seed data: Free(¥0)/Pro(¥49)/Team(¥199) plans
- JSONB limits for flexible plan configuration

Billing module (crates/zclaw-saas/src/billing/):
- types.rs: BillingPlan, Subscription, Invoice, Payment, UsageQuota
- service.rs: plan CRUD, subscription lookup, usage tracking, quota check
- handlers.rs: REST API (plans list/detail, subscription, usage)
- mod.rs: routes registered at /api/v1/billing/*

Cargo.toml: added chrono feature to sqlx for DateTime<Utc> support
This commit is contained in:
iven
2026-04-01 23:59:46 +08:00
parent c6bd4aea27
commit 9487cd7f72
9 changed files with 743 additions and 25 deletions

View File

@@ -0,0 +1,55 @@
//! 计费 HTTP 处理器
use axum::{
extract::{Extension, Path, State},
Json,
};
use crate::auth::types::AuthContext;
use crate::error::SaasResult;
use crate::state::AppState;
use super::service;
use super::types::*;
/// GET /api/v1/billing/plans — 列出所有活跃计划
pub async fn list_plans(
State(state): State<AppState>,
) -> SaasResult<Json<Vec<BillingPlan>>> {
let plans = service::list_plans(&state.db).await?;
Ok(Json(plans))
}
/// GET /api/v1/billing/plans/:id — 获取单个计划详情
pub async fn get_plan(
State(state): State<AppState>,
Path(plan_id): Path<String>,
) -> SaasResult<Json<BillingPlan>> {
let plan = service::get_plan(&state.db, &plan_id).await?
.ok_or_else(|| crate::error::SaasError::NotFound("计划不存在".into()))?;
Ok(Json(plan))
}
/// GET /api/v1/billing/subscription — 获取当前订阅
pub async fn get_subscription(
State(state): State<AppState>,
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<serde_json::Value>> {
let plan = service::get_account_plan(&state.db, &ctx.account_id).await?;
let sub = service::get_active_subscription(&state.db, &ctx.account_id).await?;
let usage = service::get_or_create_usage(&state.db, &ctx.account_id).await?;
Ok(Json(serde_json::json!({
"plan": plan,
"subscription": sub,
"usage": usage,
})))
}
/// GET /api/v1/billing/usage — 获取当月用量
pub async fn get_usage(
State(state): State<AppState>,
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<UsageQuota>> {
let usage = service::get_or_create_usage(&state.db, &ctx.account_id).await?;
Ok(Json(usage))
}