Files
silk/docs/superpowers/plans/2026-08-12-knowledge-base.md
T
2026-08-12 10:46:46 +08:00

30 KiB
Raw Blame History

知识库初始版(蚕病百科 + AI 结果解读)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 交付知识库初始版:Go 后端蚕病百科/知识文章 API + 规格书种子数据,Web 后台管理页,小程序农户查看页。

Architecture: 沿用现有分层(Go/Gin/GORM、React/AntD Pro、Taro 4)。新增 diseases(蚕病百科)与 knowledge_articles(AI 结果解读等文章)两表;CRUD 复用现有 handler 模式与 bindUpdates;种子数据启动时幂等写入;权限码 knowledge:read / knowledge:write 控制读写。

Tech Stack: Go 1.23 / Gin / GORMPostgreSQL);React 19 / antd 5 / @ant-design/pro-componentsTaro 4 + React + Sass。

Global Constraints

  • 不得修改生成产物(miniapp/dist/web/dist/);只改源文件。
  • 蚕病内容以规格书 .cc-connect/attachments/蚕病智能防控平台规格说明书.md 附录 11.1、3.1.4(风险分级)、3.8(处置方向/排查清单)为准,不编造。
  • 风险分级阈值(绿 0-30 / 黄 31-60 / 橙 61-80 / 红 81-100)来自规格书 3.1.4。
  • 权限:读 knowledge:read,写 knowledge:write
  • 字段风格沿用现有约定:Go JSON camelCase、DB 列 snake_case、web/miniapp 接口类型与后端 JSON 一致。
  • 验证命令:后端 go test ./... && go build ./... && go vet ./...Web npm run lint && npm run build;小程序 npm run build:weapp
  • 本仓库当前无前端测试基建(Vitest/Jest 均未配置,属后续工作计划 #27),UI 验证以 lint + 类型检查 + 构建为准;Go 侧对纯逻辑(种子数据)写单元测试(TDD)。

Task 1: Go 模型 + 种子数据测试(TDD RED)

Files:

  • Create: server-go/internal/model/knowledge.go
  • Test: server-go/internal/model/knowledge_seed_test.go

Interfaces:

  • Produces: model.Diseasemodel.KnowledgeArticle 结构体;包变量 model.SeedDiseases []Diseasemodel.SeedKnowledgeArticles []KnowledgeArticleTask 2 填充内容)。

  • Step 1: 写失败测试(先建 knowledge.go 只含结构体定义,不含种子变量)

server-go/internal/model/knowledge_seed_test.go

package model

import "testing"

func TestSeedDiseasesCoverCoreFour(t *testing.T) {
	core := []string{"核型多角体病", "白僵病", "软化病", "微粒子病"}
	got := map[string]bool{}
	for _, d := range SeedDiseases {
		got[d.Name] = true
	}
	for _, name := range core {
		if !got[name] {
			t.Errorf("核心蚕病缺失: %s", name)
		}
	}
}

func TestSeedDiseasesRequiredFields(t *testing.T) {
	for _, d := range SeedDiseases {
		if d.Name == "" || d.Category == "" {
			t.Errorf("病种名称/类别不能为空: %+v", d)
		}
		if d.Pathogen == "" || d.Symptoms == "" || d.Transmission == "" || d.Prevention == "" {
			t.Errorf("病种必备字段缺失(病原/症状/传播途径/防治指南): %s", d.Name)
		}
	}
}

func TestSeedDiseasesNamesUnique(t *testing.T) {
	seen := map[string]bool{}
	for _, d := range SeedDiseases {
		if seen[d.Name] {
			t.Errorf("病种名称重复: %s", d.Name)
		}
		seen[d.Name] = true
	}
}

func TestSeedKnowledgeArticlesHasAIGuide(t *testing.T) {
	found := false
	for _, a := range SeedKnowledgeArticles {
		if a.Kind == "ai_guide" && a.Title != "" && a.Content != "" {
			found = true
		}
	}
	if !found {
		t.Error("缺少 AI 结果解读文章(kind=ai_guide")
	}
}
  • Step 2: 运行测试确认失败

