variant_contract.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package service
  2. import (
  3. "crypto/rand"
  4. "encoding/json"
  5. "fmt"
  6. "math/big"
  7. "net/http"
  8. "strings"
  9. "cmr-backend/internal/apperr"
  10. )
  11. const (
  12. AssignmentModeManual = "manual"
  13. AssignmentModeRandom = "random"
  14. AssignmentModeServerAssigned = "server-assigned"
  15. )
  16. type CourseVariantView struct {
  17. ID string `json:"id"`
  18. Name string `json:"name"`
  19. Description *string `json:"description,omitempty"`
  20. RouteCode *string `json:"routeCode,omitempty"`
  21. Selectable bool `json:"selectable"`
  22. }
  23. type VariantBindingView struct {
  24. ID string `json:"id"`
  25. Name string `json:"name"`
  26. RouteCode *string `json:"routeCode,omitempty"`
  27. AssignmentMode string `json:"assignmentMode"`
  28. }
  29. type VariantPlan struct {
  30. AssignmentMode *string
  31. CourseVariants []CourseVariantView
  32. }
  33. func resolveVariantPlan(payloadJSON *string) VariantPlan {
  34. if payloadJSON == nil || strings.TrimSpace(*payloadJSON) == "" {
  35. return VariantPlan{}
  36. }
  37. var payload map[string]any
  38. if err := json.Unmarshal([]byte(*payloadJSON), &payload); err != nil {
  39. return VariantPlan{}
  40. }
  41. play, _ := payload["play"].(map[string]any)
  42. if len(play) == 0 {
  43. return VariantPlan{}
  44. }
  45. result := VariantPlan{}
  46. if rawMode, ok := play["assignmentMode"].(string); ok {
  47. if normalized := normalizeAssignmentMode(rawMode); normalized != nil {
  48. result.AssignmentMode = normalized
  49. }
  50. }
  51. rawVariants, _ := play["courseVariants"].([]any)
  52. if len(rawVariants) == 0 {
  53. return result
  54. }
  55. for _, raw := range rawVariants {
  56. item, ok := raw.(map[string]any)
  57. if !ok {
  58. continue
  59. }
  60. id, _ := item["id"].(string)
  61. name, _ := item["name"].(string)
  62. id = strings.TrimSpace(id)
  63. name = strings.TrimSpace(name)
  64. if id == "" || name == "" {
  65. continue
  66. }
  67. var description *string
  68. if value, ok := item["description"].(string); ok && strings.TrimSpace(value) != "" {
  69. trimmed := strings.TrimSpace(value)
  70. description = &trimmed
  71. }
  72. var routeCode *string
  73. if value, ok := item["routeCode"].(string); ok && strings.TrimSpace(value) != "" {
  74. trimmed := strings.TrimSpace(value)
  75. routeCode = &trimmed
  76. }
  77. selectable := true
  78. if value, ok := item["selectable"].(bool); ok {
  79. selectable = value
  80. }
  81. result.CourseVariants = append(result.CourseVariants, CourseVariantView{
  82. ID: id,
  83. Name: name,
  84. Description: description,
  85. RouteCode: routeCode,
  86. Selectable: selectable,
  87. })
  88. }
  89. return result
  90. }
  91. func resolveLaunchVariant(plan VariantPlan, requestedVariantID string) (*VariantBindingView, error) {
  92. requestedVariantID = strings.TrimSpace(requestedVariantID)
  93. if len(plan.CourseVariants) == 0 {
  94. return nil, nil
  95. }
  96. mode := AssignmentModeManual
  97. if plan.AssignmentMode != nil {
  98. mode = *plan.AssignmentMode
  99. }
  100. if requestedVariantID != "" {
  101. for _, item := range plan.CourseVariants {
  102. if item.ID == requestedVariantID {
  103. if !item.Selectable && mode == AssignmentModeManual {
  104. return nil, apperr.New(http.StatusBadRequest, "variant_not_selectable", "requested variant is not selectable")
  105. }
  106. return &VariantBindingView{
  107. ID: item.ID,
  108. Name: item.Name,
  109. RouteCode: item.RouteCode,
  110. AssignmentMode: mode,
  111. }, nil
  112. }
  113. }
  114. return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "requested variant does not exist")
  115. }
  116. selected, err := selectDefaultVariant(plan.CourseVariants, mode)
  117. if err != nil {
  118. return nil, err
  119. }
  120. return &VariantBindingView{
  121. ID: selected.ID,
  122. Name: selected.Name,
  123. RouteCode: selected.RouteCode,
  124. AssignmentMode: mode,
  125. }, nil
  126. }
  127. func normalizeAssignmentMode(value string) *string {
  128. switch strings.TrimSpace(value) {
  129. case AssignmentModeManual:
  130. mode := AssignmentModeManual
  131. return &mode
  132. case AssignmentModeRandom:
  133. mode := AssignmentModeRandom
  134. return &mode
  135. case AssignmentModeServerAssigned:
  136. mode := AssignmentModeServerAssigned
  137. return &mode
  138. default:
  139. return nil
  140. }
  141. }
  142. func selectDefaultVariant(items []CourseVariantView, mode string) (*CourseVariantView, error) {
  143. candidates := make([]CourseVariantView, 0, len(items))
  144. for _, item := range items {
  145. if item.Selectable {
  146. candidates = append(candidates, item)
  147. }
  148. }
  149. if len(candidates) == 0 {
  150. candidates = append(candidates, items...)
  151. }
  152. if len(candidates) == 0 {
  153. return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "course variants are empty")
  154. }
  155. switch mode {
  156. case AssignmentModeRandom:
  157. index, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates))))
  158. if err != nil {
  159. return nil, apperr.New(http.StatusInternalServerError, "variant_select_failed", fmt.Sprintf("failed to select random variant: %v", err))
  160. }
  161. selected := candidates[int(index.Int64())]
  162. return &selected, nil
  163. case AssignmentModeServerAssigned, AssignmentModeManual:
  164. fallthrough
  165. default:
  166. selected := candidates[0]
  167. return &selected, nil
  168. }
  169. }