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 (
"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,

View File

@ -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)
}