充值奖励结算
This commit is contained in:
parent
810dc0345b
commit
74063d0908
@ -81,8 +81,11 @@ type InviteConfig struct {
|
|||||||
|
|
||||||
// RechargeRewardConfig 保存充值奖励活动配置。
|
// RechargeRewardConfig 保存充值奖励活动配置。
|
||||||
type RechargeRewardConfig struct {
|
type RechargeRewardConfig struct {
|
||||||
DefaultSysOrigin string
|
DefaultSysOrigin string
|
||||||
Timezone string
|
Timezone string
|
||||||
|
StorageTimezone string
|
||||||
|
StartDate string
|
||||||
|
CoinSellerGoldPerUSD int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskCenterConfig 保存任务中心模块配置。
|
// TaskCenterConfig 保存任务中心模块配置。
|
||||||
@ -313,6 +316,18 @@ func Load() Config {
|
|||||||
[]string{"CHATAPP_RECHARGE_REWARD_TIMEZONE", "RECHARGE_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
[]string{"CHATAPP_RECHARGE_REWARD_TIMEZONE", "RECHARGE_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
"Asia/Riyadh",
|
"Asia/Riyadh",
|
||||||
),
|
),
|
||||||
|
StorageTimezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_STORAGE_TIMEZONE", "RECHARGE_REWARD_STORAGE_TIMEZONE"},
|
||||||
|
"Asia/Shanghai",
|
||||||
|
),
|
||||||
|
StartDate: getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_START_DATE", "RECHARGE_REWARD_START_DATE"},
|
||||||
|
"2026-05-21",
|
||||||
|
),
|
||||||
|
CoinSellerGoldPerUSD: int64(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_COIN_SELLER_GOLD_PER_USD", "RECHARGE_REWARD_COIN_SELLER_GOLD_PER_USD"},
|
||||||
|
100000,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
TaskCenter: TaskCenterConfig{
|
TaskCenter: TaskCenterConfig{
|
||||||
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
|||||||
@ -29,3 +29,39 @@ type RechargeRewardLevel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (RechargeRewardLevel) TableName() string { return "recharge_reward_level" }
|
func (RechargeRewardLevel) TableName() string { return "recharge_reward_level" }
|
||||||
|
|
||||||
|
// RechargeRewardGrantRecord 记录某个充值周期内用户各档位的实际发放状态。
|
||||||
|
type RechargeRewardGrantRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_recharge_reward_grant_config_cycle,priority:1"`
|
||||||
|
LevelID int64 `gorm:"column:level_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_recharge_reward_grant_level,priority:1;index:idx_recharge_reward_grant_date,priority:1"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;uniqueIndex:uk_recharge_reward_grant_level,priority:2;index:idx_recharge_reward_grant_config_cycle,priority:2"`
|
||||||
|
RechargeDate int `gorm:"column:recharge_date;index:idx_recharge_reward_grant_user,priority:2;index:idx_recharge_reward_grant_date,priority:2"`
|
||||||
|
PeriodStartAt time.Time `gorm:"column:period_start_at"`
|
||||||
|
PeriodEndAt time.Time `gorm:"column:period_end_at"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_recharge_reward_grant_level,priority:3;index:idx_recharge_reward_grant_user,priority:1"`
|
||||||
|
Account string `gorm:"column:account;size:64"`
|
||||||
|
UserAvatar string `gorm:"column:user_avatar;size:1024"`
|
||||||
|
UserNickname string `gorm:"column:user_nickname;size:255"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32"`
|
||||||
|
CountryName string `gorm:"column:country_name;size:128"`
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_recharge_reward_grant_level,priority:4"`
|
||||||
|
RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
RewardGroupTrackID int64 `gorm:"column:reward_group_track_id"`
|
||||||
|
GoldEventID string `gorm:"column:gold_event_id;size:128;index:idx_recharge_reward_gold_event"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_recharge_reward_grant_config_cycle,priority:3;index:idx_recharge_reward_grant_date,priority:3"`
|
||||||
|
RewardGroupStatus string `gorm:"column:reward_group_status;size:32"`
|
||||||
|
RewardGoldStatus string `gorm:"column:reward_gold_status;size:32"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1024"`
|
||||||
|
SentAt *time.Time `gorm:"column:sent_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RechargeRewardGrantRecord) TableName() string { return "recharge_reward_grant_record" }
|
||||||
|
|||||||
@ -2,48 +2,33 @@ package rechargereward
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var personalMonthlyRechargeTypes = []string{
|
|
||||||
"UNDEFINED",
|
|
||||||
"HUAWEI",
|
|
||||||
"GOOGLE",
|
|
||||||
"APPLE",
|
|
||||||
"PAYER_MAX",
|
|
||||||
"SHIPPING_AGENT",
|
|
||||||
"STRIPE",
|
|
||||||
"SALARY_EXCHANGE",
|
|
||||||
"PAY_PAL",
|
|
||||||
"CLIPSPAY",
|
|
||||||
"SELLER_AGENT",
|
|
||||||
}
|
|
||||||
|
|
||||||
type rechargeRewardClaimRecordRow struct {
|
type rechargeRewardClaimRecordRow struct {
|
||||||
UserID int64 `gorm:"column:user_id"`
|
UserID int64 `gorm:"column:user_id"`
|
||||||
RechargeAmount string `gorm:"column:recharge_amount"`
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||||
LastRechargeTime string `gorm:"column:last_recharge_time"`
|
LastRechargeTime string `gorm:"column:last_recharge_time"`
|
||||||
LastClaimTime string `gorm:"column:last_claim_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"`
|
||||||
CountryCode string `gorm:"column:country_code"`
|
CountryCode string `gorm:"column:country_code"`
|
||||||
CountryName string `gorm:"column:country_name"`
|
CountryName string `gorm:"column:country_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type rechargeRewardClaimDetailRow struct {
|
type rechargeRewardClaimDetailRow struct {
|
||||||
UserID int64 `gorm:"column:user_id"`
|
UserID int64 `gorm:"column:user_id"`
|
||||||
ActivityID int64 `gorm:"column:activity_id"`
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
Level int `gorm:"column:level"`
|
Level int `gorm:"column:level"`
|
||||||
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
|
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
|
||||||
RuleDescription string `gorm:"column:rule_description"`
|
RewardGroupName string `gorm:"column:reward_group_name"`
|
||||||
JSONData string `gorm:"column:json_data"`
|
RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"`
|
||||||
ClaimTime string `gorm:"column:claim_time"`
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
ClaimTime string `gorm:"column:claim_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageClaimRecords 分页返回真实领取过累计充值奖励的用户。
|
// PageClaimRecords 分页返回真实领取过累计充值奖励的用户。
|
||||||
@ -67,12 +52,10 @@ func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month
|
|||||||
var total int64
|
var total int64
|
||||||
countSQL := `
|
countSQL := `
|
||||||
SELECT COUNT(DISTINCT receive.user_id)
|
SELECT COUNT(DISTINCT receive.user_id)
|
||||||
FROM user_activity_reward_receive_record AS receive
|
FROM recharge_reward_grant_record AS receive
|
||||||
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
|
|
||||||
WHERE receive.recharge_date = ?
|
WHERE receive.recharge_date = ?
|
||||||
AND rule.sys_origin = ?
|
AND receive.sys_origin = ?
|
||||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
AND receive.status = 'SUCCESS'
|
||||||
AND COALESCE(rule.is_del, 0) = 0
|
|
||||||
AND (? = 0 OR receive.user_id = ?)`
|
AND (? = 0 OR receive.user_id = ?)`
|
||||||
if err := s.db.WithContext(ctx).Raw(
|
if err := s.db.WithContext(ctx).Raw(
|
||||||
countSQL,
|
countSQL,
|
||||||
@ -90,40 +73,34 @@ WHERE receive.recharge_date = ?
|
|||||||
listSQL := `
|
listSQL := `
|
||||||
SELECT
|
SELECT
|
||||||
claims.user_id,
|
claims.user_id,
|
||||||
COALESCE(recharge.recharge_amount, 0) AS recharge_amount,
|
claims.recharge_amount_cents,
|
||||||
COALESCE(recharge.last_recharge_time, '') AS last_recharge_time,
|
claims.last_recharge_time,
|
||||||
claims.last_claim_time,
|
claims.last_claim_time,
|
||||||
user.account,
|
claims.account,
|
||||||
user.user_avatar,
|
claims.user_avatar,
|
||||||
user.user_nickname,
|
claims.user_nickname,
|
||||||
user.country_code,
|
claims.country_code,
|
||||||
user.country_name
|
claims.country_name
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
receive.user_id,
|
receive.user_id,
|
||||||
MAX(receive.create_time) AS last_claim_time
|
MAX(receive.recharge_amount_cents) AS recharge_amount_cents,
|
||||||
FROM user_activity_reward_receive_record AS receive
|
COALESCE(MAX(receive.sent_at), MAX(receive.update_time), '') AS last_recharge_time,
|
||||||
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
|
COALESCE(MAX(receive.sent_at), MAX(receive.create_time), '') AS last_claim_time,
|
||||||
|
MAX(receive.account) AS account,
|
||||||
|
MAX(receive.user_avatar) AS user_avatar,
|
||||||
|
MAX(receive.user_nickname) AS user_nickname,
|
||||||
|
MAX(receive.country_code) AS country_code,
|
||||||
|
MAX(receive.country_name) AS country_name
|
||||||
|
FROM recharge_reward_grant_record AS receive
|
||||||
WHERE receive.recharge_date = ?
|
WHERE receive.recharge_date = ?
|
||||||
AND rule.sys_origin = ?
|
AND receive.sys_origin = ?
|
||||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
AND receive.status = 'SUCCESS'
|
||||||
AND COALESCE(rule.is_del, 0) = 0
|
|
||||||
AND (? = 0 OR receive.user_id = ?)
|
AND (? = 0 OR receive.user_id = ?)
|
||||||
GROUP BY receive.user_id
|
GROUP BY receive.user_id
|
||||||
ORDER BY MAX(receive.create_time) DESC, receive.user_id ASC
|
ORDER BY COALESCE(MAX(receive.sent_at), MAX(receive.create_time)) DESC, receive.user_id ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
) AS claims
|
) 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`
|
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,
|
||||||
@ -133,8 +110,6 @@ ORDER BY claims.last_claim_time DESC, claims.user_id ASC`
|
|||||||
userID,
|
userID,
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -207,7 +182,6 @@ func rechargeRewardClaimRecordView(
|
|||||||
details []rechargeRewardClaimDetailRow,
|
details []rechargeRewardClaimDetailRow,
|
||||||
rewardItems map[int64][]RechargeRewardRewardItem,
|
rewardItems map[int64][]RechargeRewardRewardItem,
|
||||||
) RechargeRewardClaimRecordView {
|
) RechargeRewardClaimRecordView {
|
||||||
rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount))
|
|
||||||
reached := claimDetailPayloads(details, rewardItems)
|
reached := claimDetailPayloads(details, rewardItems)
|
||||||
return RechargeRewardClaimRecordView{
|
return RechargeRewardClaimRecordView{
|
||||||
RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate),
|
RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate),
|
||||||
@ -219,8 +193,8 @@ func rechargeRewardClaimRecordView(
|
|||||||
CountryName: strings.TrimSpace(row.CountryName),
|
CountryName: strings.TrimSpace(row.CountryName),
|
||||||
RechargeMonth: month,
|
RechargeMonth: month,
|
||||||
RechargeDate: rechargeDate,
|
RechargeDate: rechargeDate,
|
||||||
RechargeAmount: formatAmountCents(rechargeCents),
|
RechargeAmount: formatAmountCents(row.RechargeAmountCents),
|
||||||
RechargeAmountCents: rechargeCents,
|
RechargeAmountCents: row.RechargeAmountCents,
|
||||||
ReachedLevels: reached,
|
ReachedLevels: reached,
|
||||||
ClaimedRewards: rechargeRewardClaimedRewards(reached),
|
ClaimedRewards: rechargeRewardClaimedRewards(reached),
|
||||||
LastRechargeTime: formatRechargeRewardRecordTime(row.LastRechargeTime),
|
LastRechargeTime: formatRechargeRewardRecordTime(row.LastRechargeTime),
|
||||||
@ -235,20 +209,19 @@ func (s *Service) loadClaimDetails(ctx context.Context, sysOrigin string, rechar
|
|||||||
detailSQL := `
|
detailSQL := `
|
||||||
SELECT
|
SELECT
|
||||||
receive.user_id,
|
receive.user_id,
|
||||||
receive.activity_id,
|
receive.level_id AS activity_id,
|
||||||
COALESCE(rule.sort, 0) AS level,
|
receive.level,
|
||||||
rule.resource_group_id,
|
receive.reward_group_id AS resource_group_id,
|
||||||
rule.rule_description,
|
receive.reward_group_name,
|
||||||
rule.json_data,
|
receive.recharge_threshold_cents,
|
||||||
receive.create_time AS claim_time
|
receive.reward_gold,
|
||||||
FROM user_activity_reward_receive_record AS receive
|
COALESCE(receive.sent_at, receive.create_time) AS claim_time
|
||||||
INNER JOIN props_activity_rule_config AS rule ON rule.id = receive.activity_id
|
FROM recharge_reward_grant_record AS receive
|
||||||
WHERE receive.recharge_date = ?
|
WHERE receive.recharge_date = ?
|
||||||
AND rule.sys_origin = ?
|
AND receive.sys_origin = ?
|
||||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
AND receive.status = 'SUCCESS'
|
||||||
AND COALESCE(rule.is_del, 0) = 0
|
|
||||||
AND receive.user_id IN ?
|
AND receive.user_id IN ?
|
||||||
ORDER BY receive.user_id ASC, COALESCE(rule.sort, 0) ASC, receive.create_time ASC`
|
ORDER BY receive.user_id ASC, receive.level ASC, receive.create_time ASC`
|
||||||
if err := s.db.WithContext(ctx).Raw(detailSQL, rechargeDate, sysOrigin, userIDs).Scan(&rows).Error; err != nil {
|
if err := s.db.WithContext(ctx).Raw(detailSQL, rechargeDate, sysOrigin, userIDs).Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -310,14 +283,14 @@ func claimDetailPayloads(
|
|||||||
if detail.ResourceGroupID != nil {
|
if detail.ResourceGroupID != nil {
|
||||||
items = rewardItems[*detail.ResourceGroupID]
|
items = rewardItems[*detail.ResourceGroupID]
|
||||||
}
|
}
|
||||||
thresholdCents := rechargeRewardRuleQuantityCents(detail.JSONData)
|
|
||||||
result = append(result, RechargeRewardLevelPayload{
|
result = append(result, RechargeRewardLevelPayload{
|
||||||
ID: detail.ActivityID,
|
ID: detail.ActivityID,
|
||||||
Level: level,
|
Level: level,
|
||||||
RechargeAmount: formatAmountCents(thresholdCents),
|
RechargeAmount: formatAmountCents(detail.RechargeThresholdCents),
|
||||||
RechargeAmountCents: thresholdCents,
|
RechargeAmountCents: detail.RechargeThresholdCents,
|
||||||
|
RewardGold: detail.RewardGold,
|
||||||
RewardGroupID: detail.ResourceGroupID,
|
RewardGroupID: detail.ResourceGroupID,
|
||||||
RewardGroupName: strings.TrimSpace(detail.RuleDescription),
|
RewardGroupName: strings.TrimSpace(detail.RewardGroupName),
|
||||||
RewardItems: items,
|
RewardItems: items,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Reached: true,
|
Reached: true,
|
||||||
@ -326,33 +299,6 @@ func claimDetailPayloads(
|
|||||||
return result
|
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,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
"chatapp3-golang/internal/config"
|
||||||
"chatapp3-golang/internal/integration"
|
"chatapp3-golang/internal/integration"
|
||||||
@ -22,14 +23,21 @@ func newTestService(t *testing.T, java rechargeRewardJavaGateway) (*Service, *go
|
|||||||
if err := db.AutoMigrate(
|
if err := db.AutoMigrate(
|
||||||
&model.RechargeRewardConfig{},
|
&model.RechargeRewardConfig{},
|
||||||
&model.RechargeRewardLevel{},
|
&model.RechargeRewardLevel{},
|
||||||
|
&model.RechargeRewardGrantRecord{},
|
||||||
&model.UserBaseInfo{},
|
&model.UserBaseInfo{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
t.Fatalf("auto migrate: %v", err)
|
t.Fatalf("auto migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := db.Exec(`ALTER TABLE user_base_info ADD COLUMN is_del INTEGER DEFAULT 0`).Error; err != nil {
|
||||||
|
t.Fatalf("add is_del: %v", err)
|
||||||
|
}
|
||||||
service := NewService(config.Config{
|
service := NewService(config.Config{
|
||||||
RechargeReward: config.RechargeRewardConfig{
|
RechargeReward: config.RechargeRewardConfig{
|
||||||
DefaultSysOrigin: "LIKEI",
|
DefaultSysOrigin: "LIKEI",
|
||||||
Timezone: "Asia/Riyadh",
|
Timezone: "Asia/Riyadh",
|
||||||
|
StorageTimezone: "Asia/Shanghai",
|
||||||
|
StartDate: "2020-01-01",
|
||||||
|
CoinSellerGoldPerUSD: 100000,
|
||||||
},
|
},
|
||||||
}, db, java)
|
}, db, java)
|
||||||
return service, db
|
return service, db
|
||||||
@ -107,7 +115,6 @@ func TestSaveConfigUpsertsBySysOrigin(t *testing.T) {
|
|||||||
func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
|
func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
|
||||||
groupID := int64(9001)
|
groupID := int64(9001)
|
||||||
java := rechargeRewardTestGateway{
|
java := rechargeRewardTestGateway{
|
||||||
monthly: integration.DecimalString("150.50"),
|
|
||||||
total: map[int64][]integration.UserTotalRecharge{
|
total: map[int64][]integration.UserTotalRecharge{
|
||||||
42: {
|
42: {
|
||||||
{UserID: integration.Int64Value(42), Type: "GOOGLE", Amount: integration.DecimalString("5000.00")},
|
{UserID: integration.Int64Value(42), Type: "GOOGLE", Amount: integration.DecimalString("5000.00")},
|
||||||
@ -130,7 +137,37 @@ func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
service, _ := newTestService(t, java)
|
service, db := newTestService(t, java)
|
||||||
|
createRechargeRewardSourceTables(t, db)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
rechargeAt := rechargeRewardStorageTime(time.Now().In(service.location), service.storageLocation)
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO order_purchase_history (user_id, sys_origin, evn, is_trial_period, status, pay_platform, unit_price, create_time) VALUES
|
||||||
|
(42, 'LIKEI', 'PROD', 0, 'COMPLETE', 'GOOGLE', 120.50, ?)
|
||||||
|
`, rechargeAt).Error; err != nil {
|
||||||
|
t.Fatalf("insert google recharge: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO order_user_purchase_pay (user_id, sys_origin, evn, factory_code, pay_status, receipt_type, refund_status, compute_usd_amount, create_time, update_time) VALUES
|
||||||
|
(42, 'LIKEI', 'PROD', 'MIFA_PAY', 'SUCCESSFUL', 'PAYMENT', 'NONE', 29.00, ?, ?)
|
||||||
|
`, rechargeAt, rechargeAt).Error; err != nil {
|
||||||
|
t.Fatalf("insert mifapay recharge: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO likei_wallet.user_freight_balance_running_water (accept_user_id, sys_origin, origin, type, quantity, create_time) VALUES
|
||||||
|
(42, 'LIKEI', 'SHIPMENT', 1, 100000, ?)
|
||||||
|
`, rechargeAt).Error; err != nil {
|
||||||
|
t.Fatalf("insert seller recharge: %v", err)
|
||||||
|
}
|
||||||
if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{
|
if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{
|
||||||
SysOrigin: "LIKEI",
|
SysOrigin: "LIKEI",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@ -174,39 +211,6 @@ func TestPageClaimRecordsUsesActualReceiveRecords(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
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 := 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,
|
||||||
@ -228,25 +232,33 @@ CREATE TABLE props_activity_rule_config (
|
|||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
t.Fatalf("create user: %v", err)
|
t.Fatalf("create user: %v", err)
|
||||||
}
|
}
|
||||||
if err := db.Exec(`
|
if err := db.Create(&model.RechargeRewardGrantRecord{
|
||||||
INSERT INTO user_monthly_recharge_v2 (user_id, type, amount, recharge_date, update_time) VALUES
|
ID: 8001,
|
||||||
(42, 'GOOGLE', 150.50, 202605, '2026-05-07 10:00:00'),
|
ConfigID: 1001,
|
||||||
(42, 'MIFA_PAY', 70.00, 202605, '2026-05-07 11:00:00'),
|
LevelID: 7001,
|
||||||
(43, 'GOOGLE', 300.00, 202605, '2026-05-07 12:00:00')
|
SysOrigin: "LIKEI",
|
||||||
`).Error; err != nil {
|
CycleKey: "20260521",
|
||||||
t.Fatalf("insert monthly recharge: %v", err)
|
RechargeDate: 202605,
|
||||||
}
|
PeriodStartAt: time.Date(2026, 5, 21, 0, 0, 0, 0, time.UTC),
|
||||||
if err := db.Exec(`
|
PeriodEndAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||||
INSERT INTO props_activity_rule_config (id, sys_origin, activity_type, resource_group_id, json_data, sort, rule_description, is_del) VALUES
|
UserID: 42,
|
||||||
(7001, 'LIKEI', 'CUMULATIVE_RECHARGE', 9001, '{"quantity":100}', 1, 'Gift Box', 0)
|
Account: "1000042",
|
||||||
`).Error; err != nil {
|
UserNickname: "Nina",
|
||||||
t.Fatalf("insert activity rule: %v", err)
|
CountryCode: "SA",
|
||||||
}
|
CountryName: "Saudi Arabia",
|
||||||
if err := db.Exec(`
|
RechargeAmountCents: 15050,
|
||||||
INSERT INTO user_activity_reward_receive_record (id, user_id, recharge_date, activity_id, create_time) VALUES
|
Level: 1,
|
||||||
(8001, 42, 202605, 7001, '2026-05-07 10:30:00')
|
RechargeThresholdCents: 10000,
|
||||||
`).Error; err != nil {
|
RewardGroupID: &groupID,
|
||||||
t.Fatalf("insert receive record: %v", err)
|
RewardGroupName: "Gift Box",
|
||||||
|
Status: "SUCCESS",
|
||||||
|
RewardGroupStatus: "SUCCESS",
|
||||||
|
RewardGoldStatus: "SKIPPED",
|
||||||
|
SentAt: ptrTime(time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC)),
|
||||||
|
CreateTime: time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC),
|
||||||
|
UpdateTime: time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC),
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("insert grant 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)
|
||||||
@ -302,6 +314,56 @@ func ptrFlexibleInt64(value int64) flexibleOptionalInt64 {
|
|||||||
return flexibleOptionalInt64{value: &value}
|
return flexibleOptionalInt64{value: &value}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptrTime(value time.Time) *time.Time {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRechargeRewardSourceTables(t *testing.T, db *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
if err := db.Exec(`ATTACH DATABASE ':memory:' AS likei_wallet`).Error; err != nil {
|
||||||
|
t.Fatalf("attach wallet schema: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
CREATE TABLE order_purchase_history (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
sys_origin TEXT NOT NULL,
|
||||||
|
evn TEXT NOT NULL,
|
||||||
|
is_trial_period INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
pay_platform TEXT NOT NULL,
|
||||||
|
unit_price DECIMAL(12,2) NOT NULL,
|
||||||
|
create_time DATETIME NOT NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("create order_purchase_history: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
CREATE TABLE order_user_purchase_pay (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
sys_origin TEXT NOT NULL,
|
||||||
|
evn TEXT NOT NULL,
|
||||||
|
factory_code TEXT NOT NULL,
|
||||||
|
pay_status TEXT NOT NULL,
|
||||||
|
receipt_type TEXT NOT NULL,
|
||||||
|
refund_status TEXT NOT NULL,
|
||||||
|
compute_usd_amount DECIMAL(12,2) NOT NULL,
|
||||||
|
create_time DATETIME NOT NULL,
|
||||||
|
update_time DATETIME NOT NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("create order_user_purchase_pay: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
CREATE TABLE likei_wallet.user_freight_balance_running_water (
|
||||||
|
accept_user_id INTEGER NOT NULL,
|
||||||
|
sys_origin TEXT NOT NULL,
|
||||||
|
origin TEXT NOT NULL,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
quantity DECIMAL(20,2) NOT NULL,
|
||||||
|
create_time DATETIME NOT NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("create wallet freight water: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type rechargeRewardTestGateway struct {
|
type rechargeRewardTestGateway struct {
|
||||||
monthly integration.DecimalString
|
monthly integration.DecimalString
|
||||||
total map[int64][]integration.UserTotalRecharge
|
total map[int64][]integration.UserTotalRecharge
|
||||||
|
|||||||
@ -26,11 +26,15 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo
|
|||||||
location = resolveLocation(bundle.Config.Timezone, s.location)
|
location = resolveLocation(bundle.Config.Timezone, s.location)
|
||||||
}
|
}
|
||||||
now := time.Now().In(location)
|
now := time.Now().In(location)
|
||||||
periodStart, periodEnd := currentMonthBounds(now, location)
|
period := s.displayPeriod(now, location)
|
||||||
|
currentPeriod, periodActive := s.currentPeriod(now, location)
|
||||||
|
|
||||||
currentRechargeCents, err := s.loadCurrentRechargeCents(ctx, user.UserID)
|
var currentRechargeCents int64
|
||||||
if err != nil {
|
if periodActive {
|
||||||
return nil, err
|
currentRechargeCents, err = s.loadCurrentRechargeCents(ctx, sysOrigin, user.UserID, currentPeriod)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
accumulatedRechargeCents, err := s.loadAccumulatedRechargeCents(ctx, user.UserID)
|
accumulatedRechargeCents, err := s.loadAccumulatedRechargeCents(ctx, user.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -43,9 +47,9 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo
|
|||||||
Enabled: bundle.Config != nil && bundle.Config.Enabled,
|
Enabled: bundle.Config != nil && bundle.Config.Enabled,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
Timezone: location.String(),
|
Timezone: location.String(),
|
||||||
PeriodStartAt: formatDateTime(periodStart),
|
PeriodStartAt: formatDateTime(period.StartAt),
|
||||||
PeriodEndAt: formatDateTime(periodEnd),
|
PeriodEndAt: formatDateTime(period.EndAt),
|
||||||
CountdownMillis: maxInt64(0, periodEnd.UnixMilli()-now.UnixMilli()),
|
CountdownMillis: maxInt64(0, period.EndAt.UnixMilli()-now.UnixMilli()),
|
||||||
UserID: user.UserID,
|
UserID: user.UserID,
|
||||||
CurrentRechargeAmount: formatAmountCents(currentRechargeCents),
|
CurrentRechargeAmount: formatAmountCents(currentRechargeCents),
|
||||||
CurrentRechargeAmountCents: currentRechargeCents,
|
CurrentRechargeAmountCents: currentRechargeCents,
|
||||||
@ -88,14 +92,6 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) loadCurrentRechargeCents(ctx context.Context, userID int64) (int64, error) {
|
|
||||||
amount, err := s.java.GetThisMonthTotalPersonalRecharge(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return parseIntegrationAmountCents(integrationAmount(amount)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadAccumulatedRechargeCents(ctx context.Context, userID int64) (int64, error) {
|
func (s *Service) loadAccumulatedRechargeCents(ctx context.Context, userID int64) (int64, error) {
|
||||||
recharges, err := s.java.MapUserTotalRecharge(ctx, []int64{userID})
|
recharges, err := s.java.MapUserTotalRecharge(ctx, []int64{userID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -104,12 +100,6 @@ func (s *Service) loadAccumulatedRechargeCents(ctx context.Context, userID int64
|
|||||||
return totalRechargeCents(recharges[userID]), nil
|
return totalRechargeCents(recharges[userID]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentMonthBounds(now time.Time, location *time.Location) (time.Time, time.Time) {
|
|
||||||
local := now.In(location)
|
|
||||||
start := time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, location)
|
|
||||||
return start, start.AddDate(0, 1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLocation(timezone string, fallback *time.Location) *time.Location {
|
func resolveLocation(timezone string, fallback *time.Location) *time.Location {
|
||||||
location, err := time.LoadLocation(normalizeRechargeRewardTimezone(timezone))
|
location, err := time.LoadLocation(normalizeRechargeRewardTimezone(timezone))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
75
internal/service/rechargereward/period.go
Normal file
75
internal/service/rechargereward/period.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package rechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rechargeRewardPeriod struct {
|
||||||
|
CycleKey string
|
||||||
|
RechargeDate int
|
||||||
|
StartAt time.Time
|
||||||
|
EndAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) currentPeriod(now time.Time, location *time.Location) (rechargeRewardPeriod, bool) {
|
||||||
|
startAt := rechargeRewardStartAt(s.cfg.RechargeReward.StartDate, location)
|
||||||
|
return currentRechargeRewardPeriod(now, startAt, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) displayPeriod(now time.Time, location *time.Location) rechargeRewardPeriod {
|
||||||
|
startAt := rechargeRewardStartAt(s.cfg.RechargeReward.StartDate, location)
|
||||||
|
period, ok := currentRechargeRewardPeriod(now, startAt, location)
|
||||||
|
if ok {
|
||||||
|
return period
|
||||||
|
}
|
||||||
|
monthStart := time.Date(startAt.Year(), startAt.Month(), 1, 0, 0, 0, 0, location)
|
||||||
|
return rechargeRewardPeriod{
|
||||||
|
CycleKey: startAt.Format("20060102"),
|
||||||
|
RechargeDate: startAt.Year()*100 + int(startAt.Month()),
|
||||||
|
StartAt: startAt,
|
||||||
|
EndAt: monthStart.AddDate(0, 1, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentRechargeRewardPeriod(now time.Time, startAt time.Time, location *time.Location) (rechargeRewardPeriod, bool) {
|
||||||
|
local := now.In(location)
|
||||||
|
start := startAt.In(location)
|
||||||
|
if local.Before(start) {
|
||||||
|
return rechargeRewardPeriod{}, false
|
||||||
|
}
|
||||||
|
monthStart := time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, location)
|
||||||
|
periodStart := monthStart
|
||||||
|
if periodStart.Before(start) {
|
||||||
|
periodStart = start
|
||||||
|
}
|
||||||
|
periodEnd := monthStart.AddDate(0, 1, 0)
|
||||||
|
if !local.Before(periodEnd) {
|
||||||
|
return rechargeRewardPeriod{}, false
|
||||||
|
}
|
||||||
|
return rechargeRewardPeriod{
|
||||||
|
CycleKey: periodStart.Format("20060102"),
|
||||||
|
RechargeDate: periodStart.Year()*100 + int(periodStart.Month()),
|
||||||
|
StartAt: periodStart,
|
||||||
|
EndAt: periodEnd,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func rechargeRewardStartAt(raw string, location *time.Location) time.Time {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" {
|
||||||
|
value = "2026-05-21"
|
||||||
|
}
|
||||||
|
parsed, err := time.ParseInLocation("2006-01-02", value, location)
|
||||||
|
if err != nil {
|
||||||
|
parsed, _ = time.ParseInLocation("2006-01-02", "2026-05-21", location)
|
||||||
|
}
|
||||||
|
return time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rechargeRewardStorageTime(t time.Time, storageLocation *time.Location) string {
|
||||||
|
if storageLocation == nil {
|
||||||
|
storageLocation = t.Location()
|
||||||
|
}
|
||||||
|
return t.In(storageLocation).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
102
internal/service/rechargereward/recharge_amount.go
Normal file
102
internal/service/rechargereward/recharge_amount.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package rechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rechargeRewardOrderPlatforms = []string{
|
||||||
|
"GOOGLE",
|
||||||
|
"PAYER_MAX",
|
||||||
|
"AIRWALLEX",
|
||||||
|
"PAYNICORN",
|
||||||
|
"STRIPE",
|
||||||
|
"PAY_PAL",
|
||||||
|
"CLIPSPAY",
|
||||||
|
}
|
||||||
|
|
||||||
|
type rechargeRewardAmountRow struct {
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadCurrentRechargeCents(ctx context.Context, sysOrigin string, userID int64, period rechargeRewardPeriod) (int64, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||||
|
startAt := rechargeRewardStorageTime(period.StartAt, s.storageLocation)
|
||||||
|
endAt := rechargeRewardStorageTime(period.EndAt, s.storageLocation)
|
||||||
|
coinSellerGoldPerUSD := s.cfg.RechargeReward.CoinSellerGoldPerUSD
|
||||||
|
if coinSellerGoldPerUSD <= 0 {
|
||||||
|
coinSellerGoldPerUSD = 100000
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT CAST(IFNULL(SUM(source.amount_cents), 0) AS SIGNED) AS recharge_amount_cents
|
||||||
|
FROM (
|
||||||
|
SELECT CAST(ROUND(SUM(IFNULL(t.unit_price, 0)) * 100, 0) AS SIGNED) AS amount_cents
|
||||||
|
FROM order_purchase_history AS t
|
||||||
|
INNER JOIN user_base_info AS u ON u.id = t.user_id
|
||||||
|
WHERE t.evn = 'PROD'
|
||||||
|
AND t.is_trial_period = 0
|
||||||
|
AND t.status = 'COMPLETE'
|
||||||
|
AND t.pay_platform IN ?
|
||||||
|
AND (u.is_del = 0 OR u.is_del IS NULL)
|
||||||
|
AND UPPER(TRIM(u.origin_sys)) = ?
|
||||||
|
AND t.user_id = ?
|
||||||
|
AND t.create_time >= ?
|
||||||
|
AND t.create_time < ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT CAST(ROUND(SUM(IFNULL(t.compute_usd_amount, 0)) * 100, 0) AS SIGNED) AS amount_cents
|
||||||
|
FROM order_user_purchase_pay AS t
|
||||||
|
INNER JOIN user_base_info AS u ON u.id = t.user_id
|
||||||
|
WHERE t.evn = 'PROD'
|
||||||
|
AND t.factory_code = 'MIFA_PAY'
|
||||||
|
AND t.pay_status = 'SUCCESSFUL'
|
||||||
|
AND t.receipt_type = 'PAYMENT'
|
||||||
|
AND t.refund_status = 'NONE'
|
||||||
|
AND (u.is_del = 0 OR u.is_del IS NULL)
|
||||||
|
AND UPPER(TRIM(u.origin_sys)) = ?
|
||||||
|
AND t.user_id = ?
|
||||||
|
AND IFNULL(t.update_time, t.create_time) >= ?
|
||||||
|
AND IFNULL(t.update_time, t.create_time) < ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT CAST(ROUND(SUM(IFNULL(t.quantity, 0) * 100 / ?), 0) AS SIGNED) AS amount_cents
|
||||||
|
FROM likei_wallet.user_freight_balance_running_water AS t
|
||||||
|
INNER JOIN user_base_info AS u ON u.id = t.accept_user_id
|
||||||
|
WHERE t.origin = 'SHIPMENT'
|
||||||
|
AND t.type = 1
|
||||||
|
AND t.accept_user_id IS NOT NULL
|
||||||
|
AND IFNULL(t.quantity, 0) > 0
|
||||||
|
AND (u.is_del = 0 OR u.is_del IS NULL)
|
||||||
|
AND UPPER(TRIM(u.origin_sys)) = ?
|
||||||
|
AND t.accept_user_id = ?
|
||||||
|
AND t.create_time >= ?
|
||||||
|
AND t.create_time < ?
|
||||||
|
) AS source`
|
||||||
|
|
||||||
|
var row rechargeRewardAmountRow
|
||||||
|
if err := s.db.WithContext(ctx).Raw(
|
||||||
|
query,
|
||||||
|
rechargeRewardOrderPlatforms,
|
||||||
|
sysOrigin,
|
||||||
|
userID,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
sysOrigin,
|
||||||
|
userID,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
coinSellerGoldPerUSD,
|
||||||
|
sysOrigin,
|
||||||
|
userID,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
).Scan(&row).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if row.RechargeAmountCents < 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return row.RechargeAmountCents, nil
|
||||||
|
}
|
||||||
@ -41,10 +41,11 @@ type rechargeRewardJavaGateway interface {
|
|||||||
|
|
||||||
// Service 负责充值奖励后台配置和 H5 用户态数据。
|
// Service 负责充值奖励后台配置和 H5 用户态数据。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg config.Config
|
cfg config.Config
|
||||||
db rechargeRewardDB
|
db rechargeRewardDB
|
||||||
java rechargeRewardJavaGateway
|
java rechargeRewardJavaGateway
|
||||||
location *time.Location
|
location *time.Location
|
||||||
|
storageLocation *time.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(cfg config.Config, db rechargeRewardDB, java rechargeRewardJavaGateway) *Service {
|
func NewService(cfg config.Config, db rechargeRewardDB, java rechargeRewardJavaGateway) *Service {
|
||||||
@ -56,7 +57,15 @@ func NewService(cfg config.Config, db rechargeRewardDB, java rechargeRewardJavaG
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
location = time.UTC
|
location = time.UTC
|
||||||
}
|
}
|
||||||
return &Service{cfg: cfg, db: db, java: java, location: location}
|
storageTimezone := strings.TrimSpace(cfg.RechargeReward.StorageTimezone)
|
||||||
|
if storageTimezone == "" {
|
||||||
|
storageTimezone = "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
storageLocation, err := time.LoadLocation(storageTimezone)
|
||||||
|
if err != nil {
|
||||||
|
storageLocation = location
|
||||||
|
}
|
||||||
|
return &Service{cfg: cfg, db: db, java: java, location: location, storageLocation: storageLocation}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RechargeRewardLevelPayload 是前后台共用的充值奖励档位读模型。
|
// RechargeRewardLevelPayload 是前后台共用的充值奖励档位读模型。
|
||||||
|
|||||||
38
migrations/048_recharge_reward_grant_record.sql
Normal file
38
migrations/048_recharge_reward_grant_record.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `recharge_reward_grant_record` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`config_id` bigint NOT NULL COMMENT '充值奖励主配置ID',
|
||||||
|
`level_id` bigint NOT NULL COMMENT '充值奖励档位ID',
|
||||||
|
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||||
|
`cycle_key` varchar(32) NOT NULL COMMENT '充值周期标识,周期开始日yyyyMMdd',
|
||||||
|
`recharge_date` int NOT NULL COMMENT '充值年月yyyyMM,用于后台筛选',
|
||||||
|
`period_start_at` datetime(3) NOT NULL COMMENT '周期开始时间,活动时区墙上时间',
|
||||||
|
`period_end_at` datetime(3) NOT NULL COMMENT '周期结束时间,活动时区墙上时间',
|
||||||
|
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT '' COMMENT '用户账号快照',
|
||||||
|
`user_avatar` varchar(1024) NOT NULL DEFAULT '' COMMENT '用户头像快照',
|
||||||
|
`user_nickname` varchar(255) NOT NULL DEFAULT '' COMMENT '用户昵称快照',
|
||||||
|
`country_code` varchar(32) NOT NULL DEFAULT '' COMMENT '国家编码快照',
|
||||||
|
`country_name` varchar(128) NOT NULL DEFAULT '' COMMENT '国家名称快照',
|
||||||
|
`recharge_amount_cents` bigint NOT NULL DEFAULT '0' COMMENT '发放时周期累计充值金额,单位为分',
|
||||||
|
`level` int NOT NULL COMMENT '奖励档位',
|
||||||
|
`recharge_threshold_cents` bigint NOT NULL DEFAULT '0' COMMENT '充值金额门槛,单位为分',
|
||||||
|
`reward_gold` bigint NOT NULL DEFAULT '0' COMMENT '金币奖励数量',
|
||||||
|
`reward_group_id` bigint DEFAULT NULL COMMENT '奖励组ID',
|
||||||
|
`reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励组名称快照',
|
||||||
|
`reward_group_track_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励组发放幂等追踪ID',
|
||||||
|
`gold_event_id` varchar(128) NOT NULL DEFAULT '' COMMENT '金币发放钱包事件ID',
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT '整体发放状态:PENDING/SUCCESS/FAILED',
|
||||||
|
`reward_group_status` varchar(32) NOT NULL DEFAULT 'SKIPPED' COMMENT '奖励组发放状态:PENDING/SUCCESS/FAILED/SKIPPED',
|
||||||
|
`reward_gold_status` varchar(32) NOT NULL DEFAULT 'SKIPPED' COMMENT '金币发放状态:PENDING/SUCCESS/FAILED/SKIPPED',
|
||||||
|
`retry_count` int NOT NULL DEFAULT '0' COMMENT '重试次数',
|
||||||
|
`last_error` varchar(1024) NOT NULL DEFAULT '' COMMENT '最近一次失败原因',
|
||||||
|
`sent_at` datetime(3) DEFAULT NULL COMMENT '整体发放成功时间',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_recharge_reward_grant_level` (`sys_origin`, `cycle_key`, `user_id`, `level`),
|
||||||
|
KEY `idx_recharge_reward_grant_config_cycle` (`config_id`, `cycle_key`, `status`),
|
||||||
|
KEY `idx_recharge_reward_grant_user` (`user_id`, `recharge_date`),
|
||||||
|
KEY `idx_recharge_reward_grant_date` (`sys_origin`, `recharge_date`, `status`),
|
||||||
|
KEY `idx_recharge_reward_gold_event` (`gold_event_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值奖励周期发放记录';
|
||||||
Loading…
x
Reference in New Issue
Block a user