Files
邹进 af8249158d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s
feat: 停复机规则演练与终态自动关闭轮询
统一停复机规则转正的第一次发布:总开关仍关闭,现有停复机行为不变。

- 新增停复机规则演练:GET /stop-resume-rule/dry-run 按新规则对开机与停机卡只判断不执行,返回运营商账户 × 停机原因 × 判断结果汇总与分页明细,可假设通道实名管控配置;POST /stop-resume-rule/dry-run/export 导出明细(场景 stop_resume_rule_dry_run);仅超级管理员与平台账号
- 风险停机、已销户关闭轮询不再限于独立卡,只关该卡自身轮询,不级联设备
- 错误分类规则增加 auto_disable_polling,仅终态类别可勾选;停复机(含设备停复机)命中该规则时不重试、关闭该卡轮询、回写网关扩展状态并写审计 iot_card.terminal_disable_polling
- 迁移 000250 加列与约束,并为"已销户""仅支持风险复机"置位
2026-10-25 17:52:40 +09:00

143 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gatewaylimit
import (
"context"
"regexp"
"strings"
"sync"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// ClassificationLoader 读取全部启用的错误分类规则(按优先级排序)。
type ClassificationLoader interface {
ListEnabled(ctx context.Context) ([]model.GatewayErrorClassification, error)
}
// Classification 是一次上游错误的分类结果。
type Classification struct {
Category string // 见 constants.GatewayClass*
RuleID uint // 命中的规则 ID,未命中为 0
Keyword string // 命中的关键字
MaybeAccepted bool // 上游可能已受理
// AutoDisablePolling 命中规则标记自动关闭轮询:停复机不重试,关闭该卡轮询。
AutoDisablePolling bool
}
type compiledRule struct {
row model.GatewayErrorClassification
ops map[string]bool
regex *regexp.Regexp
}
// Classifier 按后台维护的关键字规则对上游错误分类;规则在进程内缓存,超过 TTL 重新加载。
type Classifier struct {
loader ClassificationLoader
logger *zap.Logger
mu sync.Mutex
rules []compiledRule
loaded time.Time
}
// NewClassifier 创建错误分类器;loader 为 nil 时一律归为业务错误。
func NewClassifier(loader ClassificationLoader, logger *zap.Logger) *Classifier {
if logger == nil {
logger = zap.NewNop()
}
return &Classifier{loader: loader, logger: logger}
}
// Invalidate 使规则缓存立即失效,后台保存规则后调用。
func (c *Classifier) Invalidate() {
if c == nil {
return
}
c.mu.Lock()
c.loaded = time.Time{}
c.mu.Unlock()
}
// Classify 对某操作的上游返回信息分类;未命中任何启用规则时归为业务错误。
func (c *Classifier) Classify(ctx context.Context, op, message string) Classification {
if c == nil {
return Classification{Category: constants.GatewayClassBusiness}
}
return MatchRules(c.current(ctx), op, message)
}
// ClassifyWith 用给定规则列表分类,供测试接口在不写数据的前提下验证规则。
func ClassifyWith(rows []model.GatewayErrorClassification, op, message string) Classification {
return MatchRules(compileRules(rows, zap.NewNop()), op, message)
}
// MatchRules 按优先级顺序返回第一个命中的规则分类。
func MatchRules(rules []compiledRule, op, message string) Classification {
for _, rule := range rules {
if len(rule.ops) > 0 && !rule.ops[op] {
continue
}
if !ruleMatches(rule, message) {
continue
}
return Classification{Category: rule.row.Category, RuleID: rule.row.ID, Keyword: rule.row.Keyword,
MaybeAccepted: rule.row.MaybeAccepted, AutoDisablePolling: rule.row.AutoDisablePolling && rule.row.Category == constants.GatewayClassTerminal}
}
return Classification{Category: constants.GatewayClassBusiness}
}
func ruleMatches(rule compiledRule, message string) bool {
switch rule.row.MatchType {
case constants.GatewayMatchPrefix:
return strings.HasPrefix(message, rule.row.Keyword)
case constants.GatewayMatchRegex:
return rule.regex != nil && rule.regex.MatchString(message)
default:
return strings.Contains(message, rule.row.Keyword)
}
}
func (c *Classifier) current(ctx context.Context) []compiledRule {
c.mu.Lock()
defer c.mu.Unlock()
if c.loader == nil || time.Since(c.loaded) < constants.GatewayRateLimitConfigTTL {
return c.rules
}
rows, err := c.loader.ListEnabled(ctx)
c.loaded = time.Now()
if err != nil {
c.logger.Warn("加载上游错误分类规则失败,沿用上一次规则", zap.Error(err))
return c.rules
}
c.rules = compileRules(rows, c.logger)
return c.rules
}
func compileRules(rows []model.GatewayErrorClassification, logger *zap.Logger) []compiledRule {
rules := make([]compiledRule, 0, len(rows))
for _, row := range rows {
rule := compiledRule{row: row, ops: map[string]bool{}}
ops, err := DecodeOperations(row.Operations)
if err != nil {
logger.Warn("错误分类规则适用操作非法,已跳过", zap.Uint("rule_id", row.ID))
continue
}
for _, op := range ops {
rule.ops[op] = true
}
if row.MatchType == constants.GatewayMatchRegex {
re, err := regexp.Compile(row.Keyword)
if err != nil {
logger.Warn("错误分类规则正则非法,已跳过", zap.Uint("rule_id", row.ID))
continue
}
rule.regex = re
}
rules = append(rules, rule)
}
return rules
}