Files
geo/server/internal/tenant/app/kol_profile_service.go
T
root 8761e47f78 feat(kol): cache marketplace and generation reads with singleflight
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>
2026-05-24 11:33:17 +08:00

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