Run: cd server-go && go test ./internal/model/ -run TestSeed -v Expected: 编译失败 undefined: SeedDiseases / undefined: SeedKnowledgeArticles(或测试失败)

  • Step 3: 最小实现knowledge.go 定义结构体(见下方 Task 2 代码中结构体部分),并声明空变量 var SeedDiseases []Diseasevar SeedKnowledgeArticles []KnowledgeArticle 使编译通过、测试失败。

  • Step 4: 再运行测试,确认失败原因是数据缺失(如 核心蚕病缺失: 核型多角体病),而非编译错误。

  • Step 5: Commit

git add server-go/internal/model/knowledge.go server-go/internal/model/knowledge_seed_test.go
git commit -m "test(knowledge): seed 数据完整性测试(RED"

Task 2: Go 种子数据填充(TDD GREEN

Files:

  • Create: server-go/internal/model/knowledge_seed.go(完整内容见下方代码块)

Interfaces:

  • Consumes: Task 1 的 Disease / KnowledgeArticle 结构体。

  • Produces: SeedDiseases9 条,含 4 类核心病种)、SeedKnowledgeArticles(1 条 AI 结果解读)。字段值全部来自规格书附录 11.1 与 3.1.4;Prevention(防治指南)按规格书 3.8 处置方向(内源扩散/外源侵入)与分病种排查清单整理。

  • Step 1: 写入种子数据(代码见下)

package model

