thread-master/internal/model/copy_mission/usecase/usecase.go

262 lines
8.0 KiB
Go

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
}