package knowledge import ( "encoding/json" "errors" "net/http" "strconv" "strings" "git.iwork-ai.com/xdc/iqudo-top1/internal/access" "git.iwork-ai.com/xdc/iqudo-top1/internal/audit" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "git.iwork-ai.com/xdc/iqudo-top1/internal/response" "github.com/gin-gonic/gin" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Handler struct{ db *gorm.DB } func NewHandler(db *gorm.DB) *Handler { return &Handler{db: db} } type input struct { ScenarioID uint64 `json:"scenario_id" binding:"required"` Title string `json:"title" binding:"required,max=128"` Content map[string]interface{} `json:"content" binding:"required"` } type updateInput struct { Title string `json:"title" binding:"required,max=128"` Content map[string]interface{} `json:"content" binding:"required"` } func (h *Handler) List(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) type row struct { model.KnowledgeCard Content datatypes.JSON `json:"content"` Version int `json:"version"` } items := make([]row, 0) query := h.db.Table("knowledge_cards kc").Select("kc.*, kcv.content, kcv.version").Joins("JOIN scenarios sc ON sc.id = kc.scenario_id").Joins("LEFT JOIN knowledge_card_versions kcv ON kcv.knowledge_card_id = kc.id AND kcv.status = ?", "published") query = access.ScopeScenarios(query, p, "sc") err := query.Where("kc.tenant_id = ? AND kc.status <> ? AND sc.status <> ?", p.TenantID, "archived", "archived").Order("kc.updated_at DESC").Scan(&items).Error if err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识卡失败") return } response.OK(c, gin.H{"items": items, "total": len(items)}) } func (h *Handler) Update(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.Error(c, http.StatusBadRequest, "INVALID_ID", "知识卡 ID 不正确") return } var body updateInput if err := c.ShouldBindJSON(&body); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容不完整") return } if err := validateContent(body.Content); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error()) return } content, err := json.Marshal(body.Content) if err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容格式不正确") return } var card model.KnowledgeCard if err := h.db.Where("id = ? AND tenant_id = ? AND status <> ?", id, p.TenantID, "archived").First(&card).Error; err != nil || !access.CanEditScenario(h.db, p, card.ScenarioID) { response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在或不可编辑") return } var version model.KnowledgeCardVersion err = h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&card).Error; err != nil { return err } var maxVersion int if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ?", id, p.TenantID).Select("COALESCE(MAX(version), 0)").Scan(&maxVersion).Error; err != nil { return err } if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Update("status", "superseded").Error; err != nil { return err } version = model.KnowledgeCardVersion{TenantID: p.TenantID, KnowledgeCardID: id, Version: maxVersion + 1, Content: datatypes.JSON(content), Status: "published"} if err := tx.Create(&version).Error; err != nil { return err } return tx.Model(&card).Updates(map[string]interface{}{"title": body.Title, "status": "published"}).Error }) if err != nil { response.Error(c, http.StatusInternalServerError, "UPDATE_FAILED", "更新知识卡失败") return } _ = audit.Record(h.db, p, "update", "knowledge_card", id, gin.H{"version": version.Version}) response.OK(c, gin.H{"id": id, "title": body.Title, "version": version.Version, "content": datatypes.JSON(content)}) } func (h *Handler) Versions(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.Error(c, http.StatusBadRequest, "INVALID_ID", "知识卡 ID 不正确") return } var card model.KnowledgeCard if err := h.db.Where("id = ? AND tenant_id = ? AND status <> ?", id, p.TenantID, "archived").First(&card).Error; err != nil || !access.CanViewScenario(h.db, p, card.ScenarioID) { response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在") return } items := make([]model.KnowledgeCardVersion, 0) if err := h.db.Where("knowledge_card_id = ? AND tenant_id = ?", id, p.TenantID).Order("version DESC").Find(&items).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识卡版本失败") return } response.OK(c, gin.H{"items": items, "total": len(items)}) } func (h *Handler) Create(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) var body input if err := c.ShouldBindJSON(&body); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容不完整") return } if err := validateContent(body.Content); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error()) return } if !access.CanEditScenario(h.db, p, body.ScenarioID) { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑") return } content, _ := json.Marshal(body.Content) var card model.KnowledgeCard err := h.db.Transaction(func(tx *gorm.DB) error { card = model.KnowledgeCard{TenantID: p.TenantID, ScenarioID: body.ScenarioID, Title: body.Title, Status: "published", CreatedBy: p.UserID} if err := tx.Create(&card).Error; err != nil { return err } return tx.Create(&model.KnowledgeCardVersion{TenantID: p.TenantID, KnowledgeCardID: card.ID, Version: 1, Content: datatypes.JSON(content), Status: "published"}).Error }) if err != nil { response.Error(c, http.StatusInternalServerError, "CREATE_FAILED", "创建知识卡失败") return } _ = audit.Record(h.db, p, "create", "knowledge_card", card.ID, body) response.Created(c, card) } func (h *Handler) Delete(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ID", "知识卡 ID 不正确") return } var card model.KnowledgeCard if err := h.db.Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&card).Error; err != nil || !access.CanEditScenario(h.db, p, card.ScenarioID) { response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在或不可归档") return } var references int64 err = h.db.Table("sop_nodes n").Joins("JOIN sop_versions sv ON sv.id = n.sop_version_id").Joins("JOIN sops s ON s.id = sv.sop_id").Where( "n.tenant_id = ? AND s.scenario_id = ? AND sv.status IN ? AND n.type = ? AND JSON_UNQUOTE(JSON_EXTRACT(n.config, '$.knowledge_card_id')) = ?", p.TenantID, card.ScenarioID, []string{"published", "offline", "superseded"}, "knowledge", strconv.FormatUint(id, 10), ).Count(&references).Error if err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "检查知识卡引用失败") return } if references > 0 { response.Error(c, http.StatusConflict, "KNOWLEDGE_IN_USE", "知识卡已被发布版本引用,不能归档") return } result := h.db.Model(&model.KnowledgeCard{}).Where("id = ? AND tenant_id = ?", id, p.TenantID).Update("status", "archived") if result.Error != nil || result.RowsAffected == 0 { response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在") return } _ = audit.Record(h.db, p, "archive", "knowledge_card", id, nil) response.OK(c, gin.H{"id": id}) } func validateContent(content map[string]interface{}) error { standard, ok := content["standard_copy"].(string) if !ok || strings.TrimSpace(standard) == "" { return errors.New("请填写标准话术") } for _, key := range []string{"forbidden_copy", "risk_note"} { if value, exists := content[key]; exists { if _, ok := value.(string); !ok { return errors.New("知识卡文本字段格式不正确") } } } return nil }