301 lines
9.8 KiB
Go
301 lines
9.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"cmr-backend/internal/apperr"
|
|
"cmr-backend/internal/store/postgres"
|
|
)
|
|
|
|
type MapExperienceService struct {
|
|
store *postgres.Store
|
|
}
|
|
|
|
type ListExperienceMapsInput struct {
|
|
Limit int
|
|
}
|
|
|
|
type ExperienceMapSummary struct {
|
|
PlaceID string `json:"placeId"`
|
|
PlaceName string `json:"placeName"`
|
|
MapID string `json:"mapId"`
|
|
MapName string `json:"mapName"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
DefaultExperienceCount int `json:"defaultExperienceCount"`
|
|
DefaultExperienceEventIDs []string `json:"defaultExperienceEventIds"`
|
|
}
|
|
|
|
type ExperienceMapDetail struct {
|
|
PlaceID string `json:"placeId"`
|
|
PlaceName string `json:"placeName"`
|
|
MapID string `json:"mapId"`
|
|
MapName string `json:"mapName"`
|
|
CoverURL *string `json:"coverUrl,omitempty"`
|
|
Summary *string `json:"summary,omitempty"`
|
|
TileBaseURL *string `json:"tileBaseUrl,omitempty"`
|
|
TileMetaURL *string `json:"tileMetaUrl,omitempty"`
|
|
DefaultExperienceCount int `json:"defaultExperienceCount"`
|
|
DefaultExperiences []ExperienceEventSummary `json:"defaultExperiences"`
|
|
}
|
|
|
|
type ExperienceEventSummary struct {
|
|
EventID string `json:"eventId"`
|
|
Title string `json:"title"`
|
|
Subtitle *string `json:"subtitle,omitempty"`
|
|
EventType *string `json:"eventType,omitempty"`
|
|
Status string `json:"status"`
|
|
StatusCode string `json:"statusCode"`
|
|
CTAText string `json:"ctaText"`
|
|
IsDefaultExperience bool `json:"isDefaultExperience"`
|
|
ShowInEventList bool `json:"showInEventList"`
|
|
CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
|
|
CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
|
|
}
|
|
|
|
func NewMapExperienceService(store *postgres.Store) *MapExperienceService {
|
|
return &MapExperienceService{store: store}
|
|
}
|
|
|
|
func (s *MapExperienceService) ListMaps(ctx context.Context, input ListExperienceMapsInput) ([]ExperienceMapSummary, error) {
|
|
rows, err := s.store.ListMapExperienceRows(ctx, input.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mapExperienceSummaries(rows), nil
|
|
}
|
|
|
|
func (s *MapExperienceService) GetMapDetail(ctx context.Context, mapPublicID string) (*ExperienceMapDetail, error) {
|
|
mapPublicID = strings.TrimSpace(mapPublicID)
|
|
if mapPublicID == "" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_map_id", "map id is required")
|
|
}
|
|
rows, err := s.store.ListMapExperienceRowsByMapPublicID(ctx, mapPublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil, apperr.New(http.StatusNotFound, "map_not_found", "map not found")
|
|
}
|
|
return buildMapExperienceDetail(rows), nil
|
|
}
|
|
|
|
func mapExperienceSummaries(rows []postgres.MapExperienceRow) []ExperienceMapSummary {
|
|
ordered := make([]string, 0, len(rows))
|
|
index := make(map[string]*ExperienceMapSummary)
|
|
for _, row := range rows {
|
|
item, ok := index[row.MapAssetPublicID]
|
|
if !ok {
|
|
summary := &ExperienceMapSummary{
|
|
PlaceID: row.PlacePublicID,
|
|
PlaceName: row.PlaceName,
|
|
MapID: row.MapAssetPublicID,
|
|
MapName: row.MapAssetName,
|
|
CoverURL: row.MapCoverURL,
|
|
Summary: normalizeOptionalText(row.MapSummary),
|
|
DefaultExperienceEventIDs: []string{},
|
|
}
|
|
index[row.MapAssetPublicID] = summary
|
|
ordered = append(ordered, row.MapAssetPublicID)
|
|
item = summary
|
|
}
|
|
if row.EventPublicID != nil && row.EventIsDefaultExperience {
|
|
if !containsString(item.DefaultExperienceEventIDs, *row.EventPublicID) {
|
|
item.DefaultExperienceEventIDs = append(item.DefaultExperienceEventIDs, *row.EventPublicID)
|
|
item.DefaultExperienceCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
result := make([]ExperienceMapSummary, 0, len(ordered))
|
|
for _, id := range ordered {
|
|
result = append(result, *index[id])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func buildMapExperienceDetail(rows []postgres.MapExperienceRow) *ExperienceMapDetail {
|
|
first := rows[0]
|
|
result := &ExperienceMapDetail{
|
|
PlaceID: first.PlacePublicID,
|
|
PlaceName: first.PlaceName,
|
|
MapID: first.MapAssetPublicID,
|
|
MapName: first.MapAssetName,
|
|
CoverURL: first.MapCoverURL,
|
|
Summary: normalizeOptionalText(first.MapSummary),
|
|
TileBaseURL: first.TileBaseURL,
|
|
TileMetaURL: first.TileMetaURL,
|
|
DefaultExperiences: make([]ExperienceEventSummary, 0, 4),
|
|
}
|
|
seen := make(map[string]struct{})
|
|
for _, row := range rows {
|
|
if row.EventPublicID == nil || !row.EventIsDefaultExperience {
|
|
continue
|
|
}
|
|
if _, ok := seen[*row.EventPublicID]; ok {
|
|
continue
|
|
}
|
|
seen[*row.EventPublicID] = struct{}{}
|
|
result.DefaultExperiences = append(result.DefaultExperiences, buildExperienceEventSummary(row))
|
|
}
|
|
result.DefaultExperienceCount = len(result.DefaultExperiences)
|
|
return result
|
|
}
|
|
|
|
func buildExperienceEventSummary(row postgres.MapExperienceRow) ExperienceEventSummary {
|
|
statusCode, statusText := deriveExperienceEventStatus(row)
|
|
return ExperienceEventSummary{
|
|
EventID: valueOrEmpty(row.EventPublicID),
|
|
Title: fallbackText(row.EventDisplayName, "未命名活动"),
|
|
Subtitle: normalizeOptionalText(row.EventSummary),
|
|
EventType: deriveExperienceEventType(row),
|
|
Status: statusText,
|
|
StatusCode: statusCode,
|
|
CTAText: deriveExperienceEventCTA(statusCode, row.EventIsDefaultExperience),
|
|
IsDefaultExperience: row.EventIsDefaultExperience,
|
|
ShowInEventList: row.EventShowInEventList,
|
|
CurrentPresentation: buildPresentationSummaryFromMapExperienceRow(row),
|
|
CurrentContentBundle: buildContentBundleSummaryFromMapExperienceRow(row),
|
|
}
|
|
}
|
|
|
|
func deriveExperienceEventStatus(row postgres.MapExperienceRow) (string, string) {
|
|
if row.EventStatus == nil {
|
|
return "pending", "状态待确认"
|
|
}
|
|
switch strings.TrimSpace(*row.EventStatus) {
|
|
case "active":
|
|
if row.EventReleasePayloadJSON == nil || strings.TrimSpace(*row.EventReleasePayloadJSON) == "" {
|
|
return "upcoming", "即将开始"
|
|
}
|
|
if row.EventPresentationID == nil || row.EventContentBundleID == nil {
|
|
return "upcoming", "即将开始"
|
|
}
|
|
return "running", "进行中"
|
|
case "archived", "disabled", "inactive":
|
|
return "ended", "已结束"
|
|
default:
|
|
return "pending", "状态待确认"
|
|
}
|
|
}
|
|
|
|
func deriveExperienceEventCTA(statusCode string, isDefault bool) string {
|
|
if isDefault {
|
|
return "进入体验"
|
|
}
|
|
switch statusCode {
|
|
case "running":
|
|
return "进入活动"
|
|
case "ended":
|
|
return "查看回顾"
|
|
default:
|
|
return "查看详情"
|
|
}
|
|
}
|
|
|
|
func deriveExperienceEventType(row postgres.MapExperienceRow) *string {
|
|
if row.EventReleasePayloadJSON != nil {
|
|
payload, err := decodeJSONObject(*row.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(row.EventReleasePayloadJSON); plan.AssignmentMode != nil && *plan.AssignmentMode == AssignmentModeManual {
|
|
text := "多赛道"
|
|
return &text
|
|
}
|
|
}
|
|
}
|
|
if row.EventIsDefaultExperience {
|
|
text := "体验活动"
|
|
return &text
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildPresentationSummaryFromMapExperienceRow(row postgres.MapExperienceRow) *PresentationSummaryView {
|
|
if row.EventPresentationID == nil {
|
|
return nil
|
|
}
|
|
summary := &PresentationSummaryView{
|
|
PresentationID: *row.EventPresentationID,
|
|
Name: row.EventPresentationName,
|
|
PresentationType: row.EventPresentationType,
|
|
}
|
|
if row.EventPresentationSchema != nil && strings.TrimSpace(*row.EventPresentationSchema) != "" {
|
|
if schema, err := decodeJSONObject(*row.EventPresentationSchema); err == nil {
|
|
summary.TemplateKey = readStringField(schema, "templateKey")
|
|
summary.Version = readStringField(schema, "version")
|
|
}
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func buildContentBundleSummaryFromMapExperienceRow(row postgres.MapExperienceRow) *ContentBundleSummaryView {
|
|
if row.EventContentBundleID == nil {
|
|
return nil
|
|
}
|
|
summary := &ContentBundleSummaryView{
|
|
ContentBundleID: *row.EventContentBundleID,
|
|
Name: row.EventContentBundleName,
|
|
EntryURL: row.EventContentEntryURL,
|
|
AssetRootURL: row.EventContentAssetRootURL,
|
|
}
|
|
if row.EventContentMetadataJSON != nil && strings.TrimSpace(*row.EventContentMetadataJSON) != "" {
|
|
if metadata, err := decodeJSONObject(*row.EventContentMetadataJSON); err == nil {
|
|
summary.BundleType = readStringField(metadata, "bundleType")
|
|
summary.Version = readStringField(metadata, "version")
|
|
}
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func normalizeOptionalText(value *string) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(*value)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func fallbackText(value *string, fallback string) string {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
trimmed := strings.TrimSpace(*value)
|
|
if trimmed == "" {
|
|
return fallback
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
func valueOrEmpty(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
for _, item := range values {
|
|
if item == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|