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 := requireActor(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) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.GraphSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.BrandID); err != nil { return nil, err } seed := strings.TrimSpace(req.Seed) if seed == "" { return nil, app.For(code.Brand).InputMissingRequired("seed is required") } item, err := u.repo.UpsertByBrand(ctx, &entity.Graph{ TenantID: req.TenantID, OwnerUID: req.OwnerUID, BrandID: req.BrandID, Seed: seed, Nodes: req.Nodes, Edges: req.Edges, BraveSources: req.BraveSources, PainTagCount: req.PainTagCount, GeneratedAt: req.GeneratedAt, }) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *knowledgeGraphUseCase) UpdateNodeSelections(ctx context.Context, req domusecase.UpdateNodesRequest) (*domusecase.GraphSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.BrandID); err != nil { return nil, err } if len(req.Updates) == 0 { return nil, app.For(code.Brand).InputMissingRequired("updates is required") } current, err := u.repo.FindByBrand(ctx, req.TenantID, req.OwnerUID, req.BrandID) if err != nil { return nil, err } selections := map[string]bool{} for _, update := range req.Updates { id := strings.TrimSpace(update.NodeID) if id == "" { continue } selections[id] = update.SelectedForScan } nodes := make([]libkg.Node, len(current.Nodes)) copy(nodes, current.Nodes) for i := range nodes { if selected, ok := selections[nodes[i].ID]; ok { nodes[i].SelectedForScan = selected } } painCount := libkg.CountPainTagCandidates(nodes) item, err := u.repo.UpdateNodes(ctx, req.TenantID, req.OwnerUID, req.BrandID, nodes, painCount) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func requireActor(tenantID, ownerUID, brandID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Brand).InputMissingRequired("tenant_id and uid are required") } if strings.TrimSpace(brandID) == "" { return app.For(code.Brand).InputMissingRequired("brand 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), Seed: item.Seed, Nodes: item.Nodes, Edges: item.Edges, BraveSources: item.BraveSources, PainTagCount: item.PainTagCount, GeneratedAt: item.GeneratedAt, UpdateAt: item.UpdateAt, CreateAt: item.CreateAt, } }