package gameking import ( "context" "errors" "net/http" "strings" "time" "chatapp3-golang/internal/model" "gorm.io/gorm" ) // Me 返回机会账本和活动默认榜单中的当前名次。 func (s *Service) Me(ctx context.Context, user AuthUser, activityID int64) (*UserStateResponse, error) { if user.UserID <= 0 { return nil, NewAppError(http.StatusUnauthorized, "invalid_user", "authenticated user is required") } activity, err := s.resolveAppActivity(ctx, normalizeSysOrigin(user.SysOrigin), activityID, time.Now()) if err != nil { return nil, err } var row model.YumiGameKingUser err = s.db.WithContext(ctx).Where("activity_id = ? AND user_id = ?", activity.ID, user.UserID).First(&row).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } if errors.Is(err, gorm.ErrRecordNotFound) { row = model.YumiGameKingUser{ActivityID: activity.ID, UserID: user.UserID} } earned := row.TotalConsumed / activity.CoinPerDraw available := earned - row.UsedChances if available < 0 { available = 0 } remainder := row.TotalConsumed % activity.CoinPerDraw next := activity.CoinPerDraw if remainder > 0 { next -= remainder } var rank *int if row.TotalConsumed > 0 { value, rankErr := s.userRank(ctx, activity, user.UserID, activity.RankingPeriod, time.Now()) if rankErr != nil { return nil, rankErr } rank = value } return &UserStateResponse{ ActivityID: activity.ID, TotalConsumed: row.TotalConsumed, EarnedChances: earned, UsedChances: row.UsedChances, AvailableChances: available, NextChanceRemaining: next, Rank: rank, }, nil } // Ranking 返回活动时区日榜或总榜。VICTORIOUS 是旧 H5 兼容维度;当前活动没有胜利事件, // 因此显式请求时返回空榜而不是 400,supportedRankingTypes 会让新 H5 隐藏该页签。 func (s *Service) Ranking(ctx context.Context, user AuthUser, activityID int64, rankingType, period string, limit int) (*RankingResponse, error) { activity, err := s.resolveAppActivity(ctx, normalizeSysOrigin(user.SysOrigin), activityID, time.Now()) if err != nil { return nil, err } rankingType = strings.ToUpper(strings.TrimSpace(rankingType)) if rankingType == "" { rankingType = activity.RankingType } period = strings.ToUpper(strings.TrimSpace(period)) if period == "" { period = activity.RankingPeriod } if period != "DAILY" && period != rankingPeriodOverall { return nil, NewAppError(http.StatusBadRequest, "invalid_ranking_period", "period must be DAILY or OVERALL") } if rankingType != rankingTypeTycoon && rankingType != "VICTORIOUS" { return nil, NewAppError(http.StatusBadRequest, "invalid_ranking_type", "rankingType must be TYCOON or VICTORIOUS") } if limit <= 0 { limit = defaultRankingLimit } if limit > maxRankingLimit { limit = maxRankingLimit } response := &RankingResponse{RankingType: rankingType, Period: period, Entries: []RankEntryView{}} if rankingType == "VICTORIOUS" { return response, nil } now := time.Now() if period == "DAILY" { _, location, timezoneErr := normalizeTimezone(activity.Timezone) if timezoneErr != nil { return nil, timezoneErr } statDate := dateKey(now, location) response.StatDate = now.In(location).Format("2006-01-02") var rows []model.YumiGameKingUserDaily if err := s.db.WithContext(ctx). Where("activity_id = ? AND stat_date = ? AND total_consumed > 0", activity.ID, statDate). Order("total_consumed DESC, consume_reached_time ASC, user_id ASC").Limit(limit).Find(&rows).Error; err != nil { return nil, err } for index, row := range rows { entry := RankEntryView{Rank: index + 1, UserID: row.UserID, Score: row.TotalConsumed, TotalConsumed: row.TotalConsumed, Me: row.UserID == user.UserID} response.Entries = append(response.Entries, entry) } } else { var rows []model.YumiGameKingUser if err := s.db.WithContext(ctx). Where("activity_id = ? AND total_consumed > 0", activity.ID). Order("total_consumed DESC, consume_reached_time ASC, user_id ASC").Limit(limit).Find(&rows).Error; err != nil { return nil, err } for index, row := range rows { entry := RankEntryView{Rank: index + 1, UserID: row.UserID, Score: row.TotalConsumed, TotalConsumed: row.TotalConsumed, Me: row.UserID == user.UserID} response.Entries = append(response.Entries, entry) } } s.hydrateRankProfiles(ctx, response.Entries) if user.UserID > 0 { response.My, err = s.myRankEntry(ctx, activity, user.UserID, period, now) if err != nil { return nil, err } s.hydrateMyRankProfile(ctx, response.My, response.Entries) } return response, nil } func (s *Service) myRankEntry(ctx context.Context, activity model.YumiGameKingActivity, userID int64, period string, now time.Time) (*RankEntryView, error) { rank, err := s.userRank(ctx, activity, userID, period, now) if err != nil || rank == nil { return nil, err } entry := &RankEntryView{Rank: *rank, UserID: userID, Me: true} if period == "DAILY" { _, location, _ := normalizeTimezone(activity.Timezone) var row model.YumiGameKingUserDaily if err := s.db.WithContext(ctx).Where("activity_id = ? AND stat_date = ? AND user_id = ?", activity.ID, dateKey(now, location), userID).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } entry.Score, entry.TotalConsumed = row.TotalConsumed, row.TotalConsumed } else { var row model.YumiGameKingUser if err := s.db.WithContext(ctx).Where("activity_id = ? AND user_id = ?", activity.ID, userID).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } entry.Score, entry.TotalConsumed = row.TotalConsumed, row.TotalConsumed } return entry, nil } func (s *Service) userRank(ctx context.Context, activity model.YumiGameKingActivity, userID int64, period string, now time.Time) (*int, error) { if period == "DAILY" { _, location, _ := normalizeTimezone(activity.Timezone) statDate := dateKey(now, location) var row model.YumiGameKingUserDaily if err := s.db.WithContext(ctx).Where("activity_id = ? AND stat_date = ? AND user_id = ?", activity.ID, statDate, userID).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } var ahead int64 if err := s.db.WithContext(ctx).Model(&model.YumiGameKingUserDaily{}). Where("activity_id = ? AND stat_date = ? AND (total_consumed > ? OR (total_consumed = ? AND (consume_reached_time < ? OR (consume_reached_time = ? AND user_id < ?))))", activity.ID, statDate, row.TotalConsumed, row.TotalConsumed, row.ConsumeReachedTime, row.ConsumeReachedTime, userID). Count(&ahead).Error; err != nil { return nil, err } value := int(ahead) + 1 return &value, nil } var row model.YumiGameKingUser if err := s.db.WithContext(ctx).Where("activity_id = ? AND user_id = ?", activity.ID, userID).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } var ahead int64 if err := s.db.WithContext(ctx).Model(&model.YumiGameKingUser{}). Where("activity_id = ? AND (total_consumed > ? OR (total_consumed = ? AND (consume_reached_time < ? OR (consume_reached_time = ? AND user_id < ?))))", activity.ID, row.TotalConsumed, row.TotalConsumed, row.ConsumeReachedTime, row.ConsumeReachedTime, userID). Count(&ahead).Error; err != nil { return nil, err } value := int(ahead) + 1 return &value, nil } func (s *Service) hydrateRankProfiles(ctx context.Context, entries []RankEntryView) { if s.gateway == nil || len(entries) == 0 { return } seen := make(map[int64]struct{}, len(entries)) userIDs := make([]int64, 0, len(entries)) for _, entry := range entries { if entry.UserID <= 0 { continue } if _, exists := seen[entry.UserID]; exists { continue } seen[entry.UserID] = struct{}{} userIDs = append(userIDs, entry.UserID) } profiles, err := s.gateway.MapUserProfiles(ctx, userIDs) if err != nil { // 排行主数据来自本库;资料服务失败时退化为 userId,不阻断榜单。 return } for index := range entries { if profile, exists := profiles[entries[index].UserID]; exists { integrationProfile(&entries[index], profile) } } } func (s *Service) hydrateMyRankProfile(ctx context.Context, my *RankEntryView, entries []RankEntryView) { if my == nil { return } for _, entry := range entries { if entry.UserID == my.UserID { my.Account, my.Nickname, my.Avatar = entry.Account, entry.Nickname, entry.Avatar return } } values := []RankEntryView{*my} s.hydrateRankProfiles(ctx, values) *my = values[0] } func (s *Service) DrawRecords(ctx context.Context, user AuthUser, activityID int64, limit int) ([]DrawRecordView, error) { if user.UserID <= 0 { return nil, NewAppError(http.StatusUnauthorized, "invalid_user", "authenticated user is required") } activity, err := s.resolveAppActivity(ctx, normalizeSysOrigin(user.SysOrigin), activityID, time.Now()) if err != nil { return nil, err } return s.listDrawRecords(ctx, activity.ID, user.UserID, "", limit, false) } func (s *Service) ManageDrawRecords(ctx context.Context, activityID int64, status string, limit int) ([]DrawRecordView, error) { if _, err := s.loadActivity(ctx, activityID); err != nil { return nil, err } return s.listDrawRecords(ctx, activityID, 0, status, limit, true) } func (s *Service) listDrawRecords(ctx context.Context, activityID, userID int64, status string, limit int, includeDelivery bool) ([]DrawRecordView, error) { if limit <= 0 { limit = defaultRecordLimit } if limit > maxRecordLimit { limit = maxRecordLimit } query := s.db.WithContext(ctx).Where("activity_id = ?", activityID) if userID > 0 { query = query.Where("user_id = ?", userID) } if strings.TrimSpace(status) != "" { query = query.Where("delivery_status = ?", strings.ToUpper(strings.TrimSpace(status))) } var rows []model.YumiGameKingDrawRecord if err := query.Order("id DESC").Limit(limit).Find(&rows).Error; err != nil { return nil, err } result := make([]DrawRecordView, 0, len(rows)) for _, row := range rows { view := drawRecordView(row) if includeDelivery { var deliveryErr error view.DeliveryItems, deliveryErr = s.deliveryItems(ctx, OwnerDraw, row.ID) if deliveryErr != nil { return nil, deliveryErr } } result = append(result, view) } return result, nil } func (s *Service) SettlementRecords(ctx context.Context, activityID int64) ([]SettlementRecordView, error) { if _, err := s.loadActivity(ctx, activityID); err != nil { return nil, err } var rows []model.YumiGameKingSettlementRecord if err := s.db.WithContext(ctx).Where("activity_id = ?", activityID).Order("rank_no ASC").Find(&rows).Error; err != nil { return nil, err } result := make([]SettlementRecordView, 0, len(rows)) for _, row := range rows { view := settlementRecordView(row) var deliveryErr error view.DeliveryItems, deliveryErr = s.deliveryItems(ctx, OwnerSettlement, row.ID) if deliveryErr != nil { return nil, deliveryErr } result = append(result, view) } return result, nil } func drawRecordView(row model.YumiGameKingDrawRecord) DrawRecordView { return DrawRecordView{ ID: row.ID, ActivityID: row.ActivityID, UserID: row.UserID, RequestID: row.RequestID, PrizeID: row.PrizeID, PrizeName: row.PrizeName, PrizeImage: row.PrizeImage, ResourceGroupID: row.ResourceGroupID, DeliveryStatus: row.DeliveryStatus, RetryCount: row.RetryCount, FailureReason: row.FailureReason, DrawTime: unixMilli(row.DrawTime), DeliverTime: unixMilliPtr(row.DeliverTime), } } func settlementRecordView(row model.YumiGameKingSettlementRecord) SettlementRecordView { return SettlementRecordView{ ID: row.ID, ActivityID: row.ActivityID, UserID: row.UserID, Rank: row.RankNo, TotalConsumed: row.TotalConsumed, RankRewardID: row.RankRewardID, ResourceGroupID: row.ResourceGroupID, BusinessNo: row.BusinessNo, DeliveryStatus: row.DeliveryStatus, RetryCount: row.RetryCount, FailureReason: row.FailureReason, CreateTime: unixMilli(row.CreateTime), DeliverTime: unixMilliPtr(row.DeliverTime), } }