package usecase import ( "context" "strings" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libmongo "haixun-backend/internal/library/mongo" "haixun-backend/internal/library/placement" "haixun-backend/internal/model/scan_post/domain/entity" domrepo "haixun-backend/internal/model/scan_post/domain/repository" domusecase "haixun-backend/internal/model/scan_post/domain/usecase" "github.com/google/uuid" ) type scanPostUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &scanPostUseCase{repo: repo} } func (u *scanPostUseCase) ReplaceFromViralScan(ctx context.Context, req domusecase.ViralReplaceRequest) (int, error) { if err := requireViralActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return 0, err } now := clock.NowUnixNano() entities := make([]entity.ScanPost, 0, len(req.Posts)) for _, item := range req.Posts { entities = append(entities, entity.ScanPost{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, LegacyPersonaID: req.PersonaID, Flow: entity.FlowViral, ScanJobID: req.ScanJobID, SearchTag: item.SearchTag, ExternalID: item.ExternalID, Permalink: item.Permalink, Author: item.Author, Text: item.Text, Priority: item.Priority, LikeCount: item.LikeCount, ReplyCount: item.ReplyCount, EngagementScore: item.EngagementScore, PlacementScore: item.PlacementScore, Source: string(item.Source), Replies: toReplyEntities(item.Replies), CreateAt: now, }) } if err := u.repo.ReplaceForViralScan(ctx, req.TenantID, req.OwnerUID, req.PersonaID, req.ScanJobID, entities); err != nil { return 0, err } return len(entities), nil } func (u *scanPostUseCase) ReplaceFromScan(ctx context.Context, req domusecase.ReplaceRequest) (int, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.BrandID); err != nil { return 0, err } now := clock.NowUnixNano() entities := make([]entity.ScanPost, 0, len(req.Posts)) for _, item := range req.Posts { entities = append(entities, entity.ScanPost{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, BrandID: req.BrandID, Flow: entity.FlowPlacement, GraphID: req.GraphID, ScanJobID: req.ScanJobID, GraphNodeID: item.GraphNodeID, SearchTag: item.SearchTag, QueryDimension: string(item.QueryDimension), ExternalID: item.ExternalID, Permalink: item.Permalink, Author: item.Author, Text: item.Text, Priority: item.Priority, PlacementScore: item.PlacementScore, ProductFitScore: item.ProductFitScore, SolvedByProduct: item.SolvedByProduct, Source: string(item.Source), OutreachStatus: entity.OutreachStatusPending, Replies: toReplyEntities(item.Replies), CreateAt: now, }) } if err := u.repo.ReplaceForScan(ctx, req.TenantID, req.OwnerUID, req.BrandID, req.ScanJobID, entities); err != nil { return 0, err } return len(entities), nil } func (u *scanPostUseCase) Get(ctx context.Context, tenantID, ownerUID, brandID, postID string) (*domusecase.ScanPostSummary, error) { if err := requireActor(tenantID, ownerUID, brandID); err != nil { return nil, err } postID = strings.TrimSpace(postID) if postID == "" { return nil, errMissingBrand() } item, err := u.repo.Get(ctx, tenantID, ownerUID, brandID, postID) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *scanPostUseCase) UpdateOutreach(ctx context.Context, req domusecase.UpdateOutreachRequest) (*domusecase.ScanPostSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.BrandID); err != nil { return nil, err } postID := strings.TrimSpace(req.PostID) if postID == "" { return nil, errMissingBrand() } status := strings.TrimSpace(req.Status) if status == "" { return nil, app.For(code.Brand).InputMissingRequired("outreach status is required") } item, err := u.repo.UpdateOutreach(ctx, req.TenantID, req.OwnerUID, req.BrandID, postID, entity.OutreachPatch{ Status: status, PublishedReplyID: req.PublishedReplyID, PublishedPermalink: req.PublishedPermalink, }) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *scanPostUseCase) GetForPersona(ctx context.Context, tenantID, ownerUID, personaID, postID string) (*domusecase.ScanPostSummary, error) { if err := requireViralActor(tenantID, ownerUID, personaID); err != nil { return nil, err } postID = strings.TrimSpace(postID) if postID == "" { return nil, errMissingPersona() } item, err := u.repo.GetForPersona(ctx, tenantID, ownerUID, personaID, postID) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *scanPostUseCase) ListForPersona(ctx context.Context, req domusecase.PersonaListRequest) ([]domusecase.ScanPostSummary, error) { if err := requireViralActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } items, err := u.repo.ListForPersona(ctx, req.TenantID, req.OwnerUID, domrepo.PersonaListFilter{ PersonaID: req.PersonaID, Flow: entity.FlowViral, Limit: req.Limit, }) if err != nil { return nil, err } out := make([]domusecase.ScanPostSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(item)) } return out, nil } func (u *scanPostUseCase) List(ctx context.Context, req domusecase.ListRequest) ([]domusecase.ScanPostSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.BrandID); err != nil { return nil, err } items, err := u.repo.List(ctx, req.TenantID, req.OwnerUID, domrepo.ListFilter{ BrandID: req.BrandID, Priority: req.Priority, ProductFitMin: req.ProductFitMin, Recent7dOnly: req.Recent7dOnly, Limit: req.Limit, }) if err != nil { return nil, err } out := make([]domusecase.ScanPostSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(item)) } return out, nil } func requireActor(tenantID, ownerUID, brandID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return errMissingActor() } if strings.TrimSpace(brandID) == "" { return errMissingBrand() } return nil } func requireViralActor(tenantID, ownerUID, personaID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return errMissingActor() } if strings.TrimSpace(personaID) == "" { return errMissingPersona() } return nil } func errMissingPersona() error { return app.For(code.Persona).InputMissingRequired("persona_id is required") } func toReplyEntities(replies []placement.ReplyCandidate) []entity.ScanReply { if len(replies) == 0 { return nil } out := make([]entity.ScanReply, 0, len(replies)) for _, reply := range replies { out = append(out, entity.ScanReply{ ExternalID: reply.ExternalID, Author: reply.Author, Text: reply.Text, Permalink: reply.Permalink, LikeCount: reply.LikeCount, PostedAt: reply.PostedAt, }) } return out } func toReplySummaries(replies []entity.ScanReply) []domusecase.ScanReplySummary { if len(replies) == 0 { return nil } out := make([]domusecase.ScanReplySummary, 0, len(replies)) for _, reply := range replies { out = append(out, domusecase.ScanReplySummary{ ExternalID: reply.ExternalID, Author: reply.Author, Text: reply.Text, Permalink: reply.Permalink, LikeCount: reply.LikeCount, PostedAt: reply.PostedAt, }) } return out } func toSummary(item entity.ScanPost) domusecase.ScanPostSummary { return domusecase.ScanPostSummary{ ID: item.ID, BrandID: libmongo.ResolveBrandID(item.BrandID, item.LegacyPersonaID), PersonaID: strings.TrimSpace(item.LegacyPersonaID), Flow: item.Flow, GraphNodeID: item.GraphNodeID, SearchTag: item.SearchTag, QueryDimension: item.QueryDimension, ExternalID: item.ExternalID, Permalink: item.Permalink, Author: item.Author, Text: item.Text, Priority: item.Priority, LikeCount: item.LikeCount, ReplyCount: item.ReplyCount, EngagementScore: item.EngagementScore, PlacementScore: item.PlacementScore, ProductFitScore: item.ProductFitScore, SolvedByProduct: item.SolvedByProduct, Source: item.Source, ScanJobID: item.ScanJobID, OutreachStatus: item.OutreachStatus, PublishedReplyID: item.PublishedReplyID, PublishedPermalink: item.PublishedPermalink, OutreachUpdateAt: item.OutreachUpdateAt, Replies: toReplySummaries(item.Replies), CreateAt: item.CreateAt, } }