fix(sql): fix sql script
This commit is contained in:
@@ -1,21 +1,35 @@
|
||||
use crate::models::{
|
||||
CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||||
};
|
||||
use crate::models::{CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest};
|
||||
use common_telemetry::AppError;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EnabledAppsCacheEntry {
|
||||
enabled_apps: Vec<String>,
|
||||
version: i32,
|
||||
updated_at: String,
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TenantService {
|
||||
pool: PgPool,
|
||||
enabled_apps_cache: Arc<RwLock<HashMap<Uuid, EnabledAppsCacheEntry>>>,
|
||||
}
|
||||
|
||||
impl TenantService {
|
||||
/// 创建租户服务实例。
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
Self {
|
||||
pool,
|
||||
enabled_apps_cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, req))]
|
||||
@@ -31,17 +45,43 @@ impl TenantService {
|
||||
/// 异常:
|
||||
/// - 数据库写入失败(如连接异常、约束失败等)
|
||||
pub async fn create_tenant(&self, req: CreateTenantRequest) -> Result<Tenant, AppError> {
|
||||
let config = req.config.unwrap_or_else(|| Value::Object(Default::default()));
|
||||
let mut config = req
|
||||
.config
|
||||
.unwrap_or_else(|| Value::Object(Default::default()));
|
||||
if !config.is_object() {
|
||||
config = Value::Object(Default::default());
|
||||
}
|
||||
if let Some(obj) = config.as_object_mut() {
|
||||
obj.insert("enabled_apps".to_string(), Value::Array(vec![]));
|
||||
obj.insert(
|
||||
"enabled_apps_version".to_string(),
|
||||
Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
}
|
||||
let query = r#"
|
||||
INSERT INTO tenants (name, status, config)
|
||||
VALUES ($1, 'active', $2)
|
||||
RETURNING id, name, status, config
|
||||
"#;
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let tenant = sqlx::query_as::<_, Tenant>(query)
|
||||
.bind(req.name)
|
||||
.bind(config)
|
||||
.fetch_one(&self.pool)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tenant_entitlements (tenant_id, enabled_apps, version)
|
||||
VALUES ($1, '{}'::text[], 0)
|
||||
ON CONFLICT (tenant_id) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(tenant.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(tenant)
|
||||
}
|
||||
|
||||
@@ -131,4 +171,228 @@ impl TenantService {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_enabled_apps(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
) -> Result<(Vec<String>, i32, String), AppError> {
|
||||
let now = Instant::now();
|
||||
if let Some(hit) = self
|
||||
.enabled_apps_cache
|
||||
.read()
|
||||
.await
|
||||
.get(&tenant_id)
|
||||
.cloned()
|
||||
{
|
||||
if hit.expires_at > now {
|
||||
return Ok((hit.enabled_apps, hit.version, hit.updated_at));
|
||||
}
|
||||
}
|
||||
|
||||
let row = sqlx::query_as::<_, (Vec<String>, i32, chrono::DateTime<chrono::Utc>)>(
|
||||
r#"
|
||||
SELECT enabled_apps, version, updated_at
|
||||
FROM tenant_entitlements
|
||||
WHERE tenant_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
let (enabled_apps, version, updated_at) = match row {
|
||||
Some((apps, v, ts)) => (apps, v, ts.to_rfc3339()),
|
||||
None => {
|
||||
let exists: Option<Uuid> =
|
||||
sqlx::query_scalar("SELECT id FROM tenants WHERE id = $1")
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
if exists.is_none() {
|
||||
return Err(AppError::NotFound("Tenant not found".into()));
|
||||
}
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
(vec![], 0, ts)
|
||||
}
|
||||
};
|
||||
|
||||
let ttl = Duration::from_secs(60);
|
||||
self.enabled_apps_cache.write().await.insert(
|
||||
tenant_id,
|
||||
EnabledAppsCacheEntry {
|
||||
enabled_apps: enabled_apps.clone(),
|
||||
version,
|
||||
updated_at: updated_at.clone(),
|
||||
expires_at: now + ttl,
|
||||
},
|
||||
);
|
||||
Ok((enabled_apps, version, updated_at))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, enabled_apps))]
|
||||
pub async fn set_enabled_apps(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
enabled_apps: Vec<String>,
|
||||
expected_version: Option<i32>,
|
||||
actor_user_id: Uuid,
|
||||
) -> Result<(Vec<String>, i32, String), AppError> {
|
||||
let normalized = normalize_apps(enabled_apps);
|
||||
self.validate_apps_exist(&normalized).await?;
|
||||
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
let current = sqlx::query_as::<_, (Vec<String>, i32)>(
|
||||
r#"
|
||||
SELECT enabled_apps, version
|
||||
FROM tenant_entitlements
|
||||
WHERE tenant_id = $1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if current.is_none() {
|
||||
let exists: Option<Uuid> = sqlx::query_scalar("SELECT id FROM tenants WHERE id = $1")
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
if exists.is_none() {
|
||||
return Err(AppError::NotFound("Tenant not found".into()));
|
||||
}
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tenant_entitlements (tenant_id, enabled_apps, version)
|
||||
VALUES ($1, '{}'::text[], 0)
|
||||
ON CONFLICT (tenant_id) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let (before_apps, before_version) = current.unwrap_or_else(|| (vec![], 0));
|
||||
if let Some(ev) = expected_version {
|
||||
if ev != before_version {
|
||||
return Err(AppError::AlreadyExists(
|
||||
"enabled_apps:version_conflict".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let (new_version, updated_at): (i32, chrono::DateTime<chrono::Utc>) = sqlx::query_as(
|
||||
r#"
|
||||
UPDATE tenant_entitlements
|
||||
SET enabled_apps = $1,
|
||||
version = version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $2
|
||||
RETURNING version, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(&normalized)
|
||||
.bind(tenant_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE tenants
|
||||
SET config =
|
||||
jsonb_set(
|
||||
jsonb_set(COALESCE(config, '{}'::jsonb), '{enabled_apps}', to_jsonb($1::text[]), true),
|
||||
'{enabled_apps_version}', to_jsonb($2::int), true
|
||||
),
|
||||
updated_at = NOW()
|
||||
WHERE id = $3
|
||||
"#,
|
||||
)
|
||||
.bind(&normalized)
|
||||
.bind(new_version)
|
||||
.bind(tenant_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tenant_enabled_apps_history (tenant_id, version, enabled_apps, actor_user_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.bind(new_version)
|
||||
.bind(&normalized)
|
||||
.bind(actor_user_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO audit_logs (tenant_id, user_id, action, resource, status, details)
|
||||
VALUES ($1, $2, 'tenant.enabled_apps.update', 'tenant', 'allow', $3)
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.bind(actor_user_id)
|
||||
.bind(serde_json::json!({
|
||||
"before": { "enabled_apps": before_apps, "version": before_version },
|
||||
"after": { "enabled_apps": normalized, "version": new_version }
|
||||
}))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let ttl = Duration::from_secs(60);
|
||||
let now = Instant::now();
|
||||
let updated_at = updated_at.to_rfc3339();
|
||||
self.enabled_apps_cache.write().await.insert(
|
||||
tenant_id,
|
||||
EnabledAppsCacheEntry {
|
||||
enabled_apps: normalized.clone(),
|
||||
version: new_version,
|
||||
updated_at: updated_at.clone(),
|
||||
expires_at: now + ttl,
|
||||
},
|
||||
);
|
||||
|
||||
Ok((normalized, new_version, updated_at))
|
||||
}
|
||||
|
||||
async fn validate_apps_exist(&self, enabled_apps: &[String]) -> Result<(), AppError> {
|
||||
if enabled_apps.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let rows: Vec<String> = sqlx::query_scalar("SELECT id FROM apps WHERE id = ANY($1)")
|
||||
.bind(enabled_apps)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
let found: HashSet<String> = rows.into_iter().collect();
|
||||
for app in enabled_apps {
|
||||
if !found.contains(app) {
|
||||
return Err(AppError::BadRequest(format!("Unknown app: {app}")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_apps(enabled_apps: Vec<String>) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for a in enabled_apps {
|
||||
let v = a.trim().to_lowercase();
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(v.clone()) {
|
||||
out.push(v);
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user