// handlers/notifications.rs — 通知系统处理器 use actix_web::{web, get, put, post, delete, HttpResponse}; use serde::Deserialize; use sqlx::postgres::PgPool; use chrono::Utc; use crate::db; use crate::error::AppError; use crate::models::Claims; // ===== 查询参数 ===== #[derive(Debug, Deserialize)] pub struct NotificationListQuery { page: Option, limit: Option, } // ===== 创建通知请求 ===== #[derive(Debug, Deserialize)] pub struct CreateNotificationRequest { pub scope: String, // "all" 或 "user" pub user_id: Option, // scope="user" 时必填 pub type_: String, pub title: String, pub content: Option, pub priority: Option, pub link: Option, } // ===== 用户接口 ===== /// GET /api/notifications — 获取当前用户的通知列表 #[get("/api/notifications")] pub async fn list_notifications( pool: web::Data, claims: web::ReqData, query: web::Query, ) -> Result { let user_id = claims.user_id; let page = query.page.unwrap_or(1).max(1); let limit = query.limit.unwrap_or(20).clamp(1, 100); let offset = (page - 1) * limit; let rows = sqlx::query_as::<_, crate::models::Notification>( r#" SELECT id, scope, user_id, type, title, content, priority, link, is_read, created_at, expires_at FROM notifications WHERE scope = 'all' OR (scope = 'user' AND user_id = $1) ORDER BY created_at DESC LIMIT $2 OFFSET $3 "#, ) .bind(user_id) .bind(limit) .bind(offset) .fetch_all(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("查询通知失败: {}", e)))?; let total: i64 = sqlx::query_scalar( r#"SELECT COUNT(*) FROM notifications WHERE scope = 'all' OR (scope = 'user' AND user_id = $1)"#, ) .bind(user_id) .fetch_one(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("统计通知失败: {}", e)))?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "list": rows, "total": total, "page": page, "limit": limit, } }))) } /// PUT /api/notifications/{id}/read — 标记单条已读 #[put("/api/notifications/{id}/read")] pub async fn mark_notification_read( pool: web::Data, claims: web::ReqData, path: web::Path, ) -> Result { let notif_id = path.into_inner(); let user_id = claims.user_id; let affected = sqlx::query( r#"UPDATE notifications SET is_read = true WHERE id = $1 AND (scope = 'all' OR user_id = $2)"#, ) .bind(notif_id) .bind(user_id) .execute(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("标记已读失败: {}", e)))? .rows_affected(); if affected == 0 { return Err(AppError::NotFound("通知不存在".to_string())); } Ok(HttpResponse::Ok().json(serde_json::json!({"success": true}))) } /// PUT /api/notifications/read-all — 全部标记已读 #[put("/api/notifications/read-all")] pub async fn mark_all_read( pool: web::Data, claims: web::ReqData, ) -> Result { let user_id = claims.user_id; sqlx::query( r#"UPDATE notifications SET is_read = true WHERE (scope = 'all' OR user_id = $1) AND is_read = false"#, ) .bind(user_id) .execute(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("全部标记已读失败: {}", e)))?; Ok(HttpResponse::Ok().json(serde_json::json!({"success": true}))) } /// GET /api/notifications/unread-count — 未读通知数量 #[get("/api/notifications/unread-count")] pub async fn unread_count( pool: web::Data, claims: web::ReqData, ) -> Result { let user_id = claims.user_id; let count: i64 = sqlx::query_scalar( r#"SELECT COUNT(*) FROM notifications WHERE (scope = 'all' OR user_id = $1) AND is_read = false"#, ) .bind(user_id) .fetch_one(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("查询未读数失败: {}", e)))?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "count": count } }))) } // ===== 管理员接口 ===== /// POST /api/admin/notifications — 管理员创建通知 #[post("/api/admin/notifications")] pub async fn admin_create_notification( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> Result { let admin_id = claims.user_id; // 验证权限 let admin_user = db::get_user_by_id(pool.get_ref(), admin_id).await?; if !admin_user.is_admin { return Err(AppError::Forbidden("无权限".to_string())); } if body.scope != "all" && body.scope != "user" { return Err(AppError::BadRequest("scope 必须是 all 或 user".to_string())); } if body.scope == "user" && body.user_id.is_none() { return Err(AppError::BadRequest("scope=user 时必须指定 user_id".to_string())); } sqlx::query( r#" INSERT INTO notifications (scope, user_id, type, title, content, priority, link) VALUES ($1, $2, $3, $4, $5, $6, $7) "#, ) .bind(&body.scope) .bind(body.user_id) .bind(&body.type_) .bind(&body.title) .bind(&body.content) .bind(body.priority.as_deref().unwrap_or("normal")) .bind(&body.link) .execute(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("创建通知失败: {}", e)))?; Ok(HttpResponse::Ok().json(serde_json::json!({"success": true}))) } /// DELETE /api/admin/notifications/{id} — 管理员删除通知 #[delete("/api/admin/notifications/{id}")] pub async fn admin_delete_notification( pool: web::Data, claims: web::ReqData, path: web::Path, ) -> Result { let notif_id = path.into_inner(); let admin_user = db::get_user_by_id(pool.get_ref(), claims.user_id).await?; if !admin_user.is_admin { return Err(AppError::Forbidden("无权限".to_string())); } sqlx::query("DELETE FROM notifications WHERE id = $1") .bind(notif_id) .execute(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("删除通知失败: {}", e)))?; Ok(HttpResponse::Ok().json(serde_json::json!({"success": true}))) }