// SeedDiseases 蚕病百科种子数据(内容来源:规格书附录 11.1 / 3.8)
var SeedDiseases = []Disease{
	{
		Name: "核型多角体病", Category: "viral", SortOrder: 1,
		Pathogen:          "家蚕核型多角体病毒(杆状病毒科,双链DNA病毒)",
		Transmission:      "食下传染为主,创伤传染次之",
		Medium:            "多角体(2-6μm包涵体,存在于病蚕血液、脓汁、粪便中,污染蚕座、桑叶、蚕具)",
		Incubation:        "3-6天",
		Symptoms:          "体色乳白、环节肿胀如竹节、体壁紧张发亮、狂躁爬行、体壁易破流乳白色脓汁",
		HighRiskStage:     "5龄后期至蛹期,春蚕期较多发",
		HighRiskCondition: "温度突变、密集饲养等环境应激可诱发潜伏感染转急性",
		LethalTime:        "感染后3-5天",
		SpreadTrend:       "病蚕体壁破裂流脓→脓汁污染蚕座桑叶→健康蚕食下感染→群体内快速扩散",
		Recurrence:        "多角体环境存活2-3年,不彻底消毒则跨批次跨年度反复发病",
		Detection:         "LAMP 靶标:polh 基因(已有引物序列)",
		Prevention:        "立即隔离病蚕并深埋;对发病区域及周边彻底消毒;上批次发病后升级消毒方案(含蚕具浸泡);排查蚕具跨批次共用、桑叶来源变更、人员进出记录;保持温湿度稳定、控制饲养密度",
	},
	{
		Name: "白僵病", Category: "fungal", SortOrder: 2,
		Pathogen:          "白僵菌(丛梗孢科,兼性寄生菌——既有寄生能力又能腐生)",
		Transmission:      "接触传染为主(分生孢子接触蚕体壁→发芽侵入→体腔内繁殖),创伤传染次之,一般不能食下传染",
		Medium:            "分生孢子,通过气流扩散,是真菌病传播的核心媒介",
		Incubation:        "2-6天",
		Symptoms:          "体色暗淡、油渍斑、行动呆滞、死蚕僵硬覆白粉",
		HighRiskStage:     "温度25-30℃、湿度>75%时孢子萌发,连续阴雨高发",
		HighRiskCondition: "湿度>75%是孢子萌发关键条件,连续阴雨",
		LethalTime:        "小蚕2-3天,大蚕5-6天",
		SpreadTrend:       "点发→面发→暴发;尸体持续产孢,适宜湿度下指数级扩散",
		Recurrence:        "白僵菌广泛寄生于桑园害虫和野外昆虫,可腐生存活,极难彻底清除",
		Detection:         "早期无症状识别难,需结合湿度监测与 AI 巡检",
		Prevention:        "立即隔离病蚕并深埋,防止尸体产孢扩散;湿度>75%时加强通风排湿;检查并加装通风口过滤网;加强人员/蚕具进出管控;蚕房周边昆虫防治、桑园害虫治理",
	},
	{
		Name: "软化病", Category: "bacterial", SortOrder: 3,
		Pathogen:          "肠道致病菌(多种细菌)",
		Transmission:      "食下传染,环境恶化引发肠道菌群失调,与饲养管理密切相关",
		Medium:            "污染桑叶、蚕座环境",
		Incubation:        "病程缓慢,5龄集中暴发",
		Symptoms:          "食桑减少、排念珠状粪、胸部半透明(空头蚕)、群体发育不齐",
		HighRiskStage:     "蚕头密度大、通风不良、桑叶潮湿,5龄期集中暴发",
		HighRiskCondition: "密度过大、通风不良、桑叶潮湿",
		SpreadTrend:       "往往群体性发生,同批次扩散;常存在混合感染",
		Recurrence:        "与饲养管理密切相关,密度/通风/桑叶卫生不改善易反复",
		Detection:         "与质型多角体病肉眼难以区分,需分子检测(16S rRNA / LAMP)确认",
		Prevention:        "控制蚕头密度、加强通风、避免桑叶潮湿;发现病蚕及时隔离;桑叶来源批次追溯,排查桑园害虫粪便污染;混合感染风险高,建议结合检测确认病原",
	},
	{
		Name: "微粒子病", Category: "protozoan", SortOrder: 4,
		Pathogen:          "家蚕微粒子虫(Nosema bombycis),细胞内专性寄生的微孢子虫",
		Transmission:      "双重传播:食下传染(水平传播)+ 胚种传染(垂直传播,经卵传给下一代)",
		Medium:            "孢子,通过粪便、蚕蜕、尸体污染环境;通过卵细胞侵入子代",
		Incubation:        "慢性,可整个幼虫期无明显外观异常",
		Symptoms:          "发育不齐、群体陆续死亡、蚕体瘦小",
		HighRiskStage:     "种蚕生产中发病率可达50-60%;跨代累积,越种越弱",
		HighRiskCondition: "蚕种来源不明、消毒不彻底",
		SpreadTrend:       "水平传播(病蚕排孢污染环境)+ 垂直传播(带毒母蛾产卵→子代先天感染→跨代累积)",
		Recurrence:        "唯一法定检疫对象;胚种传染使带毒批次代代相传",
		Detection:         "法定检疫:母蛾镜检;qPCR 已开展推广培训;LAMP 可参考文献设计引物",
		Prevention:        "种场必须逐批检测,蚕种来源全链追溯(母蛾镜检→蚕种批次→种场供种链);隔离发病批次;可喷施“防微灵”(国内唯一获兽药批文的治疗药物)后饲蚕;排查桑园周边野外昆虫交叉感染",
	},
	{
		Name: "质型多角体病", Category: "viral", SortOrder: 5,
		Pathogen:          "家蚕质型多角体病毒(呼肠孤病毒科,双链RNA病毒)",
		Transmission:      "食下传染(经口),侵入中肠上皮细胞",
		Medium:            "多角体随病蚕粪便排出,持续污染蚕座和桑叶",
		Incubation:        "病程较缓慢",
		Symptoms:          "发育缓慢、体躯瘦小、呆滞、体色略带乳白、排出带乳白色的软粪",
		HighRiskStage:     "夏秋蚕期多发(与核型多角体病的春蚕期形成对比)",
		HighRiskCondition: "夏秋蚕期高温闷热",
		SpreadTrend:       "病蚕排毒(粪便含病毒)→蚕座环境污染→同群体经口反复感染;病程缓慢但粪便持续污染使群体发病率累积上升;可通过卵传给下一代",
		Recurrence:        "可通过卵传代,在种群中形成病毒流行病",
		Detection:         "症状与软化病肉眼难以区分,需分子检测确认",
		Prevention:        "及时清除病蚕与蚕座污染物,防止粪便持续排毒;夏秋高温闷热期加强通风降温;同区域其他蚕房发病关联排查",
	},
	{
		Name: "浓核病/传染性软化病", Category: "viral", SortOrder: 6,
		Pathogen:          "浓核病毒(BmDNV)、二分浓核病毒(BmBDV)、传染性软化病毒(BmIFV",
		Transmission:      "食下感染",
		Medium:            "传染源主要来自桑园害虫粪便污染桑叶",
		Incubation:        "—",
		Symptoms:          "空头、下痢、吐液(与质型多角体病肉眼难以区分)",
		HighRiskStage:     "夏秋蚕期多发",
		HighRiskCondition: "桑叶被桑园害虫粪便污染",
		SpreadTrend:       "桑园害虫粪便污染桑叶→食下感染→慢性消耗死亡或继发细菌感染;常存在混合感染",
		Recurrence:        "病原来自桑园害虫粪便污染,桑园防治不到位易反复",
		Detection:         "裸露病毒,不形成包涵体(多角体),普通显微镜无法观察,诊断难度大",
		Prevention:        "桑叶来源批次追溯(哪个桑园、哪天采摘);核查桑园近期害虫防治记录;保证桑叶储运条件;识别混合感染(常与质型多角体病混合)",
	},
	{
		Name: "细菌性败血病", Category: "bacterial", SortOrder: 7,
		Pathogen:          "苏云金杆菌等多种细菌",
		Transmission:      "创伤传染——细菌通过蚕体表伤口侵入血腔",
		Medium:            "环境中广泛存在的细菌,通过蚕具刮伤、蚕互相抓伤等创口侵入",
		Incubation:        "—",
		Symptoms:          "密集饲养下蚕体互相抓伤概率增大→个体发病死亡→尸体腐烂细菌扩散",
		HighRiskStage:     "密集饲养、蚕具粗糙时多发",
		HighRiskCondition: "蚕具刮伤、操作粗暴、消毒不彻底",
		SpreadTrend:       "个体发病死亡→尸体腐烂细菌扩散→接触传染加重",
		Recurrence:        "反复发作时需排查蚕具毛刺/操作粗暴/消毒盲区",
		Detection:         "—",
		Prevention:        "排查蚕具是否有尖锐边缘/毛刺;控制蚕头密度减少相互抓伤;给桑操作轻柔;反复发作时排查消毒盲区并升级消毒",
	},
	{
		Name: "猝倒病", Category: "bacterial", SortOrder: 8,
		Pathogen:          "细菌毒素(如苏云金杆菌毒素)",
		Transmission:      "食下传染——蚕食入细菌产生的毒素",
		Medium:            "桑叶被细菌污染",
		Incubation:        "急性中毒,发病快、死亡快",
		Symptoms:          "急性中毒,发病快、死亡快",
		HighRiskStage:     "—",
		HighRiskCondition: "桑叶储运环节污染",
		SpreadTrend:       "与桑叶污染直接相关",
		Recurrence:        "桑叶储运环节污染或蚕房环境",
		Detection:         "—",
		Prevention:        "桑叶来源批次追溯并检查储运条件;发现后立即更换桑叶来源;避免桑叶被细菌污染",
	},
	{
		Name: "其他真菌病(绿僵病/黄僵病/曲霉病)", Category: "fungal", SortOrder: 9,
		Pathogen:          "金龟子绿僵菌(绿僵病)、曲霉菌(曲霉病)、黄僵病病原(较少见)",
		Transmission:      "接触传染、气流扩散(真菌分生孢子)",
		Medium:            "分生孢子",
		Incubation:        "—",
		Symptoms:          "绿僵病死蚕呈绿色;黄僵病较少见;曲霉病多发于小蚕期",
		HighRiskStage:     "曲霉病多发于小蚕期;高温高湿季真菌病暴发",
		HighRiskCondition: "高温高湿、蚕座卫生差",
		SpreadTrend:       "绿僵病多为野外昆虫交叉感染",
		Recurrence:        "真菌孢子兼性腐生,环境残留难彻底清除",
		Detection:         "按死蚕孢子颜色肉眼初步区分",
		Prevention:        "与白僵病防控一致:加强通风排湿(湿度>75%重点防范);小蚕期注意蚕座卫生防曲霉病;蚕房周边昆虫防治与桑园害虫治理",
	},
}

