Files
iqudo-top1/internal/knowledge/graph.go
2026-08-18 16:27:02 +08:00

197 lines
7.5 KiB
Go

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
}