8761e47f78
Add cache-aside + singleflight to KOL marketplace list/detail, my-subscription prompts, and generation schema lookups. Invalidate the shared public-facing caches when subscriptions are added/revoked (ops) or KOL profile is updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"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/objectstorage"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
|
)
|
|
|
|
var ErrNotActiveKol = response.ErrForbidden(40341, "kol_not_active", "user is not an active KOL")
|
|
|
|
type KolProfileService struct {
|
|
repo repository.KolProfileRepository
|
|
objectStorage objectstorage.Client
|
|
cache sharedcache.Cache
|
|
}
|
|
|
|
type UpdateKolProfileServiceInput struct {
|
|
DisplayName string
|
|
Bio *string
|
|
AvatarURL *string
|
|
}
|
|
|
|
type KolProfileResponse struct {
|
|
ID int64 `json:"id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
UserID int64 `json:"user_id"`
|
|
DisplayName string `json:"display_name"`
|
|
Bio *string `json:"bio"`
|
|
AvatarURL *string `json:"avatar_url"`
|
|
MarketEnabled bool `json:"market_enabled"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func NewKolProfileService(repo repository.KolProfileRepository) *KolProfileService {
|
|
return &KolProfileService{repo: repo}
|
|
}
|
|
|
|
func (s *KolProfileService) WithObjectStorage(client objectstorage.Client) *KolProfileService {
|
|
s.objectStorage = client
|
|
return s
|
|
}
|
|
|
|
func (s *KolProfileService) WithCache(c sharedcache.Cache) *KolProfileService {
|
|
s.cache = c
|
|
return s
|
|
}
|
|
|
|
func (s *KolProfileService) RequireActiveKol(ctx context.Context, actor auth.Actor) (*repository.KolProfile, error) {
|
|
profile, err := s.repo.GetByUser(ctx, actor.TenantID, actor.UserID)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50061, "kol_profile_query_failed", err.Error())
|
|
}
|
|
if profile == nil || profile.Status != "active" {
|
|
return nil, ErrNotActiveKol
|
|
}
|
|
return profile, nil
|
|
}
|
|
|
|
func (s *KolProfileService) UpdateProfile(ctx context.Context, actor auth.Actor, input UpdateKolProfileServiceInput) (*repository.KolProfile, error) {
|
|
profile, err := s.RequireActiveKol(ctx, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
displayName, err := validateKolDisplayName(input.DisplayName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bio, err := validateKolBio(input.Bio)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
avatarURL, err := validateKolAvatarURL(input.AvatarURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, actor.TenantID, repository.UpdateKolProfileInput{
|
|
ID: profile.ID,
|
|
DisplayName: displayName,
|
|
Bio: bio,
|
|
AvatarURL: avatarURL,
|
|
}); err != nil {
|
|
return nil, response.ErrInternal(50062, "kol_profile_update_failed", err.Error())
|
|
}
|
|
|
|
updated, err := s.repo.GetByID(ctx, profile.ID)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50061, "kol_profile_query_failed", err.Error())
|
|
}
|
|
if updated == nil {
|
|
return nil, ErrNotActiveKol
|
|
}
|
|
InvalidateKolPublicCaches(ctx, s.cache)
|
|
return updated, nil
|
|
}
|
|
|
|
func NewKolProfileResponse(profile *repository.KolProfile) *KolProfileResponse {
|
|
if profile == nil {
|
|
return nil
|
|
}
|
|
|
|
return &KolProfileResponse{
|
|
ID: profile.ID,
|
|
TenantID: profile.TenantID,
|
|
UserID: profile.UserID,
|
|
DisplayName: profile.DisplayName,
|
|
Bio: profile.Bio,
|
|
AvatarURL: profile.AvatarURL,
|
|
MarketEnabled: profile.MarketEnabled,
|
|
Status: profile.Status,
|
|
CreatedAt: profile.CreatedAt,
|
|
UpdatedAt: profile.UpdatedAt,
|
|
}
|
|
}
|