feat(recharge-reward): add reached reward records page
This commit is contained in:
parent
ba8ed01b99
commit
d0007b3319
@ -50,4 +50,20 @@ func registerRechargeRewardRoutes(engine *gin.Engine, javaClient authGateway, se
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
|
||||
group.GET("/claim-record/page", func(c *gin.Context) {
|
||||
resp, err := service.PageClaimRecords(
|
||||
c.Request.Context(),
|
||||
c.Query("sysOrigin"),
|
||||
c.Query("month"),
|
||||
rechargereward.ParseInt64(c.Query("userId")),
|
||||
int(rechargereward.ParseInt64(c.Query("cursor"))),
|
||||
int(rechargereward.ParseInt64(c.Query("limit"))),
|
||||
)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
}
|
||||
|
||||
274
internal/service/rechargereward/claim_records.go
Normal file
274
internal/service/rechargereward/claim_records.go
Normal file
@ -0,0 +1,274 @@
|
||||
package rechargereward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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`
|
||||
if err := s.db.WithContext(ctx).Raw(
|
||||
countSQL,
|
||||
rechargeDate,
|
||||
personalMonthlyRechargeTypes,
|
||||
sysOrigin,
|
||||
userID,
|
||||
userID,
|
||||
minThresholdAmount,
|
||||
).Scan(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := make([]RechargeRewardClaimRecordView, 0)
|
||||
if total > 0 {
|
||||
var rows []rechargeRewardClaimRecordRow
|
||||
listSQL := `
|
||||
SELECT
|
||||
recharge.user_id,
|
||||
SUM(recharge.amount) AS recharge_amount,
|
||||
MAX(recharge.update_time) AS last_recharge_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 ?`
|
||||
if err := s.db.WithContext(ctx).Raw(
|
||||
listSQL,
|
||||
rechargeDate,
|
||||
personalMonthlyRechargeTypes,
|
||||
sysOrigin,
|
||||
userID,
|
||||
userID,
|
||||
minThresholdAmount,
|
||||
limit,
|
||||
(cursor-1)*limit,
|
||||
).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rewardItems := s.loadRewardItemsMap(ctx, levels)
|
||||
records = make([]RechargeRewardClaimRecordView, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
records = append(records, rechargeRewardClaimRecordView(row, displayMonth, rechargeDate, levels, 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,
|
||||
levels []rechargeRewardLevelSnapshot,
|
||||
rewardItems map[int64][]RechargeRewardRewardItem,
|
||||
) RechargeRewardClaimRecordView {
|
||||
rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount))
|
||||
reached := reachedRechargeRewardLevels(levels, rewardItems, rechargeCents)
|
||||
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 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
|
||||
}
|
||||
@ -22,6 +22,7 @@ func newTestService(t *testing.T, java rechargeRewardJavaGateway) (*Service, *go
|
||||
if err := db.AutoMigrate(
|
||||
&model.RechargeRewardConfig{},
|
||||
&model.RechargeRewardLevel{},
|
||||
&model.UserBaseInfo{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
@ -160,6 +161,77 @@ func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageClaimRecordsUsesMonthlyPersonalRechargeAndReachedLevels(t *testing.T) {
|
||||
groupID := int64(9001)
|
||||
service, db := newTestService(t, rechargeRewardTestGateway{
|
||||
groups: map[int64]integration.RewardGroupDetail{
|
||||
groupID: {
|
||||
ID: integration.Int64Value(groupID),
|
||||
Name: "Gift Box",
|
||||
RewardConfigList: []integration.RewardGroupItem{
|
||||
{ID: integration.Int64Value(1), Type: "PROP", Name: "Frame", Quantity: integration.Int64Value(1)},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := db.Exec(`
|
||||
CREATE TABLE user_monthly_recharge_v2 (
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
amount DECIMAL(12,2) NOT NULL,
|
||||
recharge_date INTEGER NOT NULL,
|
||||
update_time DATETIME NOT NULL
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create monthly recharge table: %v", err)
|
||||
}
|
||||
if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{
|
||||
SysOrigin: "LIKEI",
|
||||
Enabled: true,
|
||||
Timezone: "Asia/Riyadh",
|
||||
LevelConfigs: []RechargeRewardLevelInput{
|
||||
{Level: 1, RechargeAmount: mustAmount(t, "100"), RewardGroupID: ptrFlexibleInt64(groupID), Enabled: true},
|
||||
{Level: 2, RechargeAmount: mustAmount(t, "200"), RewardGold: 3000, Enabled: true},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
if err := db.Create(&model.UserBaseInfo{
|
||||
ID: 42,
|
||||
Account: "1000042",
|
||||
UserNickname: "Nina",
|
||||
OriginSys: "LIKEI",
|
||||
CountryCode: "SA",
|
||||
CountryName: "Saudi Arabia",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
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')
|
||||
`).Error; err != nil {
|
||||
t.Fatalf("insert monthly recharge: %v", err)
|
||||
}
|
||||
|
||||
resp, err := service.PageClaimRecords(context.Background(), "LIKEI", "2026-05", 0, 1, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("PageClaimRecords() error = %v", err)
|
||||
}
|
||||
if resp.Total != 1 || len(resp.Records) != 1 {
|
||||
t.Fatalf("page result total=%d records=%d", resp.Total, len(resp.Records))
|
||||
}
|
||||
record := resp.Records[0]
|
||||
if record.RechargeAmount != "150.50" {
|
||||
t.Fatalf("RechargeAmount = %q, want 150.50", record.RechargeAmount)
|
||||
}
|
||||
if len(record.ReachedLevels) != 1 || record.ReachedLevels[0].Level != 1 {
|
||||
t.Fatalf("ReachedLevels = %+v, want only level 1", record.ReachedLevels)
|
||||
}
|
||||
if len(record.ClaimedRewards) != 1 || len(record.ClaimedRewards[0].RewardItems) != 1 {
|
||||
t.Fatalf("ClaimedRewards = %+v, want level reward item", record.ClaimedRewards)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAmountCents(t *testing.T) {
|
||||
tests := map[string]int64{
|
||||
"100": 10000,
|
||||
|
||||
@ -131,6 +131,46 @@ type RechargeRewardHomeResponse struct {
|
||||
LevelConfigs []RechargeRewardLevelPayload `json:"levelConfigs"`
|
||||
}
|
||||
|
||||
// RechargeRewardClaimRecordPageResponse 是后台领取人员列表分页响应。
|
||||
type RechargeRewardClaimRecordPageResponse struct {
|
||||
Month string `json:"month"`
|
||||
RechargeDate int `json:"rechargeDate"`
|
||||
Timezone string `json:"timezone"`
|
||||
Records []RechargeRewardClaimRecordView `json:"records"`
|
||||
Total int64 `json:"total"`
|
||||
Current int `json:"current"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// RechargeRewardClaimRecordView 展示一个达标用户在当前充值奖励配置下可领取/已领取的奖励。
|
||||
type RechargeRewardClaimRecordView struct {
|
||||
RecordKey string `json:"recordKey"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
Account string `json:"account,omitempty"`
|
||||
UserAvatar string `json:"userAvatar,omitempty"`
|
||||
UserNickname string `json:"userNickname,omitempty"`
|
||||
CountryCode string `json:"countryCode,omitempty"`
|
||||
CountryName string `json:"countryName,omitempty"`
|
||||
RechargeMonth string `json:"rechargeMonth"`
|
||||
RechargeDate int `json:"rechargeDate"`
|
||||
RechargeAmount string `json:"rechargeAmount"`
|
||||
RechargeAmountCents int64 `json:"rechargeAmountCents"`
|
||||
ReachedLevels []RechargeRewardLevelPayload `json:"reachedLevels"`
|
||||
ClaimedRewards []RechargeRewardClaimedRewardPayload `json:"claimedRewards"`
|
||||
LastRechargeTime string `json:"lastRechargeTime,omitempty"`
|
||||
}
|
||||
|
||||
// RechargeRewardClaimedRewardPayload 是人员列表里按档位聚合后的奖励展示。
|
||||
type RechargeRewardClaimedRewardPayload struct {
|
||||
Level int `json:"level"`
|
||||
RechargeAmount string `json:"rechargeAmount"`
|
||||
RechargeAmountCents int64 `json:"rechargeAmountCents"`
|
||||
RewardGold int64 `json:"rewardGold"`
|
||||
RewardGroupID *int64 `json:"rewardGroupId,omitempty"`
|
||||
RewardGroupName string `json:"rewardGroupName,omitempty"`
|
||||
RewardItems []RechargeRewardRewardItem `json:"rewardItems"`
|
||||
}
|
||||
|
||||
// RechargeRewardRewardItem 是奖励组内的单个物品展示项。
|
||||
type RechargeRewardRewardItem struct {
|
||||
ID int64 `json:"id"`
|
||||
@ -263,6 +303,24 @@ func (v flexibleAmount) Cents() int64 {
|
||||
return int64(v)
|
||||
}
|
||||
|
||||
func ParseInt64(raw string) int64 {
|
||||
value, _ := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
return value
|
||||
}
|
||||
|
||||
func NormalizePage(cursor int, limit int) (int, int) {
|
||||
if cursor <= 0 {
|
||||
cursor = 1
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
return cursor, limit
|
||||
}
|
||||
|
||||
func normalizeRechargeRewardTimezone(timezone string) string {
|
||||
timezone = strings.TrimSpace(timezone)
|
||||
if timezone == "" {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user