fix(recharge-reward): show actual claim records only

This commit is contained in:
hy001 2026-05-07 17:47:18 +08:00
parent d0007b3319
commit 25aab0d6a2
2 changed files with 230 additions and 43 deletions

View File

@ -2,7 +2,9 @@ package rechargereward
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"math"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -26,6 +28,7 @@ type rechargeRewardClaimRecordRow struct {
UserID int64 `gorm:"column:user_id"` UserID int64 `gorm:"column:user_id"`
RechargeAmount string `gorm:"column:recharge_amount"` RechargeAmount string `gorm:"column:recharge_amount"`
LastRechargeTime string `gorm:"column:last_recharge_time"` LastRechargeTime string `gorm:"column:last_recharge_time"`
LastClaimTime string `gorm:"column:last_claim_time"`
Account string `gorm:"column:account"` Account string `gorm:"column:account"`
UserAvatar string `gorm:"column:user_avatar"` UserAvatar string `gorm:"column:user_avatar"`
UserNickname string `gorm:"column:user_nickname"` UserNickname string `gorm:"column:user_nickname"`
@ -33,7 +36,17 @@ type rechargeRewardClaimRecordRow struct {
CountryName string `gorm:"column:country_name"` 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) { func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month string, userID int64, cursor int, limit int) (*RechargeRewardClaimRecordPageResponse, error) {
sysOrigin = s.normalizeSysOrigin(sysOrigin) sysOrigin = s.normalizeSysOrigin(sysOrigin)
cursor, limit = NormalizePage(cursor, limit) cursor, limit = NormalizePage(cursor, limit)
@ -51,35 +64,22 @@ func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month
return nil, err 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 var total int64
countSQL := ` countSQL := `
SELECT COUNT(1) SELECT COUNT(DISTINCT receive.user_id)
FROM ( FROM user_activity_reward_receive_record AS receive
SELECT recharge.user_id INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
FROM user_monthly_recharge_v2 AS recharge WHERE receive.recharge_date = ?
INNER JOIN user_base_info AS user ON user.id = recharge.user_id AND rule.sys_origin = ?
WHERE recharge.recharge_date = ? AND rule.activity_type = 'CUMULATIVE_RECHARGE'
AND recharge.type IN ? AND COALESCE(rule.is_del, 0) = 0
AND user.origin_sys = ? AND (? = 0 OR receive.user_id = ?)`
AND (? = 0 OR recharge.user_id = ?)
GROUP BY recharge.user_id
HAVING SUM(recharge.amount) >= ?
) AS qualified`
if err := s.db.WithContext(ctx).Raw( if err := s.db.WithContext(ctx).Raw(
countSQL, countSQL,
rechargeDate, rechargeDate,
personalMonthlyRechargeTypes,
sysOrigin, sysOrigin,
userID, userID,
userID, userID,
minThresholdAmount,
).Scan(&total).Error; err != nil { ).Scan(&total).Error; err != nil {
return nil, err return nil, err
} }
@ -89,41 +89,63 @@ FROM (
var rows []rechargeRewardClaimRecordRow var rows []rechargeRewardClaimRecordRow
listSQL := ` listSQL := `
SELECT SELECT
recharge.user_id, claims.user_id,
SUM(recharge.amount) AS recharge_amount, COALESCE(recharge.recharge_amount, 0) AS recharge_amount,
MAX(recharge.update_time) AS last_recharge_time, COALESCE(recharge.last_recharge_time, '') AS last_recharge_time,
claims.last_claim_time,
user.account, user.account,
user.user_avatar, user.user_avatar,
user.user_nickname, user.user_nickname,
user.country_code, user.country_code,
user.country_name user.country_name
FROM user_monthly_recharge_v2 AS recharge FROM (
INNER JOIN user_base_info AS user ON user.id = recharge.user_id SELECT
WHERE recharge.recharge_date = ? receive.user_id,
AND recharge.type IN ? MAX(receive.create_time) AS last_claim_time
AND user.origin_sys = ? FROM user_activity_reward_receive_record AS receive
AND (? = 0 OR recharge.user_id = ?) INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
GROUP BY recharge.user_id, user.account, user.user_avatar, user.user_nickname, user.country_code, user.country_name WHERE receive.recharge_date = ?
HAVING SUM(recharge.amount) >= ? AND rule.sys_origin = ?
ORDER BY SUM(recharge.amount) DESC, recharge.user_id ASC AND rule.activity_type = 'CUMULATIVE_RECHARGE'
LIMIT ? OFFSET ?` 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( if err := s.db.WithContext(ctx).Raw(
listSQL, listSQL,
rechargeDate, rechargeDate,
personalMonthlyRechargeTypes,
sysOrigin, sysOrigin,
userID, userID,
userID, userID,
minThresholdAmount,
limit, limit,
(cursor-1)*limit, (cursor-1)*limit,
rechargeDate,
personalMonthlyRechargeTypes,
).Scan(&rows).Error; err != nil { ).Scan(&rows).Error; err != nil {
return nil, err 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)) records = make([]RechargeRewardClaimRecordView, 0, len(rows))
for _, row := range 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, row rechargeRewardClaimRecordRow,
month string, month string,
rechargeDate int, rechargeDate int,
levels []rechargeRewardLevelSnapshot, details []rechargeRewardClaimDetailRow,
rewardItems map[int64][]RechargeRewardRewardItem, rewardItems map[int64][]RechargeRewardRewardItem,
) RechargeRewardClaimRecordView { ) RechargeRewardClaimRecordView {
rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount)) rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount))
reached := reachedRechargeRewardLevels(levels, rewardItems, rechargeCents) reached := claimDetailPayloads(details, rewardItems)
return RechargeRewardClaimRecordView{ return RechargeRewardClaimRecordView{
RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate), RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate),
UserID: row.UserID, 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( func reachedRechargeRewardLevels(
levels []rechargeRewardLevelSnapshot, levels []rechargeRewardLevelSnapshot,
rewardItems map[int64][]RechargeRewardRewardItem, rewardItems map[int64][]RechargeRewardRewardItem,

View File

@ -161,7 +161,7 @@ func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
} }
} }
func TestPageClaimRecordsUsesMonthlyPersonalRechargeAndReachedLevels(t *testing.T) { func TestPageClaimRecordsUsesActualReceiveRecords(t *testing.T) {
groupID := int64(9001) groupID := int64(9001)
service, db := newTestService(t, rechargeRewardTestGateway{ service, db := newTestService(t, rechargeRewardTestGateway{
groups: map[int64]integration.RewardGroupDetail{ groups: map[int64]integration.RewardGroupDetail{
@ -184,6 +184,29 @@ CREATE TABLE user_monthly_recharge_v2 (
)`).Error; err != nil { )`).Error; err != nil {
t.Fatalf("create monthly recharge table: %v", err) 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{ if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{
SysOrigin: "LIKEI", SysOrigin: "LIKEI",
Enabled: true, Enabled: true,
@ -208,10 +231,23 @@ CREATE TABLE user_monthly_recharge_v2 (
if err := db.Exec(` if err := db.Exec(`
INSERT INTO user_monthly_recharge_v2 (user_id, type, amount, recharge_date, update_time) VALUES 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, '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 { `).Error; err != nil {
t.Fatalf("insert monthly recharge: %v", err) 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) resp, err := service.PageClaimRecords(context.Background(), "LIKEI", "2026-05", 0, 1, 20)
if err != nil { 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 { if len(record.ReachedLevels) != 1 || record.ReachedLevels[0].Level != 1 {
t.Fatalf("ReachedLevels = %+v, want only level 1", record.ReachedLevels) 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 { if len(record.ClaimedRewards) != 1 || len(record.ClaimedRewards[0].RewardItems) != 1 {
t.Fatalf("ClaimedRewards = %+v, want level reward item", record.ClaimedRewards) t.Fatalf("ClaimedRewards = %+v, want level reward item", record.ClaimedRewards)
} }