Merge remote-tracking branch 'origin/pr/395' into aether-rust-pioneer

This commit is contained in:
fawney19
2026-05-08 02:47:07 +08:00
37 changed files with 2836 additions and 103 deletions

View File

@@ -218,7 +218,14 @@ impl AnnouncementWriteRepository for InMemoryAnnouncementReadRepository {
.expect("announcement repository lock");
let original_len = announcements.len();
announcements.retain(|announcement| announcement.id != announcement_id);
Ok(announcements.len() != original_len)
let deleted = announcements.len() != original_len;
if deleted {
self.announcement_reads
.write()
.expect("announcement reads repository lock")
.retain(|(_, read_announcement_id)| read_announcement_id != announcement_id);
}
Ok(deleted)
}
async fn mark_announcement_as_read(

View File

@@ -233,12 +233,19 @@ WHERE id = ?
}
async fn delete_announcement(&self, announcement_id: &str) -> Result<bool, DataLayerError> {
let mut tx = self.pool.begin().await.map_sql_err()?;
sqlx::query("DELETE FROM announcement_reads WHERE announcement_id = ?")
.bind(announcement_id)
.execute(&mut *tx)
.await
.map_sql_err()?;
let rows_affected = sqlx::query("DELETE FROM announcements WHERE id = ?")
.bind(announcement_id)
.execute(&self.pool)
.execute(&mut *tx)
.await
.map_sql_err()?
.rows_affected();
tx.commit().await.map_sql_err()?;
Ok(rows_affected > 0)
}

View File

@@ -163,6 +163,10 @@ const DELETE_ANNOUNCEMENT_SQL: &str = r#"
DELETE FROM announcements
WHERE id = $1
"#;
const DELETE_ANNOUNCEMENT_READS_SQL: &str = r#"
DELETE FROM announcement_reads
WHERE announcement_id = $1
"#;
const MARK_ANNOUNCEMENT_AS_READ_SQL: &str = r#"
INSERT INTO announcement_reads (
@@ -295,11 +299,18 @@ impl AnnouncementWriteRepository for SqlxAnnouncementReadRepository {
}
async fn delete_announcement(&self, announcement_id: &str) -> Result<bool, DataLayerError> {
let result = sqlx::query(DELETE_ANNOUNCEMENT_SQL)
let mut tx = self.pool.begin().await.map_postgres_err()?;
sqlx::query(DELETE_ANNOUNCEMENT_READS_SQL)
.bind(announcement_id)
.execute(&self.pool)
.execute(&mut *tx)
.await
.map_postgres_err()?;
let result = sqlx::query(DELETE_ANNOUNCEMENT_SQL)
.bind(announcement_id)
.execute(&mut *tx)
.await
.map_postgres_err()?;
tx.commit().await.map_postgres_err()?;
Ok(result.rows_affected() > 0)
}

View File

@@ -233,12 +233,19 @@ WHERE id = ?
}
async fn delete_announcement(&self, announcement_id: &str) -> Result<bool, DataLayerError> {
let mut tx = self.pool.begin().await.map_sql_err()?;
sqlx::query("DELETE FROM announcement_reads WHERE announcement_id = ?")
.bind(announcement_id)
.execute(&mut *tx)
.await
.map_sql_err()?;
let rows_affected = sqlx::query("DELETE FROM announcements WHERE id = ?")
.bind(announcement_id)
.execute(&self.pool)
.execute(&mut *tx)
.await
.map_sql_err()?
.rows_affected();
tx.commit().await.map_sql_err()?;
Ok(rows_affected > 0)
}

View File

@@ -20,3 +20,28 @@ pub struct AdminSystemStats {
pub total_api_keys: u64,
pub total_requests: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdminSystemPurgeTarget {
Config,
Users,
Usage,
AuditLogs,
RequestBodies,
Stats,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct AdminSystemPurgeSummary {
pub affected: std::collections::BTreeMap<String, u64>,
}
impl AdminSystemPurgeSummary {
pub fn add(&mut self, key: impl Into<String>, count: u64) {
*self.affected.entry(key.into()).or_insert(0) += count;
}
pub fn total(&self) -> u64 {
self.affected.values().copied().sum()
}
}

View File

@@ -329,6 +329,24 @@ impl UserReadRepository for InMemoryUserReadRepository {
if let Some(is_active) = query.is_active {
rows.retain(|row| row.is_active == is_active);
}
if let Some(search) = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
let search = search.to_ascii_lowercase();
rows.retain(|row| {
row.id.to_ascii_lowercase().contains(&search)
|| row.username.to_ascii_lowercase().contains(&search)
|| row
.email
.as_deref()
.unwrap_or_default()
.to_ascii_lowercase()
.contains(&search)
});
}
Ok(rows
.into_iter()
.skip(query.skip)
@@ -2054,6 +2072,7 @@ mod tests {
limit: 10,
role: Some("user".to_string()),
is_active: Some(true),
search: None,
})
.await
.expect("paged export should succeed");

View File

@@ -224,6 +224,22 @@ impl UserReadRepository for MysqlUserReadRepository {
if let Some(is_active) = query.is_active {
builder.push(" AND is_active = ").push_bind(is_active);
}
if let Some(search) = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
let pattern = format!("%{}%", search.to_ascii_lowercase());
builder
.push(" AND (LOWER(id) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(username) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(COALESCE(email, '')) LIKE ")
.push_bind(pattern)
.push(")");
}
builder
.push(" ORDER BY id ASC LIMIT ")
.push_bind(i64::try_from(query.limit).map_err(|_| {

View File

@@ -626,6 +626,22 @@ impl SqlxUserReadRepository {
if let Some(is_active) = query.is_active {
builder.push(" AND is_active = ").push_bind(is_active);
}
if let Some(search) = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
let pattern = format!("%{}%", search.to_ascii_lowercase());
builder
.push(" AND (LOWER(id) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(username) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(COALESCE(email, '')) LIKE ")
.push_bind(pattern)
.push(")");
}
builder
.push(" ORDER BY id ASC OFFSET ")

View File

@@ -224,6 +224,22 @@ impl UserReadRepository for SqliteUserReadRepository {
if let Some(is_active) = query.is_active {
builder.push(" AND is_active = ").push_bind(is_active);
}
if let Some(search) = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
let pattern = format!("%{}%", search.to_ascii_lowercase());
builder
.push(" AND (LOWER(id) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(username) LIKE ")
.push_bind(pattern.clone())
.push(" OR LOWER(COALESCE(email, '')) LIKE ")
.push_bind(pattern)
.push(")");
}
builder
.push(" ORDER BY id ASC LIMIT ")
.push_bind(i64::try_from(query.limit).map_err(|_| {
@@ -1543,6 +1559,7 @@ INSERT INTO users (
limit: 10,
role: Some("user".to_string()),
is_active: Some(true),
search: None,
})
.await
.expect("export page should load");

View File

@@ -428,6 +428,7 @@ pub struct UserExportListQuery {
pub limit: usize,
pub role: Option<String>,
pub is_active: Option<bool>,
pub search: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]