315 lines
9.7 KiB
Go
315 lines
9.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"cmr-backend/internal/apperr"
|
|
"cmr-backend/internal/store/postgres"
|
|
)
|
|
|
|
type HomeService struct {
|
|
store *postgres.Store
|
|
}
|
|
|
|
type ListCardsInput struct {
|
|
ChannelCode string
|
|
ChannelType string
|
|
PlatformAppID string
|
|
TenantCode string
|
|
Slot string
|
|
Limit int
|
|
}
|
|
|
|
type CardResult 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"`
|
|
ShowInEventList bool `json:"showInEventList"`
|
|
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"`
|
|
}
|
|
|
|
type HomeResult struct {
|
|
Tenant struct {
|
|
ID string `json:"id"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
} `json:"tenant"`
|
|
Channel struct {
|
|
ID string `json:"id"`
|
|
Code string `json:"code"`
|
|
Type string `json:"type"`
|
|
PlatformAppID *string `json:"platformAppId,omitempty"`
|
|
DisplayName string `json:"displayName"`
|
|
Status string `json:"status"`
|
|
IsDefault bool `json:"isDefault"`
|
|
} `json:"channel"`
|
|
Cards []CardResult `json:"cards"`
|
|
}
|
|
|
|
func NewHomeService(store *postgres.Store) *HomeService {
|
|
return &HomeService{store: store}
|
|
}
|
|
|
|
func (s *HomeService) ListCards(ctx context.Context, input ListCardsInput) ([]CardResult, error) {
|
|
entry, err := s.resolveEntry(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cards, err := s.store.ListCardsForEntry(ctx, entry.TenantID, &entry.ID, normalizeSlot(input.Slot), time.Now().UTC(), input.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mapCards(cards), nil
|
|
}
|
|
|
|
func (s *HomeService) GetHome(ctx context.Context, input ListCardsInput) (*HomeResult, error) {
|
|
entry, err := s.resolveEntry(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cards, err := s.store.ListCardsForEntry(ctx, entry.TenantID, &entry.ID, normalizeSlot(input.Slot), time.Now().UTC(), input.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := &HomeResult{
|
|
Cards: mapCards(cards),
|
|
}
|
|
result.Tenant.ID = entry.TenantID
|
|
result.Tenant.Code = entry.TenantCode
|
|
result.Tenant.Name = entry.TenantName
|
|
result.Channel.ID = entry.ID
|
|
result.Channel.Code = entry.ChannelCode
|
|
result.Channel.Type = entry.ChannelType
|
|
result.Channel.PlatformAppID = entry.PlatformAppID
|
|
result.Channel.DisplayName = entry.DisplayName
|
|
result.Channel.Status = entry.Status
|
|
result.Channel.IsDefault = entry.IsDefault
|
|
return result, nil
|
|
}
|
|
|
|
func (s *HomeService) resolveEntry(ctx context.Context, input ListCardsInput) (*postgres.EntryChannel, error) {
|
|
entry, err := s.store.FindEntryChannel(ctx, postgres.FindEntryChannelParams{
|
|
ChannelCode: strings.TrimSpace(input.ChannelCode),
|
|
ChannelType: strings.TrimSpace(input.ChannelType),
|
|
PlatformAppID: strings.TrimSpace(input.PlatformAppID),
|
|
TenantCode: strings.TrimSpace(input.TenantCode),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "entry_channel_not_found", "entry channel not found")
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
func normalizeSlot(slot string) string {
|
|
slot = strings.TrimSpace(slot)
|
|
if slot == "" {
|
|
return "home_primary"
|
|
}
|
|
return slot
|
|
}
|
|
|
|
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: 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,
|
|
ShowInEventList: card.ShowInEventList,
|
|
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
|
|
}
|
|
if card.EventDisplayName != nil {
|
|
item.Event.DisplayName = *card.EventDisplayName
|
|
}
|
|
}
|
|
results = append(results, item)
|
|
}
|
|
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
|
|
}
|