package rechargereward import ( "context" "fmt" "net/http" "strings" "time" ) type rechargeRewardClaimRecordRow struct { UserID int64 `gorm:"column:user_id"` RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"` LastRechargeTime string `gorm:"column:last_recharge_time"` LastClaimTime string `gorm:"column:last_claim_time"` Account string `gorm:"column:account"` UserAvatar string `gorm:"column:user_avatar"` UserNickname string `gorm:"column:user_nickname"` CountryCode string `gorm:"column:country_code"` CountryName string `gorm:"column:country_name"` } type rechargeRewardClaimDetailRow struct { UserID int64 `gorm:"column:user_id"` ActivityID int64 `gorm:"column:activity_id"` Level int `gorm:"column:level"` ResourceGroupID *int64 `gorm:"column:resource_group_id"` RewardGroupName string `gorm:"column:reward_group_name"` RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"` RewardGold int64 `gorm:"column:reward_gold"` ClaimTime string `gorm:"column:claim_time"` } // PageClaimRecords 分页返回真实领取过累计充值奖励的用户。 func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month string, userID int64, cursor int, limit int) (*RechargeRewardClaimRecordPageResponse, error) { sysOrigin = s.normalizeSysOrigin(sysOrigin) cursor, limit = NormalizePage(cursor, limit) bundle, err := s.loadConfigBundle(ctx, sysOrigin) if err != nil { return nil, err } location := s.location if bundle.Config != nil { location = resolveLocation(bundle.Config.Timezone, s.location) } rechargeDate, displayMonth, err := normalizeRechargeRewardRecordMonth(month, time.Now().In(location)) if err != nil { return nil, err } var total int64 countSQL := ` SELECT COUNT(DISTINCT receive.user_id) FROM recharge_reward_grant_record AS receive WHERE receive.recharge_date = ? AND receive.sys_origin = ? AND receive.status = 'SUCCESS' AND (? = 0 OR receive.user_id = ?)` if err := s.db.WithContext(ctx).Raw( countSQL, rechargeDate, sysOrigin, userID, userID, ).Scan(&total).Error; err != nil { return nil, err } records := make([]RechargeRewardClaimRecordView, 0) if total > 0 { var rows []rechargeRewardClaimRecordRow listSQL := ` SELECT claims.user_id, claims.recharge_amount_cents, claims.last_recharge_time, claims.last_claim_time, claims.account, claims.user_avatar, claims.user_nickname, claims.country_code, claims.country_name FROM ( SELECT receive.user_id, MAX(receive.recharge_amount_cents) AS recharge_amount_cents, COALESCE(MAX(receive.sent_at), MAX(receive.update_time), '') AS last_recharge_time, COALESCE(MAX(receive.sent_at), MAX(receive.create_time), '') AS last_claim_time, MAX(receive.account) AS account, MAX(receive.user_avatar) AS user_avatar, MAX(receive.user_nickname) AS user_nickname, MAX(receive.country_code) AS country_code, MAX(receive.country_name) AS country_name FROM recharge_reward_grant_record AS receive WHERE receive.recharge_date = ? AND receive.sys_origin = ? AND receive.status = 'SUCCESS' AND (? = 0 OR receive.user_id = ?) GROUP BY receive.user_id ORDER BY COALESCE(MAX(receive.sent_at), MAX(receive.create_time)) DESC, receive.user_id ASC LIMIT ? OFFSET ? ) AS claims ORDER BY claims.last_claim_time DESC, claims.user_id ASC` if err := s.db.WithContext(ctx).Raw( listSQL, rechargeDate, sysOrigin, userID, userID, limit, (cursor-1)*limit, ).Scan(&rows).Error; err != nil { return nil, err } details, err := s.loadClaimDetails(ctx, sysOrigin, rechargeDate, claimRecordUserIDs(rows)) if err != nil { return nil, err } rewardItems := s.loadRewardItemsMap(ctx, sysOrigin, claimDetailLevels(details)) records = make([]RechargeRewardClaimRecordView, 0, len(rows)) for _, row := range rows { records = append(records, rechargeRewardClaimRecordView(row, displayMonth, rechargeDate, details[row.UserID], rewardItems)) } } return &RechargeRewardClaimRecordPageResponse{ Month: displayMonth, RechargeDate: rechargeDate, Timezone: location.String(), Records: records, Total: total, Current: cursor, Size: limit, }, nil } func emptyRechargeRewardClaimRecordPage(month string, rechargeDate int, timezone string, cursor int, limit int) *RechargeRewardClaimRecordPageResponse { return &RechargeRewardClaimRecordPageResponse{ Month: month, RechargeDate: rechargeDate, Timezone: timezone, Records: []RechargeRewardClaimRecordView{}, Total: 0, Current: cursor, Size: limit, } } func normalizeRechargeRewardRecordMonth(raw string, now time.Time) (int, string, error) { value := strings.TrimSpace(raw) if value == "" { value = now.Format("2006-01") } if len(value) == 6 && !strings.Contains(value, "-") { value = value[:4] + "-" + value[4:] } parsed, err := time.Parse("2006-01", value) if err != nil { return 0, "", NewAppError(http.StatusBadRequest, "invalid_month", "month must be YYYY-MM") } rechargeDate := parsed.Year()*100 + int(parsed.Month()) return rechargeDate, parsed.Format("2006-01"), nil } func enabledRechargeRewardLevels(levels []rechargeRewardLevelSnapshot) []rechargeRewardLevelSnapshot { result := make([]rechargeRewardLevelSnapshot, 0, len(levels)) for _, level := range levels { if !level.Enabled || level.RechargeAmountCents <= 0 { continue } result = append(result, level) } sortLevels(result) return result } func rechargeRewardClaimRecordView( row rechargeRewardClaimRecordRow, month string, rechargeDate int, details []rechargeRewardClaimDetailRow, rewardItems map[int64][]RechargeRewardRewardItem, ) RechargeRewardClaimRecordView { reached := claimDetailPayloads(details, rewardItems) return RechargeRewardClaimRecordView{ RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate), UserID: row.UserID, Account: strings.TrimSpace(row.Account), UserAvatar: strings.TrimSpace(row.UserAvatar), UserNickname: strings.TrimSpace(row.UserNickname), CountryCode: strings.TrimSpace(row.CountryCode), CountryName: strings.TrimSpace(row.CountryName), RechargeMonth: month, RechargeDate: rechargeDate, RechargeAmount: formatAmountCents(row.RechargeAmountCents), RechargeAmountCents: row.RechargeAmountCents, ReachedLevels: reached, ClaimedRewards: rechargeRewardClaimedRewards(reached), LastRechargeTime: formatRechargeRewardRecordTime(row.LastRechargeTime), } } func (s *Service) loadClaimDetails(ctx context.Context, sysOrigin string, rechargeDate int, userIDs []int64) (map[int64][]rechargeRewardClaimDetailRow, error) { if len(userIDs) == 0 { return map[int64][]rechargeRewardClaimDetailRow{}, nil } var rows []rechargeRewardClaimDetailRow detailSQL := ` SELECT receive.user_id, receive.level_id AS activity_id, receive.level, receive.reward_group_id AS resource_group_id, receive.reward_group_name, receive.recharge_threshold_cents, receive.reward_gold, COALESCE(receive.sent_at, receive.create_time) AS claim_time FROM recharge_reward_grant_record AS receive WHERE receive.recharge_date = ? AND receive.sys_origin = ? AND receive.status = 'SUCCESS' AND receive.user_id IN ? ORDER BY receive.user_id ASC, receive.level ASC, receive.create_time ASC` if err := s.db.WithContext(ctx).Raw(detailSQL, rechargeDate, sysOrigin, userIDs).Scan(&rows).Error; err != nil { return nil, err } result := make(map[int64][]rechargeRewardClaimDetailRow, len(userIDs)) for _, row := range rows { result[row.UserID] = append(result[row.UserID], row) } return result, nil } func claimRecordUserIDs(rows []rechargeRewardClaimRecordRow) []int64 { result := make([]int64, 0, len(rows)) seen := make(map[int64]struct{}, len(rows)) for _, row := range rows { if row.UserID <= 0 { continue } if _, exists := seen[row.UserID]; exists { continue } seen[row.UserID] = struct{}{} result = append(result, row.UserID) } return result } func claimDetailLevels(details map[int64][]rechargeRewardClaimDetailRow) []rechargeRewardLevelSnapshot { levels := make([]rechargeRewardLevelSnapshot, 0) seen := make(map[int64]struct{}) for _, rows := range details { for _, row := range rows { if row.ResourceGroupID == nil || *row.ResourceGroupID <= 0 { continue } if _, exists := seen[*row.ResourceGroupID]; exists { continue } seen[*row.ResourceGroupID] = struct{}{} levels = append(levels, rechargeRewardLevelSnapshot{ RewardGroupID: row.ResourceGroupID, Enabled: true, }) } } return levels } func claimDetailPayloads( details []rechargeRewardClaimDetailRow, rewardItems map[int64][]RechargeRewardRewardItem, ) []RechargeRewardLevelPayload { result := make([]RechargeRewardLevelPayload, 0, len(details)) for index, detail := range details { level := detail.Level if level <= 0 { level = index + 1 } var items []RechargeRewardRewardItem if detail.ResourceGroupID != nil { items = rewardItems[*detail.ResourceGroupID] } if items == nil { items = []RechargeRewardRewardItem{} } result = append(result, RechargeRewardLevelPayload{ ID: detail.ActivityID, Level: level, RechargeAmount: formatAmountCents(detail.RechargeThresholdCents), RechargeAmountCents: detail.RechargeThresholdCents, RewardGold: detail.RewardGold, RewardGroupID: detail.ResourceGroupID, RewardGroupName: strings.TrimSpace(detail.RewardGroupName), RewardItems: items, Enabled: true, Reached: true, }) } return result } func reachedRechargeRewardLevels( levels []rechargeRewardLevelSnapshot, rewardItems map[int64][]RechargeRewardRewardItem, rechargeCents int64, ) []RechargeRewardLevelPayload { result := make([]RechargeRewardLevelPayload, 0) for _, level := range levels { if rechargeCents < level.RechargeAmountCents { continue } var items []RechargeRewardRewardItem if level.RewardGroupID != nil { items = rewardItems[*level.RewardGroupID] } if items == nil { items = []RechargeRewardRewardItem{} } result = append(result, RechargeRewardLevelPayload{ ID: level.ID, Level: level.Level, RechargeAmount: formatAmountCents(level.RechargeAmountCents), RechargeAmountCents: level.RechargeAmountCents, RewardGold: level.RewardGold, RewardGroupID: level.RewardGroupID, RewardGroupName: level.RewardGroupName, RewardItems: items, Enabled: level.Enabled, Reached: true, }) } return result } func rechargeRewardClaimedRewards(levels []RechargeRewardLevelPayload) []RechargeRewardClaimedRewardPayload { result := make([]RechargeRewardClaimedRewardPayload, 0, len(levels)) for _, level := range levels { items := level.RewardItems if items == nil { items = []RechargeRewardRewardItem{} } result = append(result, RechargeRewardClaimedRewardPayload{ Level: level.Level, RechargeAmount: level.RechargeAmount, RechargeAmountCents: level.RechargeAmountCents, RewardGold: level.RewardGold, RewardGroupID: level.RewardGroupID, RewardGroupName: level.RewardGroupName, RewardItems: items, }) } return result } func formatRechargeRewardRecordTime(raw string) string { value := strings.TrimSpace(raw) if value == "" { return "" } for _, layout := range []string{ "2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05", time.RFC3339Nano, time.RFC3339, } { if parsed, err := time.Parse(layout, value); err == nil { return formatDateTime(parsed) } } if len(value) > len("2006-01-02 15:04:05") { return value[:len("2006-01-02 15:04:05")] } return value }