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 PlatformReminderController struct { beego.Controller } func (c *PlatformReminderController) platformClaims() (*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 != "platform" { return nil, fmt.Errorf("无权访问") } return claims, nil } func (c *PlatformReminderController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *PlatformReminderController) ok(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } // generateToken 生成一个随机的 ack_token func generateToken() 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 reminderFormPayload struct { Title string `json:"title"` Content string `json:"content"` ScheduleTime string `json:"schedule_time"` RemindChannels []string `json:"remind_channels"` // EMAIL, BARK, SMS, SITE_MSG 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"` // "SMS": "1380...", "EMAIL": "...", "BARK": "..." } // GetReminderList GET /platform/reminder/list func (c *PlatformReminderController) GetReminderList() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } // 联表获取日程及提醒信息 var schedules []models.PlatformSchedule qs := models.Orm.QueryTable(new(models.PlatformSchedule)) total, _ := qs.Count() _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules) if err != nil { c.jsonErr(500, 500, "查询失败: "+err.Error()) return } list := make([]map[string]interface{}, 0, len(schedules)) for _, s := range schedules { // 查询该日程关联的所有提醒记录 var reminders []models.PlatformScheduleReminder _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). 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.ok(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // GetReminderDetail GET /platform/reminder/:id func (c *PlatformReminderController) GetReminderDetail() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.jsonErr(400, 400, "无效的ID") return } var schedule models.PlatformSchedule err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule) if err != nil { c.jsonErr(404, 404, "日程未找到") return } var reminders []models.PlatformScheduleReminder _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). 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.PlatformScheduleReminder 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.ok(data) } // CreateReminder POST /platform/reminder func (c *PlatformReminderController) CreateReminder() { claims, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p reminderFormPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if strings.TrimSpace(p.ScheduleTime) == "" { c.jsonErr(400, 400, "日程发生时间不能为空") return } schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local) if err != nil { c.jsonErr(400, 400, "日程时间格式不合法,支持 YYYY-MM-DD HH:mm:ss") return } // 1. 插入日程主表 schedule := models.PlatformSchedule{ Title: "日程提醒", Content: p.Content, ScheduleTime: schedTime, UserID: uint64(claims.UserID), } schedID, err := models.Orm.Insert(&schedule) if err != nil { c.jsonErr(500, 500, "保存日程失败: "+err.Error()) return } // 2. 根据选中的渠道循环创建提醒 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.PlatformScheduleReminder{ 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 := generateToken() reminder.AckToken = &token reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes reminder.MaxSendCount = p.MaxSendCount if reminder.MaxSendCount <= 0 { reminder.MaxSendCount = 1 } } else { // SMS 或 SITE_MSG reminder.RepeatIntervalMinutes = 0 reminder.MaxSendCount = 1 } _, err = models.Orm.Insert(&reminder) if err != nil { c.jsonErr(500, 500, "创建提醒失败: "+err.Error()) return } } c.ok(map[string]interface{}{"schedule_id": schedID}) } // UpdateReminder PUT /platform/reminder/:id func (c *PlatformReminderController) UpdateReminder() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.jsonErr(400, 400, "无效的ID") return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p reminderFormPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local) if err != nil { c.jsonErr(400, 400, "日程时间格式不合法") return } // 1. 更新日程详情 var schedule models.PlatformSchedule err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule) if err != nil { c.jsonErr(404, 404, "日程未找到") return } // 检查是否所有关联的提醒都已结束 var reminders []models.PlatformScheduleReminder _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). 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.jsonErr(400, 400, "该日程提醒已全部结束,无法编辑") return } schedule.Title = "日程提醒" schedule.Content = p.Content schedule.ScheduleTime = schedTime _, err = models.Orm.Update(&schedule, "Title", "Content", "ScheduleTime") if err != nil { c.jsonErr(500, 500, "更新失败") return } // 2. 软删除原本的所有提醒 _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). Filter("schedule_id", id). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) // 3. 重新建立提醒 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.PlatformScheduleReminder{ 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 := generateToken() 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.jsonErr(500, 500, "重新创建提醒失败") return } } c.ok(nil) } // DeleteReminder DELETE /platform/reminder/:id func (c *PlatformReminderController) DeleteReminder() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, _ := strconv.ParseUint(idStr, 10, 64) if id == 0 { c.jsonErr(400, 400, "无效的ID") return } // 检查是否所有关联的提醒都已结束 var reminders []models.PlatformScheduleReminder _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). 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.jsonErr(400, 400, "该日程提醒已全部结束,无法删除") return } // 软删除日程 // 这里的 id 既可以是主表的 id,也可以是日程的 id // 我们如果是管理页面,都是基于日程维度的,所以这里 id 指代 schedule_id _, err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete() if err == nil { _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). Filter("schedule_id", id). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) } c.ok(nil) } type reminderBatchDeletePayload struct { Ids []uint64 `json:"ids"` } // BatchDeleteReminder POST /platform/reminder/batchDelete func (c *PlatformReminderController) BatchDeleteReminder() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p reminderBatchDeletePayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if len(p.Ids) == 0 { c.ok(nil) return } // 检查选中的日程是否有任何一个是全部结束的,防误操作 for _, scheduleID := range p.Ids { var reminders []models.PlatformScheduleReminder _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). 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.jsonErr(400, 400, fmt.Sprintf("选中的日程ID %d 的提醒已全部结束,无法删除", scheduleID)) return } } // 批量软删除 _, _ = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id__in", p.Ids).Delete() _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). Filter("schedule_id__in", p.Ids). Update(map[string]interface{}{ "IsDeleted": 1, "UpdateTime": time.Now(), }) c.ok(nil) } type reminderTestPayload struct { Title string `json:"title"` Content string `json:"content"` RemindChannels []string `json:"remind_channels"` } // TestReminder POST /platform/reminder/test func (c *PlatformReminderController) TestReminder() { claims, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p reminderTestPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(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.PlatformScheduleReminder{ RemindChannel: ch, ReceiverUserID: uint64(claims.UserID), AckToken: &dummyToken, } success, sendErr := sender.Send(context.Background(), reminder, "[测试]"+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.ok(results) }