286 lines
8.9 KiB
Go
286 lines
8.9 KiB
Go
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,
|
|
}
|
|
}
|