262 lines
8.0 KiB
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
|
|
}
|