feat(tenant/kol): let KOLs self-subscribe to their own published packages

Add /kol/manage/packages/:id/self-subscription POST/DELETE on the tenant
side so a KOL can grant themselves access to their own published package
without going through the marketplace approval flow. Reject self-subscribe
through the marketplace with a clear error, expose owned_by_current_tenant
on package responses, and surface a self_subscription block on KolPackage
Response. The admin-web KOL workspace gets 订阅自己 / 取消订阅 entries with
matching messaging in the marketplace detail view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 22:06:57 +08:00
parent 088dbb0ec7
commit 4dd8a1bb0e
9 changed files with 472 additions and 77 deletions
@@ -24,27 +24,29 @@ type CreateKolPackageInput struct {
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"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
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
}
@@ -52,11 +54,13 @@ func NewKolPackageService(
profileSvc *KolProfileService,
repo repository.KolPackageRepository,
promptRepo repository.KolPromptRepository,
subRepo repository.KolSubscriptionRepository,
) *KolPackageService {
return &KolPackageService{
profileSvc: profileSvc,
repo: repo,
promptRepo: promptRepo,
subRepo: subRepo,
}
}
@@ -227,6 +231,50 @@ func (s *KolPackageService) Delete(ctx context.Context, actor auth.Actor, id int
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 {
@@ -266,22 +314,32 @@ func (s *KolPackageService) buildPackageResponse(
promptCount = len(prompts)
}
var selfSubscription *KolSubscriptionResponse
if s.subRepo != nil {
sub, err := s.subRepo.GetActiveForTenant(ctx, pkg.TenantID, pkg.ID)
if err != nil {
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
selfSubscription = newKolSubscriptionResponse(sub)
}
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,
CreatedAt: pkg.CreatedAt,
UpdatedAt: pkg.UpdatedAt,
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: selfSubscription,
CreatedAt: pkg.CreatedAt,
UpdatedAt: pkg.UpdatedAt,
}, nil
}