feat(规则管理): 规则查询接口返回关联通知渠道
Motivation: 前端在展示规则详情与规则列表时,需要同时看到每条规则绑定了哪些通知渠道以及各渠道的启用状态。此前规则接口只返回规则本身,渠道绑定信息需要额外请求才能获取,增加了交互成本。本次让规则读取接口一次性携带关联渠道信息。 Changes: * 规则模型新增 Channels 字段及渠道条目结构,包含渠道标识、名称、类型和按规则维度的启用开关 * 创建、查询单条、列表查询规则接口在返回结果时填充绑定的渠道信息,无绑定时返回空列表 * 新增批量查询规则与渠道绑定关系的数据访问能力,按规则聚合返回,避免列表场景下逐条查询 * 单个渠道数据读取失败时跳过该条目,不阻断整体结果返回
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -78,7 +79,7 @@ func (h *RuleHandler) Create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rule.Channels = h.loadRuleChannels(c.Request.Context(), rule.ID)
|
||||||
c.JSON(http.StatusCreated, rule)
|
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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
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})
|
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"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rule.Channels = h.loadRuleChannels(c.Request.Context(), id)
|
||||||
c.JSON(http.StatusOK, rule)
|
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) {
|
func (h *RuleHandler) Update(c *gin.Context) {
|
||||||
id, _ := strconv.Atoi(c.Param("id"))
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
var req createRuleReq
|
var req createRuleReq
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ type Channel struct {
|
|||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
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 {
|
type Rule struct {
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
@@ -45,6 +52,9 @@ type Rule struct {
|
|||||||
Enabled int `db:"enabled" json:"enabled"`
|
Enabled int `db:"enabled" json:"enabled"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_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 {
|
type Condition struct {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"aiaa-notification-service/internal/model"
|
"aiaa-notification-service/internal/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) CreateRule(ctx context.Context, r *model.Rule, channelIDs []int) error {
|
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
|
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 {
|
func (s *Store) SetRuleChannelEnabled(ctx context.Context, ruleID, channelID int, enabled bool) error {
|
||||||
v := 0
|
v := 0
|
||||||
if enabled {
|
if enabled {
|
||||||
|
|||||||
Reference in New Issue
Block a user