package usecase import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/copy_mission/domain/entity" domrepo "haixun-backend/internal/model/copy_mission/domain/repository" domusecase "haixun-backend/internal/model/copy_mission/domain/usecase" "github.com/google/uuid" ) type missionUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &missionUseCase{repo: repo} } func (u *missionUseCase) List(ctx context.Context, tenantID, ownerUID, personaID string) (*domusecase.ListResult, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } items, err := u.repo.ListByPersona(ctx, tenantID, ownerUID, personaID) if err != nil { return nil, err } list := make([]domusecase.MissionSummary, 0, len(items)) for _, item := range items { list = append(list, toSummary(item)) } return &domusecase.ListResult{List: list}, nil } func (u *missionUseCase) Create(ctx context.Context, req domusecase.CreateRequest) (*domusecase.MissionSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } label := strings.TrimSpace(req.Label) seedQuery := strings.TrimSpace(req.SeedQuery) brief := strings.TrimSpace(req.Brief) if label == "" { return nil, app.For(code.Persona).InputMissingRequired("label is required") } if seedQuery == "" { return nil, app.For(code.Persona).InputMissingRequired("seed_query is required") } if brief == "" { return nil, app.For(code.Persona).InputMissingRequired("brief is required") } mission := &entity.Mission{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: strings.TrimSpace(req.PersonaID), Label: label, SeedQuery: seedQuery, Brief: brief, Status: entity.StatusOpen, } if req.InitialResearchMap != nil { mission.ResearchMap = *req.InitialResearchMap } if len(req.InitialSelectedTags) > 0 { mission.SelectedTags = cleanTags(req.InitialSelectedTags) } item, err := u.repo.Create(ctx, mission) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *missionUseCase) Get(ctx context.Context, tenantID, ownerUID, personaID, missionID string) (*domusecase.MissionSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } item, err := u.repo.FindByID(ctx, tenantID, ownerUID, personaID, missionID) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *missionUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.MissionSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } patch := map[string]interface{}{} if req.Patch.Label != nil { label := strings.TrimSpace(*req.Patch.Label) if label == "" { return nil, app.For(code.Persona).InputMissingRequired("label cannot be empty") } patch["label"] = label } if req.Patch.SeedQuery != nil { seed := strings.TrimSpace(*req.Patch.SeedQuery) if seed == "" { return nil, app.For(code.Persona).InputMissingRequired("seed_query cannot be empty") } patch["seed_query"] = seed } if req.Patch.Brief != nil { brief := strings.TrimSpace(*req.Patch.Brief) if brief == "" { return nil, app.For(code.Persona).InputMissingRequired("brief cannot be empty") } patch["brief"] = brief } if req.Patch.AudienceSummary != nil { patch["research_map.audience_summary"] = strings.TrimSpace(*req.Patch.AudienceSummary) } if req.Patch.ContentGoal != nil { patch["research_map.content_goal"] = strings.TrimSpace(*req.Patch.ContentGoal) } if req.Patch.QuestionsSet { patch["research_map.questions"] = cleanStringList(req.Patch.Questions) } if req.Patch.PillarsSet { patch["research_map.pillars"] = cleanStringList(req.Patch.Pillars) } if req.Patch.ExclusionsSet { patch["research_map.exclusions"] = cleanStringList(req.Patch.Exclusions) } if req.Patch.BenchmarkNotes != nil { patch["research_map.benchmark_notes"] = strings.TrimSpace(*req.Patch.BenchmarkNotes) } if req.Patch.SelectedTagsSet { patch["selected_tags"] = cleanTags(req.Patch.SelectedTags) } if req.Patch.ResearchMap != nil { patch["research_map"] = *req.Patch.ResearchMap } if req.Patch.LastScanJobID != nil { patch["last_scan_job_id"] = strings.TrimSpace(*req.Patch.LastScanJobID) } if req.Patch.Status != nil { patch["status"] = *req.Patch.Status } item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, req.MissionID, patch) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *missionUseCase) Delete(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return err } return u.repo.Delete(ctx, tenantID, ownerUID, personaID, missionID) } func requireActor(tenantID, ownerUID, personaID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Auth).AuthUnauthorized("missing actor") } if strings.TrimSpace(personaID) == "" { return app.For(code.Persona).InputMissingRequired("persona_id is required") } return nil } func toSummary(item *entity.Mission) domusecase.MissionSummary { if item == nil { return domusecase.MissionSummary{} } tags := make([]domusecase.SuggestedTagSummary, 0, len(item.ResearchMap.SuggestedTags)) for _, tag := range item.ResearchMap.SuggestedTags { tags = append(tags, domusecase.SuggestedTagSummary{ Tag: tag.Tag, Reason: tag.Reason, SearchIntent: tag.SearchIntent, SearchType: tag.SearchType, }) } accounts := make([]domusecase.SimilarAccountSummary, 0, len(item.ResearchMap.SimilarAccounts)) for _, acc := range item.ResearchMap.SimilarAccounts { accounts = append(accounts, domusecase.SimilarAccountSummary{ Username: acc.Username, Reason: acc.Reason, Source: acc.Source, Confidence: acc.Confidence, ProfileURL: acc.ProfileURL, AuthorVerified: acc.AuthorVerified, FollowerCount: acc.FollowerCount, EngagementScore: acc.EngagementScore, LikeCount: acc.LikeCount, ReplyCount: acc.ReplyCount, PostCount: acc.PostCount, }) } return domusecase.MissionSummary{ ID: item.ID, PersonaID: item.PersonaID, Label: item.Label, SeedQuery: item.SeedQuery, Brief: item.Brief, ResearchMap: toMapSummary(item.ResearchMap, tags, accounts), SelectedTags: append([]string(nil), item.SelectedTags...), LastScanJobID: item.LastScanJobID, Status: string(item.Status), CreateAt: item.CreateAt, UpdateAt: item.UpdateAt, } } func toMapSummary(m entity.ResearchMap, tags []domusecase.SuggestedTagSummary, accounts []domusecase.SimilarAccountSummary) domusecase.ResearchMapSummary { return domusecase.ResearchMapSummary{ AudienceSummary: m.AudienceSummary, ContentGoal: m.ContentGoal, Questions: append([]string(nil), m.Questions...), Pillars: append([]string(nil), m.Pillars...), Exclusions: append([]string(nil), m.Exclusions...), SuggestedTags: tags, SimilarAccounts: accounts, BenchmarkNotes: m.BenchmarkNotes, } } func cleanStringList(items []string) []string { out := make([]string, 0, len(items)) seen := make(map[string]struct{}, len(items)) for _, item := range items { trimmed := strings.TrimSpace(item) if trimmed == "" { continue } if _, ok := seen[trimmed]; ok { continue } seen[trimmed] = struct{}{} out = append(out, trimmed) } return out } func cleanTags(tags []string) []string { out := []string{} seen := map[string]struct{}{} for _, tag := range tags { tag = strings.TrimSpace(tag) if tag == "" { continue } if _, ok := seen[tag]; ok { continue } seen[tag] = struct{}{} out = append(out, tag) } return out }