package service import ( "encoding/json" "fmt" "sort" "time" ) // TelemetrySample 规则引擎遥测样本。 type TelemetrySample struct { Metric string `json:"metric"` Value float64 `json:"value"` Timestamp time.Time `json:"timestamp"` } // RuleContext 规则输入;缺失项用 nil 表示,不填 0。 type RuleContext struct { Stage string `json:"stage,omitempty"` Now time.Time `json:"now"` Samples []TelemetrySample `json:"samples"` Density *float64 `json:"density,omitempty"` Ventilation *bool `json:"ventilation,omitempty"` SeedSourceCount *int `json:"seedSourceCount,omitempty"` DisinfectionCount *int `json:"disinfectionCount,omitempty"` } // RuleVersion 可版本化规则。 type RuleVersion struct { Version string `json:"version"` Name string `json:"name"` Disease string `json:"disease"` WindowHours int `json:"windowHours"` MaxStaleness time.Duration `json:"-"` RequiresDensity bool `json:"-"` RequiresBiosecurity bool `json:"-"` } // RuleResult 规则输出,保留版本、输入快照、缺失项和计算时间。 type RuleResult struct { RuleVersion string `json:"ruleVersion"` RuleName string `json:"ruleName"` Disease string `json:"disease"` Matched bool `json:"matched"` Level string `json:"level,omitempty"` Reason string `json:"reason,omitempty"` Missing []string `json:"missing"` InputSnapshot json.RawMessage `json:"inputSnapshot"` ComputedAt time.Time `json:"computedAt"` } // EvaluateRules 按版本执行规则,所有缺失输入进入 Missing。 func EvaluateRules(ctx RuleContext, rules []RuleVersion) []RuleResult { if ctx.Now.IsZero() { ctx.Now = time.Now() } results := make([]RuleResult, 0, len(rules)) input, _ := json.Marshal(ctx) for _, rule := range rules { missing := ruleMissing(ctx, rule) result := RuleResult{ RuleVersion: rule.Version, RuleName: rule.Name, Disease: rule.Disease, Missing: missing, InputSnapshot: input, ComputedAt: ctx.Now, } if len(missing) == 0 { switch rule.Name { case "continuous_humidity": result.Matched, result.Level, result.Reason = evaluateContinuousHumidity(ctx, rule) default: result.Matched = false result.Reason = "未配置规则实现" } } results = append(results, result) } return results } func ruleMissing(ctx RuleContext, rule RuleVersion) []string { missing := []string{} hasHumidity := false hasStaleHumidity := false for _, sample := range ctx.Samples { if sample.Metric != "humidity" { continue } if ctx.Now.Sub(sample.Timestamp) > rule.MaxStaleness { hasStaleHumidity = true continue } hasHumidity = true } if rule.Name == "continuous_humidity" { if hasStaleHumidity && !hasHumidity { missing = append(missing, "humidity_stale") } if !hasHumidity { missing = append(missing, "humidity") } } if rule.RequiresDensity && ctx.Density == nil { missing = append(missing, "density") } if rule.RequiresBiosecurity { if ctx.SeedSourceCount == nil { missing = append(missing, "seed_source") } if ctx.DisinfectionCount == nil { missing = append(missing, "disinfection") } } return missing } func evaluateContinuousHumidity(ctx RuleContext, rule RuleVersion) (bool, string, string) { byDay := map[string]float64{} for _, sample := range ctx.Samples { if sample.Metric != "humidity" { continue } day := sample.Timestamp.Format("2006-01-02") if sample.Value > byDay[day] { byDay[day] = sample.Value } } days := make([]string, 0, len(byDay)) for day := range byDay { days = append(days, day) } sort.Strings(days) for i := 2; i < len(days); i++ { if consecutiveDays(days[i-2], days[i-1], days[i]) && byDay[days[i-2]] >= 80 && byDay[days[i-1]] >= 80 && byDay[days[i]] >= 80 { return true, "orange", fmt.Sprintf("连续 %d 天湿度≥80%%,满足规则 %s", rule.WindowHours, rule.Version) } } return false, "", "连续高湿天数不足或窗口内存在缺失" } func consecutiveDays(a, b, c string) bool { parse := func(s string) time.Time { t, _ := time.Parse("2006-01-02", s) return t } da, db, dc := parse(a), parse(b), parse(c) return db.Sub(da) == 24*time.Hour && dc.Sub(db) == 24*time.Hour }