package handler import ( "context" "net/http" "time" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/model" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/service" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) // NotificationHandler 通知处理器 type NotificationHandler struct { notificationSvc *service.NotificationService logger *zap.Logger db *gorm.DB } // NewNotificationHandler 创建通知处理器 func NewNotificationHandler(notificationSvc *service.NotificationService, logger *zap.Logger, db *gorm.DB) *NotificationHandler { return &NotificationHandler{ notificationSvc: notificationSvc, logger: logger, db: db, } } // GetNotifications 获取用户通知列表(未读/已读) // @Summary 获取用户通知列表 // @Tags Notifications // @Accept json // @Produce json // @Param type query string false "通知类型 (all/alert/system/update)" // @Param unread query bool false "是否仅未读" // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications [get] func (h *NotificationHandler) GetNotifications(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } notifType := c.Query("type") unreadOnly := c.Query("unread") == "true" query := h.notificationSvc.GetDB().Where("user_id = ?", userID) // 按类型筛选 if notifType != "" && notifType != "all" { query = query.Where("type = ?", notifType) } // 只看未读 if unreadOnly { query = query.Where("is_read = ?", false) } // 查询最近 100 条通知 var notifications []model.Notification query.Order("created_at DESC").Limit(100).Find(¬ifications) c.JSON(http.StatusOK, gin.H{ "data": notifications, }) } // MarkAsRead 标记通知为已读 // @Summary 标记通知为已读 // @Tags Notifications // @Accept json // @Produce json // @Param id path uint true "通知 ID" // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications/:id/read [post] func (h *NotificationHandler) MarkAsRead(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } notifID := c.Param("id") now := time.Now() result := h.notificationSvc.GetDB().Model(&model.Notification{}). Where("id = ? AND user_id = ?", notifID, userID). Updates(map[string]interface{}{ "is_read": true, "read_at": now, }) if result.Error != nil || result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "通知不存在"}) return } c.JSON(http.StatusOK, gin.H{ "message": "已标记为已读", }) } // MarkAllAsRead 标记所有通知为已读 // @Summary 标记所有通知为已读 // @Tags Notifications // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications/read-all [post] func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } now := time.Now() result := h.notificationSvc.GetDB().Model(&model.Notification{}). Where("user_id = ? AND is_read = ?", userID, false). Updates(map[string]interface{}{ "is_read": true, "read_at": now, }) c.JSON(http.StatusOK, gin.H{ "message": "已全部标记为已读", "affected": result.RowsAffected, }) } // DeleteNotification 删除通知 // @Summary 删除通知 // @Tags Notifications // @Accept json // @Produce json // @Param id path uint true "通知 ID" // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications/:id [delete] func (h *NotificationHandler) DeleteNotification(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } notifID := c.Param("id") result := h.notificationSvc.GetDB().Where("id = ? AND user_id = ?", notifID, userID). Delete(&model.Notification{}) if result.Error != nil || result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "通知不存在"}) return } c.JSON(http.StatusOK, gin.H{ "message": "通知已删除", }) } // GetUnreadCount 获取未读通知数 // @Summary 获取未读通知数 // @Tags Notifications // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications/unread-count [get] func (h *NotificationHandler) GetUnreadCount(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } var count int64 h.notificationSvc.GetDB().Model(&model.Notification{}). Where("user_id = ? AND is_read = ?", userID, false). Count(&count) c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "count": count, }, }) } // TestSendNotification 测试发送通知(开发用) // @Summary 测试发送通知 // @Tags Notifications // @Accept json // @Produce json // @Param message body object true "通知内容" // @Success 200 {object} map[string]interface{} // @Router /api/v1/notifications/test [post] func (h *NotificationHandler) TestSendNotification(c *gin.Context) { var req struct { Type string `json:"type"` Title string `json:"title"` Message string `json:"message"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } // 从上下文获取用户 ID userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } // 发送测试通知 h.notificationSvc.SendSystemNotification(userID.(uint), req.Title, req.Message) c.JSON(http.StatusOK, gin.H{ "message": "测试通知已发送", }) } // ClearExpiredNotifications 清理过期通知(定时任务) func (h *NotificationHandler) ClearExpiredNotifications() { // 启动定期清理任务(每 24 小时清理一次超过 30 天的通知) go func() { ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for range ticker.C { h.cleanupOldNotifications() } }() h.logger.Info("清理过期通知完成") } // cleanupOldNotifications 清理超过 30 天的通知记录 func (h *NotificationHandler) cleanupOldNotifications() { ctx := context.Background() cutoffTime := time.Now().AddDate(0, 0, -30) result := h.db.WithContext(ctx). Where("created_at < ?", cutoffTime). Delete(&model.Notification{}) if result.Error != nil { h.logger.Error("清理过期通知失败", zap.Error(result.Error)) } else { h.logger.Info("清理过期通知完成", zap.Int64("deleted", result.RowsAffected)) } } // StartNotificationCleaner 启动通知清理定时器 func (h *NotificationHandler) StartNotificationCleaner(stopCh <-chan struct{}) { ticker := time.NewTicker(24 * time.Hour) // 每天执行一次 defer ticker.Stop() for { select { case <-ticker.C: h.ClearExpiredNotifications() case <-stopCh: return } } }