From 682ca3df8124f2b84c6d55e37d1ac6a0c83a7377 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 27 Jun 2026 13:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20management=20handlers=20=E2=80=94=20sou?= =?UTF-8?q?rce,=20template,=20channel=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/channel.go | 91 +++++++++++++++ internal/handler/rule.go | 212 +++++++++++++++++++++++++++++++++++ internal/handler/source.go | 109 ++++++++++++++++++ internal/handler/template.go | 88 +++++++++++++++ 4 files changed, 500 insertions(+) create mode 100644 internal/handler/channel.go create mode 100644 internal/handler/rule.go create mode 100644 internal/handler/source.go create mode 100644 internal/handler/template.go diff --git a/internal/handler/channel.go b/internal/handler/channel.go new file mode 100644 index 0000000..aabf6f2 --- /dev/null +++ b/internal/handler/channel.go @@ -0,0 +1,91 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "aiaa-notification-service/internal/cache" + "aiaa-notification-service/internal/model" + "aiaa-notification-service/internal/store" + + "github.com/gin-gonic/gin" +) + +type ChannelHandler struct { + store *store.Store + cache *cache.Cache +} + +func NewChannelHandler(s *store.Store, c *cache.Cache) *ChannelHandler { + return &ChannelHandler{store: s, cache: c} +} + +type createChannelReq struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Config json.RawMessage `json:"config" binding:"required"` + Status int `json:"status"` +} + +func (h *ChannelHandler) Create(c *gin.Context) { + var req createChannelReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if req.Status == 0 { + req.Status = 1 + } + raw := json.RawMessage(req.Config) + ch := &model.Channel{Name: req.Name, Type: req.Type, Config: &raw, Status: req.Status} + if err := h.store.CreateChannel(c.Request.Context(), ch); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, ch) +} + +func (h *ChannelHandler) List(c *gin.Context) { + channels, err := h.store.ListChannels(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, channels) +} + +func (h *ChannelHandler) Get(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + ch, err := h.store.GetChannel(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) + return + } + c.JSON(http.StatusOK, ch) +} + +func (h *ChannelHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var req createChannelReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + raw := json.RawMessage(req.Config) + ch := &model.Channel{Name: req.Name, Type: req.Type, Config: &raw, Status: req.Status} + if err := h.store.UpdateChannel(c.Request.Context(), id, ch); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *ChannelHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.DeleteChannel(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} diff --git a/internal/handler/rule.go b/internal/handler/rule.go new file mode 100644 index 0000000..0e1958d --- /dev/null +++ b/internal/handler/rule.go @@ -0,0 +1,212 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "aiaa-notification-service/internal/cache" + "aiaa-notification-service/internal/model" + "aiaa-notification-service/internal/store" + + "github.com/gin-gonic/gin" +) + +type RuleHandler struct { + store *store.Store + cache *cache.Cache +} + +func NewRuleHandler(s *store.Store, c *cache.Cache) *RuleHandler { + return &RuleHandler{store: s, cache: c} +} + +type createRuleReq struct { + SourceName string `json:"source_name" binding:"required"` + Event string `json:"event" binding:"required"` + TemplateName string `json:"template_name" binding:"required"` + Channels []string `json:"channels"` + Conditions []model.Condition `json:"conditions,omitempty"` + Enabled int `json:"enabled"` +} + +func (h *RuleHandler) Create(c *gin.Context) { + var req createRuleReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if req.Enabled == 0 { + req.Enabled = 1 + } + + // Resolve names -> IDs + src, err := h.store.GetSourceByName(c.Request.Context(), req.SourceName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "source not found: " + req.SourceName}) + return + } + tmpl, err := h.store.GetTemplateByName(c.Request.Context(), req.TemplateName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "template not found: " + req.TemplateName}) + return + } + channelIDs, err := resolveChannelNames(h.store, c, req.Channels) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var condsJSON *json.RawMessage + if len(req.Conditions) > 0 { + data, _ := json.Marshal(req.Conditions) + raw := json.RawMessage(data) + condsJSON = &raw + } + + rule := &model.Rule{ + SourceID: src.ID, + Event: req.Event, + TemplateID: tmpl.ID, + Conditions: condsJSON, + Enabled: req.Enabled, + } + + if err := h.store.CreateRule(c.Request.Context(), rule, channelIDs); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, rule) +} + +func (h *RuleHandler) List(c *gin.Context) { + rules, err := h.store.ListRules(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, rules) +} + +func (h *RuleHandler) Get(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + rule, err := h.store.GetRule(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) + return + } + c.JSON(http.StatusOK, rule) +} + +func (h *RuleHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var req createRuleReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + src, err := h.store.GetSourceByName(c.Request.Context(), req.SourceName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "source not found"}) + return + } + tmpl, err := h.store.GetTemplateByName(c.Request.Context(), req.TemplateName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "template not found"}) + return + } + channelIDs, err := resolveChannelNames(h.store, c, req.Channels) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var condsJSON *json.RawMessage + if len(req.Conditions) > 0 { + data, _ := json.Marshal(req.Conditions) + raw := json.RawMessage(data) + condsJSON = &raw + } + + rule := &model.Rule{ + SourceID: src.ID, + Event: req.Event, + TemplateID: tmpl.ID, + Conditions: condsJSON, + Enabled: req.Enabled, + } + + if err := h.store.UpdateRule(c.Request.Context(), id, rule, channelIDs); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Invalidate cache + if h.cache != nil { + _ = h.cache.InvalidateRule(c.Request.Context(), src.ID, req.Event) + _ = h.cache.InvalidateChannels(c.Request.Context(), id) + } + + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *RuleHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.DeleteRule(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *RuleHandler) Enable(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.SetRuleEnabled(c.Request.Context(), id, true); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *RuleHandler) Disable(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.SetRuleEnabled(c.Request.Context(), id, false); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *RuleHandler) EnableChannel(c *gin.Context) { + ruleID, _ := strconv.Atoi(c.Param("id")) + channelID, _ := strconv.Atoi(c.Param("channel_id")) + if err := h.store.SetRuleChannelEnabled(c.Request.Context(), ruleID, channelID, true); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *RuleHandler) DisableChannel(c *gin.Context) { + ruleID, _ := strconv.Atoi(c.Param("id")) + channelID, _ := strconv.Atoi(c.Param("channel_id")) + if err := h.store.SetRuleChannelEnabled(c.Request.Context(), ruleID, channelID, false); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func resolveChannelNames(s *store.Store, c *gin.Context, names []string) ([]int, error) { + var ids []int + for _, name := range names { + ch, err := s.GetChannelByName(c.Request.Context(), name) + if err != nil { + return nil, err + } + ids = append(ids, ch.ID) + } + return ids, nil +} diff --git a/internal/handler/source.go b/internal/handler/source.go new file mode 100644 index 0000000..8f489d0 --- /dev/null +++ b/internal/handler/source.go @@ -0,0 +1,109 @@ +package handler + +import ( + "net/http" + "strconv" + + "aiaa-notification-service/internal/cache" + "aiaa-notification-service/internal/model" + "aiaa-notification-service/internal/store" + + "github.com/gin-gonic/gin" +) + +type SourceHandler struct { + store *store.Store + cache *cache.Cache +} + +func NewSourceHandler(s *store.Store, c *cache.Cache) *SourceHandler { + return &SourceHandler{store: s, cache: c} +} + +type createSourceReq struct { + Name string `json:"name" binding:"required"` + ParseMode string `json:"parse_mode"` + ParsePattern string `json:"parse_pattern"` + Status int `json:"status"` +} + +func (h *SourceHandler) Create(c *gin.Context) { + var req createSourceReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if req.ParseMode == "" { + req.ParseMode = "json" + } + if req.Status == 0 { + req.Status = 1 + } + + src := &model.Source{ + Name: req.Name, + ParseMode: req.ParseMode, + ParsePattern: req.ParsePattern, + Status: req.Status, + } + if err := h.store.CreateSource(c.Request.Context(), src); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, src) +} + +func (h *SourceHandler) List(c *gin.Context) { + sources, err := h.store.ListSources(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, sources) +} + +func (h *SourceHandler) Get(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + src, err := h.store.GetSource(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "source not found"}) + return + } + c.JSON(http.StatusOK, src) +} + +func (h *SourceHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var req createSourceReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + src := &model.Source{ + Name: req.Name, + ParseMode: req.ParseMode, + ParsePattern: req.ParsePattern, + Status: req.Status, + } + if err := h.store.UpdateSource(c.Request.Context(), id, src); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Invalidate cache + if h.cache != nil { + _ = h.cache.InvalidateBySource(c.Request.Context(), id) + } + + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *SourceHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.DeleteSource(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} diff --git a/internal/handler/template.go b/internal/handler/template.go new file mode 100644 index 0000000..b33062f --- /dev/null +++ b/internal/handler/template.go @@ -0,0 +1,88 @@ +package handler + +import ( + "net/http" + "strconv" + + "aiaa-notification-service/internal/cache" + "aiaa-notification-service/internal/model" + "aiaa-notification-service/internal/store" + + "github.com/gin-gonic/gin" +) + +type TemplateHandler struct { + store *store.Store + cache *cache.Cache +} + +func NewTemplateHandler(s *store.Store, c *cache.Cache) *TemplateHandler { + return &TemplateHandler{store: s, cache: c} +} + +type createTemplateReq struct { + Name string `json:"name" binding:"required"` + Content string `json:"content" binding:"required"` +} + +func (h *TemplateHandler) Create(c *gin.Context) { + var req createTemplateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + tmpl := &model.Template{Name: req.Name, Content: req.Content} + if err := h.store.CreateTemplate(c.Request.Context(), tmpl); err != nil { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, tmpl) +} + +func (h *TemplateHandler) List(c *gin.Context) { + templates, err := h.store.ListTemplates(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, templates) +} + +func (h *TemplateHandler) Get(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + tmpl, err := h.store.GetTemplate(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) + return + } + c.JSON(http.StatusOK, tmpl) +} + +func (h *TemplateHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var req createTemplateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + tmpl := &model.Template{Name: req.Name, Content: req.Content} + if err := h.store.UpdateTemplate(c.Request.Context(), id, tmpl); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if h.cache != nil { + _ = h.cache.InvalidateByTemplate(c.Request.Context(), id) + } + + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func (h *TemplateHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := h.store.DeleteTemplate(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +}