package controllers import ( "context" "crypto/rand" "encoding/json" "fmt" "io" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "server/services" beego "github.com/beego/beego/v2/server/web" ) type BackendReminderController struct { beego.Controller } func (c *BackendReminderController) backendReminderClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("认证信息格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("无效的token") } if claims.UserType != "backend" { return nil, fmt.Errorf("无权访问") } return claims, nil } func (c *BackendReminderController) brJsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *BackendReminderController) brOk(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } func backendGenerateToken() string { b := make([]byte, 16) _, _ = rand.Read(b) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) } type backendReminderFormPayload struct { Title string `json:"title"` Content string `json:"content"` ScheduleTime string `json:"schedule_time"` RemindChannels []string `json:"remind_channels"` AdvanceMinutes int `json:"advance_minutes"` RepeatIntervalMinutes int `json:"repeat_interval_minutes"` MaxSendCount int `json:"max_send_count"` ReceiverUserID uint64 `json:"receiver_user_id"` ReceiverTargets map[string]string `json:"receiver_targets"` } // GetReminderList GET /backend/reminder/list func (c *BackendReminderController) GetReminderList() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) keyword := strings.TrimSpace(c.GetString("keyword")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } qs := models.Orm.QueryTable(new(models.BackendSchedule)). Filter("tid", claims.TenantId) if keyword != "" { qs = qs.Filter("content__contains", keyword) } total, _ := qs.Count() var schedules []models.BackendSchedule _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules) if err != nil { c.brJsonErr(500, 500, "查询失败: "+err.Error()) return } list := make([]map[string]interface{}, 0, len(schedules)) for _, s := range schedules { var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", s.ID). Filter("is_deleted", 0). All(&reminders) channels := make([]string, 0, len(reminders)) isFinished := true if len(reminders) == 0 { isFinished = false } else { for _, r := range reminders { channels = append(channels, r.RemindChannel) if r.RemindStatus != 2 { isFinished = false } } } item := map[string]interface{}{ "id": s.ID, "title": s.Title, "content": s.Content, "schedule_time": s.ScheduleTime.Format("2006-01-02 15:04:05"), "remind_channels": channels, "user_id": s.UserID, "is_finished": isFinished, } if len(reminders) > 0 { first := reminders[0] item["advance_minutes"] = first.AdvanceMinutes item["repeat_interval_minutes"] = first.RepeatIntervalMinutes item["max_send_count"] = first.MaxSendCount item["receiver_user_id"] = first.ReceiverUserID } list = append(list, item) } c.brOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // GetReminderDetail GET /backend/reminder/:id func (c *BackendReminderController) GetReminderDetail() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.brJsonErr(400, 400, "无效的ID") return } var schedule models.BackendSchedule err = models.Orm.QueryTable(new(models.BackendSchedule)). Filter("id", id). Filter("tid", claims.TenantId). One(&schedule) if err != nil { c.brJsonErr(404, 404, "日程未找到") return } var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", schedule.ID). Filter("is_deleted", 0). All(&reminders) channels := make([]string, 0, len(reminders)) targets := make(map[string]string) var first models.BackendScheduleReminder for _, r := range reminders { channels = append(channels, r.RemindChannel) if r.ReceiverTarget != nil { targets[r.RemindChannel] = *r.ReceiverTarget } first = r } isFinished := true if len(reminders) == 0 { isFinished = false } else { for _, r := range reminders { if r.RemindStatus != 2 { isFinished = false break } } } data := map[string]interface{}{ "id": schedule.ID, "title": schedule.Title, "content": schedule.Content, "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"), "remind_channels": channels, "receiver_targets": targets, "is_finished": isFinished, } if first.ID > 0 { data["advance_minutes"] = first.AdvanceMinutes data["repeat_interval_minutes"] = first.RepeatIntervalMinutes data["max_send_count"] = first.MaxSendCount data["receiver_user_id"] = first.ReceiverUserID } c.brOk(data) } // CreateReminder POST /backend/reminder func (c *BackendReminderController) CreateReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.brJsonErr(400, 400, "参数错误") return } var p backendReminderFormPayload if err := json.Unmarshal(raw, &p); err != nil { c.brJsonErr(400, 400, "参数错误") return } if strings.TrimSpace(p.ScheduleTime) == "" { c.brJsonErr(400, 400, "日程发生时间不能为空") return } schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local) if err != nil { c.brJsonErr(400, 400, "日程时间格式不合法,支持 YYYY-MM-DD HH:mm:ss") return } content := strings.TrimSpace(p.Content) title := strings.SplitN(content, "\n", 2)[0] if len(title) > 60 { title = title[:60] } if title == "" { title = "日程提醒" } schedule := models.BackendSchedule{ Tid: claims.TenantId, Title: title, Content: content, ScheduleTime: schedTime, UserID: uint64(claims.UserID), } schedID, err := models.Orm.Insert(&schedule) if err != nil { c.brJsonErr(500, 500, "保存日程失败: "+err.Error()) return } for _, ch := range p.RemindChannels { ch = strings.ToUpper(strings.TrimSpace(ch)) if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" { continue } targetVal := p.ReceiverTargets[ch] var target *string if targetVal != "" { target = &targetVal } firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) reminder := models.BackendScheduleReminder{ Tid: claims.TenantId, ScheduleID: uint64(schedID), RemindChannel: ch, AdvanceMinutes: p.AdvanceMinutes, NextRemindTime: firstSendTime, ReceiverUserID: uint64(claims.UserID), ReceiverTarget: target, RemindStatus: 0, CreateTime: time.Now(), UpdateTime: time.Now(), } if ch == "EMAIL" || ch == "BARK" { token := backendGenerateToken() reminder.AckToken = &token reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes reminder.MaxSendCount = p.MaxSendCount if reminder.MaxSendCount <= 0 { reminder.MaxSendCount = 1 } } else { reminder.RepeatIntervalMinutes = 0 reminder.MaxSendCount = 1 } _, err = models.Orm.Insert(&reminder) if err != nil { c.brJsonErr(500, 500, "创建提醒失败: "+err.Error()) return } } c.brOk(map[string]interface{}{"schedule_id": schedID}) } // UpdateReminder PUT /backend/reminder/:id func (c *BackendReminderController) UpdateReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.brJsonErr(400, 400, "无效的ID") return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.brJsonErr(400, 400, "参数错误") return } var p backendReminderFormPayload if err := json.Unmarshal(raw, &p); err != nil { c.brJsonErr(400, 400, "参数错误") return } schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local) if err != nil { c.brJsonErr(400, 400, "日程时间格式不合法") return } var schedule models.BackendSchedule err = models.Orm.QueryTable(new(models.BackendSchedule)). Filter("id", id). Filter("tid", claims.TenantId). One(&schedule) if err != nil { c.brJsonErr(404, 404, "日程未找到") return } var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", id). Filter("is_deleted", 0). All(&reminders) isFinished := true if len(reminders) == 0 { isFinished = false } else { for _, r := range reminders { if r.RemindStatus != 2 { isFinished = false break } } } if isFinished { c.brJsonErr(400, 400, "该日程提醒已全部结束,无法编辑") return } content := strings.TrimSpace(p.Content) if content == "" { content = schedule.Content } title := strings.SplitN(content, "\n", 2)[0] if len(title) > 60 { title = title[:60] } schedule.Title = title schedule.Content = content schedule.ScheduleTime = schedTime _, err = models.Orm.Update(&schedule, "Title", "Content", "ScheduleTime") if err != nil { c.brJsonErr(500, 500, "更新失败") return } _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", id). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) for _, ch := range p.RemindChannels { ch = strings.ToUpper(strings.TrimSpace(ch)) if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" { continue } targetVal := p.ReceiverTargets[ch] var target *string if targetVal != "" { target = &targetVal } firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) reminder := models.BackendScheduleReminder{ Tid: claims.TenantId, ScheduleID: id, RemindChannel: ch, AdvanceMinutes: p.AdvanceMinutes, NextRemindTime: firstSendTime, ReceiverUserID: schedule.UserID, ReceiverTarget: target, RemindStatus: 0, CreateTime: time.Now(), UpdateTime: time.Now(), } if ch == "EMAIL" || ch == "BARK" { token := backendGenerateToken() reminder.AckToken = &token reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes reminder.MaxSendCount = p.MaxSendCount if reminder.MaxSendCount <= 0 { reminder.MaxSendCount = 1 } } else { reminder.RepeatIntervalMinutes = 0 reminder.MaxSendCount = 1 } _, err = models.Orm.Insert(&reminder) if err != nil { c.brJsonErr(500, 500, "重新创建提醒失败") return } } c.brOk(nil) } // DeleteReminder DELETE /backend/reminder/:id func (c *BackendReminderController) DeleteReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.brJsonErr(400, 400, "无效的ID") return } var schedule models.BackendSchedule err = models.Orm.QueryTable(new(models.BackendSchedule)). Filter("id", id). Filter("tid", claims.TenantId). One(&schedule) if err != nil { c.brJsonErr(404, 404, "日程未找到") return } var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", id). Filter("is_deleted", 0). All(&reminders) isFinished := true if len(reminders) == 0 { isFinished = false } else { for _, r := range reminders { if r.RemindStatus != 2 { isFinished = false break } } } if isFinished { c.brJsonErr(400, 400, "该日程提醒已全部结束,无法删除") return } _, err = models.Orm.QueryTable(new(models.BackendSchedule)).Filter("id", id).Delete() if err == nil { _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", id). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) } c.brOk(nil) } type backendReminderBatchDeletePayload struct { Ids []uint64 `json:"ids"` } // BatchDeleteReminder POST /backend/reminder/batchDelete func (c *BackendReminderController) BatchDeleteReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.brJsonErr(400, 400, "参数错误") return } var p backendReminderBatchDeletePayload if err := json.Unmarshal(raw, &p); err != nil { c.brJsonErr(400, 400, "参数错误") return } if len(p.Ids) == 0 { c.brOk(nil) return } for _, scheduleID := range p.Ids { var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", scheduleID). Filter("is_deleted", 0). All(&reminders) isFinished := true if len(reminders) == 0 { isFinished = false } else { for _, r := range reminders { if r.RemindStatus != 2 { isFinished = false break } } } if isFinished { c.brJsonErr(400, 400, fmt.Sprintf("日程ID %d 的提醒已全部结束,无法删除", scheduleID)) return } } _, _ = models.Orm.QueryTable(new(models.BackendSchedule)). Filter("id__in", p.Ids). Filter("tid", claims.TenantId). Delete() _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id__in", p.Ids). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) c.brOk(nil) } // FinishReminder POST /backend/reminder/finish/:id func (c *BackendReminderController) FinishReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.brJsonErr(400, 400, "无效的ID") return } var schedule models.BackendSchedule err = models.Orm.QueryTable(new(models.BackendSchedule)). Filter("id", id). Filter("tid", claims.TenantId). One(&schedule) if err != nil { c.brJsonErr(404, 404, "日程未找到") return } var reminders []models.BackendScheduleReminder _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)). Filter("schedule_id", id). Filter("is_deleted", 0). All(&reminders) allFinished := len(reminders) > 0 for _, r := range reminders { if r.RemindStatus != 2 { allFinished = false break } } var newStatus int8 if allFinished { newStatus = 0 } else { newStatus = 2 } now := time.Now() for _, r := range reminders { r.RemindStatus = newStatus r.UpdateTime = now models.Orm.Update(&r, "RemindStatus", "UpdateTime") } c.brOk(map[string]interface{}{"is_finished": newStatus == 2}) } type backendReminderTestPayload struct { Title string `json:"title"` Content string `json:"content"` RemindChannels []string `json:"remind_channels"` } // TestReminder POST /backend/reminder/test func (c *BackendReminderController) TestReminder() { claims, err := c.backendReminderClaims() if err != nil { c.brJsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.brJsonErr(400, 400, "参数错误") return } var p backendReminderTestPayload if err := json.Unmarshal(raw, &p); err != nil { c.brJsonErr(400, 400, "参数错误") return } if strings.TrimSpace(p.Title) == "" { p.Title = "测试提醒" } if strings.TrimSpace(p.Content) == "" { p.Content = "这是一条验证日程提醒配置的测试通知。" } senders := map[string]services.ReminderSender{ "SMS": &services.SMSSender{}, "EMAIL": &services.EmailSender{}, "BARK": &services.BarkSender{}, "SITE_MSG": &services.SiteMsgSender{}, } type TestResult struct { Channel string `json:"channel"` Success bool `json:"success"` Msg string `json:"msg"` } results := make([]TestResult, 0) for _, ch := range p.RemindChannels { ch = strings.ToUpper(strings.TrimSpace(ch)) sender, ok := senders[ch] if !ok { results = append(results, TestResult{Channel: ch, Success: false, Msg: "不支持的提醒渠道"}) continue } dummyToken := "test-token-for-verification" reminder := &models.BackendScheduleReminder{ Tid: claims.TenantId, RemindChannel: ch, ReceiverUserID: uint64(claims.UserID), AckToken: &dummyToken, } success, sendErr := sender.Send(context.Background(), services.ReminderData{ RemindChannel: reminder.RemindChannel, ReceiverUserID: reminder.ReceiverUserID, ReceiverTarget: reminder.ReceiverTarget, AckToken: reminder.AckToken, Tid: reminder.Tid, }, "[测试]"+p.Title, p.Content) msg := "发送成功" if !success { msg = "发送失败" if sendErr != nil { msg = sendErr.Error() } } results = append(results, TestResult{Channel: ch, Success: success, Msg: msg}) } c.brOk(results) }