| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- package service
- import (
- "context"
- "net/http"
- "strings"
- "time"
- "cmr-backend/internal/apperr"
- "cmr-backend/internal/platform/security"
- "cmr-backend/internal/store/postgres"
- )
- const (
- GuestLaunchSource = "public-default-experience"
- GuestIdentityProvider = "guest_device"
- GuestIdentityType = "guest"
- )
- type PublicExperienceService struct {
- store *postgres.Store
- mapService *MapExperienceService
- eventService *EventService
- }
- type PublicEventPlayInput struct {
- EventPublicID string
- }
- type PublicLaunchEventInput struct {
- EventPublicID string `json:"-"`
- ReleaseID string `json:"releaseId,omitempty"`
- VariantID string `json:"variantId,omitempty"`
- ClientType string `json:"clientType"`
- DeviceKey string `json:"deviceKey"`
- }
- func NewPublicExperienceService(store *postgres.Store, mapService *MapExperienceService, eventService *EventService) *PublicExperienceService {
- return &PublicExperienceService{
- store: store,
- mapService: mapService,
- eventService: eventService,
- }
- }
- func (s *PublicExperienceService) ListMaps(ctx context.Context, input ListExperienceMapsInput) ([]ExperienceMapSummary, error) {
- return s.mapService.ListMaps(ctx, input)
- }
- func (s *PublicExperienceService) GetMapDetail(ctx context.Context, mapPublicID string) (*ExperienceMapDetail, error) {
- return s.mapService.GetMapDetail(ctx, mapPublicID)
- }
- func (s *PublicExperienceService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
- event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
- if err != nil {
- return nil, err
- }
- if err := ensurePublicExperienceEvent(event); err != nil {
- return nil, err
- }
- return s.eventService.GetEventDetail(ctx, eventPublicID)
- }
- func (s *PublicExperienceService) GetEventPlay(ctx context.Context, input PublicEventPlayInput) (*EventPlayResult, error) {
- input.EventPublicID = strings.TrimSpace(input.EventPublicID)
- if input.EventPublicID == "" {
- return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
- }
- event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
- if err != nil {
- return nil, err
- }
- if err := ensurePublicExperienceEvent(event); err != nil {
- return nil, err
- }
- result := &EventPlayResult{}
- result.Event.ID = event.PublicID
- result.Event.Slug = event.Slug
- result.Event.DisplayName = event.DisplayName
- result.Event.Summary = event.Summary
- result.Event.Status = event.Status
- variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
- result.Play.AssignmentMode = variantPlan.AssignmentMode
- if len(variantPlan.CourseVariants) > 0 {
- result.Play.CourseVariants = variantPlan.CourseVariants
- }
- if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
- result.Release = &struct {
- ID string `json:"id"`
- ConfigLabel string `json:"configLabel"`
- ManifestURL string `json:"manifestUrl"`
- ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
- RouteCode *string `json:"routeCode,omitempty"`
- }{
- ID: *event.CurrentReleasePubID,
- ConfigLabel: *event.ConfigLabel,
- ManifestURL: *event.ManifestURL,
- ManifestChecksumSha256: event.ManifestChecksum,
- RouteCode: event.RouteCode,
- }
- }
- result.ResolvedRelease = buildResolvedReleaseFromEvent(event, GuestLaunchSource)
- result.Runtime = buildRuntimeSummaryFromEvent(event)
- if preview, err := buildPreviewFromPayload(event.ReleasePayloadJSON); err != nil {
- return nil, err
- } else {
- result.Preview = preview
- }
- result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
- if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
- return nil, err
- } else if enrichedPresentation != nil {
- result.CurrentPresentation = enrichedPresentation
- }
- result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
- if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
- return nil, err
- } else if enrichedBundle != nil {
- result.CurrentContentBundle = enrichedBundle
- }
- canLaunch, launchReason := evaluateEventLaunchReadiness(event)
- result.Play.CanLaunch = canLaunch
- if canLaunch {
- result.Play.LaunchSource = GuestLaunchSource
- result.Play.PrimaryAction = "start"
- result.Play.Reason = "guest can start default experience"
- return result, nil
- }
- result.Play.PrimaryAction = "unavailable"
- result.Play.Reason = launchReason
- return result, nil
- }
- func (s *PublicExperienceService) LaunchEvent(ctx context.Context, input PublicLaunchEventInput) (*LaunchEventResult, error) {
- input.EventPublicID = strings.TrimSpace(input.EventPublicID)
- input.ReleaseID = strings.TrimSpace(input.ReleaseID)
- input.VariantID = strings.TrimSpace(input.VariantID)
- input.DeviceKey = strings.TrimSpace(input.DeviceKey)
- if input.EventPublicID == "" || input.DeviceKey == "" {
- return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id and deviceKey are required")
- }
- if err := validateClientType(input.ClientType); err != nil {
- return nil, err
- }
- event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
- if err != nil {
- return nil, err
- }
- if err := ensurePublicExperienceEvent(event); err != nil {
- return nil, err
- }
- if canLaunch, reason := evaluateEventLaunchReadiness(event); !canLaunch {
- return nil, launchReadinessError(reason)
- }
- if input.ReleaseID != "" && event.CurrentReleasePubID != nil && input.ReleaseID != *event.CurrentReleasePubID {
- return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
- }
- variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
- variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
- if err != nil {
- return nil, err
- }
- routeCode := event.RouteCode
- var assignmentMode *string
- var variantID *string
- var variantName *string
- if variant != nil {
- resultMode := variant.AssignmentMode
- assignmentMode = &resultMode
- variantID = &variant.ID
- variantName = &variant.Name
- if variant.RouteCode != nil {
- routeCode = variant.RouteCode
- }
- }
- tx, err := s.store.Begin(ctx)
- if err != nil {
- return nil, err
- }
- defer tx.Rollback(ctx)
- guestUser, err := s.findOrCreateGuestUser(ctx, tx, input.ClientType, input.DeviceKey)
- if err != nil {
- return nil, err
- }
- if err := s.store.TouchUserLogin(ctx, tx, guestUser.ID); err != nil {
- return nil, err
- }
- sessionPublicID, err := security.GeneratePublicID("sess")
- if err != nil {
- return nil, err
- }
- sessionToken, err := security.GenerateToken(32)
- if err != nil {
- return nil, err
- }
- sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
- session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
- SessionPublicID: sessionPublicID,
- UserID: guestUser.ID,
- EventID: event.ID,
- EventReleaseID: *event.CurrentReleaseID,
- DeviceKey: input.DeviceKey,
- ClientType: input.ClientType,
- AssignmentMode: assignmentMode,
- VariantID: variantID,
- VariantName: variantName,
- RouteCode: routeCode,
- SessionTokenHash: security.HashText(sessionToken),
- SessionTokenExpiresAt: sessionTokenExpiresAt,
- })
- if err != nil {
- return nil, err
- }
- if err := tx.Commit(ctx); err != nil {
- return nil, err
- }
- result := &LaunchEventResult{}
- result.Event.ID = event.PublicID
- result.Event.DisplayName = event.DisplayName
- result.Launch.Source = GuestLaunchSource
- result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, GuestLaunchSource)
- result.Launch.Variant = variant
- result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
- result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
- if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
- return nil, err
- } else if enrichedPresentation != nil {
- result.Launch.Presentation = enrichedPresentation
- }
- result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
- if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
- return nil, err
- } else if enrichedBundle != nil {
- result.Launch.ContentBundle = enrichedBundle
- }
- result.Launch.Config.ConfigURL = *event.ManifestURL
- result.Launch.Config.ConfigLabel = *event.ConfigLabel
- result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
- result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
- result.Launch.Config.RouteCode = routeCode
- result.Launch.Business.Source = GuestLaunchSource
- result.Launch.Business.EventID = event.PublicID
- result.Launch.Business.SessionID = session.SessionPublicID
- result.Launch.Business.SessionToken = sessionToken
- result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
- result.Launch.Business.RouteCode = routeCode
- result.Launch.Business.IsGuest = true
- return result, nil
- }
- func (s *PublicExperienceService) findOrCreateGuestUser(ctx context.Context, tx postgres.Tx, clientType, deviceKey string) (*postgres.User, error) {
- providerSubject := clientType + ":" + deviceKey
- user, err := s.store.FindUserByProviderSubject(ctx, tx, GuestIdentityProvider, providerSubject)
- if err != nil {
- return nil, err
- }
- if user != nil {
- return user, nil
- }
- userPublicID, err := security.GeneratePublicID("usr")
- if err != nil {
- return nil, err
- }
- user, err = s.store.CreateUser(ctx, tx, postgres.CreateUserParams{
- PublicID: userPublicID,
- Status: "active",
- })
- if err != nil {
- return nil, err
- }
- if err := s.store.CreateIdentity(ctx, tx, postgres.CreateIdentityParams{
- UserID: user.ID,
- IdentityType: GuestIdentityType,
- Provider: GuestIdentityProvider,
- ProviderSubj: providerSubject,
- ProfileJSON: "{}",
- }); err != nil {
- return nil, err
- }
- return user, nil
- }
- func ensurePublicExperienceEvent(event *postgres.Event) error {
- if event == nil {
- return apperr.New(http.StatusNotFound, "event_not_found", "event not found")
- }
- if !event.IsDefaultExperience {
- return apperr.New(http.StatusForbidden, "event_not_public", "event is not available in guest mode")
- }
- return nil
- }
|