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:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42654 additions and 21464 deletions
@@ -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("当前为只读模式,无法更新计费规则"),
),
}