Files
asd-backend/src/handlers/notifications.rs

215 lines
6.5 KiB
Rust

// 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<i32>,
limit: Option<i32>,
}
// ===== 创建通知请求 =====
#[derive(Debug, Deserialize)]
pub struct CreateNotificationRequest {
pub scope: String, // "all" 或 "user"
pub user_id: Option<i32>, // scope="user" 时必填
pub type_: String,
pub title: String,
pub content: Option<String>,
pub priority: Option<String>,
pub link: Option<String>,
}
// ===== 用户接口 =====
/// GET /api/notifications — 获取当前用户的通知列表
#[get("/api/notifications")]
pub async fn list_notifications(
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
query: web::Query<NotificationListQuery>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
claims: web::ReqData<Claims>,
path: web::Path<i32>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
claims: web::ReqData<Claims>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
claims: web::ReqData<Claims>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
claims: web::ReqData<Claims>,
body: web::Json<CreateNotificationRequest>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
claims: web::ReqData<Claims>,
path: web::Path<i32>,
) -> Result<HttpResponse, AppError> {
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})))
}