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:
@@ -14,6 +14,7 @@ var (
|
||||
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 {
|
||||
@@ -24,21 +25,22 @@ type MarketFilter struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `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"`
|
||||
OwnedByCurrentTenant bool `json:"owned_by_current_tenant"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type KolMarketplacePromptResponse struct {
|
||||
@@ -107,7 +109,7 @@ func (s *KolMarketplaceService) ListPackages(ctx context.Context, actor auth.Act
|
||||
|
||||
result := make([]KolMarketplacePackageResponse, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
item, err := newKolMarketplacePackageResponse(pkg)
|
||||
item, err := newKolMarketplacePackageResponse(pkg, actor.TenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error())
|
||||
}
|
||||
@@ -134,7 +136,7 @@ func (s *KolMarketplaceService) GetPackage(ctx context.Context, actor auth.Actor
|
||||
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
|
||||
}
|
||||
|
||||
pkgResp, err := newKolMarketplacePackageResponse(*pkg)
|
||||
pkgResp, err := newKolMarketplacePackageResponse(*pkg, actor.TenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error())
|
||||
}
|
||||
@@ -167,6 +169,9 @@ func (s *KolMarketplaceService) Subscribe(ctx context.Context, actor auth.Actor,
|
||||
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 {
|
||||
@@ -227,28 +232,29 @@ func (s *KolMarketplaceService) ListMySubscriptionPrompts(ctx context.Context, a
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newKolMarketplacePackageResponse(pkg repository.PublishedKolPackage) (*KolMarketplacePackageResponse, error) {
|
||||
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),
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user