// SeedKnowledgeArticles 知识文章种子数据(AI 结果解读,分级阈值来自规格书 3.1.4)
var SeedKnowledgeArticles = []KnowledgeArticle{
	{
		Kind:      "ai_guide",
		Title:     "AI 巡检结果怎么看(风险分级解读)",
		Summary:   "了解 AI 拍照巡检返回的绿/黄/橙/红风险等级含义与建议动作",
		Content:   "AI 巡检将蚕病风险分为四级:\n绿(0-30分):健康,正常巡检节奏,无需操作。\n黄(31-60分):需观察,增加巡检频次,人工复查、关注异常个体。\n橙(61-80分):疑似异常,触发分子检测任务(如 LAMP),按系统指引取样送检。\n红(81-100分):高度疑似/已发病,立即隔离病蚕、启动消毒,并触发专家会诊。\n\nAI 结果仅为筛查参考,最终确诊以分子检测(LAMP/qPCR)和专家诊断为准。",
		SortOrder: 1,
	},
}
  • Step 2: 运行测试确认通过

Run: cd server-go && go test ./internal/model/ -run TestSeed -v Expected: 4 个测试全部 PASS

  • Step 3: Commit
git add server-go/internal/model/knowledge_seed.go
git commit -m "feat(knowledge): 蚕病百科与 AI 解读种子数据"

