package wheel import ( "context" "fmt" "net/http" "strings" "time" "chatapp3-golang/internal/model" "gorm.io/gorm" ) // PageRecords returns draw records without admin-only enrichment. func (s *Service) PageRecords( ctx context.Context, sysOrigin string, category string, userID int64, status string, cursor int, limit int, ) (*RecordPageResponse, error) { sysOrigin = normalizeSysOrigin(sysOrigin) if sysOrigin == "" { return nil, NewAppError(400, "bad_request", "sysOrigin is required") } cursor, limit = normalizeRecordPage(cursor, limit) query := s.db.WithContext(ctx).Model(&model.WheelDrawRecord{}). Where("sys_origin = ?", sysOrigin) if strings.TrimSpace(category) != "" { query = query.Where("category = ?", normalizeCategory(category)) } if userID > 0 { query = query.Where("user_id = ?", userID) } if status != "" { query = query.Where("status = ?", status) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, err } var rows []model.WheelDrawRecord if err := query.Order("create_time desc, id desc"). Offset((cursor - 1) * limit). Limit(limit). Find(&rows).Error; err != nil { return nil, err } records := make([]DrawRecordPayload, 0, len(rows)) for _, row := range rows { records = append(records, drawRecordPayload(row)) } return &RecordPageResponse{ Records: records, Total: total, Current: cursor, Size: limit, }, nil } // PageAdminRecords returns admin draw records with user info and range summary. func (s *Service) PageAdminRecords( ctx context.Context, sysOrigin string, category string, userID int64, status string, cursor int, limit int, startTime string, endTime string, ) (*RecordPageResponse, error) { cursor, limit = normalizeRecordPage(cursor, limit) filter, err := newWheelAdminRecordFilter(sysOrigin, category, userID, status, startTime, endTime) if err != nil { return nil, err } if filter.SysOrigin == "" { return nil, NewAppError(400, "bad_request", "sysOrigin is required") } query := s.adminRecordQuery(ctx, filter) var total int64 if err := query.Count(&total).Error; err != nil { return nil, err } var rows []model.WheelDrawRecord 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 } userInfos, err := s.adminRecordUserInfos(ctx, rows) if err != nil { return nil, err } userDrawTimes, err := s.adminRecordUserDrawTimes(ctx, filter, rows) if err != nil { return nil, err } records := make([]DrawRecordPayload, 0, len(rows)) for _, row := range rows { payload := drawRecordPayload(row) if user, ok := userInfos[row.UserID]; ok { payload.UserShortID = user.Account payload.UserAvatar = user.UserAvatar payload.UserNickname = user.UserNickname } payload.UserTotalDrawTimes = userDrawTimes[row.UserID] records = append(records, payload) } return &RecordPageResponse{ Records: records, Total: total, Current: cursor, Size: limit, TotalPaidGold: summary.TotalPaidGold, TotalRewardGold: summary.TotalRewardGold, TotalDrawTimes: summary.TotalDrawTimes, ParticipantCount: summary.ParticipantCount, StartTime: formatDateTime(filter.StartTime), EndTime: formatDateTime(filter.EndTime), }, nil } // PageUserHistory returns app draw history for the authenticated user. func (s *Service) PageUserHistory(ctx context.Context, user AuthUser, cursor int, limit int) (*RecordPageResponse, error) { return s.PageRecords(ctx, user.SysOrigin, "", user.UserID, drawStatusSuccess, cursor, limit) } // Hints returns recent successful rewards for ticker displays. func (s *Service) Hints(ctx context.Context, user AuthUser, category string, limit int) ([]DrawRewardPayload, error) { if limit <= 0 { limit = 20 } if limit > 50 { limit = 50 } query := s.db.WithContext(ctx). Where("sys_origin = ? AND status = ?", normalizeSysOrigin(user.SysOrigin), drawStatusSuccess). Order("create_time desc, id desc"). Limit(limit) if strings.TrimSpace(category) != "" { query = query.Where("category = ?", normalizeCategory(category)) } var rows []model.WheelDrawRecord if err := query.Find(&rows).Error; err != nil { return nil, err } rewards := make([]DrawRewardPayload, 0, len(rows)) for _, row := range rows { name := row.ResourceName if name == "" && row.RewardType == rewardTypeGold { name = "金币" } rewards = append(rewards, DrawRewardPayload{ RewardType: row.RewardType, ResourceID: resourceIDValue(row.ResourceID), ResourceType: row.ResourceType, ResourceName: row.ResourceName, ResourceURL: row.ResourceURL, CoverURL: row.CoverURL, DurationDays: row.DurationDays, DisplayGoldAmount: row.DisplayGoldAmount, GoldAmount: row.GoldAmount, Probability: row.Probability, TotalLimit: 0, UserLimit: 0, IssuedCount: 0, Count: 1, Name: name, Value: displayValueForRecord(row), }) } return rewards, nil } // WalletBalances returns current gold balance in the shape needed by the H5 compatibility API. func (s *Service) WalletBalances(ctx context.Context, user AuthUser) (int64, error) { if s.java == nil { return 0, nil } balances, err := s.java.MapGoldBalance(ctx, []int64{user.UserID}) if err != nil { return 0, err } return balances[user.UserID], nil } func drawRecordPayload(row model.WheelDrawRecord) DrawRecordPayload { return DrawRecordPayload{ ID: row.ID, DrawNo: row.DrawNo, EventID: row.EventID, UserID: row.UserID, SysOrigin: row.SysOrigin, Category: row.Category, DrawTimes: row.DrawTimes, PaidGold: row.PaidGold, RewardConfigID: row.RewardConfigID, RewardType: row.RewardType, ResourceID: resourceIDValue(row.ResourceID), ResourceType: row.ResourceType, ResourceName: row.ResourceName, ResourceURL: row.ResourceURL, CoverURL: row.CoverURL, DurationDays: row.DurationDays, DisplayGoldAmount: row.DisplayGoldAmount, GoldAmount: row.GoldAmount, Probability: row.Probability, Status: row.Status, RetryCount: row.RetryCount, ErrorMessage: row.ErrorMessage, GrantTime: formatOptionalDateTime(row.GrantTime), CreateTime: formatDateTime(row.CreateTime), UpdateTime: formatDateTime(row.UpdateTime), } } func displayValueForRecord(row model.WheelDrawRecord) int64 { if normalizeRewardType(row.RewardType) == rewardTypeGold { return row.GoldAmount } return row.DisplayGoldAmount } type wheelAdminRecordFilter struct { SysOrigin string Category string UserID int64 Status string StartTime time.Time EndTime time.Time } type wheelAdminRecordSummary struct { TotalPaidGold int64 TotalRewardGold int64 TotalDrawTimes int64 ParticipantCount int64 } func newWheelAdminRecordFilter(sysOrigin string, category string, userID int64, status string, startRaw string, endRaw string) (wheelAdminRecordFilter, error) { start, end, err := parseWheelAdminRecordTimeRange(startRaw, endRaw) if err != nil { return wheelAdminRecordFilter{}, err } return wheelAdminRecordFilter{ SysOrigin: normalizeSysOrigin(sysOrigin), Category: strings.ToUpper(strings.TrimSpace(category)), UserID: userID, Status: strings.ToUpper(strings.TrimSpace(status)), StartTime: start, EndTime: end, }, nil } func parseWheelAdminRecordTimeRange(startRaw string, endRaw string) (time.Time, time.Time, error) { startRaw = strings.TrimSpace(startRaw) endRaw = strings.TrimSpace(endRaw) now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) if startRaw == "" && endRaw == "" { return todayStart, todayStart.Add(24*time.Hour - time.Nanosecond), nil } var start time.Time var end time.Time var err error if startRaw != "" { start, err = parseWheelAdminRecordTime(startRaw, false) if err != nil { return time.Time{}, time.Time{}, err } } if endRaw != "" { end, err = parseWheelAdminRecordTime(endRaw, true) if err != nil { return time.Time{}, time.Time{}, err } } if startRaw == "" { start = time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, end.Location()) } if endRaw == "" { end = time.Date(start.Year(), start.Month(), start.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), start.Location()) } 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 parseWheelAdminRecordTime(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 wheelAdminRecordFilter) *gorm.DB { query := s.db.WithContext(ctx).Model(&model.WheelDrawRecord{}). Where("sys_origin = ? AND create_time >= ? AND create_time <= ?", filter.SysOrigin, filter.StartTime, filter.EndTime) if strings.TrimSpace(filter.Category) != "" { query = query.Where("category = ?", normalizeCategory(filter.Category)) } 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 wheelAdminRecordFilter) (wheelAdminRecordSummary, error) { var summary wheelAdminRecordSummary if err := s.adminRecordQuery(ctx, filter). Select("COALESCE(SUM(CASE WHEN status = ? THEN CASE WHEN reward_type = ? THEN gold_amount ELSE display_gold_amount END ELSE 0 END), 0)", drawStatusSuccess, rewardTypeGold). Scan(&summary.TotalRewardGold).Error; err != nil { return summary, err } if err := s.adminRecordQuery(ctx, filter). Select("COUNT(DISTINCT user_id)"). Scan(&summary.ParticipantCount).Error; err != nil { return summary, err } drawRows := s.adminRecordQuery(ctx, filter). Select("draw_no, MAX(paid_gold) AS paid_gold, MAX(draw_times) AS draw_times"). Group("draw_no") if err := s.db.WithContext(ctx). Table("(?) AS draw_rows", drawRows). Select("COALESCE(SUM(paid_gold), 0), COALESCE(SUM(draw_times), 0)"). Row(). Scan(&summary.TotalPaidGold, &summary.TotalDrawTimes); err != nil { return summary, err } return summary, nil } func (s *Service) adminRecordUserInfos(ctx context.Context, rows []model.WheelDrawRecord) (map[int64]model.UserBaseInfo, error) { ids := make([]int64, 0, len(rows)) seen := make(map[int64]struct{}, len(rows)) for _, row := range rows { if row.UserID <= 0 { continue } if _, ok := seen[row.UserID]; ok { continue } seen[row.UserID] = struct{}{} ids = append(ids, row.UserID) } if len(ids) == 0 { return map[int64]model.UserBaseInfo{}, nil } var users []model.UserBaseInfo if err := s.db.WithContext(ctx). Where("id IN ?", ids). Find(&users).Error; err != nil { return nil, err } result := make(map[int64]model.UserBaseInfo, len(users)) for _, user := range users { result[user.ID] = user } return result, nil } func (s *Service) adminRecordUserDrawTimes(ctx context.Context, filter wheelAdminRecordFilter, pageRows []model.WheelDrawRecord) (map[int64]int64, error) { ids := make([]int64, 0, len(pageRows)) seen := make(map[int64]struct{}, len(pageRows)) for _, row := range pageRows { if row.UserID <= 0 { continue } if _, ok := seen[row.UserID]; ok { continue } seen[row.UserID] = struct{}{} ids = append(ids, row.UserID) } if len(ids) == 0 { return map[int64]int64{}, nil } drawRows := s.adminRecordQuery(ctx, filter). Where("user_id IN ?", ids). Select("user_id, draw_no, MAX(draw_times) AS draw_times"). Group("user_id, draw_no") type userDrawTotal struct { UserID int64 TotalDrawTimes int64 } var rows []userDrawTotal if err := s.db.WithContext(ctx). Table("(?) AS user_draw_rows", drawRows). Select("user_id, COALESCE(SUM(draw_times), 0) AS total_draw_times"). Group("user_id"). Scan(&rows).Error; err != nil { return nil, err } result := make(map[int64]int64, len(rows)) for _, row := range rows { result[row.UserID] = row.TotalDrawTimes } return result, nil }