694 lines
30 KiB
Markdown
694 lines
30 KiB
Markdown
# 知识库初始版(蚕病百科 + 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<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 结果解读」。
|
||
- 蚕病百科 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<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)。
|