推进活动列表第一刀与联调回归
This commit is contained in:
@@ -129,7 +129,7 @@ func (s *EventPlayService) GetEventPlay(ctx context.Context, input EventPlayInpu
|
||||
}
|
||||
}
|
||||
|
||||
canLaunch := event.Status == "active" && event.CurrentReleaseID != nil && event.ManifestURL != nil
|
||||
canLaunch, launchReason := evaluateEventLaunchReadiness(event)
|
||||
result.Play.CanLaunch = canLaunch
|
||||
if canLaunch {
|
||||
result.Play.LaunchSource = LaunchSourceEventCurrentRelease
|
||||
@@ -141,13 +141,13 @@ func (s *EventPlayService) GetEventPlay(ctx context.Context, input EventPlayInpu
|
||||
result.Play.Reason = "user has an ongoing session for this event"
|
||||
case canLaunch:
|
||||
result.Play.PrimaryAction = "start"
|
||||
result.Play.Reason = "event is active and launchable"
|
||||
result.Play.Reason = launchReason
|
||||
case result.Play.RecentSession != nil:
|
||||
result.Play.PrimaryAction = "review_last_result"
|
||||
result.Play.Reason = "event is not launchable, but user has previous session history"
|
||||
result.Play.Reason = launchReason + ", but user has previous session history"
|
||||
default:
|
||||
result.Play.PrimaryAction = "unavailable"
|
||||
result.Play.Reason = "event is not launchable"
|
||||
result.Play.Reason = launchReason
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -152,11 +152,8 @@ func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput)
|
||||
if event == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
if event.Status != "active" {
|
||||
return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active")
|
||||
}
|
||||
if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
|
||||
return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release")
|
||||
if canLaunch, reason := evaluateEventLaunchReadiness(event); !canLaunch {
|
||||
return nil, launchReadinessError(reason)
|
||||
}
|
||||
if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
|
||||
return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
|
||||
|
||||
@@ -24,17 +24,27 @@ type ListCardsInput struct {
|
||||
}
|
||||
|
||||
type CardResult struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Subtitle *string `json:"subtitle,omitempty"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
DisplaySlot string `json:"displaySlot"`
|
||||
DisplayPriority int `json:"displayPriority"`
|
||||
Event *struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Subtitle *string `json:"subtitle,omitempty"`
|
||||
Summary *string `json:"summary,omitempty"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
DisplaySlot string `json:"displaySlot"`
|
||||
DisplayPriority int `json:"displayPriority"`
|
||||
Status string `json:"status"`
|
||||
StatusCode string `json:"statusCode"`
|
||||
TimeWindow string `json:"timeWindow"`
|
||||
CTAText string `json:"ctaText"`
|
||||
IsDefaultExperience bool `json:"isDefaultExperience"`
|
||||
EventType *string `json:"eventType,omitempty"`
|
||||
CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
|
||||
CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
|
||||
Event *struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Summary *string `json:"summary,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
} `json:"event,omitempty"`
|
||||
HTMLURL *string `json:"htmlUrl,omitempty"`
|
||||
}
|
||||
@@ -128,23 +138,35 @@ func normalizeSlot(slot string) string {
|
||||
func mapCards(cards []postgres.Card) []CardResult {
|
||||
results := make([]CardResult, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
statusCode, statusText := deriveCardStatus(card)
|
||||
item := CardResult{
|
||||
ID: card.PublicID,
|
||||
Type: card.CardType,
|
||||
Title: card.Title,
|
||||
Subtitle: card.Subtitle,
|
||||
CoverURL: card.CoverURL,
|
||||
DisplaySlot: card.DisplaySlot,
|
||||
DisplayPriority: card.DisplayPriority,
|
||||
HTMLURL: card.HTMLURL,
|
||||
ID: card.PublicID,
|
||||
Type: card.CardType,
|
||||
Title: fallbackCardTitle(card.Title),
|
||||
Subtitle: card.Subtitle,
|
||||
Summary: fallbackCardSummary(card.EventSummary),
|
||||
CoverURL: card.CoverURL,
|
||||
DisplaySlot: card.DisplaySlot,
|
||||
DisplayPriority: card.DisplayPriority,
|
||||
Status: statusText,
|
||||
StatusCode: statusCode,
|
||||
TimeWindow: deriveCardTimeWindow(card),
|
||||
CTAText: deriveCardCTAText(card, statusCode),
|
||||
IsDefaultExperience: card.IsDefaultExperience,
|
||||
EventType: deriveCardEventType(card),
|
||||
CurrentPresentation: buildCardPresentationSummary(card),
|
||||
CurrentContentBundle: buildCardContentBundleSummary(card),
|
||||
HTMLURL: card.HTMLURL,
|
||||
}
|
||||
if card.EventPublicID != nil || card.EventDisplayName != nil {
|
||||
item.Event = &struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Summary *string `json:"summary,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
}{
|
||||
Summary: card.EventSummary,
|
||||
Status: card.EventStatus,
|
||||
}
|
||||
if card.EventPublicID != nil {
|
||||
item.Event.ID = *card.EventPublicID
|
||||
@@ -157,3 +179,134 @@ func mapCards(cards []postgres.Card) []CardResult {
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func fallbackCardTitle(title string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return "未命名活动"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func fallbackCardSummary(summary *string) *string {
|
||||
if summary != nil && strings.TrimSpace(*summary) != "" {
|
||||
return summary
|
||||
}
|
||||
text := "当前暂无活动摘要"
|
||||
return &text
|
||||
}
|
||||
|
||||
func deriveCardStatus(card postgres.Card) (string, string) {
|
||||
if card.EventStatus == nil {
|
||||
return "pending", "状态待确认"
|
||||
}
|
||||
switch strings.TrimSpace(*card.EventStatus) {
|
||||
case "active":
|
||||
if card.EventCurrentReleasePubID == nil {
|
||||
return "upcoming", "即将开始"
|
||||
}
|
||||
if card.EventRuntimeBindingID == nil || card.EventPresentationID == nil || card.EventContentBundleID == nil {
|
||||
return "upcoming", "即将开始"
|
||||
}
|
||||
return "running", "进行中"
|
||||
case "archived", "disabled", "inactive":
|
||||
return "ended", "已结束"
|
||||
default:
|
||||
return "pending", "状态待确认"
|
||||
}
|
||||
}
|
||||
|
||||
func deriveCardTimeWindow(card postgres.Card) string {
|
||||
if card.StartsAt == nil && card.EndsAt == nil {
|
||||
return "时间待公布"
|
||||
}
|
||||
const layout = "01-02 15:04"
|
||||
switch {
|
||||
case card.StartsAt != nil && card.EndsAt != nil:
|
||||
return card.StartsAt.Local().Format(layout) + " - " + card.EndsAt.Local().Format(layout)
|
||||
case card.StartsAt != nil:
|
||||
return "开始于 " + card.StartsAt.Local().Format(layout)
|
||||
default:
|
||||
return "截止至 " + card.EndsAt.Local().Format(layout)
|
||||
}
|
||||
}
|
||||
|
||||
func deriveCardCTAText(card postgres.Card, statusCode string) string {
|
||||
if card.IsDefaultExperience {
|
||||
return "进入体验"
|
||||
}
|
||||
switch statusCode {
|
||||
case "running":
|
||||
return "进入活动"
|
||||
case "ended":
|
||||
return "查看回顾"
|
||||
default:
|
||||
return "查看详情"
|
||||
}
|
||||
}
|
||||
|
||||
func deriveCardEventType(card postgres.Card) *string {
|
||||
if card.EventReleasePayloadJSON != nil {
|
||||
payload, err := decodeJSONObject(*card.EventReleasePayloadJSON)
|
||||
if err == nil {
|
||||
if game, ok := payload["game"].(map[string]any); ok {
|
||||
if rawMode, ok := game["mode"].(string); ok {
|
||||
switch strings.TrimSpace(rawMode) {
|
||||
case "classic-sequential":
|
||||
text := "顺序赛"
|
||||
return &text
|
||||
case "score-o":
|
||||
text := "积分赛"
|
||||
return &text
|
||||
}
|
||||
}
|
||||
}
|
||||
if plan := resolveVariantPlan(card.EventReleasePayloadJSON); plan.AssignmentMode != nil && *plan.AssignmentMode == AssignmentModeManual {
|
||||
text := "多赛道"
|
||||
return &text
|
||||
}
|
||||
}
|
||||
}
|
||||
if card.IsDefaultExperience {
|
||||
text := "体验活动"
|
||||
return &text
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCardPresentationSummary(card postgres.Card) *PresentationSummaryView {
|
||||
if card.EventPresentationID == nil {
|
||||
return nil
|
||||
}
|
||||
summary := &PresentationSummaryView{
|
||||
PresentationID: *card.EventPresentationID,
|
||||
Name: card.EventPresentationName,
|
||||
PresentationType: card.EventPresentationType,
|
||||
}
|
||||
if card.EventPresentationSchemaJSON != nil && strings.TrimSpace(*card.EventPresentationSchemaJSON) != "" {
|
||||
if schema, err := decodeJSONObject(*card.EventPresentationSchemaJSON); err == nil {
|
||||
summary.TemplateKey = readStringField(schema, "templateKey")
|
||||
summary.Version = readStringField(schema, "version")
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func buildCardContentBundleSummary(card postgres.Card) *ContentBundleSummaryView {
|
||||
if card.EventContentBundleID == nil {
|
||||
return nil
|
||||
}
|
||||
summary := &ContentBundleSummaryView{
|
||||
ContentBundleID: *card.EventContentBundleID,
|
||||
Name: card.EventContentBundleName,
|
||||
EntryURL: card.EventContentEntryURL,
|
||||
AssetRootURL: card.EventContentAssetRootURL,
|
||||
}
|
||||
if card.EventContentMetadataJSON != nil && strings.TrimSpace(*card.EventContentMetadataJSON) != "" {
|
||||
if metadata, err := decodeJSONObject(*card.EventContentMetadataJSON); err == nil {
|
||||
summary.BundleType = readStringField(metadata, "bundleType")
|
||||
summary.Version = readStringField(metadata, "version")
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
56
backend/internal/service/launch_rules.go
Normal file
56
backend/internal/service/launch_rules.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
|
||||
const (
|
||||
launchReadyReasonOK = "event is active and launchable"
|
||||
launchReadyReasonNotActive = "event is not active"
|
||||
launchReadyReasonReleaseMissing = "event does not have a published release"
|
||||
launchReadyReasonRuntimeMissing = "current published release is missing runtime binding"
|
||||
launchReadyReasonPresentationMissing = "current published release is missing presentation binding"
|
||||
launchReadyReasonContentMissing = "current published release is missing content bundle binding"
|
||||
)
|
||||
|
||||
func evaluateEventLaunchReadiness(event *postgres.Event) (bool, string) {
|
||||
if event == nil {
|
||||
return false, launchReadyReasonReleaseMissing
|
||||
}
|
||||
if event.Status != "active" {
|
||||
return false, launchReadyReasonNotActive
|
||||
}
|
||||
if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
|
||||
return false, launchReadyReasonReleaseMissing
|
||||
}
|
||||
if buildRuntimeSummaryFromEvent(event) == nil {
|
||||
return false, launchReadyReasonRuntimeMissing
|
||||
}
|
||||
if buildPresentationSummaryFromEvent(event) == nil {
|
||||
return false, launchReadyReasonPresentationMissing
|
||||
}
|
||||
if buildContentBundleSummaryFromEvent(event) == nil {
|
||||
return false, launchReadyReasonContentMissing
|
||||
}
|
||||
return true, launchReadyReasonOK
|
||||
}
|
||||
|
||||
func launchReadinessError(reason string) error {
|
||||
switch reason {
|
||||
case launchReadyReasonNotActive:
|
||||
return apperr.New(http.StatusConflict, "event_not_launchable", reason)
|
||||
case launchReadyReasonReleaseMissing:
|
||||
return apperr.New(http.StatusConflict, "event_release_missing", reason)
|
||||
case launchReadyReasonRuntimeMissing:
|
||||
return apperr.New(http.StatusConflict, "event_release_runtime_missing", reason)
|
||||
case launchReadyReasonPresentationMissing:
|
||||
return apperr.New(http.StatusConflict, "event_release_presentation_missing", reason)
|
||||
case launchReadyReasonContentMissing:
|
||||
return apperr.New(http.StatusConflict, "event_release_content_bundle_missing", reason)
|
||||
default:
|
||||
return apperr.New(http.StatusConflict, "event_not_launchable", reason)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user