Task 3: Go handler、路由、权限、迁移

Files:

  • Create: server-go/internal/handler/knowledge.go
  • Modify: server-go/internal/model/permission_seed.go(新增 2 条权限 + 角色映射)
  • Modify: server-go/internal/database/db.goAutoMigrate 注册 + seedKnowledge
  • Modify: server-go/cmd/server/main.go(注册 RegisterKnowledgeRoutes

Interfaces:

  • Consumes: Task 1-2 的 model.Disease / model.KnowledgeArticle / 种子变量;handler.bindUpdates(现有)。
  • Produces: API GET/POST/PATCH/DELETE /knowledge/diseases(/:id)GET/POST/PATCH/DELETE /knowledge/articles(/:id);权限码 knowledge:read / knowledge:write

server-go/internal/handler/knowledge.go 完整实现:

package handler

import (
	"net/http"

	"silk-server-go/internal/middleware"
	"silk-server-go/internal/model"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

// RegisterKnowledgeRoutes 注册知识库路由
func RegisterKnowledgeRoutes(rg *gin.RouterGroup, db *gorm.DB) {
	read := middleware.RequirePermission(db, "knowledge:read")
	write := middleware.RequirePermission(db, "knowledge:write")

	rg.GET("/knowledge/diseases", read, listDiseases(db))
	rg.GET("/knowledge/diseases/:id", read, getDisease(db))
	rg.POST("/knowledge/diseases", write, createDisease(db))
	rg.PATCH("/knowledge/diseases/:id", write, updateDisease(db))
	rg.DELETE("/knowledge/diseases/:id", write, deleteDisease(db))

	rg.GET("/knowledge/articles", read, listArticles(db))
	rg.GET("/knowledge/articles/:id", read, getArticle(db))
	rg.POST("/knowledge/articles", write, createArticle(db))
	rg.PATCH("/knowledge/articles/:id", write, updateArticle(db))
	rg.DELETE("/knowledge/articles/:id", write, deleteArticle(db))
}

func listDiseases(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		q := db.Model(&model.Disease{})
		if cat := c.Query("category"); cat != "" {
			q = q.Where("category = ?", cat)
		}
		if kw := c.Query("keyword"); kw != "" {
			q = q.Where("name ILIKE ?", "%"+kw+"%")
		}
		if enabled := c.Query("enabled"); enabled != "" {
			q = q.Where("enabled = ?", enabled == "true")
		}
		var list []model.Disease
		q.Order("sort_order ASC, name ASC").Find(&list)
		c.JSON(http.StatusOK, list)
	}
}

func getDisease(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var d model.Disease
		if db.Where("id = ?", c.Param("id")).First(&d).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "disease not found"})
			return
		}
		c.JSON(http.StatusOK, d)
	}
}

