feat: CID-based routing rules engine with flexible match conditions
Routing rules: - RouteRule model: 3 match fields (caller_id, dest, ingress_trunk regex), 3 action types (node/pool/auto), time-based (days of week, time range) - Engine router.go: CompiledRule with pre-compiled regex, PickNodeForCall with cascade (dead node → next rule), pickNodeFromPool - Separate sync.RWMutex for Router — zero contention admin vs traffic - Validate-before-save pattern: engine validates before JSON save - Graceful degradation: corrupted rules skipped, no rules → PickNode() Persistence: - data/routing.json with atomic save (tmp→rename), config.Manager - Auto-created empty on first start API (admin only): - GET/POST/PUT/DELETE /api/routing/rules - PUT /api/routing/rules/:id/toggle - PUT /api/routing/rules/reorder Integration: - /api/route now uses PickNodeForCall with caller_id/dest/ingress_trunk query params (already sent by route.lua) - RouteResponse includes matched_rule field for debugging - Main.go loads routing rules on startup with graceful fallback Web UI: - /routing page: table with priority arrows, inline edit, create form, regex test tool, toggle, delete, action badges, hits counter Tests: - 28 tests: matchRule, time matching, PickNodeForCall cascade, pool scoring, validation, hit counting, compilation
This commit is contained in:
@@ -91,6 +91,38 @@ type LoginResponse struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// --- Routing Rules ---
|
||||
|
||||
// RouteRule — правило маршрутизации: CID-based, destination-based, ingress-trunk routing.
|
||||
// Правила проверяются по порядку Priority (first-match-wins).
|
||||
// Все непустые Match-поля должны совпасть (AND-логика).
|
||||
// Пустые Match-поля — не проверяются.
|
||||
type RouteRule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Priority int `json:"priority"`
|
||||
|
||||
// Match conditions — regex, пустая строка = не проверять
|
||||
MatchCallerID string `json:"match_caller_id,omitempty"`
|
||||
MatchDestination string `json:"match_destination,omitempty"`
|
||||
MatchIngressTrunk string `json:"match_ingress_trunk,omitempty"`
|
||||
|
||||
// Time-based match — пустые = не проверять
|
||||
MatchDaysOfWeek string `json:"match_days,omitempty"` // "1-5" | "1,3,5"
|
||||
MatchTimeStart string `json:"match_time_start,omitempty"` // "HH:MM"
|
||||
MatchTimeEnd string `json:"match_time_end,omitempty"` // "HH:MM"
|
||||
|
||||
// Action
|
||||
ActionType string `json:"action_type"` // "node" | "pool" | "auto"
|
||||
ActionNodeID string `json:"action_node_id,omitempty"`
|
||||
ActionPoolIDs []string `json:"action_pool_ids,omitempty"`
|
||||
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// --- /api/route ---
|
||||
|
||||
// RouteResponse — ответ на запрос маршрутизации.
|
||||
@@ -100,6 +132,9 @@ type RouteResponse struct {
|
||||
Score float64 `json:"score,omitempty"`
|
||||
SIPGateway string `json:"sip_gateway,omitempty"`
|
||||
|
||||
// Какое правило маршрутизации применилось (пустая строка = none)
|
||||
MatchedRule string `json:"matched_rule,omitempty"`
|
||||
|
||||
// Fallback
|
||||
Fallback bool `json:"fallback,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user