mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +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:
3
apps/aether-gateway/src/query/billing/mod.rs
Normal file
3
apps/aether-gateway/src/query/billing/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod sql;
|
||||
|
||||
pub(crate) use sql::*;
|
||||
726
apps/aether-gateway/src/query/billing/sql.rs
Normal file
726
apps/aether-gateway/src/query/billing/sql.rs
Normal file
@@ -0,0 +1,726 @@
|
||||
use crate::state::AdminBillingPresetApplyResult;
|
||||
use crate::{
|
||||
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingRuleRecord,
|
||||
AdminBillingRuleWriteInput, GatewayError, LocalMutationOutcome,
|
||||
};
|
||||
use aether_data::postgres::PostgresPool;
|
||||
use sqlx::Row;
|
||||
|
||||
fn internal(err: impl ToString) -> GatewayError {
|
||||
GatewayError::Internal(err.to_string())
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_billing_enabled_default_value_exists(
|
||||
pool: &PostgresPool,
|
||||
api_format: &str,
|
||||
task_type: &str,
|
||||
dimension_name: &str,
|
||||
existing_id: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM dimension_collectors
|
||||
WHERE api_format = $1
|
||||
AND task_type = $2
|
||||
AND dimension_name = $3
|
||||
AND is_enabled = TRUE
|
||||
AND default_value IS NOT NULL
|
||||
AND ($4::TEXT IS NULL OR id <> $4)
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(api_format)
|
||||
.bind(task_type)
|
||||
.bind(dimension_name)
|
||||
.bind(existing_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(internal)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_rule(
|
||||
pool: &PostgresPool,
|
||||
input: &AdminBillingRuleWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingRuleRecord>, GatewayError> {
|
||||
let rule_id = uuid::Uuid::new_v4().to_string();
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO billing_rules (
|
||||
id,
|
||||
name,
|
||||
task_type,
|
||||
global_model_id,
|
||||
model_id,
|
||||
expression,
|
||||
variables,
|
||||
dimension_mappings,
|
||||
is_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING
|
||||
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
|
||||
"#,
|
||||
)
|
||||
.bind(&rule_id)
|
||||
.bind(&input.name)
|
||||
.bind(&input.task_type)
|
||||
.bind(input.global_model_id.as_deref())
|
||||
.bind(input.model_id.as_deref())
|
||||
.bind(&input.expression)
|
||||
.bind(&input.variables)
|
||||
.bind(&input.dimension_mappings)
|
||||
.bind(input.is_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => row,
|
||||
Err(sqlx::Error::Database(err)) => {
|
||||
return Ok(LocalMutationOutcome::Invalid(format!(
|
||||
"Integrity error: {err}"
|
||||
)))
|
||||
}
|
||||
Err(err) => return Err(GatewayError::Internal(err.to_string())),
|
||||
};
|
||||
Ok(LocalMutationOutcome::Applied(admin_billing_rule_from_row(
|
||||
&row,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_billing_rules(
|
||||
pool: &PostgresPool,
|
||||
task_type: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<(Vec<AdminBillingRuleRecord>, u64), GatewayError> {
|
||||
let total = read_count(
|
||||
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)
|
||||
"#,
|
||||
)
|
||||
.bind(task_type)
|
||||
.bind(is_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(internal)?,
|
||||
)?;
|
||||
|
||||
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)
|
||||
.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(internal)?;
|
||||
|
||||
Ok((
|
||||
rows.iter()
|
||||
.map(admin_billing_rule_from_row)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
total,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_admin_billing_rule(
|
||||
pool: &PostgresPool,
|
||||
rule_id: &str,
|
||||
) -> Result<Option<AdminBillingRuleRecord>, GatewayError> {
|
||||
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(internal)?;
|
||||
row.as_ref().map(admin_billing_rule_from_row).transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_rule(
|
||||
pool: &PostgresPool,
|
||||
rule_id: &str,
|
||||
input: &AdminBillingRuleWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingRuleRecord>, GatewayError> {
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
UPDATE billing_rules
|
||||
SET
|
||||
name = $2,
|
||||
task_type = $3,
|
||||
global_model_id = $4,
|
||||
model_id = $5,
|
||||
expression = $6,
|
||||
variables = $7,
|
||||
dimension_mappings = $8,
|
||||
is_enabled = $9,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
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
|
||||
"#,
|
||||
)
|
||||
.bind(rule_id)
|
||||
.bind(&input.name)
|
||||
.bind(&input.task_type)
|
||||
.bind(input.global_model_id.as_deref())
|
||||
.bind(input.model_id.as_deref())
|
||||
.bind(&input.expression)
|
||||
.bind(&input.variables)
|
||||
.bind(&input.dimension_mappings)
|
||||
.bind(input.is_enabled)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => row,
|
||||
Err(sqlx::Error::Database(err)) => {
|
||||
return Ok(LocalMutationOutcome::Invalid(format!(
|
||||
"Integrity error: {err}"
|
||||
)))
|
||||
}
|
||||
Err(err) => return Err(GatewayError::Internal(err.to_string())),
|
||||
};
|
||||
match row {
|
||||
Some(row) => Ok(LocalMutationOutcome::Applied(admin_billing_rule_from_row(
|
||||
&row,
|
||||
)?)),
|
||||
None => Ok(LocalMutationOutcome::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_collector(
|
||||
pool: &PostgresPool,
|
||||
input: &AdminBillingCollectorWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingCollectorRecord>, GatewayError> {
|
||||
let collector_id = uuid::Uuid::new_v4().to_string();
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO dimension_collectors (
|
||||
id,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
source_type,
|
||||
source_path,
|
||||
value_type,
|
||||
transform_expression,
|
||||
default_value,
|
||||
priority,
|
||||
is_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING
|
||||
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
|
||||
"#,
|
||||
)
|
||||
.bind(&collector_id)
|
||||
.bind(&input.api_format)
|
||||
.bind(&input.task_type)
|
||||
.bind(&input.dimension_name)
|
||||
.bind(&input.source_type)
|
||||
.bind(input.source_path.as_deref())
|
||||
.bind(&input.value_type)
|
||||
.bind(input.transform_expression.as_deref())
|
||||
.bind(input.default_value.as_deref())
|
||||
.bind(input.priority)
|
||||
.bind(input.is_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => row,
|
||||
Err(sqlx::Error::Database(err)) => {
|
||||
return Ok(LocalMutationOutcome::Invalid(format!(
|
||||
"Integrity error: {err}"
|
||||
)))
|
||||
}
|
||||
Err(err) => return Err(GatewayError::Internal(err.to_string())),
|
||||
};
|
||||
Ok(LocalMutationOutcome::Applied(
|
||||
admin_billing_collector_from_row(&row)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_billing_collectors(
|
||||
pool: &PostgresPool,
|
||||
api_format: Option<&str>,
|
||||
task_type: Option<&str>,
|
||||
dimension_name: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<(Vec<AdminBillingCollectorRecord>, u64), GatewayError> {
|
||||
let total = read_count(
|
||||
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)
|
||||
.bind(task_type)
|
||||
.bind(dimension_name)
|
||||
.bind(is_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(internal)?,
|
||||
)?;
|
||||
|
||||
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)
|
||||
.bind(task_type)
|
||||
.bind(dimension_name)
|
||||
.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(internal)?;
|
||||
|
||||
Ok((
|
||||
rows.iter()
|
||||
.map(admin_billing_collector_from_row)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
total,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_admin_billing_collector(
|
||||
pool: &PostgresPool,
|
||||
collector_id: &str,
|
||||
) -> Result<Option<AdminBillingCollectorRecord>, GatewayError> {
|
||||
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(internal)?;
|
||||
row.as_ref()
|
||||
.map(admin_billing_collector_from_row)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_collector(
|
||||
pool: &PostgresPool,
|
||||
collector_id: &str,
|
||||
input: &AdminBillingCollectorWriteInput,
|
||||
) -> Result<LocalMutationOutcome<AdminBillingCollectorRecord>, GatewayError> {
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
UPDATE dimension_collectors
|
||||
SET
|
||||
api_format = $2,
|
||||
task_type = $3,
|
||||
dimension_name = $4,
|
||||
source_type = $5,
|
||||
source_path = $6,
|
||||
value_type = $7,
|
||||
transform_expression = $8,
|
||||
default_value = $9,
|
||||
priority = $10,
|
||||
is_enabled = $11,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
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
|
||||
"#,
|
||||
)
|
||||
.bind(collector_id)
|
||||
.bind(&input.api_format)
|
||||
.bind(&input.task_type)
|
||||
.bind(&input.dimension_name)
|
||||
.bind(&input.source_type)
|
||||
.bind(input.source_path.as_deref())
|
||||
.bind(&input.value_type)
|
||||
.bind(input.transform_expression.as_deref())
|
||||
.bind(input.default_value.as_deref())
|
||||
.bind(input.priority)
|
||||
.bind(input.is_enabled)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => row,
|
||||
Err(sqlx::Error::Database(err)) => {
|
||||
return Ok(LocalMutationOutcome::Invalid(format!(
|
||||
"Integrity error: {err}"
|
||||
)))
|
||||
}
|
||||
Err(err) => return Err(GatewayError::Internal(err.to_string())),
|
||||
};
|
||||
match row {
|
||||
Some(row) => Ok(LocalMutationOutcome::Applied(
|
||||
admin_billing_collector_from_row(&row)?,
|
||||
)),
|
||||
None => Ok(LocalMutationOutcome::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_billing_preset(
|
||||
pool: &PostgresPool,
|
||||
preset: &str,
|
||||
mode: &str,
|
||||
collectors: &[AdminBillingCollectorWriteInput],
|
||||
) -> Result<LocalMutationOutcome<AdminBillingPresetApplyResult>, GatewayError> {
|
||||
let mut created = 0_u64;
|
||||
let mut updated = 0_u64;
|
||||
let mut skipped = 0_u64;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for collector in collectors {
|
||||
let existing_id = match sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT id
|
||||
FROM dimension_collectors
|
||||
WHERE api_format = $1
|
||||
AND task_type = $2
|
||||
AND dimension_name = $3
|
||||
AND priority = $4
|
||||
AND is_enabled = TRUE
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&collector.api_format)
|
||||
.bind(&collector.task_type)
|
||||
.bind(&collector.dimension_name)
|
||||
.bind(collector.priority)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
errors.push(format!(
|
||||
"Failed to query collector: api_format={} task_type={} dim={}: {}",
|
||||
collector.api_format, collector.task_type, collector.dimension_name, err
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(existing_id) = existing_id {
|
||||
if mode == "overwrite" {
|
||||
match sqlx::query(
|
||||
r#"
|
||||
UPDATE dimension_collectors
|
||||
SET
|
||||
source_type = $2,
|
||||
source_path = $3,
|
||||
value_type = $4,
|
||||
transform_expression = $5,
|
||||
default_value = $6,
|
||||
is_enabled = $7,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&existing_id)
|
||||
.bind(&collector.source_type)
|
||||
.bind(collector.source_path.as_deref())
|
||||
.bind(&collector.value_type)
|
||||
.bind(collector.transform_expression.as_deref())
|
||||
.bind(collector.default_value.as_deref())
|
||||
.bind(collector.is_enabled)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => updated += 1,
|
||||
Err(err) => errors.push(format!(
|
||||
"Failed to update collector {}: {}",
|
||||
existing_id, err
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
skipped += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO dimension_collectors (
|
||||
id,
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
source_type,
|
||||
source_path,
|
||||
value_type,
|
||||
transform_expression,
|
||||
default_value,
|
||||
priority,
|
||||
is_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(uuid::Uuid::new_v4().to_string())
|
||||
.bind(&collector.api_format)
|
||||
.bind(&collector.task_type)
|
||||
.bind(&collector.dimension_name)
|
||||
.bind(&collector.source_type)
|
||||
.bind(collector.source_path.as_deref())
|
||||
.bind(&collector.value_type)
|
||||
.bind(collector.transform_expression.as_deref())
|
||||
.bind(collector.default_value.as_deref())
|
||||
.bind(collector.priority)
|
||||
.bind(collector.is_enabled)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => created += 1,
|
||||
Err(err) => errors.push(format!(
|
||||
"Failed to create collector: api_format={} task_type={} dim={}: {}",
|
||||
collector.api_format, collector.task_type, collector.dimension_name, err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LocalMutationOutcome::Applied(
|
||||
AdminBillingPresetApplyResult {
|
||||
preset: preset.to_string(),
|
||||
mode: mode.to_string(),
|
||||
created,
|
||||
updated,
|
||||
skipped,
|
||||
errors,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn read_count(row: sqlx::postgres::PgRow) -> Result<u64, GatewayError> {
|
||||
Ok(row.try_get::<i64, _>("total").map_err(internal)?.max(0) as u64)
|
||||
}
|
||||
|
||||
fn admin_billing_rule_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingRuleRecord, GatewayError> {
|
||||
Ok(AdminBillingRuleRecord {
|
||||
id: row.try_get("id").map_err(internal)?,
|
||||
name: row.try_get("name").map_err(internal)?,
|
||||
task_type: row.try_get("task_type").map_err(internal)?,
|
||||
global_model_id: row.try_get("global_model_id").map_err(internal)?,
|
||||
model_id: row.try_get("model_id").map_err(internal)?,
|
||||
expression: row.try_get("expression").map_err(internal)?,
|
||||
variables: row
|
||||
.try_get::<Option<serde_json::Value>, _>("variables")
|
||||
.map_err(internal)?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
dimension_mappings: row
|
||||
.try_get::<Option<serde_json::Value>, _>("dimension_mappings")
|
||||
.map_err(internal)?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
is_enabled: row.try_get("is_enabled").map_err(internal)?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(internal)?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(internal)?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_billing_collector_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingCollectorRecord, GatewayError> {
|
||||
Ok(AdminBillingCollectorRecord {
|
||||
id: row.try_get("id").map_err(internal)?,
|
||||
api_format: row.try_get("api_format").map_err(internal)?,
|
||||
task_type: row.try_get("task_type").map_err(internal)?,
|
||||
dimension_name: row.try_get("dimension_name").map_err(internal)?,
|
||||
source_type: row.try_get("source_type").map_err(internal)?,
|
||||
source_path: row.try_get("source_path").map_err(internal)?,
|
||||
value_type: row.try_get("value_type").map_err(internal)?,
|
||||
transform_expression: row.try_get("transform_expression").map_err(internal)?,
|
||||
default_value: row.try_get("default_value").map_err(internal)?,
|
||||
priority: row.try_get("priority").map_err(internal)?,
|
||||
is_enabled: row.try_get("is_enabled").map_err(internal)?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(internal)?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(internal)?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
2
apps/aether-gateway/src/query/mod.rs
Normal file
2
apps/aether-gateway/src/query/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod billing;
|
||||
pub(crate) mod monitoring;
|
||||
3
apps/aether-gateway/src/query/monitoring/mod.rs
Normal file
3
apps/aether-gateway/src/query/monitoring/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod sql;
|
||||
|
||||
pub(crate) use sql::*;
|
||||
261
apps/aether-gateway/src/query/monitoring/sql.rs
Normal file
261
apps/aether-gateway/src/query/monitoring/sql.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aether_data::postgres::PostgresPool;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::GatewayError;
|
||||
|
||||
fn internal(err: impl ToString) -> GatewayError {
|
||||
GatewayError::Internal(err.to_string())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_audit_logs(
|
||||
pool: &PostgresPool,
|
||||
cutoff_time: DateTime<Utc>,
|
||||
username_pattern: Option<&str>,
|
||||
event_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<Value>, usize), GatewayError> {
|
||||
let total = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM audit_logs AS a
|
||||
LEFT JOIN users AS u ON a.user_id = u.id
|
||||
WHERE a.created_at >= $1
|
||||
AND ($2::text IS NULL OR u.username ILIKE $2 ESCAPE '\')
|
||||
AND ($3::text IS NULL OR a.event_type = $3)
|
||||
"#,
|
||||
)
|
||||
.bind(cutoff_time)
|
||||
.bind(username_pattern)
|
||||
.bind(event_type)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(format!("admin audit logs count failed: {err}")))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.event_type,
|
||||
a.user_id,
|
||||
u.email AS user_email,
|
||||
u.username AS user_username,
|
||||
a.description,
|
||||
a.ip_address,
|
||||
a.status_code,
|
||||
a.error_message,
|
||||
a.event_metadata AS metadata,
|
||||
a.created_at
|
||||
FROM audit_logs AS a
|
||||
LEFT JOIN users AS u ON a.user_id = u.id
|
||||
WHERE a.created_at >= $1
|
||||
AND ($2::text IS NULL OR u.username ILIKE $2 ESCAPE '\')
|
||||
AND ($3::text IS NULL OR a.event_type = $3)
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
"#,
|
||||
)
|
||||
.bind(cutoff_time)
|
||||
.bind(username_pattern)
|
||||
.bind(event_type)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(offset).unwrap_or(i64::MAX))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(format!("admin audit logs read failed: {err}")))?;
|
||||
|
||||
Ok((
|
||||
rows.into_iter().map(admin_audit_log_row_to_json).collect(),
|
||||
usize::try_from(total.max(0)).unwrap_or(usize::MAX),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_suspicious_activities(
|
||||
pool: &PostgresPool,
|
||||
cutoff_time: DateTime<Utc>,
|
||||
) -> Result<Vec<Value>, GatewayError> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
event_type,
|
||||
user_id,
|
||||
description,
|
||||
ip_address,
|
||||
event_metadata AS metadata,
|
||||
created_at
|
||||
FROM audit_logs
|
||||
WHERE created_at >= $1
|
||||
AND event_type = ANY($2)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
"#,
|
||||
)
|
||||
.bind(cutoff_time)
|
||||
.bind(vec![
|
||||
"suspicious_activity",
|
||||
"unauthorized_access",
|
||||
"login_failed",
|
||||
"request_rate_limited",
|
||||
])
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
GatewayError::Internal(format!("admin suspicious activities read failed: {err}"))
|
||||
})?;
|
||||
|
||||
Ok(rows.into_iter().map(admin_suspicious_row_to_json).collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_user_behavior_event_counts(
|
||||
pool: &PostgresPool,
|
||||
user_id: &str,
|
||||
cutoff_time: DateTime<Utc>,
|
||||
) -> Result<BTreeMap<String, u64>, GatewayError> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT event_type, COUNT(*)::bigint AS count
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
GROUP BY event_type
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(cutoff_time)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(format!("admin user behavior read failed: {err}")))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let event_type = row.try_get::<String, _>("event_type").ok()?;
|
||||
let count = row
|
||||
.try_get::<i64, _>("count")
|
||||
.ok()
|
||||
.and_then(|value| u64::try_from(value.max(0)).ok())
|
||||
.unwrap_or(0);
|
||||
Some((event_type, count))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_user_audit_logs(
|
||||
pool: &PostgresPool,
|
||||
user_id: &str,
|
||||
cutoff_time: DateTime<Utc>,
|
||||
event_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<Value>, usize), GatewayError> {
|
||||
let total = match sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND ($3::text IS NULL OR event_type = $3)
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(cutoff_time)
|
||||
.bind(event_type)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => usize::try_from(value.max(0)).unwrap_or(usize::MAX),
|
||||
Err(err) => {
|
||||
return Err(GatewayError::Internal(format!(
|
||||
"user audit logs count failed: {err}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let rows = match sqlx::query(
|
||||
r#"
|
||||
SELECT id, event_type, description, ip_address, status_code, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND ($3::text IS NULL OR event_type = $3)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(cutoff_time)
|
||||
.bind(event_type)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(offset).unwrap_or(i64::MAX))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Err(GatewayError::Internal(format!(
|
||||
"user audit logs read failed: {err}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
rows.into_iter().map(user_audit_log_row_to_json).collect(),
|
||||
total,
|
||||
))
|
||||
}
|
||||
|
||||
fn admin_audit_log_row_to_json(row: sqlx::postgres::PgRow) -> Value {
|
||||
let created_at = row
|
||||
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
|
||||
.ok()
|
||||
.map(|value| value.to_rfc3339());
|
||||
json!({
|
||||
"id": row.try_get::<String, _>("id").ok(),
|
||||
"event_type": row.try_get::<String, _>("event_type").ok(),
|
||||
"user_id": row.try_get::<Option<String>, _>("user_id").ok().flatten(),
|
||||
"user_email": row.try_get::<Option<String>, _>("user_email").ok().flatten(),
|
||||
"user_username": row.try_get::<Option<String>, _>("user_username").ok().flatten(),
|
||||
"description": row.try_get::<Option<String>, _>("description").ok().flatten(),
|
||||
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
|
||||
"status_code": row.try_get::<Option<i32>, _>("status_code").ok().flatten(),
|
||||
"error_message": row.try_get::<Option<String>, _>("error_message").ok().flatten(),
|
||||
"metadata": row.try_get::<Option<serde_json::Value>, _>("metadata").ok().flatten(),
|
||||
"created_at": created_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_suspicious_row_to_json(row: sqlx::postgres::PgRow) -> Value {
|
||||
let created_at = row
|
||||
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
|
||||
.ok()
|
||||
.map(|value| value.to_rfc3339());
|
||||
json!({
|
||||
"id": row.try_get::<String, _>("id").ok(),
|
||||
"event_type": row.try_get::<String, _>("event_type").ok(),
|
||||
"user_id": row.try_get::<Option<String>, _>("user_id").ok().flatten(),
|
||||
"description": row.try_get::<Option<String>, _>("description").ok().flatten(),
|
||||
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
|
||||
"metadata": row.try_get::<Option<serde_json::Value>, _>("metadata").ok().flatten(),
|
||||
"created_at": created_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn user_audit_log_row_to_json(row: sqlx::postgres::PgRow) -> Value {
|
||||
let created_at = row
|
||||
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
|
||||
.ok()
|
||||
.map(|value| value.to_rfc3339());
|
||||
json!({
|
||||
"id": row.try_get::<String, _>("id").ok(),
|
||||
"event_type": row.try_get::<String, _>("event_type").ok(),
|
||||
"description": row.try_get::<String, _>("description").ok(),
|
||||
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
|
||||
"status_code": row.try_get::<Option<i32>, _>("status_code").ok().flatten(),
|
||||
"created_at": created_at,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user