feat: 多检测方式推荐引擎(#20,设备/紧急/操作者/成本偏好)
This commit is contained in:
@@ -117,6 +117,7 @@ func main() {
|
||||
handler.RegisterLampRoutes(api, db, s3Svc, cfg.S3BucketImages)
|
||||
handler.RegisterConsumableRoutes(api, db)
|
||||
handler.RegisterConsultationRoutes(api, db)
|
||||
handler.RegisterDetectionMethodRoutes(api, db)
|
||||
|
||||
// 启动高发病天气预警定时任务(未配置时跳过)
|
||||
go startWeatherAlertLoop(db, weatherSvc, time.Duration(cfg.QWeatherIntervalMin)*time.Minute)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterDetectionMethodRoutes 注册检测方式推荐路由
|
||||
func RegisterDetectionMethodRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
rg.GET("/detection-methods/recommend",
|
||||
middleware.RequirePermission(db, "lamp:read"),
|
||||
recommendDetectionMethod())
|
||||
}
|
||||
|
||||
// recommendDetectionMethod 按设备/紧急/操作者/成本偏好推荐检测方式(纯计算)
|
||||
func recommendDetectionMethod() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
in := service.DetectionRecommendInput{
|
||||
HasLamp: c.Query("hasLamp") == "true",
|
||||
HasSers: c.Query("hasSers") == "true",
|
||||
HasQPCR: c.Query("hasQpcr") == "true",
|
||||
HasHyperspectral: c.Query("hasHyperspectral") == "true",
|
||||
Urgency: c.DefaultQuery("urgency", "routine"),
|
||||
OperatorLevel: c.DefaultQuery("operatorLevel", "expert"),
|
||||
CostPreference: c.DefaultQuery("costPreference", "balanced"),
|
||||
}
|
||||
c.JSON(http.StatusOK, service.RecommendMethod(in))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package service
|
||||
|
||||
import "sort"
|
||||
|
||||
// DetectionRecommendInput 多检测方式推荐输入
|
||||
type DetectionRecommendInput struct {
|
||||
HasLamp bool
|
||||
HasSers bool
|
||||
HasQPCR bool
|
||||
HasHyperspectral bool
|
||||
Urgency string // routine/urgent
|
||||
OperatorLevel string // novice/expert
|
||||
CostPreference string // balanced/low_cost/fastest
|
||||
}
|
||||
|
||||
// DetectionRecommendation 推荐结果
|
||||
type DetectionRecommendation struct {
|
||||
Method string `json:"method"`
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason"`
|
||||
Time string `json:"time"`
|
||||
Cost string `json:"cost"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
}
|
||||
|
||||
type methodScore struct {
|
||||
info DetectionRecommendation
|
||||
time int
|
||||
cost int
|
||||
diff int
|
||||
}
|
||||
|
||||
// RecommendMethod 按设备条件/紧急程度/操作者水平/成本偏好推荐检测方式
|
||||
// 规则取自规格书 3.3.2 与 11.2 参数对比;高光谱理论可行待验证,仅在其他方式都不可用时推荐
|
||||
func RecommendMethod(in DetectionRecommendInput) []DetectionRecommendation {
|
||||
pool := make([]methodScore, 0, 4)
|
||||
if in.HasLamp {
|
||||
pool = append(pool, methodScore{
|
||||
info: DetectionRecommendation{
|
||||
Method: "lamp", Name: "LAMP", Time: "50-100 分钟", Cost: "设备 <3000 元,单次 5-20 元", Difficulty: "中",
|
||||
},
|
||||
time: 3, cost: 5, diff: 3,
|
||||
})
|
||||
}
|
||||
if in.HasSers {
|
||||
pool = append(pool, methodScore{
|
||||
info: DetectionRecommendation{
|
||||
Method: "sers", Name: "SERS", Time: "约 3 分钟", Cost: "设备 8-15 万,单次 10-30 元", Difficulty: "低",
|
||||
},
|
||||
time: 5, cost: 1, diff: 5,
|
||||
})
|
||||
}
|
||||
if in.HasQPCR {
|
||||
pool = append(pool, methodScore{
|
||||
info: DetectionRecommendation{
|
||||
Method: "qpcr", Name: "qPCR", Time: "2-3 小时", Cost: "设备 5-30 万,单次 50-100 元", Difficulty: "高",
|
||||
},
|
||||
time: 1, cost: 2, diff: 1,
|
||||
})
|
||||
}
|
||||
if in.HasHyperspectral {
|
||||
pool = append(pool, methodScore{
|
||||
info: DetectionRecommendation{
|
||||
Method: "hyperspectral", Name: "高光谱", Time: "秒级~分钟级", Cost: "设备数万-十几万,单次 0 元", Difficulty: "低(待验证)",
|
||||
},
|
||||
time: 5, cost: 2, diff: 5,
|
||||
})
|
||||
}
|
||||
if len(pool) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 高光谱理论可行待验证:仅当它是唯一可用方式时推荐,否则剔除
|
||||
if len(pool) > 1 {
|
||||
filtered := pool[:0]
|
||||
for _, m := range pool {
|
||||
if m.info.Method != "hyperspectral" {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
pool = filtered
|
||||
}
|
||||
|
||||
// 权重
|
||||
wt, wc, wd := 0.3, 0.4, 0.3
|
||||
if in.Urgency == "urgent" {
|
||||
wt, wc, wd = 0.6, 0.2, 0.2
|
||||
}
|
||||
if in.OperatorLevel == "novice" {
|
||||
wt, wc, wd = 0.2, 0.2, 0.6
|
||||
}
|
||||
if in.CostPreference == "low_cost" {
|
||||
wt, wc, wd = 0.2, 0.6, 0.2
|
||||
} else if in.CostPreference == "fastest" {
|
||||
wt, wc, wd = 0.6, 0.2, 0.2
|
||||
}
|
||||
|
||||
scores := make([]float64, len(pool))
|
||||
for i, m := range pool {
|
||||
scores[i] = wt*float64(m.time) + wc*float64(m.cost) + wd*float64(m.diff)
|
||||
}
|
||||
order := make([]int, len(pool))
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
sort.SliceStable(order, func(a, b int) bool {
|
||||
if scores[order[a]] != scores[order[b]] {
|
||||
return scores[order[a]] > scores[order[b]]
|
||||
}
|
||||
return order[a] < order[b]
|
||||
})
|
||||
|
||||
result := make([]DetectionRecommendation, 0, len(pool))
|
||||
for _, idx := range order {
|
||||
m := pool[idx]
|
||||
m.info.Reason = reasonFor(m.info.Method, in)
|
||||
result = append(result, m.info)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func reasonFor(method string, in DetectionRecommendInput) string {
|
||||
switch method {
|
||||
case "sers":
|
||||
return "出结果最快(约 3 分钟)、操作难度低,适合紧急或新手场景;设备投入较高"
|
||||
case "lamp":
|
||||
if in.CostPreference == "low_cost" {
|
||||
return "设备投入最低(<3000 元)、基层最务实,性价比高"
|
||||
}
|
||||
if in.Urgency == "urgent" {
|
||||
return "在无 SERS 时最快的可选方案(50-100 分钟),设备投入低"
|
||||
}
|
||||
return "设备投入低、基层最务实,是目前蚕业领域最常用的分子检测方案"
|
||||
case "qpcr":
|
||||
return "金标准准确率最高,适合争议仲裁;设备投入大、操作门槛高、耗时 2-3 小时"
|
||||
case "hyperspectral":
|
||||
return "非接触秒级检测,但蚕病领域理论可行待验证,建议先做可行性实验"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRecommendMethodNoDevices(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{})
|
||||
if len(got) != 0 {
|
||||
t.Errorf("无可用设备应返回空,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendMethodLowCost(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{
|
||||
HasLamp: true, HasSers: true, HasQPCR: true,
|
||||
CostPreference: "low_cost",
|
||||
})
|
||||
if len(got) == 0 || got[0].Method != "lamp" {
|
||||
t.Errorf("低成本偏好应首选 LAMP,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendMethodFastest(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{
|
||||
HasLamp: true, HasSers: true, HasQPCR: true,
|
||||
CostPreference: "fastest",
|
||||
})
|
||||
if len(got) == 0 || got[0].Method != "sers" {
|
||||
t.Errorf("最快偏好应首选 SERS,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendMethodUrgentNoSERS(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{
|
||||
HasLamp: true, HasQPCR: true,
|
||||
Urgency: "urgent",
|
||||
})
|
||||
if len(got) == 0 || got[0].Method != "lamp" {
|
||||
t.Errorf("紧急且无 SERS 应首选 LAMP,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendMethodNoviceAvoidsQPCR(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{
|
||||
HasLamp: true, HasSers: true, HasQPCR: true,
|
||||
OperatorLevel: "novice",
|
||||
})
|
||||
if len(got) == 0 {
|
||||
t.Fatal("应有推荐")
|
||||
}
|
||||
if got[len(got)-1].Method == "qpcr" {
|
||||
// qPCR 难度高,新手应排在最后(除非仅此可选)
|
||||
}
|
||||
// 至少 qPCR 不应排第一
|
||||
if got[0].Method == "qpcr" {
|
||||
t.Errorf("新手不应首选 qPCR,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendMethodOnlyHyperspectral(t *testing.T) {
|
||||
got := RecommendMethod(DetectionRecommendInput{HasHyperspectral: true})
|
||||
if len(got) != 1 || got[0].Method != "hyperspectral" {
|
||||
t.Errorf("仅有高光谱时应推荐且标注待验证,实际 %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user