func createDisease(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var d model.Disease
		if err := c.ShouldBindJSON(&d); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		d.ID = ""
		if d.Name == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "病种名称不能为空"})
			return
		}
		if err := db.Create(&d).Error; err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
			return
		}
		c.JSON(http.StatusCreated, d)
	}
}

func updateDisease(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		var d model.Disease
		if db.Where("id = ?", id).First(&d).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "disease not found"})
			return
		}
		updates, err := bindUpdates(c)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if len(updates) > 0 {
			db.Model(&model.Disease{}).Where("id = ?", id).Updates(updates)
		}
		db.Where("id = ?", id).First(&d)
		c.JSON(http.StatusOK, d)
	}
}

func deleteDisease(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		var d model.Disease
		if db.Where("id = ?", id).First(&d).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "disease not found"})
			return
		}
		db.Where("id = ?", id).Delete(&model.Disease{})
		c.JSON(http.StatusOK, gin.H{"id": id})
	}
}

func listArticles(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		q := db.Model(&model.KnowledgeArticle{})
		if kind := c.Query("kind"); kind != "" {
			q = q.Where("kind = ?", kind)
		}
		if enabled := c.Query("enabled"); enabled != "" {
			q = q.Where("enabled = ?", enabled == "true")
		}
		var list []model.KnowledgeArticle
		q.Order("sort_order ASC, created_at DESC").Find(&list)
		c.JSON(http.StatusOK, list)
	}
}

func getArticle(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var a model.KnowledgeArticle
		if db.Where("id = ?", c.Param("id")).First(&a).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "article not found"})
			return
		}
		c.JSON(http.StatusOK, a)
	}
}

func createArticle(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		var a model.KnowledgeArticle
		if err := c.ShouldBindJSON(&a); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		a.ID = ""
		if a.Kind == "" || a.Title == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "文章类型和标题不能为空"})
			return
		}
		if err := db.Create(&a).Error; err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
			return
		}
		c.JSON(http.StatusCreated, a)
	}
}

func updateArticle(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		var a model.KnowledgeArticle
		if db.Where("id = ?", id).First(&a).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "article not found"})
			return
		}
		updates, err := bindUpdates(c)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		if len(updates) > 0 {
			db.Model(&model.KnowledgeArticle{}).Where("id = ?", id).Updates(updates)
		}
		db.Where("id = ?", id).First(&a)
		c.JSON(http.StatusOK, a)
	}
}

func deleteArticle(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		var a model.KnowledgeArticle
		if db.Where("id = ?", id).First(&a).Error != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "article not found"})
			return
		}
		db.Where("id = ?", id).Delete(&model.KnowledgeArticle{})
		c.JSON(http.StatusOK, gin.H{"id": id})
	}
}

server-go/internal/model/permission_seed.go 追加:

	{"knowledge:read", "知识库查看", "查看蚕病百科和知识文章"},
	{"knowledge:write", "知识库管理", "新增、编辑、删除知识库内容"},

并在 RolePermissionMapadmin/operator 增加 "knowledge:read", "knowledge:write"viewer/farmer 增加 "knowledge:read"

server-go/internal/database/db.go

  • AutoMigrate 列表追加 &model.Disease{}, &model.KnowledgeArticle{},
  • 新增 seedKnowledge(db)(幂等,按 name / kind+title 判重,不存在才 Create),并在 InitseedPermissions(db) 之后调用。

server-go/cmd/server/main.go:在 RegisterStorageRoutes 后追加 handler.RegisterKnowledgeRoutes(api, db)

  • Step 1: 修改代码
  • Step 2: 验证

