package handler import ( "encoding/json" "html" "git.yeij.top/AskaEth/Cyrene/pkg/logger" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "git.yeij.top/AskaEth/Cyrene/gateway/internal/middleware" "git.yeij.top/AskaEth/Cyrene/gateway/internal/store" "git.yeij.top/AskaEth/Cyrene/gateway/internal/ws" ) // ReminderHandler 提醒处理器 type ReminderHandler struct { store *store.ReminderStore hub *ws.Hub } // NewReminderHandler 创建提醒处理器 func NewReminderHandler(s *store.ReminderStore, hub *ws.Hub) *ReminderHandler { return &ReminderHandler{store: s, hub: hub} } // CreateReminderRequest 创建提醒请求体 type CreateReminderRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description"` RemindAt string `json:"remind_at" binding:"required"` // ISO 8601 格式 RepeatType string `json:"repeat_type"` // none, daily, weekly, monthly SessionID string `json:"session_id"` } // UpdateReminderRequest 更新提醒请求体 type UpdateReminderRequest struct { Title string `json:"title"` Description string `json:"description"` RemindAt string `json:"remind_at"` Status string `json:"status"` // pending, completed, cancelled RepeatType string `json:"repeat_type"` // none, daily, weekly, monthly SessionID string `json:"session_id"` } // List 获取提醒列表 // GET /api/v1/reminders?user_id=xxx&status=pending&limit=50&offset=0 func (h *ReminderHandler) List(c *gin.Context) { userID := c.Query("user_id") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 user_id 参数"}) return } status := c.Query("status") limit := 50 offset := 0 if l, ok := c.GetQuery("limit"); ok { if v, err := strconv.Atoi(l); err == nil && v > 0 { limit = v } } if o, ok := c.GetQuery("offset"); ok { if v, err := strconv.Atoi(o); err == nil && v >= 0 { offset = v } } reminders, err := h.store.GetRemindersByUser(userID, status, limit, offset) if err != nil { logger.Printf("[reminder] 获取提醒列表失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "获取提醒列表失败"}) return } c.JSON(http.StatusOK, gin.H{ "reminders": reminders, "count": len(reminders), }) } // Create 创建新提醒 // POST /api/v1/reminders func (h *ReminderHandler) Create(c *gin.Context) { var req CreateReminderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效: " + err.Error()}) return } // 从 JWT 获取 userID userID := middleware.GetUserID(c) if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) return } // 解析时间 remindAt, err := time.Parse(time.RFC3339, req.RemindAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "时间格式无效,请使用 ISO 8601 格式 (例如 2024-01-01T15:00:00Z)"}) return } // 默认值 repeatType := req.RepeatType if repeatType == "" { repeatType = "none" } reminder := &store.Reminder{ ID: generateID(), UserID: userID, Title: html.EscapeString(req.Title), Description: html.EscapeString(req.Description), RemindAt: remindAt, Status: "pending", RepeatType: repeatType, SessionID: req.SessionID, Notified: false, } if err := h.store.CreateReminder(reminder); err != nil { logger.Printf("[reminder] 创建提醒失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "创建提醒失败"}) return } logger.Printf("[reminder] 提醒已创建: id=%s user=%s title=%s remind_at=%s repeat=%s", reminder.ID, userID, reminder.Title, remindAt.Format(time.RFC3339), repeatType) c.JSON(http.StatusCreated, gin.H{ "success": true, "reminder": reminder, }) } // Update 更新提醒 // PUT /api/v1/reminders/:id func (h *ReminderHandler) Update(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少提醒 ID"}) return } userID := middleware.GetUserID(c) var req UpdateReminderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效: " + err.Error()}) return } // 先获取已有提醒 reminders, err := h.store.GetRemindersByUser(userID, "", 100, 0) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取提醒失败"}) return } var existing *store.Reminder for i := range reminders { if reminders[i].ID == id { existing = &reminders[i] break } } if existing == nil { c.JSON(http.StatusNotFound, gin.H{"error": "提醒不存在"}) return } // 更新字段 if req.Title != "" { existing.Title = req.Title } if req.Description != "" { existing.Description = req.Description } if req.RemindAt != "" { remindAt, err := time.Parse(time.RFC3339, req.RemindAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "时间格式无效"}) return } existing.RemindAt = remindAt } if req.Status != "" { existing.Status = req.Status if req.Status == "completed" || req.Status == "cancelled" { now := time.Now() existing.CompletedAt = &now } } if req.RepeatType != "" { existing.RepeatType = req.RepeatType } if req.SessionID != "" { existing.SessionID = req.SessionID } if err := h.store.UpdateReminder(id, existing); err != nil { logger.Printf("[reminder] 更新提醒失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "更新提醒失败"}) return } logger.Printf("[reminder] 提醒已更新: id=%s", id) c.JSON(http.StatusOK, gin.H{ "success": true, "reminder": existing, }) } // Delete 删除提醒 // DELETE /api/v1/reminders/:id func (h *ReminderHandler) Delete(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少提醒 ID"}) return } if err := h.store.DeleteReminder(id); err != nil { logger.Printf("[reminder] 删除提醒失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "删除提醒失败"}) return } logger.Printf("[reminder] 提醒已删除: id=%s", id) c.JSON(http.StatusOK, gin.H{ "success": true, }) } // ========== 提醒调度器 ========== // StartReminderScheduler 启动提醒调度器,每 30 秒检查一次到期提醒 func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) { go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() logger.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)") for range ticker.C { checkAndNotify(s, hub) } }() } // checkAndNotify 检查到期提醒并推送通知 func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) { reminders, err := s.GetDueReminders() if err != nil { logger.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err) return } if len(reminders) == 0 { return } for _, r := range reminders { // 1. 构建 WebSocket 通知消息 notif := &ws.NotificationInfo{ ID: "reminder_" + r.ID, Type: "reminder", Title: r.Title, Body: r.Description, Timestamp: time.Now().UTC().Format(time.RFC3339), Data: map[string]interface{}{ "reminder_id": r.ID, "session_id": r.SessionID, }, } msg := ws.ServerMessage{ Type: "notification", MessageID: "reminder_" + r.ID, Timestamp: time.Now().UnixMilli(), Notification: notif, } data, err := json.Marshal(msg) if err != nil { logger.Printf("[ReminderScheduler] 序列化通知失败: %v", err) continue } // 2. 通过 Hub 向用户推送 hub.SendToUser(r.UserID, data) // 3. 标记为已通知 if err := s.MarkNotified(r.ID); err != nil { logger.Printf("[ReminderScheduler] 标记已通知失败: id=%s err=%v", r.ID, err) } // 4. 处理重复提醒 if r.RepeatType != "" && r.RepeatType != "none" { nextTime := calculateNextRemindAt(r.RemindAt, r.RepeatType) r.RemindAt = nextTime r.Notified = false if err := s.UpdateReminder(r.ID, &r); err != nil { logger.Printf("[ReminderScheduler] 更新重复提醒失败: id=%s err=%v", r.ID, err) } else { logger.Printf("[ReminderScheduler] 重复提醒已更新: id=%s next=%s", r.ID, nextTime.Format(time.RFC3339)) } } else { // 非重复提醒:标记为已完成 now := time.Now() r.Status = "completed" r.CompletedAt = &now if err := s.UpdateReminder(r.ID, &r); err != nil { logger.Printf("[ReminderScheduler] 标记提醒完成失败: id=%s err=%v", r.ID, err) } } logger.Printf("[ReminderScheduler] 提醒已推送: user=%s title=%s id=%s", r.UserID, r.Title, r.ID) } } // calculateNextRemindAt 计算下一次提醒时间 func calculateNextRemindAt(current time.Time, repeatType string) time.Time { switch repeatType { case "daily": return current.Add(24 * time.Hour) case "weekly": return current.Add(7 * 24 * time.Hour) case "monthly": return current.AddDate(0, 1, 0) default: return current } }