event_service.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. VariantID string `json:"variantId,omitempty"`
  36. ClientType string `json:"clientType"`
  37. DeviceKey string `json:"deviceKey"`
  38. }
  39. type LaunchEventResult struct {
  40. Event struct {
  41. ID string `json:"id"`
  42. DisplayName string `json:"displayName"`
  43. } `json:"event"`
  44. Launch struct {
  45. Source string `json:"source"`
  46. ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
  47. Variant *VariantBindingView `json:"variant,omitempty"`
  48. Config struct {
  49. ConfigURL string `json:"configUrl"`
  50. ConfigLabel string `json:"configLabel"`
  51. ConfigChecksumSha256 *string `json:"configChecksumSha256,omitempty"`
  52. ReleaseID string `json:"releaseId"`
  53. RouteCode *string `json:"routeCode,omitempty"`
  54. } `json:"config"`
  55. Business struct {
  56. Source string `json:"source"`
  57. EventID string `json:"eventId"`
  58. SessionID string `json:"sessionId"`
  59. SessionToken string `json:"sessionToken"`
  60. SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"`
  61. RouteCode *string `json:"routeCode,omitempty"`
  62. } `json:"business"`
  63. } `json:"launch"`
  64. }
  65. func NewEventService(store *postgres.Store) *EventService {
  66. return &EventService{store: store}
  67. }
  68. func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
  69. eventPublicID = strings.TrimSpace(eventPublicID)
  70. if eventPublicID == "" {
  71. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
  72. }
  73. event, err := s.store.GetEventByPublicID(ctx, eventPublicID)
  74. if err != nil {
  75. return nil, err
  76. }
  77. if event == nil {
  78. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  79. }
  80. result := &EventDetailResult{}
  81. result.Event.ID = event.PublicID
  82. result.Event.Slug = event.Slug
  83. result.Event.DisplayName = event.DisplayName
  84. result.Event.Summary = event.Summary
  85. result.Event.Status = event.Status
  86. if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
  87. result.Release = &struct {
  88. ID string `json:"id"`
  89. ConfigLabel string `json:"configLabel"`
  90. ManifestURL string `json:"manifestUrl"`
  91. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  92. RouteCode *string `json:"routeCode,omitempty"`
  93. }{
  94. ID: *event.CurrentReleasePubID,
  95. ConfigLabel: *event.ConfigLabel,
  96. ManifestURL: *event.ManifestURL,
  97. ManifestChecksumSha256: event.ManifestChecksum,
  98. RouteCode: event.RouteCode,
  99. }
  100. }
  101. result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  102. return result, nil
  103. }
  104. func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) {
  105. input.EventPublicID = strings.TrimSpace(input.EventPublicID)
  106. input.ReleaseID = strings.TrimSpace(input.ReleaseID)
  107. input.VariantID = strings.TrimSpace(input.VariantID)
  108. input.DeviceKey = strings.TrimSpace(input.DeviceKey)
  109. if err := validateClientType(input.ClientType); err != nil {
  110. return nil, err
  111. }
  112. if input.EventPublicID == "" || input.UserID == "" || input.DeviceKey == "" {
  113. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id, user and deviceKey are required")
  114. }
  115. event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
  116. if err != nil {
  117. return nil, err
  118. }
  119. if event == nil {
  120. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  121. }
  122. if event.Status != "active" {
  123. return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active")
  124. }
  125. if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
  126. return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release")
  127. }
  128. if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
  129. return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
  130. }
  131. variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
  132. variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
  133. if err != nil {
  134. return nil, err
  135. }
  136. routeCode := event.RouteCode
  137. var assignmentMode *string
  138. var variantID *string
  139. var variantName *string
  140. if variant != nil {
  141. resultMode := variant.AssignmentMode
  142. assignmentMode = &resultMode
  143. variantID = &variant.ID
  144. variantName = &variant.Name
  145. if variant.RouteCode != nil {
  146. routeCode = variant.RouteCode
  147. }
  148. }
  149. tx, err := s.store.Begin(ctx)
  150. if err != nil {
  151. return nil, err
  152. }
  153. defer tx.Rollback(ctx)
  154. sessionPublicID, err := security.GeneratePublicID("sess")
  155. if err != nil {
  156. return nil, err
  157. }
  158. sessionToken, err := security.GenerateToken(32)
  159. if err != nil {
  160. return nil, err
  161. }
  162. sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
  163. session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
  164. SessionPublicID: sessionPublicID,
  165. UserID: input.UserID,
  166. EventID: event.ID,
  167. EventReleaseID: *event.CurrentReleaseID,
  168. DeviceKey: input.DeviceKey,
  169. ClientType: input.ClientType,
  170. AssignmentMode: assignmentMode,
  171. VariantID: variantID,
  172. VariantName: variantName,
  173. RouteCode: routeCode,
  174. SessionTokenHash: security.HashText(sessionToken),
  175. SessionTokenExpiresAt: sessionTokenExpiresAt,
  176. })
  177. if err != nil {
  178. return nil, err
  179. }
  180. if err := tx.Commit(ctx); err != nil {
  181. return nil, err
  182. }
  183. result := &LaunchEventResult{}
  184. result.Event.ID = event.PublicID
  185. result.Event.DisplayName = event.DisplayName
  186. result.Launch.Source = LaunchSourceEventCurrentRelease
  187. result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  188. result.Launch.Variant = variant
  189. result.Launch.Config.ConfigURL = *event.ManifestURL
  190. result.Launch.Config.ConfigLabel = *event.ConfigLabel
  191. result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
  192. result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
  193. result.Launch.Config.RouteCode = routeCode
  194. result.Launch.Business.Source = "direct-event"
  195. result.Launch.Business.EventID = event.PublicID
  196. result.Launch.Business.SessionID = session.SessionPublicID
  197. result.Launch.Business.SessionToken = sessionToken
  198. result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
  199. result.Launch.Business.RouteCode = routeCode
  200. return result, nil
  201. }