Run: cd server-go && go build ./... && go vet ./... && go test ./... Expected: exit 0,无错误

  • Step 3: Commit
git add server-go/internal/handler/knowledge.go server-go/internal/model/permission_seed.go server-go/internal/database/db.go server-go/cmd/server/main.go
git commit -m "feat(knowledge): 知识库 API、权限与迁移"

Task 4: Web 后台知识库页

Files:

  • Create: web/src/dal/knowledge.ts
  • Create: web/src/pages/Knowledge.tsx
  • Modify: web/src/router.tsx(注册 /knowledge,权限 knowledge:read
  • Modify: web/src/layout/BasicLayout.tsx(菜单“知识库”,BookOutlined,权限 knowledge:read

Interfaces:

  • Consumes: 后端 Disease / KnowledgeArticle JSON 字段(与 Go JSON tag 完全一致)。

web/src/dal/knowledge.ts

import { get, post, patch, del } from '../api/http';

export interface Disease {
  id: string;
  name: string;
  category: string;
  pathogen?: string;
  transmission?: string;
  medium?: string;
  incubation?: string;
  symptoms?: string;
  highRiskStage?: string;
  highRiskCondition?: string;
  lethalTime?: string;
  spreadTrend?: string;
  recurrence?: string;
  detection?: string;
  prevention?: string;
  imageUrl?: string;
  sortOrder?: number;
  enabled?: boolean;
  createdAt?: string;
  updatedAt?: string;
}

export interface KnowledgeArticle {
  id: string;
  kind: string;
  title: string;
  summary?: string;
  content?: string;
  sortOrder?: number;
  enabled?: boolean;
  createdAt?: string;
  updatedAt?: string;
}

export const listDiseases = (params?: any) => get<Disease[]>('/knowledge/diseases', { params });
export const getDisease = (id: string) => get<Disease>(`/knowledge/diseases/${id}`);
export const createDisease = (data: Partial<Disease>) => post<Disease>('/knowledge/diseases', data);
export const updateDisease = (id: string, data: Partial<Disease>) => patch<Disease>(`/knowledge/diseases/${id}`, data);
export const deleteDisease = (id: string) => del(`/knowledge/diseases/${id}`);

export const listArticles = (params?: any) => get<KnowledgeArticle[]>('/knowledge/articles', { params });
export const createArticle = (data: Partial<KnowledgeArticle>) => post<KnowledgeArticle>('/knowledge/articles', data);
export const updateArticle = (id: string, data: Partial<KnowledgeArticle>) => patch<KnowledgeArticle>(`/knowledge/articles/${id}`, data);
export const deleteArticle = (id: string) => del(`/knowledge/articles/${id}`);

web/src/pages/Knowledge.tsx:参照 web/src/pages/Silkworm.tsx 的 ProTable + Modal 风格:

  • 页面顶部 TabsTab1「蚕病百科」,Tab2「AI 结果解读」。

  • 蚕病百科 TabProTablerowKey=id,序号列、名称、类别 Tag、高发阶段、排序、状态、操作列 编辑/删除),toolBar「新建病种」;编辑/新建 Modal 表单含全部字段(名称、类别 Select[viral/fungal/bacterial/protozoan/other]、病原、传播途径、传染媒介、潜伏期、主要症状、高发阶段、高发条件、致死时间、传播趋势、连发性特征、检测方法、防治指南 TextArea、排序、启用 Switch)。

  • AI 解读 TabProTable(序号、标题、类型 Tag、摘要、排序、操作),编辑/新建 Modal 含 kind Select[ai_guide/lamp_guide/sers_guide/seasonal_tip]、标题、摘要、正文 TextArea、排序、启用。

  • 类别中文映射:{ viral: '病毒性', fungal: '真菌性', bacterial: '细菌性', protozoan: '原虫性', other: '其他' }

  • 写操作按钮仅当 authService.hasPermission('knowledge:write') 时渲染;无权限时只读列表。

  • Step 1: 新增 dal 与页面,注册路由与菜单

  • Step 2: 验证

Run: cd web && npm run lint && npm run build Expected: exit 0,无 lint 错误,tsc + vite 构建通过

  • Step 3: Commit
git add web/src/dal/knowledge.ts web/src/pages/Knowledge.tsx web/src/router.tsx web/src/layout/BasicLayout.tsx
git commit -m "feat(web): 知识库页面(蚕病百科 + AI 解读)"

Task 5: 小程序知识库查看页

Files:

  • Create: miniapp/src/api/knowledge.ts
  • Modify: miniapp/src/types/index.ts(追加 DiseaseKnowledgeArticle 接口)
  • Create: miniapp/src/pages/knowledge/index.tsxindex.config.tsindex.module.scss
  • Create: miniapp/src/pages/knowledge/detail/index.tsxdetail/index.config.tsdetail/index.module.scss
  • Modify: miniapp/src/pages/settings/index.tsx(“监控管理”菜单追加“知识库”入口)
  • Modify: miniapp/src/app.config.tspages 追加两个页面)

