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
@@ -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
}