From 6d9cfc7aa9888f5cdb9008fed85fb48a025f6753 Mon Sep 17 00:00:00 2001 From: weijuesen Date: Wed, 12 Aug 2026 14:44:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E9=A5=B2=E5=85=BB=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E9=A3=8E=E9=99=A9=E6=8F=90=E7=A4=BA=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=8E=E7=A7=8D=E5=AD=90=E6=95=B0=E6=8D=AE=EF=BC=88#13?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-go/internal/handler/knowledge.go | 36 ++++++++++ .../model/knowledge_stage_hint_test.go | 66 ++++++++++++++++++ .../internal/model/knowledge_stage_hints.go | 67 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 server-go/internal/model/knowledge_stage_hint_test.go create mode 100644 server-go/internal/model/knowledge_stage_hints.go diff --git a/server-go/internal/handler/knowledge.go b/server-go/internal/handler/knowledge.go index f9a1df7..6226fe2 100644 --- a/server-go/internal/handler/knowledge.go +++ b/server-go/internal/handler/knowledge.go @@ -28,9 +28,45 @@ func RegisterKnowledgeRoutes(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Ser rg.PATCH("/knowledge/articles/:id", write, updateArticle(db)) rg.DELETE("/knowledge/articles/:id", write, deleteArticle(db)) + rg.GET("/knowledge/stage-hints", read, listStageHints(db)) + registerKnowledgeImageRoute(rg, db, s3, imageBucket) } +// listStageHints 饲养阶段风险提示(?stage= 过滤;GET 天然幂等) +func listStageHints(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + hints, err := model.StageHintsByStage(c.Query("stage")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + resp := make([]gin.H, 0, len(hints)) + for _, h := range hints { + diseases := make([]gin.H, 0, len(h.Diseases)) + for _, dh := range h.Diseases { + id := "" + var d model.Disease + if db.Where("name = ?", dh.Name).First(&d).Error == nil { + id = d.ID + } + diseases = append(diseases, gin.H{ + "id": id, + "name": dh.Name, + "reason": dh.Reason, + }) + } + resp = append(resp, gin.H{ + "stage": h.Stage, + "label": h.Label, + "summary": h.Summary, + "diseases": diseases, + }) + } + c.JSON(http.StatusOK, resp) + } +} + // listDiseases 蚕病百科列表(支持 category/keyword/enabled 过滤) func listDiseases(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/server-go/internal/model/knowledge_stage_hint_test.go b/server-go/internal/model/knowledge_stage_hint_test.go new file mode 100644 index 0000000..df6d62e --- /dev/null +++ b/server-go/internal/model/knowledge_stage_hint_test.go @@ -0,0 +1,66 @@ +package model + +import "testing" + +func TestSeedStageHintsKeysValid(t *testing.T) { + valid := map[string]bool{"young": true, "grown": true, "late5": true, "pupa": true} + seen := map[string]bool{} + for _, h := range SeedStageHints { + if !valid[h.Stage] { + t.Errorf("非法阶段 key: %s", h.Stage) + } + if seen[h.Stage] { + t.Errorf("阶段 key 重复: %s", h.Stage) + } + seen[h.Stage] = true + if h.Label == "" || h.Summary == "" { + t.Errorf("阶段 %s 缺少 label/summary", h.Stage) + } + if len(h.Diseases) == 0 { + t.Errorf("阶段 %s 缺少病种", h.Stage) + } + } +} + +func TestSeedStageHintsDiseasesExist(t *testing.T) { + names := map[string]bool{} + for _, d := range SeedDiseases { + names[d.Name] = true + } + for _, h := range SeedStageHints { + for _, dh := range h.Diseases { + if !names[dh.Name] { + t.Errorf("阶段 %s 引用了不存在的病种: %s", h.Stage, dh.Name) + } + if dh.Reason == "" { + t.Errorf("阶段 %s 病种 %s 缺少原因", h.Stage, dh.Name) + } + } + } +} + +func TestStageHintsByStageEmptyReturnsAll(t *testing.T) { + hints, err := StageHintsByStage("") + if err != nil { + t.Fatalf("空阶段不应报错: %v", err) + } + if len(hints) != len(SeedStageHints) { + t.Errorf("空阶段应返回全部 %d 条,实际 %d", len(SeedStageHints), len(hints)) + } +} + +func TestStageHintsByStageYoung(t *testing.T) { + hints, err := StageHintsByStage("young") + if err != nil { + t.Fatalf("young 不应报错: %v", err) + } + if len(hints) != 1 || hints[0].Stage != "young" { + t.Errorf("young 应返回 1 条且 key=young,实际 %+v", hints) + } +} + +func TestStageHintsByStageUnknown(t *testing.T) { + if _, err := StageHintsByStage("unknown"); err == nil { + t.Error("未知阶段应报错") + } +} diff --git a/server-go/internal/model/knowledge_stage_hints.go b/server-go/internal/model/knowledge_stage_hints.go new file mode 100644 index 0000000..3ebd7ad --- /dev/null +++ b/server-go/internal/model/knowledge_stage_hints.go @@ -0,0 +1,67 @@ +package model + +import "errors" + +// StageDiseaseHint 阶段风险提示中的病种条目 +type StageDiseaseHint struct { + Name string `json:"name"` + Reason string `json:"reason"` +} + +// StageHint 饲养阶段风险提示 +type StageHint struct { + Stage string `json:"stage"` + Label string `json:"label"` + Summary string `json:"summary"` + Diseases []StageDiseaseHint `json:"diseases"` +} + +// SeedStageHints 饲养阶段风险种子数据(内容来源:规格书附录 11.1 高发阶段 + 3.2.3 蚕病环境风险规则) +var SeedStageHints = []StageHint{ + { + Stage: "young", + Label: "小蚕期(1-3龄)", + Summary: "小蚕期抗病力较弱,重点防真菌感染与蚕座卫生问题", + Diseases: []StageDiseaseHint{ + {Name: "其他真菌病(绿僵病/黄僵病/曲霉病)", Reason: "曲霉病多发于小蚕期,注意蚕座卫生与湿度控制"}, + }, + }, + { + Stage: "grown", + Label: "大蚕期(4-5龄)", + Summary: "群体密度大、食桑量大,注意密度、通风与桑叶卫生", + Diseases: []StageDiseaseHint{ + {Name: "软化病", Reason: "5 龄集中暴发,控制蚕头密度、加强通风、避免桑叶潮湿"}, + {Name: "核型多角体病", Reason: "5 龄后期至蛹期高发,春蚕期较多发"}, + }, + }, + { + Stage: "late5", + Label: "5龄后期", + Summary: "临近上蔟,注意温湿度稳定,防止应激诱发急性发病", + Diseases: []StageDiseaseHint{ + {Name: "核型多角体病", Reason: "温度突变(>30℃ 或 <20℃)易诱发潜伏感染转急性发病"}, + }, + }, + { + Stage: "pupa", + Label: "蛹期", + Summary: "蛹期发病影响制种,注意温湿度稳定", + Diseases: []StageDiseaseHint{ + {Name: "核型多角体病", Reason: "5 龄后期至蛹期高发,注意温湿度稳定"}, + }, + }, +} + +// StageHintsByStage 按阶段 key 过滤风险提示;stage 为空返回全部 +func StageHintsByStage(stage string) ([]StageHint, error) { + if stage == "" { + return SeedStageHints, nil + } + for _, h := range SeedStageHints { + if h.Stage == stage { + return []StageHint{h}, nil + } + } + return nil, errors.New("未知的饲养阶段") +}