package knowledge import ( "encoding/json" "fmt" "net/http" "regexp" "strconv" "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" ) var graphKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,63}$`) type GraphInput struct { Items []GraphItemInput `json:"items"` Relations []GraphRelationInput `json:"relations"` Symptoms []SymptomInput `json:"symptoms"` } type GraphItemInput struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` Content map[string]interface{} `json:"content"` Status string `json:"status"` SortOrder int `json:"sort_order"` } type GraphRelationInput struct { From string `json:"from"` RelationType string `json:"relation_type"` To string `json:"to"` Condition map[string]interface{} `json:"condition"` SortOrder int `json:"sort_order"` } type SymptomInput struct { Key string `json:"key"` Name string `json:"name"` CopyTemplateIDs []string `json:"copy_template_ids"` Diseases []GraphItemInput `json:"diseases"` } func (h *Handler) GetGraph(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) scenarioID, ok := graphScenarioID(c) if !ok || !access.CanViewScenario(h.db, p, scenarioID) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在") } return } items := make([]model.KnowledgeItem, 0) relations := make([]model.KnowledgeRelation, 0) if err := h.db.Where("tenant_id = ? AND scenario_id = ? AND status <> ?", p.TenantID, scenarioID, "archived").Order("sort_order, id").Find(&items).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识失败") return } if err := h.db.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Order("sort_order, id").Find(&relations).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识关系失败") return } response.OK(c, gin.H{"items": items, "relations": relations}) } func (h *Handler) ReplaceGraph(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) scenarioID, ok := graphScenarioID(c) if !ok || !access.CanEditScenario(h.db, p, scenarioID) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑") } return } var input GraphInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识 JSON 格式不正确") return } items, relations, err := normalizeGraphInput(input) if err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error()) return } err = h.db.Transaction(func(tx *gorm.DB) error { var oldItems []model.KnowledgeItem var oldRelations []model.KnowledgeRelation if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Find(&oldItems).Error; err != nil { return err } if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Find(&oldRelations).Error; err != nil { return err } if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Delete(&model.KnowledgeRelation{}).Error; err != nil { return err } if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Delete(&model.KnowledgeItem{}).Error; err != nil { return err } ids := make(map[string]uint64, len(items)) for _, item := range items { raw, _ := json.Marshal(item.Content) row := model.KnowledgeItem{TenantID: p.TenantID, ScenarioID: scenarioID, ItemKey: item.Key, Name: item.Name, Type: item.Type, Content: datatypes.JSON(raw), Status: item.Status, SortOrder: item.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } ids[item.Key] = row.ID if err := audit.RecordTx(tx, p, "create", "knowledge_item", row.ID, gin.H{"scenario_id": scenarioID, "key": row.ItemKey}); err != nil { return err } } for _, relation := range relations { raw, _ := json.Marshal(relation.Condition) row := model.KnowledgeRelation{TenantID: p.TenantID, ScenarioID: scenarioID, FromKnowledgeID: ids[relation.From], RelationType: relation.RelationType, ToKnowledgeID: ids[relation.To], Condition: datatypes.JSON(raw), SortOrder: relation.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } if err := audit.RecordTx(tx, p, "create", "knowledge_relation", row.ID, gin.H{"scenario_id": scenarioID, "from": relation.From, "relation_type": relation.RelationType, "to": relation.To}); err != nil { return err } } for _, relation := range oldRelations { if err := audit.RecordTx(tx, p, "archive", "knowledge_relation", relation.ID, gin.H{"scenario_id": scenarioID, "from_knowledge_id": relation.FromKnowledgeID, "relation_type": relation.RelationType, "to_knowledge_id": relation.ToKnowledgeID, "sort_order": relation.SortOrder}); err != nil { return err } } for _, item := range oldItems { if err := audit.RecordTx(tx, p, "archive", "knowledge_item", item.ID, gin.H{"scenario_id": scenarioID, "key": item.ItemKey, "name": item.Name, "type": item.Type, "status": item.Status, "sort_order": item.SortOrder}); err != nil { return err } } return nil }) if err != nil { response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存知识关系失败") return } h.GetGraph(c) } func normalizeGraphInput(input GraphInput) ([]GraphItemInput, []GraphRelationInput, error) { items := append([]GraphItemInput{}, input.Items...) relations := append([]GraphRelationInput{}, input.Relations...) for _, symptom := range input.Symptoms { items = append(items, GraphItemInput{Key: symptom.Key, Name: symptom.Name, Type: "symptom", Status: "active"}) for _, disease := range symptom.Diseases { disease.Type = "disease" items = append(items, disease) relations = append(relations, GraphRelationInput{From: symptom.Key, RelationType: "possible_disease", To: disease.Key}) } for _, copyKey := range symptom.CopyTemplateIDs { relations = append(relations, GraphRelationInput{From: symptom.Key, RelationType: "recommended_copy", To: copyKey}) } } seen := map[string]bool{} unique := make([]GraphItemInput, 0, len(items)) for _, item := range items { if !graphKeyPattern.MatchString(item.Key) || item.Name == "" || item.Type == "" { return nil, nil, fmt.Errorf("知识 key、name 和 type 必须填写且格式正确") } if seen[item.Key] { continue } seen[item.Key] = true if item.Status == "" { item.Status = "active" } if item.Content == nil { item.Content = map[string]interface{}{} } unique = append(unique, item) } for _, relation := range relations { if !seen[relation.From] || !seen[relation.To] || relation.RelationType == "" { return nil, nil, fmt.Errorf("知识关系引用了不存在的 key") } } return unique, relations, nil } func graphScenarioID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.Error(c, http.StatusBadRequest, "INVALID_ID", "场景 ID 不正确") return 0, false } return id, true }