Merge branch 'master' of https://git.yunzer.cn/yunzerwebsite/yunzerwebsiteallinone
This commit is contained in:
@@ -66,6 +66,15 @@ func (c *ApiGetCardController) GetCard() {
|
||||
return
|
||||
}
|
||||
|
||||
// 读取机器码/MAC
|
||||
machineCode := strings.TrimSpace(c.GetString("machine_code"))
|
||||
if machineCode == "" {
|
||||
machineCode = strings.TrimSpace(c.GetString("machineCode"))
|
||||
}
|
||||
if machineCode == "" {
|
||||
machineCode = strings.TrimSpace(c.GetString("mac"))
|
||||
}
|
||||
|
||||
// 参数校验
|
||||
if platform == "" {
|
||||
c.cardErr(400, 400, "缺少参数 type(来源平台)")
|
||||
@@ -92,7 +101,7 @@ func (c *ApiGetCardController) GetCard() {
|
||||
|
||||
switch module {
|
||||
case "cursor":
|
||||
c.extractCursor(platform, dataType, startID, now)
|
||||
c.extractCursor(platform, dataType, startID, now, machineCode)
|
||||
case "windsurf":
|
||||
c.extractWindsurf(platform, dataType, startID, now)
|
||||
case "krio":
|
||||
@@ -121,8 +130,25 @@ func (c *ApiGetCardController) readOptionalStartID() (uint64, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractCursor(platform, dataType string, startID uint64, now time.Time) {
|
||||
c.extractWithProbe("cursor", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
func (c *ApiGetCardController) extractCursor(platform, dataType string, startID uint64, now time.Time, machineCode string) {
|
||||
// 优先查询该机器码是否已经绑定过未删除的卡密
|
||||
if machineCode != "" {
|
||||
var existing models.PlatformAccountPoolCursor
|
||||
err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("machine_code", machineCode).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("is_used", 0).
|
||||
OrderBy("-id").
|
||||
Limit(1).
|
||||
One(&existing)
|
||||
if err == nil {
|
||||
// 直接返回已绑定的卡密信息
|
||||
c.cardOK(buildCardResult(&existing.Account, &existing.Password, existing.Token, existing.DataType))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.extractWithProbe("cursor", platform, dataType, now, machineCode, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolCursor
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("is_extracted", 0).
|
||||
@@ -141,7 +167,7 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, startID
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, startID uint64, now time.Time) {
|
||||
c.extractWithProbe("windsurf", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
c.extractWithProbe("windsurf", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolWindsurf
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
Filter("is_extracted", 0).
|
||||
@@ -160,7 +186,7 @@ func (c *ApiGetCardController) extractWindsurf(platform, dataType string, startI
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractKrio(platform, dataType string, startID uint64, now time.Time) {
|
||||
c.extractWithProbe("krio", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
c.extractWithProbe("krio", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolKiro
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||
Filter("is_extracted", 0).
|
||||
@@ -179,7 +205,7 @@ func (c *ApiGetCardController) extractKrio(platform, dataType string, startID ui
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractCodex(platform, dataType string, startID uint64, now time.Time) {
|
||||
c.extractWithProbe("codex", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
c.extractWithProbe("codex", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolCodex
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCodex)).
|
||||
Filter("is_extracted", 0).
|
||||
@@ -203,6 +229,7 @@ type poolRowFetcher func() (id uint64, account, password *string, token, rowData
|
||||
func (c *ApiGetCardController) extractWithProbe(
|
||||
module, platform, dataType string,
|
||||
now time.Time,
|
||||
machineCode string,
|
||||
fetch poolRowFetcher,
|
||||
) {
|
||||
for {
|
||||
@@ -221,14 +248,20 @@ func (c *ApiGetCardController) extractWithProbe(
|
||||
c.cardErr(500, 500, "无效模块")
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
"update_time": now,
|
||||
}
|
||||
if module == "cursor" && machineCode != "" {
|
||||
params["machine_code"] = machineCode
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(tableName).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
"update_time": now,
|
||||
})
|
||||
Update(params)
|
||||
if err != nil {
|
||||
c.cardErr(500, 500, "提取失败")
|
||||
return
|
||||
@@ -240,10 +273,16 @@ func (c *ApiGetCardController) extractWithProbe(
|
||||
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||
return
|
||||
}
|
||||
if module == "cursor" && machineCode != "" {
|
||||
_, _ = models.Orm.QueryTable(tableName).Filter("id", id).Update(map[string]interface{}{"machine_code": "", "update_time": time.Now()})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !poolProbeToken(module, rowDataType, token, id) {
|
||||
if module == "cursor" && machineCode != "" {
|
||||
_, _ = models.Orm.QueryTable(tableName).Filter("id", id).Update(map[string]interface{}{"machine_code": "", "update_time": time.Now()})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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>
|
||||
`))
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
@@ -209,39 +208,17 @@ func (c *BackendLoginVerifyController) SaveLoginVerifyInfos() {
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var existed models.PlatformLoginVerify
|
||||
err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
|
||||
if err == nil {
|
||||
_, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).
|
||||
Filter("id", existed.ID).
|
||||
Update(map[string]interface{}{
|
||||
"open_verify_enabled": openVerifyEnabled,
|
||||
"verify_type": verifyType,
|
||||
"geetest3_id": geetest3ID,
|
||||
"geetest3_key": geetest3Key,
|
||||
"geetest4_id": geetest4ID,
|
||||
"geetest4_key": geetest4Key,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := &models.PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
Geetest3ID: geetest3ID,
|
||||
Geetest3Key: geetest3Key,
|
||||
Geetest4ID: geetest4ID,
|
||||
Geetest4Key: geetest4Key,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
err = models.SavePlatformLoginVerify(&models.PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
Geetest3ID: geetest3ID,
|
||||
Geetest3Key: geetest3Key,
|
||||
Geetest4ID: geetest4ID,
|
||||
Geetest4Key: geetest4Key,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type BackendSiteReminderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendSiteReminderController) backendClaims() (*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 *BackendSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMyList GET /backend/sitereminder/myList
|
||||
func (c *BackendSiteReminderController) GetMyList() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
var isRead *int8
|
||||
if isReadStr := c.GetString("isRead"); isReadStr != "" {
|
||||
if val, err := strconv.Atoi(isReadStr); err == nil {
|
||||
v := int8(val)
|
||||
isRead = &v
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := services.ListReminders(uint64(claims.UserID), "tenant", page, pageSize, isRead)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkRead POST /backend/sitereminder/read
|
||||
func (c *BackendSiteReminderController) MarkRead() {
|
||||
claims, err := c.backendClaims()
|
||||
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 struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkAllRead POST /backend/sitereminder/readall
|
||||
func (c *BackendSiteReminderController) MarkAllRead() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
err = services.MarkAllRemindersRead(uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /backend/sitereminder/delete
|
||||
func (c *BackendSiteReminderController) Delete() {
|
||||
claims, err := c.backendClaims()
|
||||
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 struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteReminder(p.ID, uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type PlatformBarkController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformBarkController) 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 *PlatformBarkController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetBarkInfo GET /platform/bark/info
|
||||
func (c *PlatformBarkController) GetBarkInfo() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
enabledStr := models.GetPlatformSettingValue("bark_enabled", "0")
|
||||
serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
|
||||
deviceKey := models.GetPlatformSettingValue("bark_device_key", "")
|
||||
|
||||
enabled := false
|
||||
if enabledStr == "1" {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"enabled": enabled,
|
||||
"server_url": serverURL,
|
||||
"device_key": deviceKey,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type barkEditPayload struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ServerUrl string `json:"server_url"`
|
||||
DeviceKey string `json:"device_key"`
|
||||
}
|
||||
|
||||
// EditBarkInfo POST /platform/bark/editinfo
|
||||
func (c *PlatformBarkController) EditBarkInfo() {
|
||||
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 barkEditPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
enabledStr := "0"
|
||||
if p.Enabled {
|
||||
enabledStr = "1"
|
||||
}
|
||||
|
||||
serverURL := strings.TrimSpace(p.ServerUrl)
|
||||
if serverURL == "" {
|
||||
serverURL = "https://api.day.app"
|
||||
}
|
||||
deviceKey := strings.TrimSpace(p.DeviceKey)
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"bark_enabled", "Bark推送启用状态", enabledStr, "0为关闭,1为开启"},
|
||||
{"bark_server_url", "Bark推送服务器地址", serverURL, ""},
|
||||
{"bark_device_key", "Bark设备Key", deviceKey, ""},
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type barkTestPayload struct {
|
||||
ServerUrl string `json:"server_url"`
|
||||
DeviceKey string `json:"device_key"`
|
||||
}
|
||||
|
||||
// SendTestBark POST /platform/bark/sendtest
|
||||
func (c *PlatformBarkController) SendTestBark() {
|
||||
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 barkTestPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
serverURL := strings.TrimSpace(p.ServerUrl)
|
||||
if serverURL == "" {
|
||||
serverURL = models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
|
||||
}
|
||||
deviceKey := strings.TrimSpace(p.DeviceKey)
|
||||
if deviceKey == "" {
|
||||
deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
|
||||
}
|
||||
|
||||
if deviceKey == "" {
|
||||
c.jsonErr(400, 400, "设备 Key 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 拼接发送 URL,注意去除多余斜杠
|
||||
baseURL := strings.TrimRight(serverURL, "/")
|
||||
// Bark 的格式是: base_url/device_key/title/body
|
||||
testURL := fmt.Sprintf("%s/%s/测试通知/您配置的 Bark 推送服务已连接成功!", baseURL, deviceKey)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "发送失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
c.jsonErr(500, 500, fmt.Sprintf("发送失败,HTTP 状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes)))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "测试推送已发出,请注意查收"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -105,10 +105,11 @@ func (c *PlatformCursorEquipmentController) cursorActivationSummary(row *models.
|
||||
return count, &latest
|
||||
}
|
||||
|
||||
func (c *PlatformCursorEquipmentController) cursorExtractSummary() (int64, *models.PlatformAccountPoolCursor) {
|
||||
func (c *PlatformCursorEquipmentController) cursorExtractSummary(machineCode string) (int64, *models.PlatformAccountPoolCursor) {
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("is_extracted__gt", 0)
|
||||
Filter("is_extracted__gt", 0).
|
||||
Filter("machine_code", machineCode)
|
||||
|
||||
count, _ := qs.Count()
|
||||
|
||||
@@ -122,7 +123,7 @@ func (c *PlatformCursorEquipmentController) cursorExtractSummary() (int64, *mode
|
||||
|
||||
func (c *PlatformCursorEquipmentController) rowToMap(row *models.PlatformCursorEquipment) map[string]interface{} {
|
||||
activationCount, latestActivation := c.cursorActivationSummary(row)
|
||||
extractCount, latestExtract := c.cursorExtractSummary()
|
||||
extractCount, latestExtract := c.cursorExtractSummary(row.MachineCode)
|
||||
|
||||
var bindActivationCode interface{}
|
||||
var activationCodeId interface{}
|
||||
@@ -676,7 +677,8 @@ func (c *PlatformCursorEquipmentController) ExtractRecords() {
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("is_extracted__gt", 0)
|
||||
Filter("is_extracted__gt", 0).
|
||||
Filter("machine_code", equipment.MachineCode)
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
|
||||
@@ -97,12 +97,44 @@ type emailFormPayload struct {
|
||||
Password string `json:"password"`
|
||||
Encryption string `json:"encryption"`
|
||||
Timeout interface{} `json:"timeout"`
|
||||
Status interface{} `json:"status"`
|
||||
}
|
||||
type testEmailPayload struct {
|
||||
emailFormPayload
|
||||
TestEmail string `json:"testEmail"`
|
||||
}
|
||||
|
||||
func parseInt8Flexible(v interface{}) int8 {
|
||||
if v == nil {
|
||||
return 1
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
if x {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case float64:
|
||||
return int8(x)
|
||||
case int:
|
||||
return int8(x)
|
||||
case int8:
|
||||
return x
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if s == "" {
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.ParseInt(s, 10, 8)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
return int8(n)
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func parseUintFlexible(v interface{}) uint {
|
||||
if v == nil {
|
||||
return 0
|
||||
@@ -191,7 +223,8 @@ func (c *PlatformEmailController) EditInfo() {
|
||||
s := strings.TrimSpace(p.FromName)
|
||||
fn = &s
|
||||
}
|
||||
err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, 1, nil)
|
||||
status := parseInt8Flexible(p.Status)
|
||||
err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, status, nil)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存邮箱配置失败: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -85,37 +85,18 @@ func (c *PlatformLoginVerifyController) SaveLoginVerifyInfos() {
|
||||
}
|
||||
}
|
||||
|
||||
var existed models.PlatformLoginVerify
|
||||
err := models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
|
||||
if err == nil {
|
||||
update := map[string]interface{}{
|
||||
"open_verify_enabled": openVerifyEnabled,
|
||||
"verify_type": verifyType,
|
||||
"geetest3_id": p.Geetest3ID,
|
||||
"geetest3_key": p.Geetest3Key,
|
||||
"geetest4_id": p.Geetest4ID,
|
||||
"geetest4_key": p.Geetest4Key,
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).Filter("id", existed.ID).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := &models.PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
Geetest3ID: p.Geetest3ID,
|
||||
Geetest3Key: p.Geetest3Key,
|
||||
Geetest4ID: p.Geetest4ID,
|
||||
Geetest4Key: p.Geetest4Key,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
err := models.SavePlatformLoginVerify(&models.PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
Geetest3ID: p.Geetest3ID,
|
||||
Geetest3Key: p.Geetest3Key,
|
||||
Geetest4ID: p.Geetest4ID,
|
||||
Geetest4Key: p.Geetest4Key,
|
||||
})
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type PlatformSiteReminderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformSiteReminderController) 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 *PlatformSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetConfig GET /platform/sitereminder/config
|
||||
func (c *PlatformSiteReminderController) GetConfig() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
cfg, err := services.GetSiteReminderConfig()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cfg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveConfig POST /platform/sitereminder/config
|
||||
func (c *PlatformSiteReminderController) SaveConfig() {
|
||||
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 struct {
|
||||
RetentionDays int `json:"retention_days"`
|
||||
AutoRead int8 `json:"auto_read"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := services.SaveSiteReminderConfig(p.RetentionDays, p.AutoRead); err != nil {
|
||||
c.jsonErr(500, 500, "保存配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Send POST /platform/sitereminder/send
|
||||
func (c *PlatformSiteReminderController) Send() {
|
||||
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 struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
TargetType string `json:"target_type"` // platform, tenant_all, role, tenant
|
||||
TargetRoleID uint64 `json:"target_role_id"`
|
||||
TargetTenantID uint64 `json:"target_tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
p.Content = strings.TrimSpace(p.Content)
|
||||
if p.Title == "" || p.Content == "" {
|
||||
c.jsonErr(400, 400, "标题与内容不能为空")
|
||||
return
|
||||
}
|
||||
if p.TargetType == "" {
|
||||
c.jsonErr(400, 400, "发送目标类型不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.SendSiteReminder(p.Title, p.Content, uint64(claims.UserID), "platform", p.TargetType, p.TargetRoleID, p.TargetTenantID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "发送失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发送成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMyList GET /platform/sitereminder/myList
|
||||
func (c *PlatformSiteReminderController) GetMyList() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
var isRead *int8
|
||||
if isReadStr := c.GetString("isRead"); isReadStr != "" {
|
||||
if val, err := strconv.Atoi(isReadStr); err == nil {
|
||||
v := int8(val)
|
||||
isRead = &v
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := services.ListReminders(uint64(claims.UserID), "platform", page, pageSize, isRead)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkRead POST /platform/sitereminder/read
|
||||
func (c *PlatformSiteReminderController) MarkRead() {
|
||||
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 struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkAllRead POST /platform/sitereminder/readall
|
||||
func (c *PlatformSiteReminderController) MarkAllRead() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
err = services.MarkAllRemindersRead(uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /platform/sitereminder/delete
|
||||
func (c *PlatformSiteReminderController) Delete() {
|
||||
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 struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteReminder(p.ID, uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetSentList GET /platform/sitereminder/sentList
|
||||
func (c *PlatformSiteReminderController) GetSentList() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
|
||||
list, total, err := services.ListSentReminders(uint64(claims.UserID), page, pageSize)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取发送列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateSent POST /platform/sitereminder/updateSent
|
||||
func (c *PlatformSiteReminderController) UpdateSent() {
|
||||
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 struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetRoleID uint64 `json:"target_role_id"`
|
||||
TargetTenantID uint64 `json:"target_tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
p.Content = strings.TrimSpace(p.Content)
|
||||
if p.BatchID == "" || p.Title == "" || p.Content == "" {
|
||||
c.jsonErr(400, 400, "批次号、标题与内容不能为空")
|
||||
return
|
||||
}
|
||||
if p.TargetType == "" {
|
||||
c.jsonErr(400, 400, "发送目标类型不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.UpdateSentReminder(p.BatchID, p.Title, p.Content, p.TargetType, p.TargetRoleID, p.TargetTenantID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "修改失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteSentBatch POST /platform/sitereminder/deleteSent
|
||||
func (c *PlatformSiteReminderController) DeleteSentBatch() {
|
||||
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 struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BatchID == "" {
|
||||
c.jsonErr(400, 400, "批次号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteSentReminderBatch(p.BatchID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -55,24 +55,8 @@ func (c *PlatformSMSController) GetSmsInfo() {
|
||||
return
|
||||
}
|
||||
|
||||
var row models.SystemSMS
|
||||
// 优先默认通道,其次 custom
|
||||
err := models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("is_default", 1).
|
||||
Filter("status", 1).
|
||||
OrderBy("-weight", "-id").
|
||||
Limit(1).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
_ = models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("config_code", "custom").
|
||||
OrderBy("-id").
|
||||
Limit(1).
|
||||
One(&row)
|
||||
}
|
||||
|
||||
backendURL := strings.TrimSpace(row.ApiURL)
|
||||
apiKey := strings.TrimSpace(row.ApiKey)
|
||||
backendURL := models.GetPlatformSettingValue("sms_custom_url", "")
|
||||
apiKey := models.GetPlatformSettingValue("sms_custom_key", "")
|
||||
|
||||
data := []map[string]interface{}{{
|
||||
"backend_url": backendURL,
|
||||
@@ -93,7 +77,7 @@ type smsEditPayload struct {
|
||||
}
|
||||
|
||||
// EditSmsInfo POST /platform/sms/editinfo
|
||||
// 将旧前端的 backendUrl/apiKey 落到 yz_system_sms 的 api_url/api_key(写入 config_code=custom)
|
||||
// 将旧前端的 backendUrl/apiKey 落到 yz_platform_normal_setting 表中
|
||||
func (c *PlatformSMSController) EditSmsInfo() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
@@ -128,44 +112,46 @@ func (c *PlatformSMSController) EditSmsInfo() {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保只有一个默认:先清空默认,再 upsert custom 为默认
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemSMS)).Update(map[string]interface{}{"is_default": 0})
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"sms_custom_url", "自定义短信网关地址", backendURL, ""},
|
||||
{"sms_custom_key", "自定义短信API KEY", apiKey, ""},
|
||||
}
|
||||
|
||||
var existed models.SystemSMS
|
||||
e := models.Orm.QueryTable(new(models.SystemSMS)).Filter("config_code", "custom").Limit(1).One(&existed)
|
||||
if e == nil && existed.ID > 0 {
|
||||
_, err = models.Orm.QueryTable(new(models.SystemSMS)).Filter("id", existed.ID).Update(map[string]interface{}{
|
||||
"config_name": "自定义网关",
|
||||
"channel_type": 2,
|
||||
"api_url": backendURL,
|
||||
"api_key": apiKey,
|
||||
"weight": 10,
|
||||
"is_default": 1,
|
||||
"status": 1,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := &models.SystemSMS{
|
||||
ConfigCode: "custom",
|
||||
ConfigName: "自定义网关",
|
||||
ChannelType: 2,
|
||||
ApiURL: backendURL,
|
||||
ApiKey: apiKey,
|
||||
ApiSecret: "",
|
||||
SignName: "",
|
||||
TemplateID: "",
|
||||
TestPhone: "",
|
||||
Weight: 10,
|
||||
IsDefault: 1,
|
||||
Status: 1,
|
||||
Remark: "",
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,18 +218,11 @@ func (c *PlatformSMSController) SendTestSms() {
|
||||
|
||||
// 兜底:body 未带时从默认配置取
|
||||
if backendURL == "" || apiKey == "" {
|
||||
var row models.SystemSMS
|
||||
_ = models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("is_default", 1).
|
||||
Filter("status", 1).
|
||||
OrderBy("-weight", "-id").
|
||||
Limit(1).
|
||||
One(&row)
|
||||
if backendURL == "" {
|
||||
backendURL = strings.TrimSpace(row.ApiURL)
|
||||
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
|
||||
}
|
||||
if apiKey == "" {
|
||||
apiKey = strings.TrimSpace(row.ApiKey)
|
||||
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
|
||||
}
|
||||
}
|
||||
if backendURL == "" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"server/models"
|
||||
"server/services"
|
||||
_ "server/routers"
|
||||
"server/version"
|
||||
|
||||
@@ -21,5 +22,8 @@ func main() {
|
||||
// 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
|
||||
beego.SetStaticPath("/uploads", "uploads")
|
||||
|
||||
// 启动日程提醒定时任务
|
||||
services.StartReminderScheduler(make(chan struct{}))
|
||||
|
||||
beego.Run()
|
||||
}
|
||||
|
||||
+10
-3
@@ -43,14 +43,11 @@ func Init(_ string) {
|
||||
new(AdminRole),
|
||||
new(SystemFile),
|
||||
new(SystemFilesCategory),
|
||||
new(SystemEmail),
|
||||
new(SystemSMS),
|
||||
new(SystemSMSTask),
|
||||
new(SystemOperationLog),
|
||||
new(SystemDomainPool),
|
||||
new(SystemTenantDomain),
|
||||
new(SystemModules),
|
||||
new(PlatformLoginVerify),
|
||||
new(StorageConfig),
|
||||
new(TenantSiteSetting),
|
||||
new(ComplaintCategory),
|
||||
@@ -67,6 +64,16 @@ func Init(_ string) {
|
||||
|
||||
new(CmsArticleCategory),
|
||||
new(CmsArticle),
|
||||
|
||||
new(SystemReminderList),
|
||||
|
||||
new(SystemNormalSetting),
|
||||
new(PlatformNormalSetting),
|
||||
new(BackendNormalSetting),
|
||||
|
||||
new(PlatformSchedule),
|
||||
new(PlatformScheduleReminder),
|
||||
new(PlatformScheduleReminderSendLog),
|
||||
)
|
||||
|
||||
// 创建全局 Ormer
|
||||
|
||||
@@ -75,6 +75,7 @@ type PlatformAccountPoolCursor struct {
|
||||
IsUsed *int8 `orm:"column(is_used);null" json:"is_used"` // 0=用完/不可用 1=可用 NULL=未探测
|
||||
ExtractedTime *time.Time `orm:"column(extracted_time);type(datetime);null" json:"extracted_time"`
|
||||
ExtractedPlatform *string `orm:"column(extracted_platform);size(32);null" json:"extracted_platform"`
|
||||
MachineCode string `orm:"column(machine_code);size(128);default('')" json:"machine_code"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
|
||||
@@ -15,20 +15,118 @@ type PlatformLoginVerify struct {
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *PlatformLoginVerify) TableName() string {
|
||||
return "yz_system_login_verify"
|
||||
}
|
||||
|
||||
func GetPlatformLoginVerify() (*PlatformLoginVerify, error) {
|
||||
var cfg PlatformLoginVerify
|
||||
err := Orm.QueryTable(new(PlatformLoginVerify)).OrderBy("-id").One(&cfg)
|
||||
if err != nil {
|
||||
// 默认配置:验证码
|
||||
return &PlatformLoginVerify{OpenVerifyEnabled: 1, VerifyType: "captcha"}, nil
|
||||
// 从 yz_platform_normal_setting 表中按 code 获取各个配置
|
||||
enabledStr := GetPlatformSettingValue("login_verify_enabled", "1")
|
||||
verifyType := GetPlatformSettingValue("login_verify_type", "captcha")
|
||||
geetest3ID := GetPlatformSettingValue("login_verify_geetest3_id", "")
|
||||
geetest3Key := GetPlatformSettingValue("login_verify_geetest3_key", "")
|
||||
geetest4ID := GetPlatformSettingValue("login_verify_geetest4_id", "")
|
||||
geetest4Key := GetPlatformSettingValue("login_verify_geetest4_key", "")
|
||||
|
||||
openVerifyEnabled := int8(1)
|
||||
if enabledStr == "0" {
|
||||
openVerifyEnabled = 0
|
||||
}
|
||||
if cfg.VerifyType == "" {
|
||||
cfg.VerifyType = "captcha"
|
||||
|
||||
cfg := &PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
}
|
||||
return &cfg, nil
|
||||
if geetest3ID != "" {
|
||||
cfg.Geetest3ID = &geetest3ID
|
||||
}
|
||||
if geetest3Key != "" {
|
||||
cfg.Geetest3Key = &geetest3Key
|
||||
}
|
||||
if geetest4ID != "" {
|
||||
cfg.Geetest4ID = &geetest4ID
|
||||
}
|
||||
if geetest4Key != "" {
|
||||
cfg.Geetest4Key = &geetest4Key
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func GetPlatformSettingValue(code string, defaultVal string) string {
|
||||
var setting PlatformNormalSetting
|
||||
err := Orm.QueryTable(new(PlatformNormalSetting)).
|
||||
Filter("code", code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return setting.Value
|
||||
}
|
||||
|
||||
func SavePlatformLoginVerify(cfg *PlatformLoginVerify) error {
|
||||
openVerifyEnabledStr := "1"
|
||||
if cfg.OpenVerifyEnabled == 0 {
|
||||
openVerifyEnabledStr = "0"
|
||||
}
|
||||
geetest3ID := ""
|
||||
if cfg.Geetest3ID != nil {
|
||||
geetest3ID = *cfg.Geetest3ID
|
||||
}
|
||||
geetest3Key := ""
|
||||
if cfg.Geetest3Key != nil {
|
||||
geetest3Key = *cfg.Geetest3Key
|
||||
}
|
||||
geetest4ID := ""
|
||||
if cfg.Geetest4ID != nil {
|
||||
geetest4ID = *cfg.Geetest4ID
|
||||
}
|
||||
geetest4Key := ""
|
||||
if cfg.Geetest4Key != nil {
|
||||
geetest4Key = *cfg.Geetest4Key
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"login_verify_enabled", "登录验证开启状态", openVerifyEnabledStr, "0为关闭,1为开启"},
|
||||
{"login_verify_type", "登录验证类型", cfg.VerifyType, "支持 captcha/sms/geetest/email"},
|
||||
{"login_verify_geetest3_id", "极验3 ID", geetest3ID, ""},
|
||||
{"login_verify_geetest3_key", "极验3 Key", geetest3Key, ""},
|
||||
{"login_verify_geetest4_id", "极验4 ID", geetest4ID, ""},
|
||||
{"login_verify_geetest4_key", "极验4 Key", geetest4Key, ""},
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting PlatformNormalSetting
|
||||
err := Orm.QueryTable(new(PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
newSetting := PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// PlatformSchedule 日程主表: yz_platform_schedule
|
||||
type PlatformSchedule struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Title string `orm:"column(title);size(255)" json:"title"`
|
||||
Content string `orm:"column(content);type(text)" json:"content"`
|
||||
ScheduleTime time.Time `orm:"column(schedule_time);type(datetime)" json:"schedule_time"`
|
||||
UserID uint64 `orm:"column(user_id)" json:"user_id"`
|
||||
}
|
||||
|
||||
func (m *PlatformSchedule) TableName() string {
|
||||
return "yz_platform_schedule"
|
||||
}
|
||||
|
||||
// PlatformScheduleReminder 日程提醒主表: yz_platform_schedule_reminder
|
||||
type PlatformScheduleReminder struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
ScheduleID uint64 `orm:"column(schedule_id)" json:"schedule_id"`
|
||||
RemindChannel string `orm:"column(remind_channel);size(20)" json:"remind_channel"` // SMS/EMAIL/BARK/SITE_MSG
|
||||
AdvanceMinutes int `orm:"column(advance_minutes);default(0)" json:"advance_minutes"`
|
||||
RepeatIntervalMinutes int `orm:"column(repeat_interval_minutes);default(0)" json:"repeat_interval_minutes"`
|
||||
NextRemindTime time.Time `orm:"column(next_remind_time);type(datetime)" json:"next_remind_time"`
|
||||
SendCount int `orm:"column(send_count);default(0)" json:"send_count"`
|
||||
MaxSendCount int `orm:"column(max_send_count);default(1)" json:"max_send_count"`
|
||||
AckToken *string `orm:"column(ack_token);size(64);null" json:"ack_token"`
|
||||
AckStatus int8 `orm:"column(ack_status);default(0)" json:"ack_status"` // 0-未确认 1-已确认
|
||||
AckTime *time.Time `orm:"column(ack_time);type(datetime);null" json:"ack_time"`
|
||||
ReceiverUserID uint64 `orm:"column(receiver_user_id)" json:"receiver_user_id"`
|
||||
ReceiverTarget *string `orm:"column(receiver_target);size(255);null" json:"receiver_target"`
|
||||
RemindStatus int8 `orm:"column(remind_status);default(0)" json:"remind_status"` // 0-待提醒 1-提醒中 2-已结束
|
||||
ScanLock string `orm:"column(scan_lock);size(64);default('')" json:"scan_lock"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *PlatformScheduleReminder) TableName() string {
|
||||
return "yz_platform_schedule_reminder"
|
||||
}
|
||||
|
||||
// PlatformScheduleReminderSendLog 提醒实际发送流水: yz_platform_schedule_reminder_send_log
|
||||
type PlatformScheduleReminderSendLog struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
ReminderID uint64 `orm:"column(reminder_id)" json:"reminder_id"`
|
||||
SendTime time.Time `orm:"column(send_time);type(datetime)" json:"send_time"`
|
||||
SendResult int8 `orm:"column(send_result)" json:"send_result"` // 0-失败 1-成功
|
||||
FailReason *string `orm:"column(fail_reason);size(255);null" json:"fail_reason"`
|
||||
}
|
||||
|
||||
func (m *PlatformScheduleReminderSendLog) TableName() string {
|
||||
return "yz_platform_schedule_reminder_send_log"
|
||||
}
|
||||
@@ -18,6 +18,4 @@ type SystemEmail struct {
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *SystemEmail) TableName() string {
|
||||
return "yz_system_email"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemNormalSetting 系统通用配置表: yz_system_normal_setting
|
||||
type SystemNormalSetting struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Name string `orm:"column(name);size(128);default('')" json:"name"`
|
||||
Value string `orm:"column(value);type(text);null" json:"value"`
|
||||
Code string `orm:"column(code);size(64);default('')" json:"code"`
|
||||
Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *SystemNormalSetting) TableName() string {
|
||||
return "yz_system_normal_setting"
|
||||
}
|
||||
|
||||
// PlatformNormalSetting 平台通用配置表: yz_platform_normal_setting
|
||||
type PlatformNormalSetting struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Name string `orm:"column(name);size(128);default('')" json:"name"`
|
||||
Value string `orm:"column(value);type(text);null" json:"value"`
|
||||
Code string `orm:"column(code);size(64);default('')" json:"code"`
|
||||
Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *PlatformNormalSetting) TableName() string {
|
||||
return "yz_platform_normal_setting"
|
||||
}
|
||||
|
||||
// BackendNormalSetting 管理端通用配置表: yz_backend_normal_setting
|
||||
type BackendNormalSetting struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Name string `orm:"column(name);size(128);default('')" json:"name"`
|
||||
Value string `orm:"column(value);type(text);null" json:"value"`
|
||||
Code string `orm:"column(code);size(64);default('')" json:"code"`
|
||||
Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendNormalSetting) TableName() string {
|
||||
return "yz_backend_normal_setting"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemReminderList 站内信消息列表表 yz_system_reminderlist
|
||||
type SystemReminderList struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Title string `orm:"column(title);size(255)" json:"title"`
|
||||
Content string `orm:"column(content);type(text)" json:"content"`
|
||||
SenderID uint64 `orm:"column(sender_id);default(0)" json:"sender_id"`
|
||||
SenderType string `orm:"column(sender_type);size(32);default('system')" json:"sender_type"` // system, platform, tenant
|
||||
ReceiverID uint64 `orm:"column(receiver_id)" json:"receiver_id"`
|
||||
ReceiverType string `orm:"column(receiver_type);size(32)" json:"receiver_type"` // platform, tenant
|
||||
IsRead int8 `orm:"column(is_read);default(0)" json:"is_read"` // 0-未读, 1-已读
|
||||
ReadTime *time.Time `orm:"column(read_time);type(datetime);null" json:"read_time"`
|
||||
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
BatchID string `orm:"column(batch_id);size(64);default('')" json:"batch_id"`
|
||||
TargetType string `orm:"column(target_type);size(32);default('')" json:"target_type"`
|
||||
TargetRoleID uint64 `orm:"column(target_role_id);default(0)" json:"target_role_id"`
|
||||
TargetTenantID uint64 `orm:"column(target_tenant_id);default(0)" json:"target_tenant_id"`
|
||||
}
|
||||
|
||||
func (m *SystemReminderList) TableName() string {
|
||||
return "yz_system_reminderlist"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemSiteReminder 站内信配置表 yz_system_sitereminder
|
||||
type SystemSiteReminder struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
RetentionDays int `orm:"column(retention_days);default(30)" json:"retention_days"`
|
||||
AutoRead int8 `orm:"column(auto_read);default(0)" json:"auto_read"`
|
||||
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,4 @@ type SystemSMS struct {
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *SystemSMS) TableName() string {
|
||||
return "yz_system_sms"
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,7 @@ func Register() {
|
||||
// 对外提卡接口(无需登录)
|
||||
// GET /api/getcard?type=xianyu&module=cursor&data_type=tk
|
||||
beego.Router("/api/getcard", &controllers.ApiGetCardController{}, "get:GetCard")
|
||||
|
||||
// 日程提醒确认接口(无需登录)
|
||||
beego.Router("/api/schedule/reminder/ack", &controllers.ApiReminderController{}, "get:AckReminder")
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
|
||||
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
|
||||
|
||||
// 站内信(yz_system_reminderlist)
|
||||
beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList")
|
||||
beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead")
|
||||
beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead")
|
||||
beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete")
|
||||
|
||||
// 文件管理(yz_system_files / yz_system_files_category)
|
||||
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
|
||||
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
|
||||
|
||||
@@ -126,6 +126,17 @@ func Register() {
|
||||
beego.Router("/platform/email/editinfo", &controllers.PlatformEmailController{}, "post:EditInfo")
|
||||
beego.Router("/platform/email/sendtestemail", &controllers.PlatformEmailController{}, "post:SendTestEmail")
|
||||
|
||||
// 站内信配置与发送(yz_system_sitereminder / yz_system_reminderlist)
|
||||
beego.Router("/platform/sitereminder/config", &controllers.PlatformSiteReminderController{}, "get:GetConfig;post:SaveConfig")
|
||||
beego.Router("/platform/sitereminder/send", &controllers.PlatformSiteReminderController{}, "post:Send")
|
||||
beego.Router("/platform/sitereminder/myList", &controllers.PlatformSiteReminderController{}, "get:GetMyList")
|
||||
beego.Router("/platform/sitereminder/read", &controllers.PlatformSiteReminderController{}, "post:MarkRead")
|
||||
beego.Router("/platform/sitereminder/readall", &controllers.PlatformSiteReminderController{}, "post:MarkAllRead")
|
||||
beego.Router("/platform/sitereminder/delete", &controllers.PlatformSiteReminderController{}, "post:Delete")
|
||||
beego.Router("/platform/sitereminder/sentList", &controllers.PlatformSiteReminderController{}, "get:GetSentList")
|
||||
beego.Router("/platform/sitereminder/updateSent", &controllers.PlatformSiteReminderController{}, "post:UpdateSent")
|
||||
beego.Router("/platform/sitereminder/deleteSent", &controllers.PlatformSiteReminderController{}, "post:DeleteSentBatch")
|
||||
|
||||
// 短信配置(yz_system_sms)
|
||||
beego.Router("/platform/sms/info", &controllers.PlatformSMSController{}, "get:GetSmsInfo")
|
||||
beego.Router("/platform/sms/editinfo", &controllers.PlatformSMSController{}, "post:EditSmsInfo")
|
||||
@@ -133,6 +144,11 @@ func Register() {
|
||||
beego.Router("/platform/sms/taskList", &controllers.PlatformSMSController{}, "get:GetSmsTaskList")
|
||||
beego.Router("/platform/sms/taskEdit/:id", &controllers.PlatformSMSController{}, "post:EditSmsTask")
|
||||
|
||||
// Bark 推送配置
|
||||
beego.Router("/platform/bark/info", &controllers.PlatformBarkController{}, "get:GetBarkInfo")
|
||||
beego.Router("/platform/bark/editinfo", &controllers.PlatformBarkController{}, "post:EditBarkInfo")
|
||||
beego.Router("/platform/bark/sendtest", &controllers.PlatformBarkController{}, "post:SendTestBark")
|
||||
|
||||
// 文件管理(yz_system_files / yz_system_files_category)
|
||||
beego.Router("/platform/usercate", &controllers.PlatformFileController{}, "get:GetUserCate")
|
||||
beego.Router("/platform/allfiles", &controllers.PlatformFileController{}, "get:GetAllFiles")
|
||||
@@ -240,4 +256,11 @@ func Register() {
|
||||
beego.Router("/platform/notebook/create", &controllers.PlatformNotebookController{}, "post:Create")
|
||||
beego.Router("/platform/notebook/update/:id", &controllers.PlatformNotebookController{}, "post:Update")
|
||||
beego.Router("/platform/notebook/delete/:id", &controllers.PlatformNotebookController{}, "delete:Delete")
|
||||
|
||||
// 日程提醒管理
|
||||
beego.Router("/platform/reminder/list", &controllers.PlatformReminderController{}, "get:GetReminderList")
|
||||
beego.Router("/platform/reminder/test", &controllers.PlatformReminderController{}, "post:TestReminder")
|
||||
beego.Router("/platform/reminder/:id", &controllers.PlatformReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder")
|
||||
beego.Router("/platform/reminder", &controllers.PlatformReminderController{}, "post:CreateReminder")
|
||||
beego.Router("/platform/reminder/batchDelete", &controllers.PlatformReminderController{}, "post:BatchDeleteReminder")
|
||||
}
|
||||
|
||||
@@ -157,25 +157,11 @@ func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
|
||||
}
|
||||
|
||||
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
|
||||
var row models.SystemSMS
|
||||
err = models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("is_default", 1).
|
||||
Filter("status", 1).
|
||||
OrderBy("-weight", "-id").
|
||||
Limit(1).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
err2 := models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("config_code", "custom").
|
||||
OrderBy("-id").
|
||||
Limit(1).
|
||||
One(&row)
|
||||
if err2 != nil {
|
||||
return "", "", err2
|
||||
}
|
||||
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
|
||||
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
|
||||
if backendURL == "" || apiKey == "" {
|
||||
return "", "", fmt.Errorf("短信网关未配置")
|
||||
}
|
||||
backendURL = strings.TrimSpace(row.ApiURL)
|
||||
apiKey = strings.TrimSpace(row.ApiKey)
|
||||
return backendURL, apiKey, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// ReminderSender 提醒发送接口
|
||||
type ReminderSender interface {
|
||||
Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error)
|
||||
}
|
||||
|
||||
// SMSSender 短信发送实现
|
||||
type SMSSender struct{}
|
||||
|
||||
func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
|
||||
backendURL, apiKey, err := getDefaultSystemSMSConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
phone := ""
|
||||
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
|
||||
phone = *reminder.ReceiverTarget
|
||||
} else {
|
||||
var user models.AdminUser
|
||||
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
|
||||
phone = *user.Phone
|
||||
}
|
||||
}
|
||||
if phone == "" {
|
||||
return false, fmt.Errorf("未配置手机号")
|
||||
}
|
||||
|
||||
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
|
||||
payload := map[string]interface{}{
|
||||
"phone": phone,
|
||||
"content": title + ": " + content,
|
||||
}
|
||||
bs, _ := json.Marshal(payload)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", apiKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// EmailSender 邮件发送实现
|
||||
type EmailSender struct{}
|
||||
|
||||
func (s *EmailSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
|
||||
emails, err := ListSystemEmails()
|
||||
if err != nil || len(emails) == 0 {
|
||||
return false, fmt.Errorf("未配置系统邮箱")
|
||||
}
|
||||
emailCfg := emails[0]
|
||||
if emailCfg.FromAddress == "" || emailCfg.Host == "" {
|
||||
return false, fmt.Errorf("未配置系统邮箱")
|
||||
}
|
||||
|
||||
toEmail := ""
|
||||
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
|
||||
toEmail = *reminder.ReceiverTarget
|
||||
} else {
|
||||
var user models.AdminUser
|
||||
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
|
||||
toEmail = *user.Email
|
||||
}
|
||||
}
|
||||
if toEmail == "" {
|
||||
return false, fmt.Errorf("未配置收件邮箱")
|
||||
}
|
||||
|
||||
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
|
||||
ackToken := ""
|
||||
if reminder.AckToken != nil {
|
||||
ackToken = *reminder.AckToken
|
||||
}
|
||||
|
||||
// 构造 HTML 邮件
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 5px; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #409EFF; margin-bottom: 20px;">日程提醒:%s</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333;">%s</p>
|
||||
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;" />
|
||||
`, title, content)
|
||||
|
||||
if ackToken != "" {
|
||||
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
|
||||
htmlBody += fmt.Sprintf(`
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<a href="%s" target="_blank" style="background-color: #409EFF; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
|
||||
收到,确认此提醒
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 15px;">确认收到后,系统将不再向您发送该日程的重复提醒。</p>
|
||||
`, ackURL)
|
||||
}
|
||||
|
||||
htmlBody += "</div>"
|
||||
|
||||
cfg := SMTPConfig{
|
||||
FromAddress: emailCfg.FromAddress,
|
||||
Host: emailCfg.Host,
|
||||
Port: emailCfg.Port,
|
||||
Password: emailCfg.Password,
|
||||
Encryption: emailCfg.Encryption,
|
||||
Timeout: emailCfg.Timeout,
|
||||
}
|
||||
if emailCfg.FromName != nil {
|
||||
cfg.FromName = *emailCfg.FromName
|
||||
}
|
||||
|
||||
err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// BarkSender Bark 推送实现
|
||||
type BarkSender struct{}
|
||||
|
||||
func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
|
||||
deviceKey := ""
|
||||
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
|
||||
deviceKey = *reminder.ReceiverTarget
|
||||
} else {
|
||||
deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
|
||||
}
|
||||
if deviceKey == "" {
|
||||
return false, fmt.Errorf("Bark 设备 Key 未配置")
|
||||
}
|
||||
|
||||
serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
|
||||
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
|
||||
ackToken := ""
|
||||
if reminder.AckToken != nil {
|
||||
ackToken = *reminder.AckToken
|
||||
}
|
||||
|
||||
baseURL := strings.TrimRight(serverURL, "/")
|
||||
escapedTitle := url.PathEscape(title)
|
||||
pushContent := content
|
||||
if ackToken != "" {
|
||||
pushContent += "\n确认收到请点击→"
|
||||
}
|
||||
escapedContent := url.PathEscape(pushContent)
|
||||
|
||||
barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
|
||||
|
||||
if ackToken != "" {
|
||||
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
|
||||
// Bark 官方推送支持 url 参数
|
||||
barkURL += "?url=" + url.QueryEscape(ackURL)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SiteMsgSender 站内信发送实现
|
||||
type SiteMsgSender struct{}
|
||||
|
||||
func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
|
||||
now := time.Now()
|
||||
msg := &models.SystemReminderList{
|
||||
Title: title,
|
||||
Content: content,
|
||||
SenderID: 0,
|
||||
SenderType: "system",
|
||||
ReceiverID: reminder.ReceiverUserID,
|
||||
ReceiverType: "platform", // 平台端用户
|
||||
IsRead: 0,
|
||||
CreateTime: &now,
|
||||
}
|
||||
_, err := models.Orm.Insert(msg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// generateUUID 生成一个安全的随机 UUID 字符
|
||||
func generateUUID() 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:])
|
||||
}
|
||||
|
||||
// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
|
||||
func StartReminderScheduler(stopChan chan struct{}) {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
scanAndSendReminders()
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func scanAndSendReminders() {
|
||||
// 1. 生成唯一扫描批次号用于抢占锁定
|
||||
scanBatch := generateUUID()
|
||||
now := time.Now()
|
||||
|
||||
// 2. 抢占待处理的数据(乐观锁防并发重复发送)
|
||||
_, err := models.Orm.Raw(`
|
||||
UPDATE yz_platform_schedule_reminder
|
||||
SET scan_lock = ?, update_time = NOW()
|
||||
WHERE next_remind_time <= ?
|
||||
AND remind_status IN (0, 1)
|
||||
AND is_deleted = 0
|
||||
AND (scan_lock = '' OR scan_lock IS NULL)
|
||||
`, scanBatch, now).Exec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 查询自己锁定成功的数据
|
||||
var list []models.PlatformScheduleReminder
|
||||
_, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
|
||||
Filter("scan_lock", scanBatch).
|
||||
Filter("remind_status__in", 0, 1).
|
||||
Filter("is_deleted", 0).
|
||||
All(&list)
|
||||
if err != nil || len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 实例分发发送
|
||||
senders := map[string]ReminderSender{
|
||||
"SMS": &SMSSender{},
|
||||
"EMAIL": &EmailSender{},
|
||||
"BARK": &BarkSender{},
|
||||
"SITE_MSG": &SiteMsgSender{},
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
reminder := &list[i]
|
||||
|
||||
// 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒")
|
||||
var schedule models.PlatformSchedule
|
||||
err := models.Orm.QueryTable(new(models.PlatformSchedule)).
|
||||
Filter("id", reminder.ScheduleID).
|
||||
One(&schedule)
|
||||
title := "日程提醒"
|
||||
content := "您有一个待处理的日程时间已到,请注意查收。"
|
||||
if err == nil {
|
||||
content = schedule.Content
|
||||
}
|
||||
|
||||
sender, ok := senders[reminder.RemindChannel]
|
||||
if !ok {
|
||||
// 未知渠道,直接强制置为结束
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
|
||||
Filter("id", reminder.ID).
|
||||
Update(map[string]interface{}{
|
||||
"remind_status": 2,
|
||||
"scan_lock": "",
|
||||
"update_time": time.Now(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 执行发送
|
||||
ctx := context.Background()
|
||||
success, sendErr := sender.Send(ctx, reminder, title, content)
|
||||
|
||||
// 3.2 记录发送流水日志
|
||||
sendResult := int8(0)
|
||||
var failReason *string
|
||||
if success {
|
||||
sendResult = 1
|
||||
} else if sendErr != nil {
|
||||
errStr := sendErr.Error()
|
||||
if len(errStr) > 255 {
|
||||
errStr = errStr[:255]
|
||||
}
|
||||
failReason = &errStr
|
||||
}
|
||||
|
||||
logRow := &models.PlatformScheduleReminderSendLog{
|
||||
ReminderID: reminder.ID,
|
||||
SendTime: time.Now(),
|
||||
SendResult: sendResult,
|
||||
FailReason: failReason,
|
||||
}
|
||||
_, _ = models.Orm.Insert(logRow)
|
||||
|
||||
// 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
|
||||
newSendCount := reminder.SendCount + 1
|
||||
newStatus := reminder.RemindStatus
|
||||
|
||||
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
|
||||
// 一次性发送:发送后直接置为结束
|
||||
newStatus = 2
|
||||
} else {
|
||||
// 重复发送渠道 EMAIL / BARK
|
||||
// 如果还没被 Ack,且没有达到 max_send_count,继续提醒
|
||||
if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
|
||||
newStatus = 1 // 提醒中
|
||||
// 更新下次发送时间
|
||||
reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
|
||||
} else {
|
||||
// 达到最大上限或者已 Ack
|
||||
newStatus = 2
|
||||
}
|
||||
}
|
||||
|
||||
// 3.4 回写主表记录
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
|
||||
Filter("id", reminder.ID).
|
||||
Update(map[string]interface{}{
|
||||
"SendCount": newSendCount,
|
||||
"NextRemindTime": reminder.NextRemindTime,
|
||||
"RemindStatus": newStatus,
|
||||
"ScanLock": "", // 释放扫描锁
|
||||
"UpdateTime": time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,100 @@ func SendTestEmailSMTP(cfg SMTPConfig, to string) error {
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
// SendHTMLEmailSMTP 发送一封 HTML 格式邮件
|
||||
func SendHTMLEmailSMTP(cfg SMTPConfig, to string, subject string, htmlBody string) error {
|
||||
to = strings.TrimSpace(to)
|
||||
if to == "" {
|
||||
return fmt.Errorf("收件人不能为空")
|
||||
}
|
||||
if cfg.Host == "" || cfg.FromAddress == "" {
|
||||
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 465
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
||||
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
||||
if enc == "" {
|
||||
enc = "ssl"
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
switch enc {
|
||||
case "ssl":
|
||||
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
case "tls":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("STARTTLS 失败: %w", err)
|
||||
}
|
||||
}
|
||||
case "none":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP 认证失败: %w", err)
|
||||
}
|
||||
if err = client.Mail(cfg.FromAddress); err != nil {
|
||||
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO 失败: %w", err)
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA 失败: %w", err)
|
||||
}
|
||||
fromName := strings.TrimSpace(cfg.FromName)
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
||||
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
||||
if _, err = wc.Write([]byte(headers + htmlBody)); err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %w", err)
|
||||
}
|
||||
if err = wc.Close(); err != nil {
|
||||
return fmt.Errorf("结束 DATA 失败: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func formatFromHeader(name, addr string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
|
||||
@@ -2,19 +2,51 @@ package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// ListSystemEmails 返回全部邮箱配置(按 id 升序,通常仅一条)
|
||||
// ListSystemEmails 返回从 yz_platform_normal_setting 组装的邮箱配置(切片,通常仅一条)
|
||||
func ListSystemEmails() ([]models.SystemEmail, error) {
|
||||
var rows []models.SystemEmail
|
||||
_, err := models.Orm.QueryTable(new(models.SystemEmail)).OrderBy("id").All(&rows)
|
||||
return rows, err
|
||||
enabledStr := models.GetPlatformSettingValue("email_enabled", "0")
|
||||
fromAddress := models.GetPlatformSettingValue("email_from_address", "")
|
||||
fromName := models.GetPlatformSettingValue("email_from_name", "")
|
||||
host := models.GetPlatformSettingValue("email_host", "")
|
||||
portStr := models.GetPlatformSettingValue("email_port", "465")
|
||||
password := models.GetPlatformSettingValue("email_password", "")
|
||||
encryption := models.GetPlatformSettingValue("email_encryption", "ssl")
|
||||
timeoutStr := models.GetPlatformSettingValue("email_timeout", "30")
|
||||
|
||||
status := int8(0)
|
||||
if enabledStr == "1" {
|
||||
status = 1
|
||||
}
|
||||
portVal, _ := strconv.ParseUint(portStr, 10, 32)
|
||||
timeoutVal, _ := strconv.ParseUint(timeoutStr, 10, 32)
|
||||
|
||||
row := models.SystemEmail{
|
||||
ID: 1,
|
||||
FromAddress: fromAddress,
|
||||
Host: host,
|
||||
Port: uint(portVal),
|
||||
Password: password,
|
||||
Encryption: encryption,
|
||||
Timeout: uint(timeoutVal),
|
||||
Status: status,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if fromName != "" {
|
||||
row.FromName = &fromName
|
||||
}
|
||||
|
||||
return []models.SystemEmail{row}, nil
|
||||
}
|
||||
|
||||
// UpsertFirstSystemEmail 若已有记录则更新第一条,否则插入
|
||||
// UpsertFirstSystemEmail 将邮箱配置保存到 yz_platform_normal_setting 表中
|
||||
func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, port uint, password string, encryption string, timeout uint, status int8, remark *string) error {
|
||||
if encryption == "" {
|
||||
encryption = "ssl"
|
||||
@@ -25,53 +57,79 @@ func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, p
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
if status == 0 {
|
||||
status = 1
|
||||
}
|
||||
fromAddress = strings.TrimSpace(fromAddress)
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemEmail)).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
fn := ""
|
||||
if fromName != nil {
|
||||
fn = *fromName
|
||||
}
|
||||
if cnt == 0 {
|
||||
if strings.TrimSpace(password) == "" {
|
||||
|
||||
statusStr := "0"
|
||||
if status == 1 {
|
||||
statusStr = "1"
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"email_enabled", "邮件服务启用状态", statusStr, "0为关闭,1为开启"},
|
||||
{"email_from_address", "发件人邮箱", fromAddress, ""},
|
||||
{"email_from_name", "发件人名称", fn, ""},
|
||||
{"email_host", "SMTP 服务器地址", host, ""},
|
||||
{"email_port", "SMTP 端口", strconv.FormatUint(uint64(port), 10), ""},
|
||||
{"email_encryption", "邮件加密方式", encryption, "支持 ssl/tls/none"},
|
||||
{"email_timeout", "邮件发送超时时间", strconv.FormatUint(uint64(timeout), 10), ""},
|
||||
}
|
||||
|
||||
// 如果传入了新密码,或者目前还没有保存过密码,才更新密码
|
||||
if strings.TrimSpace(password) != "" {
|
||||
settings = append(settings, struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{"email_password", "邮件授权码/密码", strings.TrimSpace(password), ""})
|
||||
} else {
|
||||
// 校验:如果完全没有配置过密码,必须填写密码
|
||||
existingPass := models.GetPlatformSettingValue("email_password", "")
|
||||
if existingPass == "" {
|
||||
return fmt.Errorf("首次保存必须填写授权码/密码")
|
||||
}
|
||||
row := &models.SystemEmail{
|
||||
FromAddress: fromAddress,
|
||||
FromName: fromName,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Password: strings.TrimSpace(password),
|
||||
Encryption: encryption,
|
||||
Timeout: timeout,
|
||||
Status: status,
|
||||
Remark: remark,
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = models.Orm.Insert(row)
|
||||
return err
|
||||
}
|
||||
|
||||
var first models.SystemEmail
|
||||
if err := models.Orm.QueryTable(new(models.SystemEmail)).OrderBy("id").Limit(1).One(&first); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
up := map[string]interface{}{
|
||||
"from_address": fromAddress,
|
||||
"from_name": fromName,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"encryption": encryption,
|
||||
"timeout": timeout,
|
||||
"status": status,
|
||||
"remark": remark,
|
||||
}
|
||||
if strings.TrimSpace(password) != "" {
|
||||
up["password"] = strings.TrimSpace(password)
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.SystemEmail)).Filter("id", first.ID).Update(up)
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// GetSiteReminderConfig 获取站内信配置(从 yz_platform_normal_setting 读取)
|
||||
func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
|
||||
retentionDaysStr := models.GetPlatformSettingValue("sitemsg_retention_days", "30")
|
||||
autoReadStr := models.GetPlatformSettingValue("sitemsg_auto_read", "0")
|
||||
|
||||
retentionDays, _ := strconv.Atoi(retentionDaysStr)
|
||||
if retentionDays <= 0 {
|
||||
retentionDays = 30
|
||||
}
|
||||
autoRead := int8(0)
|
||||
if autoReadStr == "1" {
|
||||
autoRead = 1
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.SystemSiteReminder{
|
||||
ID: 1,
|
||||
RetentionDays: retentionDays,
|
||||
AutoRead: autoRead,
|
||||
CreateTime: &now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// SaveSiteReminderConfig 保存/更新配置
|
||||
func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
|
||||
if retentionDays <= 0 {
|
||||
retentionDays = 30
|
||||
}
|
||||
|
||||
autoReadStr := "0"
|
||||
if autoRead == 1 {
|
||||
autoReadStr = "1"
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"sitemsg_retention_days", "站内信消息保留天数", strconv.Itoa(retentionDays), ""},
|
||||
{"sitemsg_auto_read", "自动标记已读状态", autoReadStr, "0为关闭,1为开启"},
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendSiteReminder 发送站内信
|
||||
// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
|
||||
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
|
||||
var receiverIDs []uint64
|
||||
var receiverType string
|
||||
|
||||
switch targetType {
|
||||
case "platform":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询平台用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant_all":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询租户用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "role":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据角色查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据租户查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("未知的发送目标类型: %s", targetType)
|
||||
}
|
||||
|
||||
if len(receiverIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
|
||||
var reminders []models.SystemReminderList
|
||||
for _, rid := range receiverIDs {
|
||||
reminders = append(reminders, models.SystemReminderList{
|
||||
Title: title,
|
||||
Content: content,
|
||||
SenderID: senderID,
|
||||
SenderType: senderType,
|
||||
ReceiverID: rid,
|
||||
ReceiverType: receiverType,
|
||||
IsRead: 0,
|
||||
CreateTime: &now,
|
||||
BatchID: batchID,
|
||||
TargetType: targetType,
|
||||
TargetRoleID: targetRoleID,
|
||||
TargetTenantID: targetTenantID,
|
||||
})
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
_, err := models.Orm.InsertMulti(100, reminders)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListReminders 列表查询
|
||||
func ListReminders(receiverID uint64, receiverType string, page, pageSize int, isRead *int8) ([]models.SystemReminderList, int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
var list []models.SystemReminderList
|
||||
qs := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
if isRead != nil {
|
||||
qs = qs.Filter("is_read", *isRead)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time", "-id").Limit(pageSize, offset).All(&list)
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// MarkReminderRead 标记单条已读
|
||||
func MarkReminderRead(id uint64, receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("id", id).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Update(map[string]interface{}{
|
||||
"is_read": 1,
|
||||
"read_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkAllRemindersRead 一键全部已读
|
||||
func MarkAllRemindersRead(receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Filter("is_read", 0).
|
||||
Update(map[string]interface{}{
|
||||
"is_read": 1,
|
||||
"read_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteReminder 删除消息
|
||||
func DeleteReminder(id uint64, receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("id", id).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Update(map[string]interface{}{
|
||||
"delete_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// AutoCleanExpiredReminders 自动清理过期站内信
|
||||
func AutoCleanExpiredReminders() error {
|
||||
cfg, err := GetSiteReminderConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.RetentionDays <= 0 {
|
||||
return nil
|
||||
}
|
||||
expireTime := time.Now().AddDate(0, 0, -cfg.RetentionDays)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("create_time__lt", expireTime).
|
||||
Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListSentReminders 获取已发送的消息列表(按 batch_id 分组)
|
||||
func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemReminderList, int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var total int64
|
||||
err := models.Orm.Raw("SELECT COUNT(DISTINCT batch_id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL", senderID).QueryRow(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var list []models.SystemReminderList
|
||||
_, err = models.Orm.Raw("SELECT * FROM yz_system_reminderlist WHERE id IN (SELECT MIN(id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL GROUP BY batch_id) ORDER BY id DESC LIMIT ? OFFSET ?", senderID, pageSize, offset).QueryRows(&list)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
|
||||
func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
|
||||
// 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
|
||||
var firstRecord models.SystemReminderList
|
||||
err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("找不到该批次的站内信记录: %w", err)
|
||||
}
|
||||
senderID := firstRecord.SenderID
|
||||
senderType := firstRecord.SenderType
|
||||
|
||||
// 2. 根据新的目标接收群体获取接收人列表
|
||||
var receiverIDs []uint64
|
||||
var receiverType string
|
||||
|
||||
switch targetType {
|
||||
case "platform":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询平台用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant_all":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询租户用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "role":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据角色查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据租户查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("未知的发送目标类型: %s", targetType)
|
||||
}
|
||||
|
||||
// 3. 获取该批次中现有的所有记录 (包括已删除的)
|
||||
var existingRecords []models.SystemReminderList
|
||||
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).All(&existingRecords)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取现有记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 建立 map 快速查找,Key 为 "receiverType_receiverID"
|
||||
existingMap := make(map[string]*models.SystemReminderList)
|
||||
for i := range existingRecords {
|
||||
key := fmt.Sprintf("%s_%d", existingRecords[i].ReceiverType, existingRecords[i].ReceiverID)
|
||||
existingMap[key] = &existingRecords[i]
|
||||
}
|
||||
|
||||
newReceiverMap := make(map[string]bool)
|
||||
for _, rid := range receiverIDs {
|
||||
key := fmt.Sprintf("%s_%d", receiverType, rid)
|
||||
newReceiverMap[key] = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 事务处理
|
||||
err = models.Orm.DoTx(func(c context.Context, txOrm orm.TxOrmer) error {
|
||||
// A. 对于已经不在新接收者列表中的用户,软删除
|
||||
for key, rec := range existingMap {
|
||||
if !newReceiverMap[key] {
|
||||
if rec.DeleteTime == nil {
|
||||
rec.DeleteTime = &now
|
||||
if _, e := txOrm.Update(rec, "DeleteTime"); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
|
||||
var newInserts []models.SystemReminderList
|
||||
for _, rid := range receiverIDs {
|
||||
key := fmt.Sprintf("%s_%d", receiverType, rid)
|
||||
if rec, exists := existingMap[key]; exists {
|
||||
rec.Title = title
|
||||
rec.Content = content
|
||||
rec.TargetType = targetType
|
||||
rec.TargetRoleID = targetRoleID
|
||||
rec.TargetTenantID = targetTenantID
|
||||
|
||||
cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
|
||||
if rec.DeleteTime != nil {
|
||||
rec.DeleteTime = nil
|
||||
rec.IsRead = 0
|
||||
rec.ReadTime = nil
|
||||
cols = append(cols, "DeleteTime", "IsRead", "ReadTime")
|
||||
}
|
||||
if _, e := txOrm.Update(rec, cols...); e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
// C. 对于新增加的接收者,插入新记录
|
||||
newInserts = append(newInserts, models.SystemReminderList{
|
||||
Title: title,
|
||||
Content: content,
|
||||
SenderID: senderID,
|
||||
SenderType: senderType,
|
||||
ReceiverID: rid,
|
||||
ReceiverType: receiverType,
|
||||
IsRead: 0,
|
||||
CreateTime: &now,
|
||||
BatchID: batchID,
|
||||
TargetType: targetType,
|
||||
TargetRoleID: targetRoleID,
|
||||
TargetTenantID: targetTenantID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(newInserts) > 0 {
|
||||
if _, e := txOrm.InsertMulti(100, newInserts); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSentReminderBatch 删除已发送消息(删除该批次下所有记录)
|
||||
func DeleteSentReminderBatch(batchID string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("batch_id", batchID).
|
||||
Update(map[string]interface{}{
|
||||
"delete_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user