437 lines
13 KiB
Go
437 lines
13 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
|
)
|
|
|
|
type CreateKolPackageInput struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description *string `json:"description"`
|
|
CoverURL *string `json:"cover_url"`
|
|
Industry *string `json:"industry"`
|
|
Tags []string `json:"tags"`
|
|
PriceNote *string `json:"price_note"`
|
|
}
|
|
|
|
type UpdateKolPackageInput = CreateKolPackageInput
|
|
|
|
type KolPackageResponse 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"`
|
|
SelfSubscription *KolSubscriptionResponse `json:"self_subscription"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type KolPackageService struct {
|
|
profileSvc *KolProfileService
|
|
repo repository.KolPackageRepository
|
|
promptRepo repository.KolPromptRepository
|
|
subRepo repository.KolSubscriptionRepository
|
|
cache sharedcache.Cache
|
|
}
|
|
|
|
func NewKolPackageService(
|
|
profileSvc *KolProfileService,
|
|
repo repository.KolPackageRepository,
|
|
promptRepo repository.KolPromptRepository,
|
|
subRepo repository.KolSubscriptionRepository,
|
|
) *KolPackageService {
|
|
return &KolPackageService{
|
|
profileSvc: profileSvc,
|
|
repo: repo,
|
|
promptRepo: promptRepo,
|
|
subRepo: subRepo,
|
|
}
|
|
}
|
|
|
|
func (s *KolPackageService) WithCache(c sharedcache.Cache) *KolPackageService {
|
|
s.cache = c
|
|
return s
|
|
}
|
|
|
|
func (s *KolPackageService) List(ctx context.Context, actor auth.Actor) ([]KolPackageResponse, error) {
|
|
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
packages, err := s.repo.ListByProfile(ctx, actor.TenantID, profile.ID)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
|
|
}
|
|
|
|
packageIDs := kolPackageIDs(packages)
|
|
promptCounts, err := s.packagePromptCounts(ctx, actor.TenantID, packageIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
selfSubscriptions, err := s.packageSelfSubscriptions(ctx, actor.TenantID, packageIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]KolPackageResponse, 0, len(packages))
|
|
for _, pkg := range packages {
|
|
item, err := s.buildPackageResponseWithStats(profile, &pkg, promptCounts[pkg.ID], selfSubscriptions[pkg.ID])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, *item)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *KolPackageService) Get(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
|
|
profile, pkg, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.buildPackageResponse(ctx, profile, pkg)
|
|
}
|
|
|
|
func (s *KolPackageService) Create(ctx context.Context, actor auth.Actor, input CreateKolPackageInput) (*KolPackageResponse, error) {
|
|
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name := strings.TrimSpace(input.Name)
|
|
if name == "" {
|
|
return nil, response.ErrBadRequest(40061, "kol_package_name_required", "package name is required")
|
|
}
|
|
|
|
tagsJSON, err := json.Marshal(normalizeKolStringList(input.Tags))
|
|
if err != nil {
|
|
return nil, response.ErrBadRequest(40062, "kol_package_tags_invalid", "tags must be valid json")
|
|
}
|
|
|
|
pkg, err := s.repo.Create(ctx, repository.CreateKolPackageInput{
|
|
TenantID: actor.TenantID,
|
|
KolProfileID: profile.ID,
|
|
Name: name,
|
|
Description: normalizeKolOptionalString(input.Description),
|
|
CoverURL: normalizeKolOptionalString(input.CoverURL),
|
|
Industry: normalizeKolOptionalString(input.Industry),
|
|
Tags: tagsJSON,
|
|
PriceNote: normalizeKolOptionalString(input.PriceNote),
|
|
})
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50063, "kol_package_create_failed", err.Error())
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, pkg)
|
|
}
|
|
|
|
func (s *KolPackageService) Update(ctx context.Context, actor auth.Actor, id int64, input UpdateKolPackageInput) (*KolPackageResponse, error) {
|
|
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name := strings.TrimSpace(input.Name)
|
|
if name == "" {
|
|
return nil, response.ErrBadRequest(40061, "kol_package_name_required", "package name is required")
|
|
}
|
|
|
|
tagsJSON, err := json.Marshal(normalizeKolStringList(input.Tags))
|
|
if err != nil {
|
|
return nil, response.ErrBadRequest(40062, "kol_package_tags_invalid", "tags must be valid json")
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, actor.TenantID, repository.UpdateKolPackageInput{
|
|
ID: id,
|
|
Name: name,
|
|
Description: normalizeKolOptionalString(input.Description),
|
|
CoverURL: normalizeKolOptionalString(input.CoverURL),
|
|
Industry: normalizeKolOptionalString(input.Industry),
|
|
Tags: tagsJSON,
|
|
PriceNote: normalizeKolOptionalString(input.PriceNote),
|
|
}); err != nil {
|
|
return nil, response.ErrInternal(50064, "kol_package_update_failed", err.Error())
|
|
}
|
|
|
|
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
|
|
}
|
|
if updated == nil {
|
|
return nil, errKolPackageNotFound
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, updated)
|
|
}
|
|
|
|
func (s *KolPackageService) Publish(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
|
|
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.repo.UpdateStatus(ctx, actor.TenantID, id, "published"); err != nil {
|
|
return nil, response.ErrInternal(50065, "kol_package_publish_failed", err.Error())
|
|
}
|
|
|
|
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
|
|
}
|
|
if updated == nil {
|
|
return nil, errKolPackageNotFound
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, updated)
|
|
}
|
|
|
|
func (s *KolPackageService) Archive(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
|
|
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.repo.UpdateStatus(ctx, actor.TenantID, id, "archived"); err != nil {
|
|
return nil, response.ErrInternal(50066, "kol_package_archive_failed", err.Error())
|
|
}
|
|
|
|
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
|
|
}
|
|
if updated == nil {
|
|
return nil, errKolPackageNotFound
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, updated)
|
|
}
|
|
|
|
func (s *KolPackageService) Delete(ctx context.Context, actor auth.Actor, id int64) error {
|
|
if _, _, err := s.loadOwnedPackage(ctx, actor, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.repo.SoftDelete(ctx, actor.TenantID, id); err != nil {
|
|
return response.ErrInternal(50067, "kol_package_delete_failed", err.Error())
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return nil
|
|
}
|
|
|
|
func (s *KolPackageService) SelfSubscribe(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
|
|
profile, pkg, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pkg.Status != "published" {
|
|
return nil, response.ErrConflict(40973, "kol_package_not_published", "package must be published before self subscription")
|
|
}
|
|
if s.subRepo == nil {
|
|
return nil, response.ErrInternal(50093, "kol_subscription_repo_missing", "subscription repository is not configured")
|
|
}
|
|
|
|
if _, err := s.subRepo.SelfSubscribe(ctx, actor.TenantID, id); err != nil {
|
|
if isUniqueConstraintError(err, "uk_kol_subscription_active") {
|
|
return nil, errKolSubscriptionExists
|
|
}
|
|
return nil, response.ErrInternal(50094, "kol_self_subscription_failed", err.Error())
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, pkg)
|
|
}
|
|
|
|
func (s *KolPackageService) CancelSelfSubscription(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
|
|
profile, pkg, err := s.loadOwnedPackage(ctx, actor, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.subRepo == nil {
|
|
return nil, response.ErrInternal(50093, "kol_subscription_repo_missing", "subscription repository is not configured")
|
|
}
|
|
|
|
sub, err := s.subRepo.CancelSelfSubscription(ctx, actor.TenantID, id)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50095, "kol_self_subscription_cancel_failed", err.Error())
|
|
}
|
|
if sub == nil {
|
|
return nil, errKolSubscriptionNotFound
|
|
}
|
|
|
|
InvalidateKolPromptCaches(ctx, s.cache)
|
|
return s.buildPackageResponse(ctx, profile, pkg)
|
|
}
|
|
|
|
func (s *KolPackageService) loadOwnedPackage(ctx context.Context, actor auth.Actor, id int64) (*repository.KolProfile, *repository.KolPackage, error) {
|
|
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
pkg, err := s.repo.GetByID(ctx, actor.TenantID, id)
|
|
if err != nil {
|
|
return nil, nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
|
|
}
|
|
if pkg == nil {
|
|
return nil, nil, errKolPackageNotFound
|
|
}
|
|
if pkg.KolProfileID != profile.ID {
|
|
return nil, nil, ErrPackageNotOwned
|
|
}
|
|
|
|
return profile, pkg, nil
|
|
}
|
|
|
|
func (s *KolPackageService) buildPackageResponse(
|
|
ctx context.Context,
|
|
profile *repository.KolProfile,
|
|
pkg *repository.KolPackage,
|
|
) (*KolPackageResponse, error) {
|
|
promptCounts, err := s.packagePromptCounts(ctx, pkg.TenantID, []int64{pkg.ID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selfSubscriptions, err := s.packageSelfSubscriptions(ctx, pkg.TenantID, []int64{pkg.ID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.buildPackageResponseWithStats(profile, pkg, promptCounts[pkg.ID], selfSubscriptions[pkg.ID])
|
|
}
|
|
|
|
func (s *KolPackageService) buildPackageResponseWithStats(
|
|
profile *repository.KolProfile,
|
|
pkg *repository.KolPackage,
|
|
promptCount int,
|
|
selfSubscription *repository.KolSubscription,
|
|
) (*KolPackageResponse, error) {
|
|
tags, err := decodeKolStringList(pkg.Tags)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50068, "kol_package_decode_failed", err.Error())
|
|
}
|
|
|
|
return &KolPackageResponse{
|
|
ID: pkg.ID,
|
|
KolProfileID: pkg.KolProfileID,
|
|
KolDisplayName: profile.DisplayName,
|
|
KolAvatarURL: profile.AvatarURL,
|
|
Name: pkg.Name,
|
|
Description: pkg.Description,
|
|
CoverURL: pkg.CoverURL,
|
|
Industry: pkg.Industry,
|
|
Tags: tags,
|
|
PriceNote: pkg.PriceNote,
|
|
Status: pkg.Status,
|
|
PromptCount: promptCount,
|
|
SubscriberCount: 0,
|
|
SelfSubscription: newKolSubscriptionResponse(selfSubscription),
|
|
CreatedAt: pkg.CreatedAt,
|
|
UpdatedAt: pkg.UpdatedAt,
|
|
}, nil
|
|
}
|
|
|
|
func (s *KolPackageService) packagePromptCounts(ctx context.Context, tenantID int64, packageIDs []int64) (map[int64]int, error) {
|
|
if s.promptRepo == nil {
|
|
return map[int64]int{}, nil
|
|
}
|
|
counts, err := s.promptRepo.CountByPackages(ctx, tenantID, packageIDs)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50069, "kol_prompt_query_failed", err.Error())
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func (s *KolPackageService) packageSelfSubscriptions(ctx context.Context, tenantID int64, packageIDs []int64) (map[int64]*repository.KolSubscription, error) {
|
|
result := make(map[int64]*repository.KolSubscription, len(packageIDs))
|
|
if s.subRepo == nil {
|
|
return result, nil
|
|
}
|
|
|
|
subscriptions, err := s.subRepo.ListActiveForTenantByPackages(ctx, tenantID, packageIDs)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
|
|
}
|
|
for packageID, sub := range subscriptions {
|
|
subscription := sub
|
|
result[packageID] = &subscription
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func kolPackageIDs(packages []repository.KolPackage) []int64 {
|
|
ids := make([]int64, 0, len(packages))
|
|
for _, pkg := range packages {
|
|
ids = append(ids, pkg.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func normalizeKolOptionalString(value *string) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(*value)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func normalizeKolStringList(values []string) []string {
|
|
if len(values) == 0 {
|
|
return []string{}
|
|
}
|
|
|
|
result := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[trimmed]; exists {
|
|
continue
|
|
}
|
|
seen[trimmed] = struct{}{}
|
|
result = append(result, trimmed)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func decodeKolStringList(raw []byte) ([]string, error) {
|
|
if len(raw) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
var values []string
|
|
if err := json.Unmarshal(raw, &values); err != nil {
|
|
return nil, err
|
|
}
|
|
return normalizeKolStringList(values), nil
|
|
}
|