feat(billing): activate real-time quota enforcement pipeline

- Wire relay handler to increment_usage() for JSON responses (tokens + relay_requests)
- Wire relay handler to increment_dimension("relay_requests") for SSE streams
- Add increment_dimension() function for hand_executions/pipeline_runs dimensions
- Schedule AggregateUsageWorker hourly for reconciliation (run_on_start=true)
- Mount mock payment routes in dev mode (ZCLAW_SAAS_DEV=true)

Previously the quota middleware always allowed requests because usage
counters were never incremented. Now relay requests update billing_usage_quotas
in real-time, with the aggregator providing hourly reconciliation.
This commit is contained in:
iven
2026-04-02 01:52:01 +08:00
parent 8263b236fd
commit 11e3d37468
4 changed files with 131 additions and 54 deletions

View File

@@ -146,7 +146,10 @@ pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<
Ok(usage)
}
/// 增加用量计数
/// 增加用量计数Relay 请求tokens + relay_requests +1
///
/// 在 relay handler 响应成功后直接调用,实现实时配额更新。
/// 聚合器 `AggregateUsageWorker` 每小时做一次对账修正。
pub async fn increment_usage(
pool: &PgPool,
account_id: &str,
@@ -170,6 +173,39 @@ pub async fn increment_usage(
Ok(())
}
/// 增加单一维度用量计数hand_executions / pipeline_runs / relay_requests
///
/// 使用静态 SQL 分支(白名单),避免动态列名注入风险。
pub async fn increment_dimension(
pool: &PgPool,
account_id: &str,
dimension: &str,
) -> SaasResult<()> {
let usage = get_or_create_usage(pool, account_id).await?;
match dimension {
"relay_requests" => {
sqlx::query(
"UPDATE billing_usage_quotas SET relay_requests = relay_requests + 1, updated_at = NOW() WHERE id = $1"
).bind(&usage.id).execute(pool).await?;
}
"hand_executions" => {
sqlx::query(
"UPDATE billing_usage_quotas SET hand_executions = hand_executions + 1, updated_at = NOW() WHERE id = $1"
).bind(&usage.id).execute(pool).await?;
}
"pipeline_runs" => {
sqlx::query(
"UPDATE billing_usage_quotas SET pipeline_runs = pipeline_runs + 1, updated_at = NOW() WHERE id = $1"
).bind(&usage.id).execute(pool).await?;
}
_ => return Err(crate::error::SaasError::InvalidInput(
format!("Unknown usage dimension: {}", dimension)
)),
}
Ok(())
}
/// 检查用量配额
pub async fn check_quota(
pool: &PgPool,