mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
refactor(api): extract repository layer and thin HTTP handlers
Introduce internal/repository for data access and cache-backed system config reads. Move business logic into logics.go across admin push, user, template, cache, system_config, and upload/handler packages. Remove Gin from internal/util by relocating request-scoped helpers to oauth/gin_context.go. Propagate request context for config lookups in user flows. Slim model entities and delete model-level DB/cache helpers. Wire handlers to logics/repository so targeted packages no longer call db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
This commit is contained in:
@@ -4,22 +4,21 @@
|
||||
// Package push defines push notification HTTP routes.
|
||||
package push
|
||||
|
||||
import ("context"
|
||||
"encoding/json"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/pkg/push"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
// UpdateEventRequest 更新事件请求参数
|
||||
type UpdateEventRequest struct {
|
||||
@@ -37,30 +36,7 @@ type TestPushRequest struct {
|
||||
|
||||
// SyncEvents automatically registers/updates built-in events in the database.
|
||||
func SyncEvents(ctx context.Context) error {
|
||||
for _, meta := range BuiltInEvents {
|
||||
var event model.PushEvent
|
||||
err := db.DB(ctx).Where("event_key = ?", meta.Key).First(&event).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
var defaultTemplateStr string
|
||||
if defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate); err == nil {
|
||||
defaultTemplateStr = string(defaultTemplateBytes)
|
||||
}
|
||||
event = model.PushEvent{
|
||||
EventKey: meta.Key,
|
||||
Name: meta.Name,
|
||||
Channels: []string{},
|
||||
Targets: []string{},
|
||||
Template: defaultTemplateStr,
|
||||
Enabled: false,
|
||||
}
|
||||
if err := db.DB(ctx).Create(&event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return syncBuiltInEvents(ctx)
|
||||
}
|
||||
|
||||
// ListEvents 获取通知事件列表
|
||||
@@ -73,9 +49,8 @@ func SyncEvents(ctx context.Context) error {
|
||||
// @Router /api/v1/admin/push/events [get]
|
||||
func ListEvents(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var events []model.PushEvent
|
||||
if err := db.DB(ctx).Order("created_at DESC").Find(&events).Error; err != nil {
|
||||
events, err := listPushEvents(ctx)
|
||||
if err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -113,46 +88,6 @@ func ListBuiltInEvents(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(BuiltInEvents))
|
||||
}
|
||||
|
||||
func getEventInfo(req CreateEventRequest) (string, string, []byte, error) {
|
||||
if req.TaskType != "" {
|
||||
// 1. 检查关联任务是否存在
|
||||
meta := task.GetTaskMetaByAsynqTask(req.TaskType)
|
||||
if meta == nil {
|
||||
return "", "", nil, errors.New("unsupported task type")
|
||||
}
|
||||
eventKey := "task_completed:" + req.TaskType
|
||||
eventName := "任务完成: " + meta.Name
|
||||
|
||||
defaultTemplate := NotificationMessage{
|
||||
Title: "任务完成: " + meta.Name,
|
||||
Content: "异步任务 {{task_name}} (ID: {{task_id}}) 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。",
|
||||
Level: defaultLevelInfo,
|
||||
}
|
||||
defaultTemplateBytes, err := json.Marshal(defaultTemplate)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
return eventKey, eventName, defaultTemplateBytes, nil
|
||||
}
|
||||
|
||||
if req.EventKey == "" {
|
||||
return "", "", nil, errors.New("either event_key or task_type must be provided")
|
||||
}
|
||||
|
||||
// 1. 检查内置事件是否存在
|
||||
meta, found := findBuiltInEvent(req.EventKey)
|
||||
if !found {
|
||||
return "", "", nil, errors.New("unsupported built-in event key")
|
||||
}
|
||||
|
||||
defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
return req.EventKey, meta.Name, defaultTemplateBytes, nil
|
||||
}
|
||||
|
||||
// CreateEvent 创建通知事件
|
||||
// @Summary 创建通知事件
|
||||
// @Description 绑定系统内置事件或异步任务、推送渠道、接收目标并创建通知事件配置,需要管理员权限
|
||||
@@ -170,70 +105,11 @@ func CreateEvent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
eventKey, eventName, defaultTemplateBytes, err := getEventInfo(req)
|
||||
event, err := createPushEvent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查是否已经创建过该事件的配置
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", eventKey).Count(&count).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
response.AbortBadRequest(c, "this notification event is already configured")
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 模板处理
|
||||
templateStr := strings.TrimSpace(req.Template)
|
||||
if templateStr == "" {
|
||||
templateStr = string(defaultTemplateBytes)
|
||||
} else {
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal([]byte(templateStr), &tempMap); err != nil {
|
||||
response.AbortBadRequest(c, "custom template is not a valid JSON format")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建事件记录
|
||||
channels := req.Channels
|
||||
if channels == nil {
|
||||
channels = []string{}
|
||||
}
|
||||
targets := req.Targets
|
||||
if targets == nil {
|
||||
targets = []string{}
|
||||
}
|
||||
|
||||
event := model.PushEvent{
|
||||
EventKey: eventKey,
|
||||
Name: eventName,
|
||||
TaskType: req.TaskType,
|
||||
Channels: channels,
|
||||
Targets: targets,
|
||||
Template: templateStr,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
|
||||
if err := event.Validate(); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&event).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存一致性:清除旧事件缓存
|
||||
model.DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(event))
|
||||
}
|
||||
|
||||
@@ -247,32 +123,20 @@ func CreateEvent(c *gin.Context) {
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Router /api/v1/admin/push/events/{id} [delete]
|
||||
func DeleteEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, "invalid event id")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var event model.PushEvent
|
||||
if err := db.DB(ctx).First(&event, id).Error; err != nil {
|
||||
if err := deletePushEvent(c.Request.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, "notification event not found")
|
||||
} else {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Delete(&event).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存一致性:清除事件缓存
|
||||
model.DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -288,8 +152,7 @@ func DeleteEvent(c *gin.Context) {
|
||||
// @Success 200 {object} response.Any{data=string} "修改成功"
|
||||
// @Router /api/v1/admin/push/events/{id} [put]
|
||||
func UpdateEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, "invalid event id")
|
||||
return
|
||||
@@ -301,34 +164,14 @@ func UpdateEvent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var event model.PushEvent
|
||||
if err := db.DB(c.Request.Context()).First(&event, id).Error; err != nil {
|
||||
if err := updatePushEvent(c.Request.Context(), id, req); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, "notification event not found")
|
||||
} else {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
event.Channels = req.Channels
|
||||
event.Targets = req.Targets
|
||||
event.Template = req.Template
|
||||
event.Enabled = req.Enabled
|
||||
|
||||
if err := event.Validate(); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Save(&event).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存一致性:清除事件缓存
|
||||
model.DeleteActivePushEventCache(c.Request.Context(), event.EventKey)
|
||||
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -342,37 +185,22 @@ func UpdateEvent(c *gin.Context) {
|
||||
// @Success 200 {object} response.Any{data=string} "切换成功"
|
||||
// @Router /api/v1/admin/push/events/{id}/toggle [post]
|
||||
func ToggleEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, "invalid event id")
|
||||
return
|
||||
}
|
||||
|
||||
var event model.PushEvent
|
||||
if err := db.DB(c.Request.Context()).First(&event, id).Error; err != nil {
|
||||
enabled, err := togglePushEvent(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, "notification event not found")
|
||||
} else {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
event.Enabled = !event.Enabled
|
||||
if event.Enabled && len(event.Channels) == 0 {
|
||||
response.AbortBadRequest(c, "cannot enable event without any push channels configured")
|
||||
return
|
||||
}
|
||||
if err := db.DB(c.Request.Context()).Model(&event).Update("enabled", event.Enabled).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存一致性:清除事件缓存
|
||||
model.DeleteActivePushEventCache(c.Request.Context(), event.EventKey)
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(event.Enabled))
|
||||
c.JSON(http.StatusOK, response.OK(enabled))
|
||||
}
|
||||
|
||||
// pushHistoriesResponse 推送历史分页响应
|
||||
@@ -396,37 +224,22 @@ type pushHistoriesResponse struct {
|
||||
// @Success 200 {object} response.Any{data=pushHistoriesResponse} "推送历史列表"
|
||||
// @Router /api/v1/admin/push/histories [get]
|
||||
func ListHistories(c *gin.Context) {
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
eventKey := c.Query("event_key")
|
||||
status := c.Query("status")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||
if err != nil || pageSize < 1 {
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
query := db.DB(c.Request.Context()).Model(&model.PushHistory{}).Order("created_at DESC")
|
||||
if eventKey != "" {
|
||||
query = query.Where("event_key = ?", eventKey)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var results []model.PushHistory
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(&results).Error; err != nil {
|
||||
total, results, err := listPushHistories(c.Request.Context(), repository.PushHistoryListFilter{
|
||||
EventKey: c.Query("event_key"),
|
||||
Status: c.Query("status"),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -459,44 +272,21 @@ func TestPush(c *gin.Context) {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 校验配置
|
||||
if err := pusher.ValidateConfig(req.Config); err != nil {
|
||||
response.AbortBadRequest(c, fmt.Sprintf("validation failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 邮件渠道需要从系统设置中拉取发件人 SMTP 信息做测试 (除非配了独立的)
|
||||
if req.Config.Channel == channelEmail && (req.Config.URL == "" || req.Config.Key == "") {
|
||||
var smtpHost, smtpPort, smtpUser, smtpPass model.SystemConfig
|
||||
ctx := c.Request.Context()
|
||||
_ = smtpHost.GetByKey(ctx, model.ConfigKeySMTPHost)
|
||||
_ = smtpPort.GetByKey(ctx, model.ConfigKeySMTPPort)
|
||||
_ = smtpUser.GetByKey(ctx, model.ConfigKeySMTPUsername)
|
||||
_ = smtpPass.GetByKey(ctx, model.ConfigKeySMTPPassword)
|
||||
|
||||
if smtpHost.Value != "" && smtpUser.Value != "" {
|
||||
port := smtpPort.Value
|
||||
if port == "" {
|
||||
port = "587"
|
||||
}
|
||||
req.Config.URL = smtpHost.Value + ":" + port
|
||||
req.Config.Key = smtpUser.Value
|
||||
req.Config.Secret = smtpPass.Value
|
||||
}
|
||||
}
|
||||
applySMTPFallbackToPushConfig(c.Request.Context(), &req.Config)
|
||||
|
||||
testBody := map[string]any{
|
||||
keyTitle: "测试通道推送",
|
||||
keyContent: "当您收到这条消息,说明当前渠道连通性测试通过。",
|
||||
keyLevel: defaultLevelInfo,
|
||||
}
|
||||
|
||||
err = pusher.Send(c.Request.Context(), req.Config, req.Target, testBody, "", nil)
|
||||
if err != nil {
|
||||
if err := pusher.Send(c.Request.Context(), req.Config, req.Target, testBody, "", nil); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user