From 13df3d5190395015b9f9aca553aebf9247163e53 Mon Sep 17 00:00:00 2001 From: weijuesen Date: Wed, 12 Aug 2026 10:46:46 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=9F=A5=E8=AF=86=E5=BA=93=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E7=89=88=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-08-12-knowledge-base.md | 693 ++++++++++++++++++ 1 file changed, 693 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-12-knowledge-base.md diff --git a/docs/superpowers/plans/2026-08-12-knowledge-base.md b/docs/superpowers/plans/2026-08-12-knowledge-base.md new file mode 100644 index 0000000..285b715 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-knowledge-base.md @@ -0,0 +1,693 @@ +# 知识库初始版(蚕病百科 + 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 / GORM(PostgreSQL);React 19 / antd 5 / @ant-design/pro-components;Taro 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.Disease`、`model.KnowledgeArticle` 结构体;包变量 `model.SeedDiseases []Disease`、`model.SeedKnowledgeArticles []KnowledgeArticle`(Task 2 填充内容)。 + +- [ ] **Step 1: 写失败测试**(先建 `knowledge.go` 只含结构体定义,不含种子变量) + +`server-go/internal/model/knowledge_seed_test.go`: + +```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 []Disease`、`var SeedKnowledgeArticles []KnowledgeArticle` 使编译通过、测试失败。 + +- [ ] **Step 4: 再运行测试,确认失败原因是数据缺失**(如 `核心蚕病缺失: 核型多角体病`),而非编译错误。 + +- [ ] **Step 5: Commit** + +```bash +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: `SeedDiseases`(9 条,含 4 类核心病种)、`SeedKnowledgeArticles`(1 条 AI 结果解读)。字段值全部来自规格书附录 11.1 与 3.1.4;`Prevention`(防治指南)按规格书 3.8 处置方向(内源扩散/外源侵入)与分病种排查清单整理。 + +- [ ] **Step 1: 写入种子数据**(代码见下) + +```go +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** + +```bash +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.go`(AutoMigrate 注册 + `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` 完整实现: + +```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` 追加: + +```go + {"knowledge:read", "知识库查看", "查看蚕病百科和知识文章"}, + {"knowledge:write", "知识库管理", "新增、编辑、删除知识库内容"}, +``` + +并在 `RolePermissionMap`:admin/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`),并在 `Init` 中 `seedPermissions(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** + +```bash +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`: + +```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('/knowledge/diseases', { params }); +export const getDisease = (id: string) => get(`/knowledge/diseases/${id}`); +export const createDisease = (data: Partial) => post('/knowledge/diseases', data); +export const updateDisease = (id: string, data: Partial) => patch(`/knowledge/diseases/${id}`, data); +export const deleteDisease = (id: string) => del(`/knowledge/diseases/${id}`); + +export const listArticles = (params?: any) => get('/knowledge/articles', { params }); +export const createArticle = (data: Partial) => post('/knowledge/articles', data); +export const updateArticle = (id: string, data: Partial) => patch(`/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 风格: +- 页面顶部 `Tabs`:Tab1「蚕病百科」,Tab2「AI 结果解读」。 +- 蚕病百科 Tab:ProTable(rowKey=id,序号列、名称、类别 Tag、高发阶段、排序、状态、操作列 编辑/删除),toolBar「新建病种」;编辑/新建 Modal 表单含全部字段(名称、类别 Select[viral/fungal/bacterial/protozoan/other]、病原、传播途径、传染媒介、潜伏期、主要症状、高发阶段、高发条件、致死时间、传播趋势、连发性特征、检测方法、防治指南 TextArea、排序、启用 Switch)。 +- AI 解读 Tab:ProTable(序号、标题、类型 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** + +```bash +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`(追加 `Disease`、`KnowledgeArticle` 接口) +- Create: `miniapp/src/pages/knowledge/index.tsx`、`index.config.ts`、`index.module.scss` +- Create: `miniapp/src/pages/knowledge/detail/index.tsx`、`detail/index.config.ts`、`detail/index.module.scss` +- Modify: `miniapp/src/pages/settings/index.tsx`(“监控管理”菜单追加“知识库”入口) +- Modify: `miniapp/src/app.config.ts`(pages 追加两个页面) + +**Interfaces:** +- Consumes: 后端 `GET /knowledge/diseases`、`GET /knowledge/articles?kind=ai_guide`。 + +`miniapp/src/api/knowledge.ts`: + +```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('/knowledge/diseases'); +export const listAIGuides = () => get('/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` 区分:`disease` 用 `get('/knowledge/diseases/'+id)` 渲染分段卡片(病原/传播途径/传染媒介/潜伏期/主要症状/高发阶段/高发条件/致死时间/传播趋势/连发性/检测方法/防治指南);`ai_guide` 用 `get('/knowledge/articles/'+id)` 渲染标题+正文(`white-space: pre-wrap`)。 +- 样式参照 `miniapp/src/pages/settings/index.module.scss` 的卡片风格。 +- `settings/index.tsx` 的 `handleMenuTap` 增加 `case 'knowledge': Taro.navigateTo({ url: '/pages/knowledge/index' }); break;`,菜单项文案“知识库”。 + +- [ ] **Step 1: 新增 api、类型与两个页面,注册路由与设置页入口** +- [ ] **Step 2: 验证** + +Run: `cd miniapp && npm run build:weapp` +Expected: exit 0,`miniapp/dist` 重新生成(生成产物不提交) + +- [ ] **Step 3: Commit** + +```bash +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)。