package usecase import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/placement" "haixun-backend/internal/model/brand/domain/entity" domrepo "haixun-backend/internal/model/brand/domain/repository" domusecase "haixun-backend/internal/model/brand/domain/usecase" "github.com/google/uuid" ) type brandUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &brandUseCase{repo: repo} } func (u *brandUseCase) List(ctx context.Context, tenantID, ownerUID string) (*domusecase.ListResult, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } items, err := u.repo.ListByOwner(ctx, tenantID, ownerUID) if err != nil { return nil, err } list := make([]domusecase.BrandSummary, 0, len(items)) for _, item := range items { list = append(list, toSummary(item)) } return &domusecase.ListResult{List: list}, nil } func (u *brandUseCase) Create(ctx context.Context, req domusecase.CreateRequest) (*domusecase.BrandSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID); err != nil { return nil, err } displayName := strings.TrimSpace(req.DisplayName) if displayName == "" { existing, err := u.repo.ListByOwner(ctx, req.TenantID, req.OwnerUID) if err != nil { return nil, err } displayName = "品牌 " + itoa(len(existing)+1) } productBrief := strings.TrimSpace(req.ProductBrief) if productBrief == "" && strings.TrimSpace(req.ProductContext) != "" { productBrief = strings.TrimSpace(req.ProductContext) } brand := &entity.Brand{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, DisplayName: displayName, SeedQuery: strings.TrimSpace(req.SeedQuery), Brief: strings.TrimSpace(req.Brief), ProductBrief: productBrief, ProductContext: strings.TrimSpace(req.ProductContext), TargetAudience: strings.TrimSpace(req.TargetAudience), Goals: strings.TrimSpace(req.Goals), Status: entity.StatusOpen, } if req.ResearchMap != nil { brand.ResearchMap = *req.ResearchMap } item, err := u.repo.Create(ctx, brand) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *brandUseCase) Get(ctx context.Context, tenantID, ownerUID, brandID string) (*domusecase.BrandSummary, error) { item, err := u.assertOwned(ctx, tenantID, ownerUID, brandID) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *brandUseCase) Delete(ctx context.Context, tenantID, ownerUID, brandID string) error { if _, err := u.assertOwned(ctx, tenantID, ownerUID, brandID); err != nil { return err } return u.repo.SoftDelete(ctx, tenantID, ownerUID, brandID) } func (u *brandUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.BrandSummary, error) { brand, err := u.assertOwned(ctx, req.TenantID, req.OwnerUID, req.BrandID) if err != nil { return nil, err } patch := patchToMap(req.Patch) if req.Patch.ProductID != nil { snapshot := placement.ResolveBrandProductContext(*brand, strings.TrimSpace(*req.Patch.ProductID), "") patch["product_context"] = snapshot } item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.BrandID, patch) if err != nil { return nil, err } summary := toSummary(item) return &summary, nil } func (u *brandUseCase) assertOwned(ctx context.Context, tenantID, ownerUID, brandID string) (*entity.Brand, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } if strings.TrimSpace(brandID) == "" { return nil, app.For(code.Brand).InputMissingRequired("brand id is required") } return u.repo.FindByID(ctx, tenantID, ownerUID, brandID) } func requireActor(tenantID, ownerUID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Brand).InputMissingRequired("tenant_id and uid are required") } return nil } func toSummary(item *entity.Brand) domusecase.BrandSummary { if item == nil { return domusecase.BrandSummary{} } products := make([]domusecase.ProductSummary, 0, len(item.Products)) for _, product := range item.Products { products = append(products, toProductSummary(product)) } return domusecase.BrandSummary{ ID: item.ID, DisplayName: item.DisplayName, TopicName: item.TopicName, SeedQuery: item.SeedQuery, Brief: item.Brief, ProductBrief: item.ProductBrief, ProductContext: item.ProductContext, ProductID: item.ProductID, Products: products, TargetAudience: item.TargetAudience, Goals: item.Goals, ResearchMap: item.ResearchMap, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt, } } func patchToMap(patch domusecase.BrandPatch) map[string]interface{} { out := map[string]interface{}{} if patch.DisplayName != nil { out["display_name"] = strings.TrimSpace(*patch.DisplayName) } if patch.TopicName != nil { out["topic_name"] = strings.TrimSpace(*patch.TopicName) } if patch.SeedQuery != nil { out["seed_query"] = strings.TrimSpace(*patch.SeedQuery) } if patch.Brief != nil { out["brief"] = strings.TrimSpace(*patch.Brief) } if patch.ProductBrief != nil { out["product_brief"] = strings.TrimSpace(*patch.ProductBrief) } if patch.ProductContext != nil { out["product_context"] = strings.TrimSpace(*patch.ProductContext) } if patch.ProductID != nil { out["product_id"] = strings.TrimSpace(*patch.ProductID) } if patch.TargetAudience != nil { out["target_audience"] = strings.TrimSpace(*patch.TargetAudience) } if patch.Goals != nil { out["goals"] = strings.TrimSpace(*patch.Goals) } if patch.AudienceSummary != nil { out["research_map.audience_summary"] = strings.TrimSpace(*patch.AudienceSummary) } if patch.ContentGoal != nil { out["research_map.content_goal"] = strings.TrimSpace(*patch.ContentGoal) } if patch.QuestionsSet { out["research_map.questions"] = cleanStringList(patch.Questions) } if patch.PillarsSet { out["research_map.pillars"] = cleanStringList(patch.Pillars) } if patch.ExclusionsSet { out["research_map.exclusions"] = cleanStringList(patch.Exclusions) } if patch.ResearchMap != nil { out["research_map"] = *patch.ResearchMap } if patch.PatrolKeywordsSet { out["research_map.patrol_keywords"] = libkg.NormalizePatrolKeywordList(patch.PatrolKeywords) } return out } 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 itoa(n int) string { if n <= 0 { return "1" } buf := make([]byte, 0, 12) for n > 0 { buf = append(buf, byte('0'+n%10)) n /= 10 } for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { buf[i], buf[j] = buf[j], buf[i] } return string(buf) }