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

694 lines
30 KiB
Markdown
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.
# 知识库初始版(蚕病百科 + 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.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<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 风格:
- 页面顶部 `Tabs`Tab1「蚕病百科」,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**
```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<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` 区分:`disease``get<Disease>('/knowledge/diseases/'+id)` 渲染分段卡片(病原/传播途径/传染媒介/潜伏期/主要症状/高发阶段/高发条件/致死时间/传播趋势/连发性/检测方法/防治指南);`ai_guide``get<KnowledgeArticle>('/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)。