From 7651c575360204d8ac3b1734324da661a0ec2f8e Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 18 Aug 2026 14:09:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=A7=84=E5=88=99=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=85=B3=E8=81=94=E9=80=9A=E7=9F=A5=E6=B8=A0=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: 前端在展示规则详情与规则列表时,需要同时看到每条规则绑定了哪些通知渠道以及各渠道的启用状态。此前规则接口只返回规则本身,渠道绑定信息需要额外请求才能获取,增加了交互成本。本次让规则读取接口一次性携带关联渠道信息。 Changes: * 规则模型新增 Channels 字段及渠道条目结构,包含渠道标识、名称、类型和按规则维度的启用开关 * 创建、查询单条、列表查询规则接口在返回结果时填充绑定的渠道信息,无绑定时返回空列表 * 新增批量查询规则与渠道绑定关系的数据访问能力,按规则聚合返回,避免列表场景下逐条查询 * 单个渠道数据读取失败时跳过该条目,不阻断整体结果返回 --- internal/handler/rule.go | 76 +++++++++++++++++++++++++++++++++++++++- internal/model/model.go | 10 ++++++ internal/store/rule.go | 26 ++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/internal/handler/rule.go b/internal/handler/rule.go index dfaca2f..f66059d 100644 --- a/internal/handler/rule.go +++ b/internal/handler/rule.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/json" "net/http" "strconv" @@ -78,7 +79,7 @@ func (h *RuleHandler) Create(c *gin.Context) { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } - + rule.Channels = h.loadRuleChannels(c.Request.Context(), rule.ID) c.JSON(http.StatusCreated, rule) } @@ -95,6 +96,10 @@ func (h *RuleHandler) List(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + if err := h.fillRuleChannels(c, rules); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusOK, gin.H{"data": rules, "total": total, "page": page.Page}) } @@ -105,9 +110,78 @@ func (h *RuleHandler) Get(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) return } + rule.Channels = h.loadRuleChannels(c.Request.Context(), id) c.JSON(http.StatusOK, rule) } +// loadRuleChannels returns all channels bound to a rule, each with the per-rule +// enabled switch, ordered by rule_channel id. +func (h *RuleHandler) loadRuleChannels(ctx context.Context, ruleID int) []model.RuleChannelItem { + byRule, err := h.store.ListRuleChannels(ctx, []int{ruleID}) + if err != nil || len(byRule) == 0 { + return []model.RuleChannelItem{} + } + rcs := byRule[ruleID] + if len(rcs) == 0 { + return []model.RuleChannelItem{} + } + items := make([]model.RuleChannelItem, 0, len(rcs)) + for _, rc := range rcs { + ch, err := h.store.GetChannel(ctx, rc.ChannelID) + if err != nil { + continue + } + items = append(items, model.RuleChannelItem{ + ID: ch.ID, + Name: ch.Name, + Type: ch.Type, + Enabled: rc.Enabled, + }) + } + return items +} + +// fillRuleChannels bulk-loads bound channels for rules and attaches them. +func (h *RuleHandler) fillRuleChannels(ctx context.Context, rules []model.Rule) error { + if len(rules) == 0 { + return nil + } + ids := make([]int, 0, len(rules)) + for i := range rules { + ids = append(ids, rules[i].ID) + } + byRule, err := h.store.ListRuleChannels(ctx, ids) + if err != nil { + return err + } + if len(byRule) == 0 { + return nil + } + for i := range rules { + r := &rules[i] + rcs := byRule[r.ID] + if len(rcs) == 0 { + r.Channels = []model.RuleChannelItem{} + continue + } + items := make([]model.RuleChannelItem, 0, len(rcs)) + for _, rc := range rcs { + ch, err := h.store.GetChannel(ctx, rc.ChannelID) + if err != nil { + continue + } + items = append(items, model.RuleChannelItem{ + ID: ch.ID, + Name: ch.Name, + Type: ch.Type, + Enabled: rc.Enabled, + }) + } + r.Channels = items + } + return nil +} + func (h *RuleHandler) Update(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req createRuleReq diff --git a/internal/model/model.go b/internal/model/model.go index 7ef32c9..8a246eb 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -35,6 +35,13 @@ type Channel struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +type RuleChannelItem struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled int `json:"enabled"` +} + type Rule struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` @@ -45,6 +52,9 @@ type Rule struct { Enabled int `db:"enabled" json:"enabled"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + // Channels holds the channels bound to this rule. Populated by handlers when + // reading rules; each item includes the per-rule enabled switch. + Channels []RuleChannelItem `json:"channels"` } type Condition struct { diff --git a/internal/store/rule.go b/internal/store/rule.go index 9793b6a..c4eea58 100644 --- a/internal/store/rule.go +++ b/internal/store/rule.go @@ -7,6 +7,8 @@ import ( "fmt" "aiaa-notification-service/internal/model" + + "github.com/jmoiron/sqlx" ) func (s *Store) CreateRule(ctx context.Context, r *model.Rule, channelIDs []int) error { @@ -158,6 +160,30 @@ func (s *Store) GetRuleChannels(ctx context.Context, ruleID int) ([]model.RuleCh return rcs, nil } +// ListRuleChannels maps each rule to its bound channels (regardless of enabled +// state) and the per-rule enabled switch. +func (s *Store) ListRuleChannels(ctx context.Context, ruleIDs []int) (map[int][]model.RuleChannel, error) { + result := make(map[int][]model.RuleChannel, len(ruleIDs)) + if len(ruleIDs) == 0 { + return result, nil + } + + query, args, err := sqlx.In(`SELECT id, rule_id, channel_id, enabled FROM notification_rule_channel WHERE rule_id IN (?)`, ruleIDs) + if err != nil { + return nil, fmt.Errorf("build rule channels query: %w", err) + } + query = s.DB.Rebind(query) + + rcs := make([]model.RuleChannel, 0) + if err := s.DB.SelectContext(ctx, &rcs, query, args...); err != nil { + return nil, fmt.Errorf("list rule channels: %w", err) + } + for _, rc := range rcs { + result[rc.RuleID] = append(result[rc.RuleID], rc) + } + return result, nil +} + func (s *Store) SetRuleChannelEnabled(ctx context.Context, ruleID, channelID int, enabled bool) error { v := 0 if enabled {