feat(ai): Phase 1C 管理看板 — 用量/成本/功能开关三合一

- UsageService 新增 get_daily_usage + aggregate_daily 日聚合能力
- 新增 3 个管理端点: /ai/admin/daily-usage, /ai/admin/flags (GET+POST)
- AiUsageDashboard 扩展为三 Tab: 用量概览/成本分析/功能开关
- 功能开关支持 Switch 实时切换,权限码 ai.admin.flags
- 日聚合用量 30 天趋势表,含 Token/成本汇总统计
This commit is contained in:
iven
2026-05-18 23:36:33 +08:00
parent 5ba28ea349
commit 89581b070f
5 changed files with 536 additions and 89 deletions

View File

@@ -832,6 +832,107 @@ where
Ok(Json(ApiResponse::ok(estimate)))
}
// === AI 管理看板 ===
#[derive(Debug, Deserialize, utoipa::IntoParams)]
pub struct DailyUsageQuery {
pub start_date: String,
pub end_date: String,
}
#[utoipa::path(
get,
path = "/ai/admin/daily-usage",
params(DailyUsageQuery),
responses((status = 200, description = "按日聚合用量")),
tag = "AI 管理",
security(("bearer_auth" = [])),
)]
pub async fn admin_daily_usage<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<DailyUsageQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.admin.dashboard")?;
let start_date = chrono::NaiveDate::parse_from_str(&params.start_date, "%Y-%m-%d")
.map_err(|_| erp_core::error::AppError::Validation("start_date 格式错误".into()))?;
let end_date = chrono::NaiveDate::parse_from_str(&params.end_date, "%Y-%m-%d")
.map_err(|_| erp_core::error::AppError::Validation("end_date 格式错误".into()))?;
let rows = state
.usage
.get_daily_usage(ctx.tenant_id, start_date, end_date)
.await?;
Ok(Json(ApiResponse::ok(serde_json::json!({
"data": rows,
"start_date": params.start_date,
"end_date": params.end_date,
}))))
}
#[utoipa::path(
get,
path = "/ai/admin/flags",
responses((status = 200, description = "功能开关列表")),
tag = "AI 管理",
security(("bearer_auth" = [])),
)]
pub async fn admin_list_flags<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<
Json<ApiResponse<Vec<crate::service::feature_flag_service::FeatureFlag>>>,
erp_core::error::AppError,
>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.admin.flags")?;
let flags = state.feature_flags.get_all(ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(flags)))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateFlagBody {
pub feature: String,
pub enabled: bool,
}
#[utoipa::path(
post,
path = "/ai/admin/flags",
request_body = UpdateFlagBody,
responses((status = 200, description = "更新功能开关")),
tag = "AI 管理",
security(("bearer_auth" = [])),
)]
pub async fn admin_update_flag<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Json(body): Json<UpdateFlagBody>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.admin.flags")?;
state
.feature_flags
.set_enabled(ctx.tenant_id, &body.feature, body.enabled, ctx.user_id)
.await?;
Ok(Json(ApiResponse::ok(serde_json::json!({
"feature": body.feature,
"enabled": body.enabled,
}))))
}
// === SSE 流构建辅助 ===
fn build_sse_stream(