feat: 完善环境规则、会诊治理、知识审核与效果评估
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user