package service import ( "math" "strings" ) // RiskRuleVersion 当前风险规则版本;权重仍沿用试点公式,待数据校准后升版。 const RiskRuleVersion = "risk-v2-2026.08.14" // RiskInput 风险评分输入。nil 表示未采集,不能按 0 参与归一化。 type RiskInput struct { AI *float64 // AI 异常概率(权重 0.5) Env *float64 // 环境风险系数(权重 0.2) Stage *float64 // 饲养阶段风险系数(权重 0.15) Uniformity *float64 // 群体整齐度偏离度(权重 0.15) ModelVersion string } // RiskAssessment 可解释风险输出。 type RiskAssessment struct { Score float64 `json:"score"` Level string `json:"level"` Confidence string `json:"confidence"` ModelVersion string `json:"modelVersion"` RuleVersion string `json:"ruleVersion"` Components map[string]*float64 `json:"components"` Missing []string `json:"missing"` } // ComputeRiskScore 按可用权重归一化计算 0~100 风险分,缺失组件返回 null。 // 当前 0.5/0.2/0.15/0.15 是待试点校准规则,不是科学结论。 func ComputeRiskScore(in RiskInput) RiskAssessment { defs := []struct { key string weight float64 value *float64 }{ {"aiAbnormalProbability", 0.5, in.AI}, {"environment", 0.2, in.Env}, {"stage", 0.15, in.Stage}, {"uniformity", 0.15, in.Uniformity}, } components := make(map[string]*float64, len(defs)) missing := make([]string, 0, len(defs)) weighted := 0.0 totalWeight := 0.0 for _, def := range defs { if def.value == nil { components[def.key] = nil missing = append(missing, def.key) continue } value := clamp01(*def.value) components[def.key] = &value weighted += def.weight * value totalWeight += def.weight } score := 0.0 if totalWeight > 0 { score = math.Round(clamp01(weighted/totalWeight)*10000) / 100 } return RiskAssessment{ Score: score, Level: RiskLevel(score), Confidence: RiskConfidence(in), ModelVersion: in.ModelVersion, RuleVersion: RiskRuleVersion, Components: components, Missing: missing, } } // RiskConfidence 当前仅给出 low/medium/unknown,避免把未校准规则描述为 high。 func RiskConfidence(in RiskInput) string { if in.AI == nil { return "unknown" } if *in.AI >= 0.25 { return "medium" } return "low" } // AbnormalProbability 从 AI 检测结果计算异常概率:healthy/unknown 不贡献风险。 // abnormalClasses 可选;缺省时任一非 healthy/unknown 类别都视为异常类别。 func AbnormalProbability(detections []AIDetection, abnormalClasses ...string) float64 { configured := make(map[string]struct{}, len(abnormalClasses)) for _, class := range abnormalClasses { if class = strings.ToLower(strings.TrimSpace(class)); class != "" { configured[class] = struct{}{} } } best := 0.0 for _, d := range detections { class := strings.ToLower(strings.TrimSpace(d.ClassName)) if class == "" || class == "healthy" || class == "unknown" { continue } if len(configured) > 0 { if _, ok := configured[class]; !ok { continue } } if d.Confidence > best { best = d.Confidence } } return clamp01(best) } func clamp01(v float64) float64 { return math.Max(0, math.Min(1, v)) } // RiskLevel 按规格书 3.1.4 分级:绿 0-30 / 黄 31-60 / 橙 61-80 / 红 81-100 func RiskLevel(score float64) string { switch { case score <= 30: return "green" case score <= 60: return "yellow" case score <= 80: return "orange" default: return "red" } } // StageCoefficient 蚕房阶段 → 阶段风险系数(Room.Stage 粗粒度映射; // 待 #7 蚕匾/批次管理的龄期字段落地后细化) func StageCoefficient(stage string) float64 { switch stage { case "pupa": return 0.5 // 核型多角体病 5龄后期至蛹期高发 case "larva": return 0.4 // 软化病 5 龄集中暴发等 case "moth": return 0.2 case "egg": return 0.1 default: return 0 } } // EnvCoefficient 由最新温湿度计算环境风险系数(规则取自规格书 3.2.3) func EnvCoefficient(temp, humidity *float64) float64 { coef := 0.0 if humidity != nil { switch { case *humidity >= 80: coef = math.Max(coef, 0.8) // 白僵病等真菌病高湿条件 case *humidity >= 75: coef = math.Max(coef, 0.5) } } if temp != nil { if *temp > 30 || *temp < 20 { coef = math.Max(coef, 0.5) // 温度突变诱发核型多角体病 } } return coef }