Files
silk/server-go/internal/service/risk.go
T
2026-08-14 01:16:50 +08:00

166 lines
4.5 KiB
Go
Raw 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 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
}