迁移公告,邮件

This commit is contained in:
2026-08-13 22:04:27 +08:00
parent 21c264fcd7
commit 9044bafdff
18 changed files with 946 additions and 28 deletions
+127
View File
@@ -0,0 +1,127 @@
package server
import (
"encoding/json"
"strconv"
"time"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type announcementRequest struct {
ID uint `json:"ID"`
Title string `json:"title"`
Content string `json:"content"`
UserID *uint `json:"userID"`
Attachments json.RawMessage `json:"attachments"`
}
func announcementInput(req announcementRequest) service.AnnouncementInput {
return service.AnnouncementInput{ID: req.ID, Title: req.Title, Content: req.Content, UserID: req.UserID, Attachments: req.Attachments}
}
func registerAnnouncementRoutes(private, public *gin.RouterGroup, svc *service.AnnouncementService) {
privateInfo := private.Group("/info")
privateInfo.POST("/createInfo", func(c *gin.Context) {
var req announcementRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Create(c.Request.Context(), announcementInput(req)); err != nil {
fail(c, "创建失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "创建成功")
})
privateInfo.DELETE("/deleteInfo", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := svc.Delete(c.Request.Context(), uint(id)); err != nil {
fail(c, "删除失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "删除成功")
})
privateInfo.DELETE("/deleteInfoByIds", func(c *gin.Context) {
values := c.QueryArray("IDs[]")
if len(values) == 0 {
values = c.QueryArray("IDs")
}
ids := make([]uint, 0, len(values))
for _, value := range values {
id, err := strconv.ParseUint(value, 10, 64)
if err != nil || id == 0 {
fail(c, "参数错误")
return
}
ids = append(ids, uint(id))
}
if err := svc.DeleteByIDs(c.Request.Context(), ids); err != nil {
fail(c, "批量删除失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "批量删除成功")
})
privateInfo.PUT("/updateInfo", func(c *gin.Context) {
var req announcementRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Update(c.Request.Context(), announcementInput(req)); err != nil {
fail(c, "更新失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "更新成功")
})
privateInfo.GET("/findInfo", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Find(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败:"+err.Error())
return
}
okWithData(c, item)
})
privateInfo.GET("/getInfoList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
start, startErr := parseAnnouncementTime(c.Query("startCreatedAt"))
end, endErr := parseAnnouncementTime(c.Query("endCreatedAt"))
if startErr != nil || endErr != nil || (start == nil) != (end == nil) {
fail(c, "创建日期范围不合法")
return
}
items, total, err := svc.List(c.Request.Context(), page, pageSize, start, end)
if err != nil {
fail(c, "获取失败:"+err.Error())
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: pageSize}, "获取成功")
})
publicInfo := public.Group("/info")
publicInfo.GET("/getInfoDataSource", func(c *gin.Context) {
users, err := svc.UserOptions(c.Request.Context())
if err != nil {
fail(c, "查询失败:"+err.Error())
return
}
okWithData(c, gin.H{"userID": users})
})
publicInfo.GET("/getInfoPublic", func(c *gin.Context) {
writeResult(c, codeSuccess, gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功")
})
}
func parseAnnouncementTime(value string) (*time.Time, error) {
if value == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, err
}
return &parsed, nil
}
+74
View File
@@ -0,0 +1,74 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerEmailRoutes(group *gin.RouterGroup, svc *service.EmailService) {
email := group.Group("/email")
email.POST("/emailTest", func(c *gin.Context) {
if err := svc.Test(c.Request.Context()); err != nil {
fail(c, "发送失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "发送成功")
})
email.POST("/sendEmail", func(c *gin.Context) {
var req struct {
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Send(c.Request.Context(), req.To, req.Subject, req.Body); err != nil {
fail(c, "发送失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "发送成功")
})
}
func emailErrorAlert(svc *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.Path, "/email/") {
c.Next()
return
}
writer := &captureWriter{ResponseWriter: c.Writer}
c.Writer = writer
started := time.Now()
c.Next()
failed := c.Writer.Status() >= 400
if !failed {
var result struct {
Code int `json:"code"`
}
failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != codeSuccess
}
if !failed {
return
}
username := "Unknown"
if claims := currentClaims(c); claims != nil && claims.Username != "" {
username = claims.Username
}
subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
body := bytes.NewBuffer(nil)
fmt.Fprintf(body, "请求方式:%s<br>请求路径:%s<br>状态码:%d<br>耗时:%s<br>错误响应:<pre>%s</pre>", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes()))
if err := svc.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil {
logger.Error("send HTTP error email", "error", err)
}
}
}
+4 -2
View File
@@ -30,10 +30,10 @@ type GinServer struct {
logger *slog.Logger
}
func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.SystemService, access *service.AccessService, settings *service.SettingsService, versions *service.VersionService, exports *service.ExportService, audit *service.AuditService, tasks *service.TaskService, media *service.MediaService, scheduler *TaskScheduler, logger *slog.Logger) *GinServer {
func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.SystemService, access *service.AccessService, settings *service.SettingsService, versions *service.VersionService, exports *service.ExportService, audit *service.AuditService, tasks *service.TaskService, media *service.MediaService, announcements *service.AnnouncementService, emails *service.EmailService, scheduler *TaskScheduler, logger *slog.Logger) *GinServer {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(ginRequestMeta(), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
if config != nil && config.Local != nil && config.Local.StorePath != "" {
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if pathPrefix != "/" {
@@ -61,6 +61,8 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
registerAuditRoutes(private, public, audit)
registerTaskRoutes(private, tasks, scheduler)
registerMediaRoutes(private, media)
registerAnnouncementRoutes(private, public, announcements)
registerEmailRoutes(private, emails)
engine.NoRoute(func(c *gin.Context) {
fail(c, "请求的接口不存在")
+27 -1
View File
@@ -38,6 +38,7 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
system := group.Group("/system")
system.POST("/getSystemConfig", func(c *gin.Context) {
admin := gin.H{"routerPrefix": ""}
email := gin.H{}
if config != nil {
admin["routerPrefix"] = config.RouterPrefix
if config.Jwt != nil {
@@ -49,8 +50,11 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
if config.Local != nil {
admin["local"] = gin.H{"storePath": config.Local.StorePath, "pathPrefix": config.Local.PathPrefix}
}
if config.Email != nil {
email = gin.H{"to": config.Email.To, "from": config.Email.From, "host": config.Email.Host, "secret": "******", "nickname": config.Email.Nickname, "port": config.Email.Port, "is-ssl": config.Email.IsSsl, "is-loginauth": config.Email.IsLoginAuth}
}
}
writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin}}, "获取成功")
writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin, "email": email}}, "获取成功")
})
system.POST("/setSystemConfig", func(c *gin.Context) {
var req struct {
@@ -66,6 +70,16 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
PathPrefix string `json:"pathPrefix"`
} `json:"local"`
} `json:"admin"`
Email *struct {
To string `json:"to"`
From string `json:"from"`
Host string `json:"host"`
Secret string `json:"secret"`
Nickname string `json:"nickname"`
Port int32 `json:"port"`
IsSSL bool `json:"is-ssl"`
IsLoginAuth bool `json:"is-loginauth"`
} `json:"email"`
} `json:"config"`
}
if c.ShouldBindJSON(&req) != nil {
@@ -90,6 +104,18 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
config.Local.PathPrefix = req.Config.Admin.Local.PathPrefix
}
}
if config.Email != nil && req.Config.Email != nil {
config.Email.To = req.Config.Email.To
config.Email.From = req.Config.Email.From
config.Email.Host = req.Config.Email.Host
config.Email.Nickname = req.Config.Email.Nickname
config.Email.Port = req.Config.Email.Port
config.Email.IsSsl = req.Config.Email.IsSSL
config.Email.IsLoginAuth = req.Config.Email.IsLoginAuth
if req.Config.Email.Secret != "" && req.Config.Email.Secret != "******" {
config.Email.Secret = req.Config.Email.Secret
}
}
}
ok(c)
})