event_service.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package service
  2. import (
  3. "context"
  4. "net/http"
  5. "strings"
  6. "time"
  7. "cmr-backend/internal/apperr"
  8. "cmr-backend/internal/platform/security"
  9. "cmr-backend/internal/store/postgres"
  10. )
  11. type EventService struct {
  12. store *postgres.Store
  13. }
  14. type EventDetailResult struct {
  15. Event struct {
  16. ID string `json:"id"`
  17. Slug string `json:"slug"`
  18. DisplayName string `json:"displayName"`
  19. Summary *string `json:"summary,omitempty"`
  20. Status string `json:"status"`
  21. } `json:"event"`
  22. Release *struct {
  23. ID string `json:"id"`
  24. ConfigLabel string `json:"configLabel"`
  25. ManifestURL string `json:"manifestUrl"`
  26. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  27. RouteCode *string `json:"routeCode,omitempty"`
  28. } `json:"release,omitempty"`
  29. ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
  30. }
  31. type LaunchEventInput struct {
  32. EventPublicID string
  33. UserID string
  34. ReleaseID string `json:"releaseId,omitempty"`
  35. ClientType string `json:"clientType"`
  36. DeviceKey string `json:"deviceKey"`
  37. }
  38. type LaunchEventResult struct {
  39. Event struct {
  40. ID string `json:"id"`
  41. DisplayName string `json:"displayName"`
  42. } `json:"event"`
  43. Launch struct {
  44. Source string `json:"source"`
  45. ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
  46. Config struct {
  47. ConfigURL string `json:"configUrl"`
  48. ConfigLabel string `json:"configLabel"`
  49. ConfigChecksumSha256 *string `json:"configChecksumSha256,omitempty"`
  50. ReleaseID string `json:"releaseId"`
  51. RouteCode *string `json:"routeCode,omitempty"`
  52. } `json:"config"`
  53. Business struct {
  54. Source string `json:"source"`
  55. EventID string `json:"eventId"`
  56. SessionID string `json:"sessionId"`
  57. SessionToken string `json:"sessionToken"`
  58. SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"`
  59. RouteCode *string `json:"routeCode,omitempty"`
  60. } `json:"business"`
  61. } `json:"launch"`
  62. }
  63. func NewEventService(store *postgres.Store) *EventService {
  64. return &EventService{store: store}
  65. }
  66. func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
  67. eventPublicID = strings.TrimSpace(eventPublicID)
  68. if eventPublicID == "" {
  69. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
  70. }
  71. event, err := s.store.GetEventByPublicID(ctx, eventPublicID)
  72. if err != nil {
  73. return nil, err
  74. }
  75. if event == nil {
  76. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  77. }
  78. result := &EventDetailResult{}
  79. result.Event.ID = event.PublicID
  80. result.Event.Slug = event.Slug
  81. result.Event.DisplayName = event.DisplayName
  82. result.Event.Summary = event.Summary
  83. result.Event.Status = event.Status
  84. if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
  85. result.Release = &struct {
  86. ID string `json:"id"`
  87. ConfigLabel string `json:"configLabel"`
  88. ManifestURL string `json:"manifestUrl"`
  89. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  90. RouteCode *string `json:"routeCode,omitempty"`
  91. }{
  92. ID: *event.CurrentReleasePubID,
  93. ConfigLabel: *event.ConfigLabel,
  94. ManifestURL: *event.ManifestURL,
  95. ManifestChecksumSha256: event.ManifestChecksum,
  96. RouteCode: event.RouteCode,
  97. }
  98. }
  99. result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  100. return result, nil
  101. }
  102. func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) {
  103. input.EventPublicID = strings.TrimSpace(input.EventPublicID)
  104. input.ReleaseID = strings.TrimSpace(input.ReleaseID)
  105. input.DeviceKey = strings.TrimSpace(input.DeviceKey)
  106. if err := validateClientType(input.ClientType); err != nil {
  107. return nil, err
  108. }
  109. if input.EventPublicID == "" || input.UserID == "" || input.DeviceKey == "" {
  110. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id, user and deviceKey are required")
  111. }
  112. event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
  113. if err != nil {
  114. return nil, err
  115. }
  116. if event == nil {
  117. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  118. }
  119. if event.Status != "active" {
  120. return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active")
  121. }
  122. if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
  123. return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release")
  124. }
  125. if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
  126. return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
  127. }
  128. tx, err := s.store.Begin(ctx)
  129. if err != nil {
  130. return nil, err
  131. }
  132. defer tx.Rollback(ctx)
  133. sessionPublicID, err := security.GeneratePublicID("sess")
  134. if err != nil {
  135. return nil, err
  136. }
  137. sessionToken, err := security.GenerateToken(32)
  138. if err != nil {
  139. return nil, err
  140. }
  141. sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
  142. session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
  143. SessionPublicID: sessionPublicID,
  144. UserID: input.UserID,
  145. EventID: event.ID,
  146. EventReleaseID: *event.CurrentReleaseID,
  147. DeviceKey: input.DeviceKey,
  148. ClientType: input.ClientType,
  149. RouteCode: event.RouteCode,
  150. SessionTokenHash: security.HashText(sessionToken),
  151. SessionTokenExpiresAt: sessionTokenExpiresAt,
  152. })
  153. if err != nil {
  154. return nil, err
  155. }
  156. if err := tx.Commit(ctx); err != nil {
  157. return nil, err
  158. }
  159. result := &LaunchEventResult{}
  160. result.Event.ID = event.PublicID
  161. result.Event.DisplayName = event.DisplayName
  162. result.Launch.Source = LaunchSourceEventCurrentRelease
  163. result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  164. result.Launch.Config.ConfigURL = *event.ManifestURL
  165. result.Launch.Config.ConfigLabel = *event.ConfigLabel
  166. result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
  167. result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
  168. result.Launch.Config.RouteCode = event.RouteCode
  169. result.Launch.Business.Source = "direct-event"
  170. result.Launch.Business.EventID = event.PublicID
  171. result.Launch.Business.SessionID = session.SessionPublicID
  172. result.Launch.Business.SessionToken = sessionToken
  173. result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
  174. result.Launch.Business.RouteCode = event.RouteCode
  175. return result, nil
  176. }