| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- 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
- }
|