feat: Events API routes migrated to Rust

- GET /api/events — list with filters (type, read, pagination)
- GET /api/events/count — unread count
- POST /api/events/{id}/read — mark one as read
- POST /api/events/read-all — mark all as read
- DELETE /api/events — clear all events
- Event model with sqlx FromRow
This commit is contained in:
boos
2026-05-17 13:28:16 +08:00
parent 8568859da6
commit af05b40799
4 changed files with 130 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
/// System event for admin notifications.
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Event {
pub id: i64,
#[sqlx(rename = "type")]
pub event_type: String,
pub title: String,
pub message: Option<String>,
pub level: Option<String>,
#[sqlx(rename = "read")]
pub is_read: Option<bool>,
pub related_id: Option<i64>,
pub related_type: Option<String>,
pub created_at: Option<String>,
}
+1
View File
@@ -2,6 +2,7 @@
pub mod account;
pub mod downstream_api_key;
pub mod event;
pub mod route_channel;
pub mod setting;
pub mod site;
+109
View File
@@ -0,0 +1,109 @@
// Events routes — system event notifications
use crate::{
db::models::event::Event,
error::{AppError, AppResult},
AppState,
};
use axum::{
extract::{Path, Query, State},
routing::{delete, get, post},
Json, Router,
};
use serde::Deserialize;
use serde_json::{json, Value};
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct ListEventsQuery {
limit: Option<i64>,
offset: Option<i64>,
#[serde(rename = "type")]
filter_type: Option<String>,
read: Option<String>, // "true" | "false"
}
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/events", get(list_events).delete(clear_events))
.route("/api/events/count", get(unread_count))
.route("/api/events/{id}/read", post(mark_read))
.route("/api/events/read-all", post(mark_all_read))
}
/// GET /api/events
async fn list_events(
State(state): State<AppState>,
Query(q): Query<ListEventsQuery>,
) -> AppResult<Json<Value>> {
info!("Listing events");
let limit = q.limit.unwrap_or(30).clamp(1, 500);
let offset = q.offset.unwrap_or(0).max(0);
let mut sql = "SELECT * FROM events WHERE 1=1".to_string();
let mut bind_idx = 0;
let mut params: Vec<String> = vec![];
if let Some(ref t) = q.filter_type {
bind_idx += 1;
sql.push_str(&format!(" AND type = ?{}", bind_idx));
params.push(t.clone());
}
if let Some(ref r) = q.read {
if r == "true" {
sql.push_str(" AND read = 1");
} else if r == "false" {
sql.push_str(" AND read = 0");
}
}
sql.push_str(" ORDER BY created_at DESC");
sql.push_str(&format!(" LIMIT {} OFFSET {}", limit, offset));
let mut query = sqlx::query_as::<_, Event>(&sql);
for p in &params {
query = query.bind(p);
}
let events: Vec<Event> = query.fetch_all(&*state.db).await?;
Ok(Json(json!({ "data": events })))
}
/// GET /api/events/count — unread count
async fn unread_count(State(state): State<AppState>) -> AppResult<Json<Value>> {
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM events WHERE read = 0")
.fetch_one(&*state.db)
.await?;
Ok(Json(json!({ "count": count.0 })))
}
/// POST /api/events/{id}/read
async fn mark_read(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> AppResult<Json<Value>> {
info!("Marking event {} as read", id);
sqlx::query("UPDATE events SET read = 1 WHERE id = ?")
.bind(id)
.execute(&*state.db)
.await?;
Ok(Json(json!({ "success": true })))
}
/// POST /api/events/read-all
async fn mark_all_read(State(state): State<AppState>) -> AppResult<Json<Value>> {
info!("Marking all events as read");
sqlx::query("UPDATE events SET read = 1 WHERE read = 0")
.execute(&*state.db)
.await?;
Ok(Json(json!({ "success": true })))
}
/// DELETE /api/events
async fn clear_events(State(state): State<AppState>) -> AppResult<Json<Value>> {
info!("Clearing all events");
sqlx::query("DELETE FROM events")
.execute(&*state.db)
.await?;
Ok(Json(json!({ "success": true })))
}
+2
View File
@@ -4,6 +4,7 @@ use axum::{routing::get, Router};
mod admin;
mod auth;
mod comfyui;
mod events;
mod health;
mod proxy;
@@ -14,5 +15,6 @@ pub fn build_router(state: AppState) -> Router {
.merge(proxy::routes())
.merge(comfyui::routes())
.merge(auth::routes())
.merge(events::routes())
.with_state(state)
}