feat(knowledge): 知识库 API、权限与迁移
This commit is contained in:
@@ -106,6 +106,7 @@ func main() {
|
|||||||
handler.RegisterVideoClipRoutes(api, db, cfg)
|
handler.RegisterVideoClipRoutes(api, db, cfg)
|
||||||
handler.RegisterVideoRecordRoutes(api, db, mediaSvc, cfg)
|
handler.RegisterVideoRecordRoutes(api, db, mediaSvc, cfg)
|
||||||
handler.RegisterStorageRoutes(api, db)
|
handler.RegisterStorageRoutes(api, db)
|
||||||
|
handler.RegisterKnowledgeRoutes(api, db)
|
||||||
|
|
||||||
// 启动后台设备状态同步(每 30 秒查询 WVP 设备在线状态)
|
// 启动后台设备状态同步(每 30 秒查询 WVP 设备在线状态)
|
||||||
go startDeviceStatusSync(db, mediaSvc)
|
go startDeviceStatusSync(db, mediaSvc)
|
||||||
|
|||||||
@@ -27,15 +27,40 @@ func Init(cfg *config.Config) error {
|
|||||||
&model.Threshold{}, &model.Alarm{}, &model.Camera{}, &model.VideoClip{},
|
&model.Threshold{}, &model.Alarm{}, &model.Camera{}, &model.VideoClip{},
|
||||||
&model.AuditLog{}, &model.Telemetry{},
|
&model.AuditLog{}, &model.Telemetry{},
|
||||||
&model.Permission{}, &model.RolePermission{},
|
&model.Permission{}, &model.RolePermission{},
|
||||||
|
&model.Disease{}, &model.KnowledgeArticle{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
slog.Warn("自动迁移有警告(可忽略)", "err", err)
|
slog.Warn("自动迁移有警告(可忽略)", "err", err)
|
||||||
}
|
}
|
||||||
// 初始化权限种子数据
|
// 初始化权限种子数据
|
||||||
seedPermissions(db)
|
seedPermissions(db)
|
||||||
|
// 初始化知识库种子数据
|
||||||
|
seedKnowledge(db)
|
||||||
slog.Info("数据库连接成功,自动迁移完成")
|
slog.Info("数据库连接成功,自动迁移完成")
|
||||||
return nil
|
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 初始化权限和角色-权限映射种子数据(幂等)
|
// seedPermissions 初始化权限和角色-权限映射种子数据(幂等)
|
||||||
func seedPermissions(db *gorm.DB) {
|
func seedPermissions(db *gorm.DB) {
|
||||||
// 1. 写入权限定义(已存在则跳过)
|
// 1. 写入权限定义(已存在则跳过)
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ var AllPermissions = []PermissionDef{
|
|||||||
{"video:record", "视频录制", "启动和停止视频录制"},
|
{"video:record", "视频录制", "启动和停止视频录制"},
|
||||||
{"energy:view", "能耗查看", "查看能耗数据"},
|
{"energy:view", "能耗查看", "查看能耗数据"},
|
||||||
{"log:read", "日志查看", "查看控制日志"},
|
{"log:read", "日志查看", "查看控制日志"},
|
||||||
|
{"knowledge:read", "知识库查看", "查看蚕病百科和知识文章"},
|
||||||
|
{"knowledge:write", "知识库管理", "新增、编辑、删除知识库内容"},
|
||||||
{"user:manage", "用户管理", "管理用户、角色和权限"},
|
{"user:manage", "用户管理", "管理用户、角色和权限"},
|
||||||
{"audit:read", "审计查看", "查看审计日志"},
|
{"audit:read", "审计查看", "查看审计日志"},
|
||||||
}
|
}
|
||||||
@@ -32,19 +34,23 @@ var RolePermissionMap = map[string][]string{
|
|||||||
"dashboard:view", "room:read", "room:write", "device:read", "device:control",
|
"dashboard:view", "room:read", "room:write", "device:read", "device:control",
|
||||||
"threshold:read", "threshold:write", "alarm:read", "alarm:ack",
|
"threshold:read", "threshold:write", "alarm:read", "alarm:ack",
|
||||||
"video:read", "video:record", "energy:view", "log:read",
|
"video:read", "video:record", "energy:view", "log:read",
|
||||||
|
"knowledge:read", "knowledge:write",
|
||||||
"user:manage", "audit:read",
|
"user:manage", "audit:read",
|
||||||
},
|
},
|
||||||
RoleOperator: {
|
RoleOperator: {
|
||||||
"dashboard:view", "room:read", "room:write", "device:read", "device:control",
|
"dashboard:view", "room:read", "room:write", "device:read", "device:control",
|
||||||
"threshold:read", "threshold:write", "alarm:read", "alarm:ack",
|
"threshold:read", "threshold:write", "alarm:read", "alarm:ack",
|
||||||
"video:read", "video:record", "energy:view", "log:read",
|
"video:read", "video:record", "energy:view", "log:read",
|
||||||
|
"knowledge:read", "knowledge:write",
|
||||||
},
|
},
|
||||||
RoleViewer: {
|
RoleViewer: {
|
||||||
"dashboard:view", "room:read", "device:read",
|
"dashboard:view", "room:read", "device:read",
|
||||||
"threshold:read", "alarm:read", "video:read", "energy:view",
|
"threshold:read", "alarm:read", "video:read", "energy:view",
|
||||||
|
"knowledge:read",
|
||||||
},
|
},
|
||||||
RoleFarmer: {
|
RoleFarmer: {
|
||||||
"dashboard:view", "room:read", "device:read", "device:control",
|
"dashboard:view", "room:read", "device:read", "device:control",
|
||||||
"alarm:read", "alarm:ack", "video:read", "energy:view",
|
"alarm:read", "alarm:ack", "video:read", "energy:view",
|
||||||
|
"knowledge:read",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user