完善多赛道联调与全局产品架构
This commit is contained in:
189
backend/internal/service/variant_contract.go
Normal file
189
backend/internal/service/variant_contract.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
)
|
||||
|
||||
const (
|
||||
AssignmentModeManual = "manual"
|
||||
AssignmentModeRandom = "random"
|
||||
AssignmentModeServerAssigned = "server-assigned"
|
||||
)
|
||||
|
||||
type CourseVariantView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Selectable bool `json:"selectable"`
|
||||
}
|
||||
|
||||
type VariantBindingView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
AssignmentMode string `json:"assignmentMode"`
|
||||
}
|
||||
|
||||
type VariantPlan struct {
|
||||
AssignmentMode *string
|
||||
CourseVariants []CourseVariantView
|
||||
}
|
||||
|
||||
func resolveVariantPlan(payloadJSON *string) VariantPlan {
|
||||
if payloadJSON == nil || strings.TrimSpace(*payloadJSON) == "" {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(*payloadJSON), &payload); err != nil {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
play, _ := payload["play"].(map[string]any)
|
||||
if len(play) == 0 {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
result := VariantPlan{}
|
||||
if rawMode, ok := play["assignmentMode"].(string); ok {
|
||||
if normalized := normalizeAssignmentMode(rawMode); normalized != nil {
|
||||
result.AssignmentMode = normalized
|
||||
}
|
||||
}
|
||||
|
||||
rawVariants, _ := play["courseVariants"].([]any)
|
||||
if len(rawVariants) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, raw := range rawVariants {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := item["id"].(string)
|
||||
name, _ := item["name"].(string)
|
||||
id = strings.TrimSpace(id)
|
||||
name = strings.TrimSpace(name)
|
||||
if id == "" || name == "" {
|
||||
continue
|
||||
}
|
||||
var description *string
|
||||
if value, ok := item["description"].(string); ok && strings.TrimSpace(value) != "" {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
description = &trimmed
|
||||
}
|
||||
var routeCode *string
|
||||
if value, ok := item["routeCode"].(string); ok && strings.TrimSpace(value) != "" {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
routeCode = &trimmed
|
||||
}
|
||||
selectable := true
|
||||
if value, ok := item["selectable"].(bool); ok {
|
||||
selectable = value
|
||||
}
|
||||
result.CourseVariants = append(result.CourseVariants, CourseVariantView{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Description: description,
|
||||
RouteCode: routeCode,
|
||||
Selectable: selectable,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveLaunchVariant(plan VariantPlan, requestedVariantID string) (*VariantBindingView, error) {
|
||||
requestedVariantID = strings.TrimSpace(requestedVariantID)
|
||||
if len(plan.CourseVariants) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mode := AssignmentModeManual
|
||||
if plan.AssignmentMode != nil {
|
||||
mode = *plan.AssignmentMode
|
||||
}
|
||||
|
||||
if requestedVariantID != "" {
|
||||
for _, item := range plan.CourseVariants {
|
||||
if item.ID == requestedVariantID {
|
||||
if !item.Selectable && mode == AssignmentModeManual {
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_selectable", "requested variant is not selectable")
|
||||
}
|
||||
return &VariantBindingView{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
RouteCode: item.RouteCode,
|
||||
AssignmentMode: mode,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "requested variant does not exist")
|
||||
}
|
||||
|
||||
selected, err := selectDefaultVariant(plan.CourseVariants, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &VariantBindingView{
|
||||
ID: selected.ID,
|
||||
Name: selected.Name,
|
||||
RouteCode: selected.RouteCode,
|
||||
AssignmentMode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeAssignmentMode(value string) *string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case AssignmentModeManual:
|
||||
mode := AssignmentModeManual
|
||||
return &mode
|
||||
case AssignmentModeRandom:
|
||||
mode := AssignmentModeRandom
|
||||
return &mode
|
||||
case AssignmentModeServerAssigned:
|
||||
mode := AssignmentModeServerAssigned
|
||||
return &mode
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func selectDefaultVariant(items []CourseVariantView, mode string) (*CourseVariantView, error) {
|
||||
candidates := make([]CourseVariantView, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Selectable {
|
||||
candidates = append(candidates, item)
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
candidates = append(candidates, items...)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "course variants are empty")
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case AssignmentModeRandom:
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates))))
|
||||
if err != nil {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "variant_select_failed", fmt.Sprintf("failed to select random variant: %v", err))
|
||||
}
|
||||
selected := candidates[int(index.Int64())]
|
||||
return &selected, nil
|
||||
case AssignmentModeServerAssigned, AssignmentModeManual:
|
||||
fallthrough
|
||||
default:
|
||||
selected := candidates[0]
|
||||
return &selected, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user