feat: 通知系统(notifications表+CRUD接口+管理员接口)
This commit is contained in:
214
src/handlers/notifications.rs
Normal file
214
src/handlers/notifications.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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 AS 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})))
|
||||
}
|
||||
Reference in New Issue
Block a user