package smashegg import ( "context" "fmt" "net/http" "strings" "time" "chatapp3-golang/internal/model" "gorm.io/gorm" ) // PageUserRecords returns a user's own smash egg draw records. func (s *Service) PageUserRecords(ctx context.Context, user AuthUser, cursor int, limit int) (*RecordPageResponse, error) { cursor, limit = normalizeRecordPage(cursor, limit) var total int64 query := s.db.WithContext(ctx).Model(&model.SmashEggDrawRecord{}). Where("sys_origin = ? AND user_id = ?", normalizeSysOrigin(user.SysOrigin), user.UserID) if err := query.Count(&total).Error; err != nil { return nil, err } var rows []model.SmashEggDrawRecord if err := query.Order("create_time desc, id desc"). Offset((cursor - 1) * limit). Limit(limit). Find(&rows).Error; err != nil { return nil, err } return &RecordPageResponse{ Records: drawRecordPayloads(rows), Total: total, Current: cursor, Size: limit, }, nil } // PageAdminRecords returns admin smash egg draw records. func (s *Service) PageAdminRecords(ctx context.Context, sysOrigin string, userID int64, status string, cursor int, limit int, startTime string, endTime string) (*RecordPageResponse, error) { cursor, limit = normalizeRecordPage(cursor, limit) filter, err := newAdminRecordFilter(sysOrigin, userID, status, startTime, endTime) if err != nil { return nil, err } query := s.adminRecordQuery(ctx, filter) var total int64 if err := query.Count(&total).Error; err != nil { return nil, err } var rows []model.SmashEggDrawRecord if err := s.adminRecordQuery(ctx, filter). Order("create_time desc, id desc"). Offset((cursor - 1) * limit). Limit(limit). Find(&rows).Error; err != nil { return nil, err } summary, err := s.adminRecordSummary(ctx, filter) if err != nil { return nil, err } return &RecordPageResponse{ Records: drawRecordPayloads(rows), Total: total, Current: cursor, Size: limit, TotalPaidGold: summary.TotalPaidGold, TotalRewardGold: summary.TotalRewardGold, ReturnRatioBasisPoints: summary.ReturnRatioBasisPoints, ReturnRatioPercent: basisPointsPercent(summary.ReturnRatioBasisPoints), StartTime: formatDateTime(filter.StartTime), EndTime: formatDateTime(filter.EndTime), }, nil } type adminRecordFilter struct { SysOrigin string UserID int64 Status string StartTime time.Time EndTime time.Time } type adminRecordSummary struct { TotalPaidGold int64 TotalRewardGold int64 ReturnRatioBasisPoints int } func newAdminRecordFilter(sysOrigin string, userID int64, status string, startRaw string, endRaw string) (adminRecordFilter, error) { start, end, err := parseAdminRecordTimeRange(startRaw, endRaw) if err != nil { return adminRecordFilter{}, err } return adminRecordFilter{ SysOrigin: normalizeSysOrigin(sysOrigin), UserID: userID, Status: strings.ToUpper(strings.TrimSpace(status)), StartTime: start, EndTime: end, }, nil } func parseAdminRecordTimeRange(startRaw string, endRaw string) (time.Time, time.Time, error) { startRaw = strings.TrimSpace(startRaw) endRaw = strings.TrimSpace(endRaw) now := time.Now() if startRaw == "" && endRaw == "" { return now.AddDate(0, 0, -7), now, nil } var start time.Time var end time.Time var err error if startRaw != "" { start, err = parseAdminRecordTime(startRaw, false) if err != nil { return time.Time{}, time.Time{}, err } } if endRaw != "" { end, err = parseAdminRecordTime(endRaw, true) if err != nil { return time.Time{}, time.Time{}, err } } if startRaw == "" { start = end.AddDate(0, 0, -7) } if endRaw == "" { end = start.AddDate(0, 0, 7) } if end.Before(start) { return time.Time{}, time.Time{}, NewAppError(http.StatusBadRequest, "bad_time_range", "endTime must be greater than startTime") } return start, end, nil } func parseAdminRecordTime(raw string, endOfDay bool) (time.Time, error) { for _, layout := range []string{ "2006-01-02 15:04:05", time.RFC3339, "2006-01-02T15:04:05.000Z07:00", "2006-01-02", } { var parsed time.Time var err error if strings.Contains(layout, "Z07:00") { parsed, err = time.Parse(layout, raw) } else { parsed, err = time.ParseInLocation(layout, raw, time.Local) } if err == nil { if layout == "2006-01-02" && endOfDay { parsed = parsed.Add(24*time.Hour - time.Nanosecond) } return parsed, nil } } return time.Time{}, NewAppError(http.StatusBadRequest, "bad_time", fmt.Sprintf("invalid time: %s", raw)) } func (s *Service) adminRecordQuery(ctx context.Context, filter adminRecordFilter) *gorm.DB { query := s.db.WithContext(ctx).Model(&model.SmashEggDrawRecord{}). Where("create_time >= ? AND create_time <= ?", filter.StartTime, filter.EndTime) if filter.SysOrigin != "" { query = query.Where("sys_origin = ?", filter.SysOrigin) } if filter.UserID > 0 { query = query.Where("user_id = ?", filter.UserID) } if filter.Status != "" { query = query.Where("status = ?", filter.Status) } return query } func (s *Service) adminRecordSummary(ctx context.Context, filter adminRecordFilter) (adminRecordSummary, error) { var summary adminRecordSummary if err := s.adminRecordQuery(ctx, filter). Select("COALESCE(SUM(reward_value_gold), 0)"). Scan(&summary.TotalRewardGold).Error; err != nil { return summary, err } drawPaid := s.adminRecordQuery(ctx, filter). Select("draw_no, MAX(paid_gold) AS paid_gold"). Group("draw_no") if err := s.db.WithContext(ctx). Table("(?) AS draw_paid", drawPaid). Select("COALESCE(SUM(paid_gold), 0)"). Scan(&summary.TotalPaidGold).Error; err != nil { return summary, err } if summary.TotalPaidGold > 0 { summary.ReturnRatioBasisPoints = int((summary.TotalRewardGold*10000 + summary.TotalPaidGold/2) / summary.TotalPaidGold) } return summary, nil } // ListRecentNotices returns recent successful prize notices. func (s *Service) ListRecentNotices(ctx context.Context, sysOrigin string, limit int) ([]DrawRecordPayload, error) { if limit <= 0 { limit = 20 } if limit > 100 { limit = 100 } var rows []model.SmashEggDrawRecord query := s.db.WithContext(ctx).Where("status = ?", drawStatusSuccess) if normalized := normalizeSysOrigin(sysOrigin); normalized != "" { query = query.Where("sys_origin = ?", normalized) } if err := query.Order("create_time desc, id desc").Limit(limit).Find(&rows).Error; err != nil { return nil, err } return drawRecordPayloads(rows), nil } // PageDayRank returns today's top users by reward value. func (s *Service) PageDayRank(ctx context.Context, sysOrigin string, cursor int, limit int) ([]DayRankPayload, error) { cursor, limit = normalizeRecordPage(cursor, limit) normalized := normalizeSysOrigin(sysOrigin) now := time.Now() start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) end := start.Add(24 * time.Hour) type row struct { UserID int64 Score int64 } var rows []row query := s.db.WithContext(ctx).Model(&model.SmashEggDrawRecord{}). Select("user_id, COALESCE(SUM(reward_value_gold), 0) AS score"). Where("status = ? AND create_time >= ? AND create_time < ?", drawStatusSuccess, start, end) if normalized != "" { query = query.Where("sys_origin = ?", normalized) } if err := query.Group("user_id"). Order("score desc, user_id asc"). Offset((cursor - 1) * limit). Limit(limit). Scan(&rows).Error; err != nil { return nil, err } result := make([]DayRankPayload, 0, len(rows)) for _, item := range rows { result = append(result, DayRankPayload{ UserID: item.UserID, Score: item.Score, }) } return result, nil } type DayRankPayload struct { UserID int64 `json:"userId"` Score int64 `json:"score"` } func drawRecordPayloads(rows []model.SmashEggDrawRecord) []DrawRecordPayload { records := make([]DrawRecordPayload, 0, len(rows)) for _, row := range rows { records = append(records, drawRecordPayload(row)) } return records } func drawRecordPayload(row model.SmashEggDrawRecord) DrawRecordPayload { return DrawRecordPayload{ ID: row.ID, DrawNo: row.DrawNo, EventID: row.EventID, UserID: row.UserID, SysOrigin: row.SysOrigin, PoolType: normalizePoolType(row.PoolType), DrawOptionID: row.DrawOptionID, DrawTimes: row.DrawTimes, PaidGold: row.PaidGold, RewardType: row.RewardType, ResourceID: ResourceID(int64PtrValue(row.RewardGroupID)), ResourceType: row.ResourceType, ResourceName: row.RewardGroupName, ResourceURL: row.ResourceURL, RewardGroupID: row.RewardGroupID, RewardGroupName: row.RewardGroupName, CoverURL: row.CoverURL, AnimationURL: row.AnimationURL, DurationDays: row.DurationDays, DisplayGoldAmount: row.DisplayGoldAmount, GoldAmount: row.GoldAmount, RewardValueGold: row.RewardValueGold, Probability: row.Probability, Status: row.Status, ErrorMessage: row.ErrorMessage, CreateTime: formatDateTime(row.CreateTime), UpdateTime: formatDateTime(row.UpdateTime), } }