mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
use super::{
|
||||
admin_billing_optional_bool_filter, admin_billing_optional_epoch_value,
|
||||
admin_billing_optional_filter, admin_billing_pages, admin_billing_parse_page,
|
||||
admin_billing_parse_page_size, admin_billing_validate_safe_expression,
|
||||
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
|
||||
build_admin_billing_read_only_response, default_admin_billing_true,
|
||||
normalize_admin_billing_optional_text, normalize_admin_billing_required_text,
|
||||
admin_billing_optional_bool_filter, admin_billing_optional_filter, admin_billing_pages,
|
||||
admin_billing_parse_page, admin_billing_parse_page_size,
|
||||
admin_billing_validate_safe_expression, build_admin_billing_bad_request_response,
|
||||
build_admin_billing_not_found_response, build_admin_billing_read_only_response,
|
||||
default_admin_billing_true, normalize_admin_billing_optional_text,
|
||||
normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::gateway::handlers::unix_secs_to_rfc3339;
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -16,7 +17,6 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
fn default_admin_billing_collector_value_type() -> String {
|
||||
"float".to_string()
|
||||
@@ -43,7 +43,7 @@ struct AdminBillingCollectorUpsertRequest {
|
||||
}
|
||||
|
||||
fn build_admin_billing_collector_payload_from_record(
|
||||
record: &crate::gateway::AdminBillingCollectorRecord,
|
||||
record: &crate::AdminBillingCollectorRecord,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.id,
|
||||
@@ -75,26 +75,6 @@ fn admin_billing_collector_id_from_path(request_path: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_billing_collector_payload(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
Ok(json!({
|
||||
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"api_format": row.try_get::<String, _>("api_format").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"task_type": row.try_get::<String, _>("task_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"dimension_name": row.try_get::<String, _>("dimension_name").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"source_type": row.try_get::<String, _>("source_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"source_path": row.try_get::<Option<String>, _>("source_path").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"value_type": row.try_get::<String, _>("value_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"transform_expression": row.try_get::<Option<String>, _>("transform_expression").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"default_value": row.try_get::<Option<String>, _>("default_value").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"priority": row.try_get::<i32, _>("priority").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"is_enabled": row.try_get::<bool, _>("is_enabled").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"created_at": admin_billing_optional_epoch_value(row, "created_at_unix_secs")?,
|
||||
"updated_at": admin_billing_optional_epoch_value(row, "updated_at_unix_secs")?,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn build_admin_list_dimension_collectors_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -116,7 +96,7 @@ async fn build_admin_list_dimension_collectors_response(
|
||||
Err(detail) => return Ok(build_admin_billing_bad_request_response(detail)),
|
||||
};
|
||||
|
||||
if let Some((items, total)) = state
|
||||
let (items, total) = state
|
||||
.list_admin_billing_collectors(
|
||||
api_format.as_deref(),
|
||||
task_type.as_deref(),
|
||||
@@ -126,90 +106,13 @@ async fn build_admin_list_dimension_collectors_response(
|
||||
page_size,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Json(json!({
|
||||
"items": items
|
||||
.iter()
|
||||
.map(build_admin_billing_collector_payload_from_record)
|
||||
.collect::<Vec<_>>(),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": admin_billing_pages(total, page_size),
|
||||
}))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let mut total = 0_u64;
|
||||
let mut items = Vec::new();
|
||||
if let Some(pool) = state.postgres_pool() {
|
||||
let count_row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM dimension_collectors
|
||||
WHERE ($1::TEXT IS NULL OR api_format = $1)
|
||||
AND ($2::TEXT IS NULL OR task_type = $2)
|
||||
AND ($3::TEXT IS NULL OR dimension_name = $3)
|
||||
AND ($4::BOOL IS NULL OR is_enabled = $4)
|
||||
"#,
|
||||
)
|
||||
.bind(api_format.as_deref())
|
||||
.bind(task_type.as_deref())
|
||||
.bind(dimension_name.as_deref())
|
||||
.bind(is_enabled)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
total = count_row
|
||||
.try_get::<i64, _>("total")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64;
|
||||
|
||||
let offset = u64::from(page.saturating_sub(1) * page_size);
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
source_type,
|
||||
source_path,
|
||||
value_type,
|
||||
transform_expression,
|
||||
default_value,
|
||||
priority,
|
||||
is_enabled,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
FROM dimension_collectors
|
||||
WHERE ($1::TEXT IS NULL OR api_format = $1)
|
||||
AND ($2::TEXT IS NULL OR task_type = $2)
|
||||
AND ($3::TEXT IS NULL OR dimension_name = $3)
|
||||
AND ($4::BOOL IS NULL OR is_enabled = $4)
|
||||
ORDER BY updated_at DESC, priority DESC, id ASC
|
||||
OFFSET $5
|
||||
LIMIT $6
|
||||
"#,
|
||||
)
|
||||
.bind(api_format.as_deref())
|
||||
.bind(task_type.as_deref())
|
||||
.bind(dimension_name.as_deref())
|
||||
.bind(is_enabled)
|
||||
.bind(i64::try_from(offset).map_err(|err| GatewayError::Internal(err.to_string()))?)
|
||||
.bind(i64::from(page_size))
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
items = rows
|
||||
.iter()
|
||||
.map(admin_billing_collector_payload)
|
||||
.collect::<Result<Vec<_>, GatewayError>>()?;
|
||||
}
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Json(json!({
|
||||
"items": items,
|
||||
"items": items
|
||||
.iter()
|
||||
.map(build_admin_billing_collector_payload_from_record)
|
||||
.collect::<Vec<_>>(),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
@@ -229,45 +132,10 @@ async fn build_admin_get_dimension_collector_response(
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(record) = state.read_admin_billing_collector(&collector_id).await? {
|
||||
return Ok(
|
||||
Json(build_admin_billing_collector_payload_from_record(&record)).into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return Ok(build_admin_billing_not_found_response(
|
||||
"Dimension collector not found",
|
||||
));
|
||||
};
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
source_type,
|
||||
source_path,
|
||||
value_type,
|
||||
transform_expression,
|
||||
default_value,
|
||||
priority,
|
||||
is_enabled,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
FROM dimension_collectors
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&collector_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
match row {
|
||||
Some(row) => Ok(Json(admin_billing_collector_payload(&row)?).into_response()),
|
||||
match state.read_admin_billing_collector(&collector_id).await? {
|
||||
Some(record) => {
|
||||
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
|
||||
}
|
||||
None => Ok(build_admin_billing_not_found_response(
|
||||
"Dimension collector not found",
|
||||
)),
|
||||
@@ -278,7 +146,7 @@ async fn parse_admin_billing_collector_request(
|
||||
state: &AppState,
|
||||
request_body: Option<&Bytes>,
|
||||
existing_id: Option<&str>,
|
||||
) -> Result<crate::gateway::AdminBillingCollectorWriteInput, Response<Body>> {
|
||||
) -> Result<crate::AdminBillingCollectorWriteInput, Response<Body>> {
|
||||
let Some(request_body) = request_body else {
|
||||
return Err(build_admin_billing_bad_request_response("请求体不能为空"));
|
||||
};
|
||||
@@ -391,7 +259,7 @@ async fn parse_admin_billing_collector_request(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(crate::gateway::AdminBillingCollectorWriteInput {
|
||||
Ok(crate::AdminBillingCollectorWriteInput {
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
@@ -414,16 +282,16 @@ async fn build_admin_create_dimension_collector_response(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
match state.create_admin_billing_collector(&input).await? {
|
||||
crate::gateway::LocalMutationOutcome::Applied(record) => {
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::NotFound => Ok(
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Dimension collector not found"),
|
||||
),
|
||||
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法创建维度采集器"),
|
||||
),
|
||||
}
|
||||
@@ -450,16 +318,16 @@ async fn build_admin_update_dimension_collector_response(
|
||||
.update_admin_billing_collector(&collector_id, &input)
|
||||
.await?
|
||||
{
|
||||
crate::gateway::LocalMutationOutcome::Applied(record) => {
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::NotFound => Ok(
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Dimension collector not found"),
|
||||
),
|
||||
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法更新维度采集器"),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use super::{
|
||||
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
|
||||
build_admin_billing_read_only_response, normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
response::{IntoResponse, Response},
|
||||
@@ -36,9 +38,9 @@ fn build_admin_billing_presets_payload() -> serde_json::Value {
|
||||
}
|
||||
|
||||
fn build_admin_billing_aether_core_collectors(
|
||||
) -> Vec<crate::gateway::AdminBillingCollectorWriteInput> {
|
||||
) -> Vec<crate::AdminBillingCollectorWriteInput> {
|
||||
vec![
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "input_tokens".to_string(),
|
||||
@@ -50,7 +52,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "output_tokens".to_string(),
|
||||
@@ -62,7 +64,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "CLAUDE:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "input_tokens".to_string(),
|
||||
@@ -74,7 +76,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "CLAUDE:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "output_tokens".to_string(),
|
||||
@@ -86,7 +88,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "input_tokens".to_string(),
|
||||
@@ -98,7 +100,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "chat".to_string(),
|
||||
dimension_name: "output_tokens".to_string(),
|
||||
@@ -110,7 +112,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_resolution_key".to_string(),
|
||||
@@ -122,7 +124,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_resolution_key".to_string(),
|
||||
@@ -134,7 +136,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 0,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_size_bytes".to_string(),
|
||||
@@ -146,7 +148,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 0,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_duration_seconds".to_string(),
|
||||
@@ -158,7 +160,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_duration_seconds".to_string(),
|
||||
@@ -170,7 +172,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 0,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_resolution_key".to_string(),
|
||||
@@ -182,7 +184,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_resolution_key".to_string(),
|
||||
@@ -194,7 +196,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 0,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_size_bytes".to_string(),
|
||||
@@ -206,7 +208,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 0,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_duration_seconds".to_string(),
|
||||
@@ -218,7 +220,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
},
|
||||
crate::gateway::AdminBillingCollectorWriteInput {
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "GEMINI:CHAT".to_string(),
|
||||
task_type: "video".to_string(),
|
||||
dimension_name: "video_duration_seconds".to_string(),
|
||||
@@ -237,7 +239,7 @@ fn resolve_admin_billing_preset_collectors(
|
||||
preset: &str,
|
||||
) -> Option<(
|
||||
&'static str,
|
||||
Vec<crate::gateway::AdminBillingCollectorWriteInput>,
|
||||
Vec<crate::AdminBillingCollectorWriteInput>,
|
||||
)> {
|
||||
let normalized = preset.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
@@ -277,6 +279,7 @@ fn parse_admin_billing_preset_apply_request(
|
||||
|
||||
async fn build_admin_apply_billing_preset_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let (preset, mode) = match parse_admin_billing_preset_apply_request(request_body) {
|
||||
@@ -301,23 +304,32 @@ async fn build_admin_apply_billing_preset_response(
|
||||
.apply_admin_billing_preset(resolved_preset, &mode, &collectors)
|
||||
.await?
|
||||
{
|
||||
crate::gateway::LocalMutationOutcome::Applied(result) => Ok(Json(json!({
|
||||
"ok": result.errors.is_empty(),
|
||||
"preset": result.preset,
|
||||
"mode": result.mode,
|
||||
"created": result.created,
|
||||
"updated": result.updated,
|
||||
"skipped": result.skipped,
|
||||
"errors": result.errors,
|
||||
}))
|
||||
.into_response()),
|
||||
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
|
||||
crate::LocalMutationOutcome::Applied(result) => {
|
||||
let response = Json(json!({
|
||||
"ok": result.errors.is_empty(),
|
||||
"preset": result.preset,
|
||||
"mode": result.mode,
|
||||
"created": result.created,
|
||||
"updated": result.updated,
|
||||
"skipped": result.skipped,
|
||||
"errors": result.errors,
|
||||
}))
|
||||
.into_response();
|
||||
Ok(attach_admin_audit_response(
|
||||
response,
|
||||
"admin_billing_preset_applied",
|
||||
"apply_billing_preset",
|
||||
"billing_preset",
|
||||
resolved_preset,
|
||||
))
|
||||
}
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法应用计费预设"),
|
||||
),
|
||||
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::NotFound => Ok(
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing preset not found"),
|
||||
),
|
||||
}
|
||||
@@ -353,7 +365,8 @@ pub(super) async fn maybe_build_local_admin_billing_presets_response(
|
||||
) =>
|
||||
{
|
||||
Ok(Some(
|
||||
build_admin_apply_billing_preset_response(state, request_body).await?,
|
||||
build_admin_apply_billing_preset_response(state, request_context, request_body)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
_ => Ok(None),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use super::{
|
||||
admin_billing_optional_bool_filter, admin_billing_optional_epoch_value,
|
||||
admin_billing_optional_filter, admin_billing_pages, admin_billing_parse_page,
|
||||
admin_billing_parse_page_size, admin_billing_validate_safe_expression,
|
||||
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
|
||||
build_admin_billing_read_only_response, default_admin_billing_json_object,
|
||||
default_admin_billing_true, normalize_admin_billing_optional_text,
|
||||
normalize_admin_billing_required_text,
|
||||
admin_billing_optional_bool_filter, admin_billing_optional_filter, admin_billing_pages,
|
||||
admin_billing_parse_page, admin_billing_parse_page_size,
|
||||
admin_billing_validate_safe_expression, build_admin_billing_bad_request_response,
|
||||
build_admin_billing_not_found_response, build_admin_billing_read_only_response,
|
||||
default_admin_billing_json_object, default_admin_billing_true,
|
||||
normalize_admin_billing_optional_text, normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::gateway::handlers::unix_secs_to_rfc3339;
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
@@ -17,7 +17,6 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
fn default_admin_billing_rule_task_type() -> String {
|
||||
"chat".to_string()
|
||||
@@ -42,7 +41,7 @@ struct AdminBillingRuleUpsertRequest {
|
||||
}
|
||||
|
||||
fn build_admin_billing_rule_payload_from_record(
|
||||
record: &crate::gateway::AdminBillingRuleRecord,
|
||||
record: &crate::AdminBillingRuleRecord,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.id,
|
||||
@@ -74,7 +73,7 @@ fn admin_billing_rule_id_from_path(request_path: &str) -> Option<String> {
|
||||
|
||||
fn parse_admin_billing_rule_request(
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<crate::gateway::AdminBillingRuleWriteInput, Response<Body>> {
|
||||
) -> Result<crate::AdminBillingRuleWriteInput, Response<Body>> {
|
||||
let Some(request_body) = request_body else {
|
||||
return Err(build_admin_billing_bad_request_response("请求体不能为空"));
|
||||
};
|
||||
@@ -158,7 +157,7 @@ fn parse_admin_billing_rule_request(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(crate::gateway::AdminBillingRuleWriteInput {
|
||||
Ok(crate::AdminBillingRuleWriteInput {
|
||||
name,
|
||||
task_type,
|
||||
global_model_id,
|
||||
@@ -170,24 +169,6 @@ fn parse_admin_billing_rule_request(
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_billing_rule_payload(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
Ok(json!({
|
||||
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"name": row.try_get::<String, _>("name").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"task_type": row.try_get::<String, _>("task_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"global_model_id": row.try_get::<Option<String>, _>("global_model_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"model_id": row.try_get::<Option<String>, _>("model_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"expression": row.try_get::<String, _>("expression").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"variables": row.try_get::<Option<serde_json::Value>, _>("variables").map_err(|err| GatewayError::Internal(err.to_string()))?.unwrap_or_else(|| json!({})),
|
||||
"dimension_mappings": row.try_get::<Option<serde_json::Value>, _>("dimension_mappings").map_err(|err| GatewayError::Internal(err.to_string()))?.unwrap_or_else(|| json!({})),
|
||||
"is_enabled": row.try_get::<bool, _>("is_enabled").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"created_at": admin_billing_optional_epoch_value(row, "created_at_unix_secs")?,
|
||||
"updated_at": admin_billing_optional_epoch_value(row, "updated_at_unix_secs")?,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn build_admin_list_billing_rules_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -207,72 +188,20 @@ async fn build_admin_list_billing_rules_response(
|
||||
Err(detail) => return Ok(build_admin_billing_bad_request_response(detail)),
|
||||
};
|
||||
|
||||
let mut total = 0_u64;
|
||||
let mut items = Vec::new();
|
||||
if let Some((records, record_total)) = state
|
||||
let (items, total) = if let Some((records, record_total)) = state
|
||||
.list_admin_billing_rules(task_type.as_deref(), is_enabled, page, page_size)
|
||||
.await?
|
||||
{
|
||||
total = record_total;
|
||||
items = records
|
||||
.iter()
|
||||
.map(build_admin_billing_rule_payload_from_record)
|
||||
.collect::<Vec<_>>();
|
||||
} else if let Some(pool) = state.postgres_pool() {
|
||||
let count_row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM billing_rules
|
||||
WHERE ($1::TEXT IS NULL OR task_type = $1)
|
||||
AND ($2::BOOL IS NULL OR is_enabled = $2)
|
||||
"#,
|
||||
(
|
||||
records
|
||||
.iter()
|
||||
.map(build_admin_billing_rule_payload_from_record)
|
||||
.collect::<Vec<_>>(),
|
||||
record_total,
|
||||
)
|
||||
.bind(task_type.as_deref())
|
||||
.bind(is_enabled)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
total = count_row
|
||||
.try_get::<i64, _>("total")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64;
|
||||
|
||||
let offset = u64::from(page.saturating_sub(1) * page_size);
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
task_type,
|
||||
global_model_id,
|
||||
model_id,
|
||||
expression,
|
||||
variables,
|
||||
dimension_mappings,
|
||||
is_enabled,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
FROM billing_rules
|
||||
WHERE ($1::TEXT IS NULL OR task_type = $1)
|
||||
AND ($2::BOOL IS NULL OR is_enabled = $2)
|
||||
ORDER BY updated_at DESC
|
||||
OFFSET $3
|
||||
LIMIT $4
|
||||
"#,
|
||||
)
|
||||
.bind(task_type.as_deref())
|
||||
.bind(is_enabled)
|
||||
.bind(i64::try_from(offset).map_err(|err| GatewayError::Internal(err.to_string()))?)
|
||||
.bind(i64::from(page_size))
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
items = rows
|
||||
.iter()
|
||||
.map(admin_billing_rule_payload)
|
||||
.collect::<Result<Vec<_>, GatewayError>>()?;
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), 0)
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"items": items,
|
||||
@@ -291,40 +220,10 @@ async fn build_admin_get_billing_rule_response(
|
||||
let Some(rule_id) = admin_billing_rule_id_from_path(&request_context.request_path) else {
|
||||
return Ok(build_admin_billing_bad_request_response("缺少 rule_id"));
|
||||
};
|
||||
if let Some(record) = state.read_admin_billing_rule(&rule_id).await? {
|
||||
return Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response());
|
||||
}
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return Ok(build_admin_billing_not_found_response(
|
||||
"Billing rule not found",
|
||||
));
|
||||
};
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
task_type,
|
||||
global_model_id,
|
||||
model_id,
|
||||
expression,
|
||||
variables,
|
||||
dimension_mappings,
|
||||
is_enabled,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
FROM billing_rules
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&rule_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
|
||||
match row {
|
||||
Some(row) => Ok(Json(admin_billing_rule_payload(&row)?).into_response()),
|
||||
match state.read_admin_billing_rule(&rule_id).await? {
|
||||
Some(record) => {
|
||||
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
|
||||
}
|
||||
None => Ok(build_admin_billing_not_found_response(
|
||||
"Billing rule not found",
|
||||
)),
|
||||
@@ -340,16 +239,16 @@ async fn build_admin_create_billing_rule_response(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
match state.create_admin_billing_rule(&input).await? {
|
||||
crate::gateway::LocalMutationOutcome::Applied(record) => {
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::NotFound => Ok(
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing rule not found"),
|
||||
),
|
||||
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法创建计费规则"),
|
||||
),
|
||||
}
|
||||
@@ -368,16 +267,16 @@ async fn build_admin_update_billing_rule_response(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
match state.update_admin_billing_rule(&rule_id, &input).await? {
|
||||
crate::gateway::LocalMutationOutcome::Applied(record) => {
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::NotFound => Ok(
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing rule not found"),
|
||||
),
|
||||
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法更新计费规则"),
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user