package api import ( "fmt" "net/http" "sort" "time" "github.com/pulse-lets-go/internal/models" "github.com/pulse-lets-go/internal/util" ) // handleGetRoutingRules — GET /api/routing/rules — список всех правил с hits. func (a *API) handleGetRoutingRules(w http.ResponseWriter, r *http.Request) { rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил маршрутизации") return } // Сортировка по priority sort.Slice(rules, func(i, j int) bool { return rules[i].Priority < rules[j].Priority }) // Подмешиваем hits из engine в модель compiledRules := a.engine.GetCompiledRules() hitsMap := make(map[string]int64, len(compiledRules)) for _, cr := range compiledRules { hitsMap[cr.ID] = cr.GetHits() } type RuleWithHits struct { models.RouteRule Hits int64 `json:"hits"` } result := make([]RuleWithHits, len(rules)) for i, r := range rules { result[i] = RuleWithHits{RouteRule: r, Hits: hitsMap[r.ID]} } writeJSON(w, http.StatusOK, result) } // handleCreateRoutingRule — POST /api/routing/rules — создание нового правила. func (a *API) handleCreateRoutingRule(w http.ResponseWriter, r *http.Request) { var rule models.RouteRule if err := decodeJSON(r, &rule); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } // Генерируем ID rule.ID = "rr-" + util.RandomHex(6) now := time.Now().UTC() rule.CreatedAt = now rule.UpdatedAt = now rule.Enabled = true if rule.Priority <= 0 { rule.Priority = 10 } // Читаем текущие правила rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил") return } // Добавляем новое правило rules = append(rules, rule) // Валидируем (validate-before-save) if err := a.engine.ValidateRules(rules); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // Сохраняем на диск if err := a.configManager.SaveRoutingRules(rules); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения правила") return } // Загружаем в память a.engine.SetRules(rules) writeJSON(w, http.StatusCreated, rule) } // handleUpdateRoutingRule — PUT /api/routing/rules/{id} — обновление правила. func (a *API) handleUpdateRoutingRule(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { writeError(w, http.StatusBadRequest, "id обязателен") return } var updates models.RouteRule if err := decodeJSON(r, &updates); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил") return } idx := -1 for i, rule := range rules { if rule.ID == id { idx = i break } } if idx < 0 { writeError(w, http.StatusNotFound, "правило не найдено") return } // Частичное обновление: применяем только непустые поля target := &rules[idx] if updates.Name != "" { target.Name = updates.Name } if updates.Priority > 0 { target.Priority = updates.Priority } if updates.MatchCallerID != "" { target.MatchCallerID = updates.MatchCallerID } if updates.MatchDestination != "" { target.MatchDestination = updates.MatchDestination } if updates.MatchIngressTrunk != "" { target.MatchIngressTrunk = updates.MatchIngressTrunk } if updates.MatchDaysOfWeek != "" { target.MatchDaysOfWeek = updates.MatchDaysOfWeek } if updates.MatchTimeStart != "" { target.MatchTimeStart = updates.MatchTimeStart } if updates.MatchTimeEnd != "" { target.MatchTimeEnd = updates.MatchTimeEnd } if updates.ActionType != "" { target.ActionType = updates.ActionType target.ActionNodeID = updates.ActionNodeID target.ActionPoolIDs = updates.ActionPoolIDs } if updates.Description != "" { target.Description = updates.Description } // Позволяем очистить match-поля через пустую строку в JSON (omitempty не подходит) // Для этого используем указатели, но для упрощения: если поле не передано — оставляем старое // Если передано явно (даже пустое) — нужно смотреть raw JSON, здесь упрощённо target.UpdatedAt = time.Now().UTC() // Валидируем if err := a.engine.ValidateRules(rules); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // Сохраняем if err := a.configManager.SaveRoutingRules(rules); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения правила") return } // Загружаем в память a.engine.SetRules(rules) writeJSON(w, http.StatusOK, *target) } // handleDeleteRoutingRule — DELETE /api/routing/rules/{id} — удаление правила. func (a *API) handleDeleteRoutingRule(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { writeError(w, http.StatusBadRequest, "id обязателен") return } rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил") return } found := false filtered := make([]models.RouteRule, 0, len(rules)) for _, rule := range rules { if rule.ID == id { found = true continue } filtered = append(filtered, rule) } if !found { writeError(w, http.StatusNotFound, "правило не найдено") return } if err := a.configManager.SaveRoutingRules(filtered); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения правил") return } // Загружаем в память (валидация не нужна — удаление не может нарушить валидность) a.engine.SetRules(filtered) writeJSON(w, http.StatusNoContent, nil) } // handleToggleRoutingRule — PUT /api/routing/rules/{id}/toggle — вкл/выкл правила. func (a *API) handleToggleRoutingRule(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { writeError(w, http.StatusBadRequest, "id обязателен") return } var req struct { Enabled bool `json:"enabled"` } if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил") return } idx := -1 for i, rule := range rules { if rule.ID == id { idx = i break } } if idx < 0 { writeError(w, http.StatusNotFound, "правило не найдено") return } rules[idx].Enabled = req.Enabled rules[idx].UpdatedAt = time.Now().UTC() if err := a.configManager.SaveRoutingRules(rules); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения правила") return } a.engine.SetRules(rules) writeJSON(w, http.StatusOK, rules[idx]) } // handleReorderRoutingRules — PUT /api/routing/rules/reorder — переупорядочивание правил. // Принимает массив ID в новом порядке. Переназначает Priority = index * 10. func (a *API) handleReorderRoutingRules(w http.ResponseWriter, r *http.Request) { var order []string if err := decodeJSON(r, &order); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON: ожидается массив id") return } if len(order) == 0 { writeError(w, http.StatusBadRequest, "пустой порядок") return } rules, err := a.configManager.ReadRoutingRules() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения правил") return } // Строим map для быстрого поиска ruleMap := make(map[string]*models.RouteRule, len(rules)) for i := range rules { ruleMap[rules[i].ID] = &rules[i] } // Проверяем что все ID из order присутствуют for _, id := range order { if _, ok := ruleMap[id]; !ok { writeError(w, http.StatusBadRequest, fmt.Sprintf("правило с id '%s' не найдено", id)) return } } // Переназначаем приоритеты now := time.Now().UTC() for i, id := range order { r := ruleMap[id] r.Priority = (i + 1) * 10 r.UpdatedAt = now } if err := a.configManager.SaveRoutingRules(rules); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения порядка") return } a.engine.SetRules(rules) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }