2026-07-18 15:32:58 +08:00

331 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 兼容维度;当前活动没有胜利事件,
// 因此显式请求时返回空榜而不是 400supportedRankingTypes 会让新 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),
}
}