Interfaces:

  • Consumes: 后端 GET /knowledge/diseasesGET /knowledge/articles?kind=ai_guide

miniapp/src/api/knowledge.ts

import { get } from './request';

export interface Disease {
  id: string;
  name: string;
  category: string;
  pathogen?: string;
  transmission?: string;
  medium?: string;
  incubation?: string;
  symptoms?: string;
  highRiskStage?: string;
  highRiskCondition?: string;
  lethalTime?: string;
  spreadTrend?: string;
  recurrence?: string;
  detection?: string;
  prevention?: string;
  sortOrder?: number;
}

export interface KnowledgeArticle {
  id: string;
  kind: string;
  title: string;
  summary?: string;
  content?: string;
  sortOrder?: number;
}

export const listDiseases = () => get<Disease[]>('/knowledge/diseases');
export const listAIGuides = () => get<KnowledgeArticle[]>('/knowledge/articles', { kind: 'ai_guide' });

页面行为:

  • pages/knowledge/index:页面标题“知识库”;顶部渲染 AI 结果解读卡片(listAIGuides 取第一条,展示标题+摘要,点击 navigateTo('/pages/knowledge/detail/index?kind=ai_guide&id=xx'));下方按类别分组渲染病种列表(类别中文 Tag + 病种名 + 症状摘要),点击进入 detail?kind=disease&id=xx

  • pages/knowledge/detail:按 kind 区分:diseaseget<Disease>('/knowledge/diseases/'+id) 渲染分段卡片(病原/传播途径/传染媒介/潜伏期/主要症状/高发阶段/高发条件/致死时间/传播趋势/连发性/检测方法/防治指南);ai_guideget<KnowledgeArticle>('/knowledge/articles/'+id) 渲染标题+正文(white-space: pre-wrap)。

  • 样式参照 miniapp/src/pages/settings/index.module.scss 的卡片风格。

  • settings/index.tsxhandleMenuTap 增加 case 'knowledge': Taro.navigateTo({ url: '/pages/knowledge/index' }); break;,菜单项文案“知识库”。

  • Step 1: 新增 api、类型与两个页面,注册路由与设置页入口

  • Step 2: 验证

Run: cd miniapp && npm run build:weapp Expected: exit 0miniapp/dist 重新生成(生成产物不提交)

  • Step 3: Commit
git add miniapp/src
git commit -m "feat(miniapp): 知识库查看页"

Task 6: 全量验证与交付

  • Step 1: 后端

Run: cd server-go && go test ./... && go build ./... && go vet ./...

  • Step 2: Web

Run: cd web && npm run lint && npm run build

  • Step 3: 小程序

Run: cd miniapp && npm run build:weapp

  • Step 4: 检查变更范围

Run: git status --short,确认只包含知识库相关源文件,未动生成产物。

  • Step 5: 交付说明

汇总:修改文件清单、验证输出、种子数据说明(9 病种 + 1 AI 文章)、风险提示(handler 端到端 API 需 PostgreSQL 运行环境联调;前端测试基建未配置,属于计划 #27)。