| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- 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"`
- 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,
- 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
- }
|