package rechargereward import ( "context" "encoding/json" "fmt" "math" "net/http" "strings" "time" ) var personalMonthlyRechargeTypes = []string{ "UNDEFINED", "HUAWEI", "GOOGLE", "APPLE", "PAYER_MAX", "SHIPPING_AGENT", "STRIPE", "SALARY_EXCHANGE", "PAY_PAL", "CLIPSPAY", "SELLER_AGENT", } type rechargeRewardClaimRecordRow struct { UserID int64 `gorm:"column:user_id"` RechargeAmount string `gorm:"column:recharge_amount"` 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"` RuleDescription string `gorm:"column:rule_description"` JSONData string `gorm:"column:json_data"` 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 user_activity_reward_receive_record AS receive INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id WHERE receive.recharge_date = ? AND rule.sys_origin = ? AND rule.activity_type = 'CUMULATIVE_RECHARGE' AND COALESCE(rule.is_del, 0) = 0 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, COALESCE(recharge.recharge_amount, 0) AS recharge_amount, COALESCE(recharge.last_recharge_time, '') AS last_recharge_time, claims.last_claim_time, user.account, user.user_avatar, user.user_nickname, user.country_code, user.country_name FROM ( SELECT receive.user_id, MAX(receive.create_time) AS last_claim_time FROM user_activity_reward_receive_record AS receive INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id WHERE receive.recharge_date = ? AND rule.sys_origin = ? AND rule.activity_type = 'CUMULATIVE_RECHARGE' AND COALESCE(rule.is_del, 0) = 0 AND (? = 0 OR receive.user_id = ?) GROUP BY receive.user_id ORDER BY MAX(receive.create_time) DESC, receive.user_id ASC LIMIT ? OFFSET ? ) AS claims INNER JOIN user_base_info AS user ON user.id = claims.user_id LEFT JOIN ( SELECT recharge.user_id, SUM(recharge.amount) AS recharge_amount, MAX(recharge.update_time) AS last_recharge_time FROM user_monthly_recharge_v2 AS recharge WHERE recharge.recharge_date = ? AND recharge.type IN ? GROUP BY recharge.user_id ) AS recharge ON recharge.user_id = claims.user_id 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, rechargeDate, personalMonthlyRechargeTypes, ).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, 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 { rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount)) 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(rechargeCents), RechargeAmountCents: rechargeCents, 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.activity_id, COALESCE(rule.sort, 0) AS level, rule.resource_group_id, rule.rule_description, rule.json_data, receive.create_time AS claim_time FROM user_activity_reward_receive_record AS receive INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id WHERE receive.recharge_date = ? AND rule.sys_origin = ? AND rule.activity_type = 'CUMULATIVE_RECHARGE' AND COALESCE(rule.is_del, 0) = 0 AND receive.user_id IN ? ORDER BY receive.user_id ASC, COALESCE(rule.sort, 0) 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] } thresholdCents := rechargeRewardRuleQuantityCents(detail.JSONData) result = append(result, RechargeRewardLevelPayload{ ID: detail.ActivityID, Level: level, RechargeAmount: formatAmountCents(thresholdCents), RechargeAmountCents: thresholdCents, RewardGroupID: detail.ResourceGroupID, RewardGroupName: strings.TrimSpace(detail.RuleDescription), RewardItems: items, Enabled: true, Reached: true, }) } return result } func rechargeRewardRuleQuantityCents(raw string) int64 { var payload map[string]any if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil { return 0 } value, ok := payload["quantity"] if !ok { return 0 } switch typed := value.(type) { case float64: return int64(math.Round(typed * 100)) case string: cents, err := parseAmountCents(typed) if err != nil { return 0 } return cents default: cents, err := parseAmountCents(fmt.Sprint(typed)) if err != nil { return 0 } return cents } } 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] } 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 { result = append(result, RechargeRewardClaimedRewardPayload{ Level: level.Level, RechargeAmount: level.RechargeAmount, RechargeAmountCents: level.RechargeAmountCents, RewardGold: level.RewardGold, RewardGroupID: level.RewardGroupID, RewardGroupName: level.RewardGroupName, RewardItems: level.RewardItems, }) } 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 }