package app import ( "context" "time" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/tenant/repository" ) var ( errKolMarketplacePackageNotFound = response.ErrNotFound(40471, "kol_marketplace_package_not_found", "package not found") errKolSubscriptionNotFound = response.ErrNotFound(40472, "kol_subscription_not_found", "subscription not found") errKolSubscriptionExists = response.ErrConflict(40971, "kol_subscription_exists", "subscription already exists") errKolSubscriptionNotPending = response.ErrConflict(40972, "kol_subscription_not_pending", "subscription is not pending") errKolSelfSubscribeViaMarket = response.ErrBadRequest(40074, "kol_self_subscribe_via_marketplace", "自己的订阅包请在 KOL 工作台订阅") ) type MarketFilter struct { Industry *string Keyword *string Offset int Limit int } type KolMarketplacePackageResponse struct { ID int64 `json:"id"` KolProfileID int64 `json:"kol_profile_id"` KolDisplayName string `json:"kol_display_name"` KolAvatarURL *string `json:"kol_avatar_url"` Name string `json:"name"` Description *string `json:"description"` CoverURL *string `json:"cover_url"` Industry *string `json:"industry"` Tags []string `json:"tags"` PriceNote *string `json:"price_note"` Status string `json:"status"` PromptCount int `json:"prompt_count"` SubscriberCount int `json:"subscriber_count"` OwnedByCurrentTenant bool `json:"owned_by_current_tenant"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type KolMarketplacePromptResponse struct { ID int64 `json:"id"` Name string `json:"name"` PlatformHint *string `json:"platform_hint"` Status string `json:"status"` PublishedRevisionNo *int `json:"published_revision_no"` CreatedAt string `json:"created_at"` } type KolMarketplacePackageDetailResponse struct { KolMarketplacePackageResponse KolBio *string `json:"kol_bio"` Prompts []KolMarketplacePromptResponse `json:"prompts"` Subscription *KolSubscriptionResponse `json:"subscription"` } type KolSubscriptionResponse struct { ID int64 `json:"id"` PackageID int64 `json:"package_id"` Status string `json:"status"` StartAt *string `json:"start_at"` EndAt *string `json:"end_at"` CreatedAt string `json:"created_at"` } type KolSubscriptionPromptCardResponse struct { ID int64 `json:"id"` PackageID int64 `json:"package_id"` PackageName string `json:"package_name"` PromptID int64 `json:"prompt_id"` PromptName string `json:"prompt_name"` PlatformHint *string `json:"platform_hint"` KolDisplayName string `json:"kol_display_name"` GrantedAt string `json:"granted_at"` } type KolMarketplaceService struct { marketplaceRepo repository.KolMarketplaceRepository subRepo repository.KolSubscriptionRepository } func NewKolMarketplaceService( marketplaceRepo repository.KolMarketplaceRepository, subRepo repository.KolSubscriptionRepository, ) *KolMarketplaceService { return &KolMarketplaceService{ marketplaceRepo: marketplaceRepo, subRepo: subRepo, } } func (s *KolMarketplaceService) ListPackages(ctx context.Context, actor auth.Actor, filter MarketFilter) ([]KolMarketplacePackageResponse, error) { filter = normalizeMarketFilter(filter) packages, err := s.marketplaceRepo.ListPublished(ctx, repository.ListPublishedFilter{ Industry: filter.Industry, Keyword: filter.Keyword, Offset: filter.Offset, Limit: filter.Limit, }) if err != nil { return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error()) } result := make([]KolMarketplacePackageResponse, 0, len(packages)) for _, pkg := range packages { item, err := newKolMarketplacePackageResponse(pkg, actor.TenantID) if err != nil { return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error()) } result = append(result, *item) } return result, nil } func (s *KolMarketplaceService) GetPackage(ctx context.Context, actor auth.Actor, id int64) (*KolMarketplacePackageDetailResponse, error) { pkg, err := s.marketplaceRepo.GetPublished(ctx, id) if err != nil { return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error()) } if pkg == nil { return nil, errKolMarketplacePackageNotFound } prompts, err := s.marketplaceRepo.ListPublicPrompts(ctx, id) if err != nil { return nil, response.ErrInternal(50092, "kol_marketplace_prompt_query_failed", err.Error()) } sub, err := s.subRepo.GetActiveForTenant(ctx, actor.TenantID, id) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } pkgResp, err := newKolMarketplacePackageResponse(*pkg, actor.TenantID) if err != nil { return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error()) } resp := &KolMarketplacePackageDetailResponse{ KolMarketplacePackageResponse: *pkgResp, KolBio: pkg.KolBio, Prompts: make([]KolMarketplacePromptResponse, 0, len(prompts)), Subscription: newKolSubscriptionResponse(sub), } for _, prompt := range prompts { resp.Prompts = append(resp.Prompts, KolMarketplacePromptResponse{ ID: prompt.ID, Name: prompt.Name, PlatformHint: prompt.PlatformHint, Status: prompt.Status, PublishedRevisionNo: prompt.PublishedRevisionNo, CreatedAt: prompt.CreatedAt.Format(time.RFC3339), }) } return resp, nil } func (s *KolMarketplaceService) Subscribe(ctx context.Context, actor auth.Actor, packageID int64) (*KolSubscriptionResponse, error) { pkg, err := s.marketplaceRepo.GetPublished(ctx, packageID) if err != nil { return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error()) } if pkg == nil { return nil, errKolMarketplacePackageNotFound } if pkg.TenantID == actor.TenantID { return nil, errKolSelfSubscribeViaMarket } existing, err := s.subRepo.GetActiveForTenant(ctx, actor.TenantID, packageID) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if existing != nil { return nil, errKolSubscriptionExists } sub, err := s.subRepo.Create(ctx, actor.TenantID, packageID) if err != nil { if isUniqueConstraintError(err, "uk_kol_subscription_active") { return nil, errKolSubscriptionExists } return nil, response.ErrInternal(50094, "kol_subscription_create_failed", err.Error()) } return newKolSubscriptionResponse(sub), nil } func (s *KolMarketplaceService) ListMySubscriptions(ctx context.Context, actor auth.Actor) ([]KolSubscriptionResponse, error) { subs, err := s.subRepo.ListByTenant(ctx, actor.TenantID) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } result := make([]KolSubscriptionResponse, 0, len(subs)) for _, sub := range subs { if resp := newKolSubscriptionResponse(&sub); resp != nil { result = append(result, *resp) } } return result, nil } func (s *KolMarketplaceService) ListMySubscriptionPrompts(ctx context.Context, actor auth.Actor) ([]KolSubscriptionPromptCardResponse, error) { rows, err := s.subRepo.ListSubscriptionPromptsByTenant(ctx, actor.TenantID) if err != nil { return nil, response.ErrInternal(50095, "kol_subscription_prompt_query_failed", err.Error()) } result := make([]KolSubscriptionPromptCardResponse, 0, len(rows)) for _, row := range rows { grantedAt := "" if row.GrantedAt != nil { grantedAt = row.GrantedAt.Format(time.RFC3339) } result = append(result, KolSubscriptionPromptCardResponse{ ID: row.ID, PackageID: row.PackageID, PackageName: row.PackageName, PromptID: row.PromptID, PromptName: row.PromptName, PlatformHint: row.PlatformHint, KolDisplayName: row.KolDisplayName, GrantedAt: grantedAt, }) } return result, nil } func newKolMarketplacePackageResponse(pkg repository.PublishedKolPackage, viewerTenantID int64) (*KolMarketplacePackageResponse, error) { tags, err := decodeKolStringList(pkg.Tags) if err != nil { return nil, err } return &KolMarketplacePackageResponse{ ID: pkg.ID, KolProfileID: pkg.KolProfileID, KolDisplayName: pkg.KolDisplayName, KolAvatarURL: pkg.KolAvatarURL, Name: pkg.Name, Description: pkg.Description, CoverURL: pkg.CoverURL, Industry: pkg.Industry, Tags: tags, PriceNote: pkg.PriceNote, Status: pkg.Status, PromptCount: int(pkg.PromptCount), SubscriberCount: int(pkg.SubscriberCount), OwnedByCurrentTenant: pkg.TenantID == viewerTenantID, CreatedAt: pkg.CreatedAt.Format(time.RFC3339), UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339), }, nil } func newKolSubscriptionResponse(sub *repository.KolSubscription) *KolSubscriptionResponse { if sub == nil { return nil } return &KolSubscriptionResponse{ ID: sub.ID, PackageID: sub.PackageID, Status: sub.Status, StartAt: timeStringPtr(sub.StartAt), EndAt: timeStringPtr(sub.EndAt), CreatedAt: sub.CreatedAt.Format(time.RFC3339), } } func normalizeMarketFilter(filter MarketFilter) MarketFilter { if filter.Offset < 0 { filter.Offset = 0 } if filter.Limit <= 0 { filter.Limit = 20 } if filter.Limit > 100 { filter.Limit = 100 } filter.Industry = normalizeKolOptionalString(filter.Industry) filter.Keyword = normalizeKolOptionalString(filter.Keyword) return filter }