diff --git a/internal/service/rechargereward/claim_records.go b/internal/service/rechargereward/claim_records.go index 3ce5d43..5c66267 100644 --- a/internal/service/rechargereward/claim_records.go +++ b/internal/service/rechargereward/claim_records.go @@ -2,7 +2,9 @@ package rechargereward import ( "context" + "encoding/json" "fmt" + "math" "net/http" "strings" "time" @@ -26,6 +28,7 @@ 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"` @@ -33,7 +36,17 @@ type rechargeRewardClaimRecordRow struct { CountryName string `gorm:"column:country_name"` } -// PageClaimRecords 分页返回当前配置下已达到充值奖励档位的用户。 +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) @@ -51,35 +64,22 @@ func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month return nil, err } - levels := enabledRechargeRewardLevels(bundle.Levels) - if len(levels) == 0 { - return emptyRechargeRewardClaimRecordPage(displayMonth, rechargeDate, location.String(), cursor, limit), nil - } - minThresholdCents := levels[0].RechargeAmountCents - minThresholdAmount := float64(minThresholdCents) / 100 - var total int64 countSQL := ` -SELECT COUNT(1) -FROM ( - SELECT recharge.user_id - FROM user_monthly_recharge_v2 AS recharge - INNER JOIN user_base_info AS user ON user.id = recharge.user_id - WHERE recharge.recharge_date = ? - AND recharge.type IN ? - AND user.origin_sys = ? - AND (? = 0 OR recharge.user_id = ?) - GROUP BY recharge.user_id - HAVING SUM(recharge.amount) >= ? -) AS qualified` +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, - personalMonthlyRechargeTypes, sysOrigin, userID, userID, - minThresholdAmount, ).Scan(&total).Error; err != nil { return nil, err } @@ -89,41 +89,63 @@ FROM ( var rows []rechargeRewardClaimRecordRow listSQL := ` SELECT - recharge.user_id, - SUM(recharge.amount) AS recharge_amount, - MAX(recharge.update_time) AS last_recharge_time, + 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 user_monthly_recharge_v2 AS recharge -INNER JOIN user_base_info AS user ON user.id = recharge.user_id -WHERE recharge.recharge_date = ? - AND recharge.type IN ? - AND user.origin_sys = ? - AND (? = 0 OR recharge.user_id = ?) -GROUP BY recharge.user_id, user.account, user.user_avatar, user.user_nickname, user.country_code, user.country_name -HAVING SUM(recharge.amount) >= ? -ORDER BY SUM(recharge.amount) DESC, recharge.user_id ASC -LIMIT ? OFFSET ?` +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, - personalMonthlyRechargeTypes, sysOrigin, userID, userID, - minThresholdAmount, limit, (cursor-1)*limit, + rechargeDate, + personalMonthlyRechargeTypes, ).Scan(&rows).Error; err != nil { return nil, err } - rewardItems := s.loadRewardItemsMap(ctx, levels) + 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, levels, rewardItems)) + records = append(records, rechargeRewardClaimRecordView(row, displayMonth, rechargeDate, details[row.UserID], rewardItems)) } } @@ -182,11 +204,11 @@ func rechargeRewardClaimRecordView( row rechargeRewardClaimRecordRow, month string, rechargeDate int, - levels []rechargeRewardLevelSnapshot, + details []rechargeRewardClaimDetailRow, rewardItems map[int64][]RechargeRewardRewardItem, ) RechargeRewardClaimRecordView { rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount)) - reached := reachedRechargeRewardLevels(levels, rewardItems, rechargeCents) + reached := claimDetailPayloads(details, rewardItems) return RechargeRewardClaimRecordView{ RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate), UserID: row.UserID, @@ -205,6 +227,132 @@ func rechargeRewardClaimRecordView( } } +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, diff --git a/internal/service/rechargereward/config_test.go b/internal/service/rechargereward/config_test.go index 6d94a13..358211e 100644 --- a/internal/service/rechargereward/config_test.go +++ b/internal/service/rechargereward/config_test.go @@ -161,7 +161,7 @@ func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) { } } -func TestPageClaimRecordsUsesMonthlyPersonalRechargeAndReachedLevels(t *testing.T) { +func TestPageClaimRecordsUsesActualReceiveRecords(t *testing.T) { groupID := int64(9001) service, db := newTestService(t, rechargeRewardTestGateway{ groups: map[int64]integration.RewardGroupDetail{ @@ -184,6 +184,29 @@ CREATE TABLE user_monthly_recharge_v2 ( )`).Error; err != nil { t.Fatalf("create monthly recharge table: %v", err) } + if err := db.Exec(` +CREATE TABLE user_activity_reward_receive_record ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + recharge_date INTEGER NOT NULL, + activity_id INTEGER NOT NULL, + create_time DATETIME NOT NULL +)`).Error; err != nil { + t.Fatalf("create receive record table: %v", err) + } + if err := db.Exec(` +CREATE TABLE props_activity_rule_config ( + id INTEGER PRIMARY KEY, + sys_origin TEXT NOT NULL, + activity_type TEXT NOT NULL, + resource_group_id INTEGER NOT NULL, + json_data TEXT NOT NULL, + sort INTEGER NOT NULL, + rule_description TEXT NOT NULL, + is_del INTEGER NOT NULL +)`).Error; err != nil { + t.Fatalf("create activity rule table: %v", err) + } if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, @@ -208,10 +231,23 @@ CREATE TABLE user_monthly_recharge_v2 ( if err := db.Exec(` INSERT INTO user_monthly_recharge_v2 (user_id, type, amount, recharge_date, update_time) VALUES (42, 'GOOGLE', 150.50, 202605, '2026-05-07 10:00:00'), - (42, 'MIFA_PAY', 70.00, 202605, '2026-05-07 11:00:00') + (42, 'MIFA_PAY', 70.00, 202605, '2026-05-07 11:00:00'), + (43, 'GOOGLE', 300.00, 202605, '2026-05-07 12:00:00') `).Error; err != nil { t.Fatalf("insert monthly recharge: %v", err) } + if err := db.Exec(` +INSERT INTO props_activity_rule_config (id, sys_origin, activity_type, resource_group_id, json_data, sort, rule_description, is_del) VALUES + (7001, 'LIKEI', 'CUMULATIVE_RECHARGE', 9001, '{"quantity":100}', 1, 'Gift Box', 0) +`).Error; err != nil { + t.Fatalf("insert activity rule: %v", err) + } + if err := db.Exec(` +INSERT INTO user_activity_reward_receive_record (id, user_id, recharge_date, activity_id, create_time) VALUES + (8001, 42, 202605, 7001, '2026-05-07 10:30:00') +`).Error; err != nil { + t.Fatalf("insert receive record: %v", err) + } resp, err := service.PageClaimRecords(context.Background(), "LIKEI", "2026-05", 0, 1, 20) if err != nil { @@ -227,6 +263,9 @@ INSERT INTO user_monthly_recharge_v2 (user_id, type, amount, recharge_date, upda if len(record.ReachedLevels) != 1 || record.ReachedLevels[0].Level != 1 { t.Fatalf("ReachedLevels = %+v, want only level 1", record.ReachedLevels) } + if record.ReachedLevels[0].RechargeAmount != "100.00" { + t.Fatalf("ReachedLevels[0].RechargeAmount = %q, want 100.00", record.ReachedLevels[0].RechargeAmount) + } if len(record.ClaimedRewards) != 1 || len(record.ClaimedRewards[0].RewardItems) != 1 { t.Fatalf("ClaimedRewards = %+v, want level reward item", record.ClaimedRewards) }