修复日程提醒功能

This commit is contained in:
2026-07-20 11:41:39 +08:00
parent 7ab6958538
commit f00ba9f9cc
19 changed files with 1735 additions and 1725 deletions
+168 -109
View File
@@ -1,109 +1,168 @@
package controllers
import (
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
type ApiReminderController struct {
beego.Controller
}
// AckReminder GET /api/schedule/reminder/ack
// 邮件/Bark 客户端访问此接口进行提醒确认
func (c *ApiReminderController) AckReminder() {
token := c.GetString("token")
if token == "" {
c.Ctx.Output.SetStatus(400)
_ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
return
}
var reminder models.PlatformScheduleReminder
err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("ack_token", token).
Filter("is_deleted", 0).
One(&reminder)
if err != nil {
c.Ctx.Output.SetStatus(404)
_ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
return
}
if reminder.AckStatus == 1 {
// 已经确认过了,直接显示已确认成功的 HTML
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
_ = c.Ctx.Output.Body([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认收到提醒</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>提示</h2>
<p>该日程提醒在此之前已确认过了。</p>
<p style="color: #909399; font-size: 14px;">无需重复点击,感谢您的使用!</p>
</div>
</body>
</html>
`))
return
}
// 更新确认状态为已确认,置 remind_status 为已结束(2)
now := time.Now()
reminder.AckStatus = 1
reminder.AckTime = &now
reminder.RemindStatus = 2
reminder.UpdateTime = now
_, err = models.Orm.Update(&reminder, "AckStatus", "AckTime", "RemindStatus", "UpdateTime")
if err != nil {
c.Ctx.Output.SetStatus(500)
_ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
return
}
// 统一关闭该日程下的所有其他待提醒/提醒中渠道,防止重复打扰
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("ScheduleID", reminder.ScheduleID).
Filter("RemindStatus__in", 0, 1).
Update(map[string]interface{}{
"RemindStatus": int8(2),
"UpdateTime": now,
})
// 成功确认
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
_ = c.Ctx.Output.Body([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认成功</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>确认成功</h2>
<p>您已成功确认收到该日程提醒!</p>
<p style="color: #909399; font-size: 14px;">系统已停止向您重复推送,感谢您的配合。</p>
</div>
</body>
</html>
`))
}
package controllers
import (
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
type ApiReminderController struct {
beego.Controller
}
// AckReminder GET /api/schedule/reminder/ack
// 邮件/Bark 客户端访问此接口进行提醒确认
// backend 与 platform 使用各自的提醒表,这里按 token 自动识别来源。
func (c *ApiReminderController) AckReminder() {
token := c.GetString("token")
if token == "" {
c.Ctx.Output.SetStatus(400)
_ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
return
}
type reminderState struct {
AckStatus int8
ScheduleID uint64
RemindStatus int8
}
var state reminderState
table := ""
var backendReminder models.BackendScheduleReminder
if err := models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("ack_token", token).
Filter("is_deleted", 0).
One(&backendReminder); err == nil {
state = reminderState{
AckStatus: backendReminder.AckStatus,
ScheduleID: backendReminder.ScheduleID,
RemindStatus: backendReminder.RemindStatus,
}
table = "backend"
} else {
var platformReminder models.PlatformScheduleReminder
if err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("ack_token", token).
Filter("is_deleted", 0).
One(&platformReminder); err != nil {
c.Ctx.Output.SetStatus(404)
_ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
return
}
state = reminderState{
AckStatus: platformReminder.AckStatus,
ScheduleID: platformReminder.ScheduleID,
RemindStatus: platformReminder.RemindStatus,
}
table = "platform"
}
if state.AckStatus == 1 {
writeReminderAckAlreadyConfirmed(c)
return
}
now := time.Now()
var err error
if table == "backend" {
_, err = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("ack_token", token).
Filter("is_deleted", 0).
Update(map[string]interface{}{
"AckStatus": int8(1),
"AckTime": now,
"RemindStatus": int8(2),
"UpdateTime": now,
})
} else {
_, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("ack_token", token).
Filter("is_deleted", 0).
Update(map[string]interface{}{
"AckStatus": int8(1),
"AckTime": now,
"RemindStatus": int8(2),
"UpdateTime": now,
})
}
if err != nil {
c.Ctx.Output.SetStatus(500)
_ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
return
}
// 只关闭命中来源表中、同一日程的其他待提醒记录,避免两套表互相影响。
if table == "backend" {
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", state.ScheduleID).
Filter("remind_status__in", 0, 1).
Update(map[string]interface{}{
"RemindStatus": int8(2),
"UpdateTime": now,
})
} else {
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("schedule_id", state.ScheduleID).
Filter("remind_status__in", 0, 1).
Update(map[string]interface{}{
"RemindStatus": int8(2),
"UpdateTime": now,
})
}
writeReminderAckSuccess(c)
}
func writeReminderAckAlreadyConfirmed(c *ApiReminderController) {
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
_ = c.Ctx.Output.Body([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认收到提醒</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>提示</h2>
<p>该日程提醒在此之前已确认过了。</p>
<p style="color: #909399; font-size: 14px;">无需重复点击,感谢您的使用!</p>
</div>
</body>
</html>
`))
}
func writeReminderAckSuccess(c *ApiReminderController) {
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
_ = c.Ctx.Output.Body([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认成功</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>确认成功</h2>
<p>您已成功确认收到该日程提醒!</p>
<p style="color: #909399; font-size: 14px;">系统已停止向您重复推送,感谢您的配合。</p>
</div>
</body>
</html>
`))
}
+51 -39
View File
@@ -13,6 +13,7 @@ import (
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
@@ -23,18 +24,18 @@ type AppReminderController struct {
func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
return nil, orm.ErrNoRows
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
return nil, orm.ErrNoRows
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
return nil, err
}
if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" {
return nil, fmt.Errorf("无权访问")
return nil, orm.ErrNoRows
}
return claims, nil
}
@@ -71,7 +72,7 @@ type appSchedulePayload struct {
func (c *AppReminderController) GetList() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -87,8 +88,9 @@ func (c *AppReminderController) GetList() {
pageSize = 20
}
qs := models.Orm.QueryTable(new(models.PlatformSchedule)).
Filter("user_id", claims.UserID)
qs := models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId)
if keyword != "" {
qs = qs.Filter("content__contains", keyword)
@@ -96,17 +98,20 @@ func (c *AppReminderController) GetList() {
total, _ := qs.Count()
var schedules []models.PlatformSchedule
var schedules []models.BackendSchedule
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules)
if err != nil {
if err != nil && err != orm.ErrNoRows {
c.jsonErr(500, 500, "查询失败: "+err.Error())
return
}
if schedules == nil {
schedules = []models.BackendSchedule{}
}
list := make([]map[string]interface{}, 0, len(schedules))
for _, s := range schedules {
var reminders []models.PlatformScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", s.ID).
Filter("is_deleted", 0).
All(&reminders)
@@ -166,7 +171,7 @@ func (c *AppReminderController) GetList() {
func (c *AppReminderController) GetDetail() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -177,24 +182,25 @@ func (c *AppReminderController) GetDetail() {
return
}
var schedule models.PlatformSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)).
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
var reminders []models.PlatformScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
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))
var first models.PlatformScheduleReminder
var first models.BackendScheduleReminder
for _, r := range reminders {
channels = append(channels, r.RemindChannel)
first = r
@@ -236,7 +242,7 @@ func (c *AppReminderController) GetDetail() {
func (c *AppReminderController) Create() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -275,7 +281,8 @@ func (c *AppReminderController) Create() {
title = "日程提醒"
}
schedule := models.PlatformSchedule{
schedule := models.BackendSchedule{
Tid: claims.TenantId,
Title: title,
Content: content,
ScheduleTime: schedTime,
@@ -298,7 +305,8 @@ func (c *AppReminderController) Create() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.PlatformScheduleReminder{
reminder := models.BackendScheduleReminder{
Tid: claims.TenantId,
ScheduleID: uint64(schedID),
RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes,
@@ -336,7 +344,7 @@ func (c *AppReminderController) Create() {
func (c *AppReminderController) Update() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -364,18 +372,19 @@ func (c *AppReminderController) Update() {
return
}
var schedule models.PlatformSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)).
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
var reminders []models.PlatformScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
@@ -414,7 +423,7 @@ func (c *AppReminderController) Update() {
}
// 软删除旧提醒,重建
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Update(map[string]interface{}{
"IsDeleted": 1,
@@ -432,7 +441,8 @@ func (c *AppReminderController) Update() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.PlatformScheduleReminder{
reminder := models.BackendScheduleReminder{
Tid: claims.TenantId,
ScheduleID: id,
RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes,
@@ -470,7 +480,7 @@ func (c *AppReminderController) Update() {
func (c *AppReminderController) Delete() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -481,18 +491,19 @@ func (c *AppReminderController) Delete() {
return
}
var schedule models.PlatformSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)).
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
var reminders []models.PlatformScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
@@ -512,9 +523,9 @@ func (c *AppReminderController) Delete() {
return
}
_, err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete()
_, err = models.Orm.QueryTable(new(models.BackendSchedule)).Filter("id", id).Delete()
if err == nil {
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Update(map[string]interface{}{
"IsDeleted": 1,
@@ -530,7 +541,7 @@ func (c *AppReminderController) Delete() {
func (c *AppReminderController) ToggleComplete() {
claims, err := c.appClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -541,18 +552,19 @@ func (c *AppReminderController) ToggleComplete() {
return
}
var schedule models.PlatformSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)).
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
var reminders []models.PlatformScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
+35 -28
View File
@@ -210,13 +210,13 @@ func (c *BackendReminderController) GetReminderDetail() {
}
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,
"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,
"is_finished": isFinished,
}
if first.ID > 0 {
data["advance_minutes"] = first.AdvanceMinutes
@@ -295,16 +295,16 @@ func (c *BackendReminderController) CreateReminder() {
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(),
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" {
@@ -433,16 +433,16 @@ func (c *BackendReminderController) UpdateReminder() {
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(),
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" {
@@ -703,13 +703,20 @@ func (c *BackendReminderController) TestReminder() {
}
dummyToken := "test-token-for-verification"
reminder := &models.PlatformScheduleReminder{
reminder := &models.BackendScheduleReminder{
Tid: claims.TenantId,
RemindChannel: ch,
ReceiverUserID: uint64(claims.UserID),
AckToken: &dummyToken,
}
success, sendErr := sender.Send(context.Background(), reminder, "[测试]"+p.Title, p.Content)
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 = "发送失败"
File diff suppressed because it is too large Load Diff