diff --git a/server-go/cmd/server/main.go b/server-go/cmd/server/main.go index eae12a3..3f42692 100644 --- a/server-go/cmd/server/main.go +++ b/server-go/cmd/server/main.go @@ -106,6 +106,7 @@ func main() { handler.RegisterVideoClipRoutes(api, db, cfg) handler.RegisterVideoRecordRoutes(api, db, mediaSvc, cfg) handler.RegisterStorageRoutes(api, db) + handler.RegisterKnowledgeRoutes(api, db) // 启动后台设备状态同步(每 30 秒查询 WVP 设备在线状态) go startDeviceStatusSync(db, mediaSvc) diff --git a/server-go/internal/database/db.go b/server-go/internal/database/db.go index 6e90423..8e7fc94 100644 --- a/server-go/internal/database/db.go +++ b/server-go/internal/database/db.go @@ -27,15 +27,40 @@ func Init(cfg *config.Config) error { &model.Threshold{}, &model.Alarm{}, &model.Camera{}, &model.VideoClip{}, &model.AuditLog{}, &model.Telemetry{}, &model.Permission{}, &model.RolePermission{}, + &model.Disease{}, &model.KnowledgeArticle{}, ); err != nil { slog.Warn("自动迁移有警告(可忽略)", "err", err) } // 初始化权限种子数据 seedPermissions(db) + // 初始化知识库种子数据 + seedKnowledge(db) slog.Info("数据库连接成功,自动迁移完成") return nil } +// seedKnowledge 初始化知识库种子数据(幂等:按名称/类型+标题判重) +func seedKnowledge(db *gorm.DB) { + for _, d := range model.SeedDiseases { + var cnt int64 + db.Model(&model.Disease{}).Where("name = ?", d.Name).Count(&cnt) + if cnt == 0 { + if err := db.Create(&d).Error; err != nil { + slog.Warn("写入病种种子失败", "name", d.Name, "err", err) + } + } + } + for _, a := range model.SeedKnowledgeArticles { + var cnt int64 + db.Model(&model.KnowledgeArticle{}).Where("kind = ? AND title = ?", a.Kind, a.Title).Count(&cnt) + if cnt == 0 { + if err := db.Create(&a).Error; err != nil { + slog.Warn("写入知识文章种子失败", "title", a.Title, "err", err) + } + } + } +} + // seedPermissions 初始化权限和角色-权限映射种子数据(幂等) func seedPermissions(db *gorm.DB) { // 1. 写入权限定义(已存在则跳过) diff --git a/server-go/internal/handler/knowledge.go b/server-go/internal/handler/knowledge.go new file mode 100644 index 0000000..7ce7d25 --- /dev/null +++ b/server-go/internal/handler/knowledge.go @@ -0,0 +1,202 @@ +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)) +} + +// listDiseases 蚕病百科列表(支持 category/keyword/enabled 过滤) +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) + } +} + +// getDisease 蚕病百科详情 +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) + } +} + +// createDisease 新建病种 +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) + } +} + +// updateDisease 更新病种 +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) + } +} + +// deleteDisease 删除病种 +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}) + } +} + +// listArticles 知识文章列表(支持 kind/enabled 过滤) +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) + } +} + +// getArticle 知识文章详情 +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) + } +} + +// createArticle 新建知识文章 +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) + } +} + +// updateArticle 更新知识文章 +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) + } +} + +// deleteArticle 删除知识文章 +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}) + } +} diff --git a/server-go/internal/model/permission_seed.go b/server-go/internal/model/permission_seed.go index 1d0abfd..7595955 100644 --- a/server-go/internal/model/permission_seed.go +++ b/server-go/internal/model/permission_seed.go @@ -22,6 +22,8 @@ var AllPermissions = []PermissionDef{ {"video:record", "视频录制", "启动和停止视频录制"}, {"energy:view", "能耗查看", "查看能耗数据"}, {"log:read", "日志查看", "查看控制日志"}, + {"knowledge:read", "知识库查看", "查看蚕病百科和知识文章"}, + {"knowledge:write", "知识库管理", "新增、编辑、删除知识库内容"}, {"user:manage", "用户管理", "管理用户、角色和权限"}, {"audit:read", "审计查看", "查看审计日志"}, } @@ -32,19 +34,23 @@ var RolePermissionMap = map[string][]string{ "dashboard:view", "room:read", "room:write", "device:read", "device:control", "threshold:read", "threshold:write", "alarm:read", "alarm:ack", "video:read", "video:record", "energy:view", "log:read", + "knowledge:read", "knowledge:write", "user:manage", "audit:read", }, RoleOperator: { "dashboard:view", "room:read", "room:write", "device:read", "device:control", "threshold:read", "threshold:write", "alarm:read", "alarm:ack", "video:read", "video:record", "energy:view", "log:read", + "knowledge:read", "knowledge:write", }, RoleViewer: { "dashboard:view", "room:read", "device:read", "threshold:read", "alarm:read", "video:read", "energy:view", + "knowledge:read", }, RoleFarmer: { "dashboard:view", "room:read", "device:read", "device:control", "alarm:read", "alarm:ack", "video:read", "energy:view", + "knowledge:read", }, }