package usecase import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" libmongo "haixun-backend/internal/library/mongo" "haixun-backend/internal/model/knowledge_graph/domain/entity" domrepo "haixun-backend/internal/model/knowledge_graph/domain/repository" domusecase "haixun-backend/internal/model/knowledge_graph/domain/usecase" ) type knowledgeGraphUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &knowledgeGraphUseCase{repo: repo} } func (u *knowledgeGraphUseCase) Get(ctx context.Context, tenantID, ownerUID, brandID string) (*domusecase.GraphSummary, error) { if err := requireScope(tenantID, ownerUID, brandID, ""); err != nil { return nil, err } item, err := u.repo.FindByBrand(ctx, tenantID, ownerUID, brandID) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *knowledgeGraphUseCase) GetByTopic(ctx context.Context, tenantID, ownerUID, topicID, brandID string) (*domusecase.GraphSummary, error) { if err := requireScope(tenantID, ownerUID, "", topicID); err != nil { return nil, err } item, err := u.repo.FindByTopic(ctx, tenantID, ownerUID, topicID) if err != nil { return nil, err } if item == nil { brandID = strings.TrimSpace(brandID) if brandID != "" { legacy, legacyErr := u.repo.FindByBrand(ctx, tenantID, ownerUID, brandID) if legacyErr != nil { return nil, legacyErr } if legacy != nil { legacyTopicID := strings.TrimSpace(legacy.TopicID) if legacyTopicID == "" || legacyTopicID == topicID { if legacyTopicID == "" { if err := u.repo.AttachTopicID(ctx, tenantID, ownerUID, brandID, topicID); err != nil { return nil, err } legacy.TopicID = topicID } item = legacy } } } } summary := toSummary(item) return &summary, nil } func (u *knowledgeGraphUseCase) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.GraphSummary, error) { topicID := strings.TrimSpace(req.TopicID) brandID := strings.TrimSpace(req.BrandID) if err := requireScope(req.TenantID, req.OwnerUID, brandID, topicID); err != nil { return nil, err } seed := strings.TrimSpace(req.Seed) if seed == "" { return nil, app.For(code.Brand).InputMissingRequired("seed is required") } graph := &entity.Graph{ TenantID: req.TenantID, OwnerUID: req.OwnerUID, BrandID: brandID, TopicID: topicID, Seed: seed, Nodes: req.Nodes, Edges: req.Edges, BraveSources: req.BraveSources, ExpandStrategy: req.ExpandStrategy, PainTagCount: req.PainTagCount, GeneratedAt: req.GeneratedAt, } var ( item *entity.Graph err error ) if topicID != "" { item, err = u.repo.UpsertByTopic(ctx, graph) } else { item, err = u.repo.UpsertByBrand(ctx, graph) } if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *knowledgeGraphUseCase) UpdateNodes(ctx context.Context, req domusecase.UpdateNodesRequest) (*domusecase.GraphSummary, error) { topicID := strings.TrimSpace(req.TopicID) brandID := strings.TrimSpace(req.BrandID) if err := requireScope(req.TenantID, req.OwnerUID, brandID, topicID); err != nil { return nil, err } if len(req.Updates) == 0 { return nil, app.For(code.Brand).InputMissingRequired("updates is required") } var ( current *entity.Graph err error ) if topicID != "" { current, err = u.repo.FindByTopic(ctx, req.TenantID, req.OwnerUID, topicID) } else { current, err = u.repo.FindByBrand(ctx, req.TenantID, req.OwnerUID, brandID) } if err != nil { return nil, err } changes := map[string]domusecase.NodeUpdate{} for _, update := range req.Updates { id := strings.TrimSpace(update.NodeID) if id == "" { continue } changes[id] = update } nodes := make([]libkg.Node, len(current.Nodes)) copy(nodes, current.Nodes) for i := range nodes { update, ok := changes[nodes[i].ID] if !ok { continue } if update.SelectedForScan != nil { nodes[i].SelectedForScan = *update.SelectedForScan } if update.RelevanceTagsSet { nodes[i].PatrolRelevance = libkg.SanitizePatrolKeywordList(update.RelevanceTags) } if update.RecencyTagsSet { nodes[i].PatrolRecency = libkg.SanitizePatrolKeywordList(update.RecencyTags) } if update.RelevanceTagsSet || update.RecencyTagsSet { nodes[i].DerivedTags = libkg.DerivePatrolTagsForNode(nodes[i], libkg.PatrolTagInput{}) } } painCount := libkg.CountPainTagCandidates(nodes) var item *entity.Graph if topicID != "" { item, err = u.repo.UpdateNodesByTopic(ctx, req.TenantID, req.OwnerUID, topicID, nodes, painCount) } else { item, err = u.repo.UpdateNodes(ctx, req.TenantID, req.OwnerUID, brandID, nodes, painCount) } if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *knowledgeGraphUseCase) AttachTopicID(ctx context.Context, tenantID, ownerUID, brandID, topicID string) error { if err := requireScope(tenantID, ownerUID, brandID, topicID); err != nil { return err } return u.repo.AttachTopicID(ctx, tenantID, ownerUID, brandID, topicID) } func requireScope(tenantID, ownerUID, brandID, topicID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Brand).InputMissingRequired("tenant_id and uid are required") } if strings.TrimSpace(topicID) == "" && strings.TrimSpace(brandID) == "" { return app.For(code.Brand).InputMissingRequired("brand id or topic id is required") } return nil } func toSummary(item *entity.Graph) domusecase.GraphSummary { if item == nil { return domusecase.GraphSummary{} } return domusecase.GraphSummary{ ID: item.ID, BrandID: libmongo.ResolveBrandID(item.BrandID, item.LegacyPersonaID), TopicID: strings.TrimSpace(item.TopicID), Seed: item.Seed, Nodes: item.Nodes, Edges: item.Edges, BraveSources: item.BraveSources, ExpandStrategy: item.ExpandStrategy, PainTagCount: item.PainTagCount, GeneratedAt: item.GeneratedAt, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt, } }