充值奖励结算
This commit is contained in:
parent
810dc0345b
commit
74063d0908
@ -81,8 +81,11 @@ type InviteConfig struct {
|
||||
|
||||
// RechargeRewardConfig 保存充值奖励活动配置。
|
||||
type RechargeRewardConfig struct {
|
||||
DefaultSysOrigin string
|
||||
Timezone string
|
||||
DefaultSysOrigin string
|
||||
Timezone string
|
||||
StorageTimezone string
|
||||
StartDate string
|
||||
CoinSellerGoldPerUSD int64
|
||||
}
|
||||
|
||||
// TaskCenterConfig 保存任务中心模块配置。
|
||||
@ -313,6 +316,18 @@ func Load() Config {
|
||||
[]string{"CHATAPP_RECHARGE_REWARD_TIMEZONE", "RECHARGE_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||
"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{
|
||||
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||
|
||||
@ -29,3 +29,39 @@ type RechargeRewardLevel struct {
|
||||
}
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"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"`
|
||||
LastClaimTime string `gorm:"column:last_claim_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"`
|
||||
UserID int64 `gorm:"column:user_id"`
|
||||
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||
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"`
|
||||
CountryCode string `gorm:"column:country_code"`
|
||||
CountryName string `gorm:"column:country_name"`
|
||||
}
|
||||
|
||||
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"`
|
||||
UserID int64 `gorm:"column:user_id"`
|
||||
ActivityID int64 `gorm:"column:activity_id"`
|
||||
Level int `gorm:"column:level"`
|
||||
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
|
||||
RewardGroupName string `gorm:"column:reward_group_name"`
|
||||
RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"`
|
||||
RewardGold int64 `gorm:"column:reward_gold"`
|
||||
ClaimTime string `gorm:"column:claim_time"`
|
||||
}
|
||||
|
||||
// PageClaimRecords 分页返回真实领取过累计充值奖励的用户。
|
||||
@ -67,12 +52,10 @@ func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, month
|
||||
var total int64
|
||||
countSQL := `
|
||||
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
|
||||
FROM recharge_reward_grant_record AS receive
|
||||
WHERE receive.recharge_date = ?
|
||||
AND rule.sys_origin = ?
|
||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
||||
AND COALESCE(rule.is_del, 0) = 0
|
||||
AND receive.sys_origin = ?
|
||||
AND receive.status = 'SUCCESS'
|
||||
AND (? = 0 OR receive.user_id = ?)`
|
||||
if err := s.db.WithContext(ctx).Raw(
|
||||
countSQL,
|
||||
@ -90,40 +73,34 @@ WHERE receive.recharge_date = ?
|
||||
listSQL := `
|
||||
SELECT
|
||||
claims.user_id,
|
||||
COALESCE(recharge.recharge_amount, 0) AS recharge_amount,
|
||||
COALESCE(recharge.last_recharge_time, '') AS last_recharge_time,
|
||||
claims.recharge_amount_cents,
|
||||
claims.last_recharge_time,
|
||||
claims.last_claim_time,
|
||||
user.account,
|
||||
user.user_avatar,
|
||||
user.user_nickname,
|
||||
user.country_code,
|
||||
user.country_name
|
||||
claims.account,
|
||||
claims.user_avatar,
|
||||
claims.user_nickname,
|
||||
claims.country_code,
|
||||
claims.country_name
|
||||
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
|
||||
MAX(receive.recharge_amount_cents) AS recharge_amount_cents,
|
||||
COALESCE(MAX(receive.sent_at), MAX(receive.update_time), '') AS last_recharge_time,
|
||||
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 = ?
|
||||
AND rule.sys_origin = ?
|
||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
||||
AND COALESCE(rule.is_del, 0) = 0
|
||||
AND receive.sys_origin = ?
|
||||
AND receive.status = 'SUCCESS'
|
||||
AND (? = 0 OR 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 ?
|
||||
) 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,
|
||||
@ -133,8 +110,6 @@ ORDER BY claims.last_claim_time DESC, claims.user_id ASC`
|
||||
userID,
|
||||
limit,
|
||||
(cursor-1)*limit,
|
||||
rechargeDate,
|
||||
personalMonthlyRechargeTypes,
|
||||
).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -207,7 +182,6 @@ func rechargeRewardClaimRecordView(
|
||||
details []rechargeRewardClaimDetailRow,
|
||||
rewardItems map[int64][]RechargeRewardRewardItem,
|
||||
) RechargeRewardClaimRecordView {
|
||||
rechargeCents := parseIntegrationAmountCents(integrationAmount(row.RechargeAmount))
|
||||
reached := claimDetailPayloads(details, rewardItems)
|
||||
return RechargeRewardClaimRecordView{
|
||||
RecordKey: fmt.Sprintf("%d-%d", row.UserID, rechargeDate),
|
||||
@ -219,8 +193,8 @@ func rechargeRewardClaimRecordView(
|
||||
CountryName: strings.TrimSpace(row.CountryName),
|
||||
RechargeMonth: month,
|
||||
RechargeDate: rechargeDate,
|
||||
RechargeAmount: formatAmountCents(rechargeCents),
|
||||
RechargeAmountCents: rechargeCents,
|
||||
RechargeAmount: formatAmountCents(row.RechargeAmountCents),
|
||||
RechargeAmountCents: row.RechargeAmountCents,
|
||||
ReachedLevels: reached,
|
||||
ClaimedRewards: rechargeRewardClaimedRewards(reached),
|
||||
LastRechargeTime: formatRechargeRewardRecordTime(row.LastRechargeTime),
|
||||
@ -235,20 +209,19 @@ func (s *Service) loadClaimDetails(ctx context.Context, sysOrigin string, rechar
|
||||
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
|
||||
receive.level_id AS activity_id,
|
||||
receive.level,
|
||||
receive.reward_group_id AS resource_group_id,
|
||||
receive.reward_group_name,
|
||||
receive.recharge_threshold_cents,
|
||||
receive.reward_gold,
|
||||
COALESCE(receive.sent_at, receive.create_time) AS claim_time
|
||||
FROM recharge_reward_grant_record AS receive
|
||||
WHERE receive.recharge_date = ?
|
||||
AND rule.sys_origin = ?
|
||||
AND rule.activity_type = 'CUMULATIVE_RECHARGE'
|
||||
AND COALESCE(rule.is_del, 0) = 0
|
||||
AND receive.sys_origin = ?
|
||||
AND receive.status = 'SUCCESS'
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -310,14 +283,14 @@ func claimDetailPayloads(
|
||||
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,
|
||||
RechargeAmount: formatAmountCents(detail.RechargeThresholdCents),
|
||||
RechargeAmountCents: detail.RechargeThresholdCents,
|
||||
RewardGold: detail.RewardGold,
|
||||
RewardGroupID: detail.ResourceGroupID,
|
||||
RewardGroupName: strings.TrimSpace(detail.RuleDescription),
|
||||
RewardGroupName: strings.TrimSpace(detail.RewardGroupName),
|
||||
RewardItems: items,
|
||||
Enabled: true,
|
||||
Reached: true,
|
||||
@ -326,33 +299,6 @@ func claimDetailPayloads(
|
||||
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,
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
@ -22,14 +23,21 @@ func newTestService(t *testing.T, java rechargeRewardJavaGateway) (*Service, *go
|
||||
if err := db.AutoMigrate(
|
||||
&model.RechargeRewardConfig{},
|
||||
&model.RechargeRewardLevel{},
|
||||
&model.RechargeRewardGrantRecord{},
|
||||
&model.UserBaseInfo{},
|
||||
); err != nil {
|
||||
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{
|
||||
RechargeReward: config.RechargeRewardConfig{
|
||||
DefaultSysOrigin: "LIKEI",
|
||||
Timezone: "Asia/Riyadh",
|
||||
DefaultSysOrigin: "LIKEI",
|
||||
Timezone: "Asia/Riyadh",
|
||||
StorageTimezone: "Asia/Shanghai",
|
||||
StartDate: "2020-01-01",
|
||||
CoinSellerGoldPerUSD: 100000,
|
||||
},
|
||||
}, db, java)
|
||||
return service, db
|
||||
@ -107,7 +115,6 @@ func TestSaveConfigUpsertsBySysOrigin(t *testing.T) {
|
||||
func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) {
|
||||
groupID := int64(9001)
|
||||
java := rechargeRewardTestGateway{
|
||||
monthly: integration.DecimalString("150.50"),
|
||||
total: map[int64][]integration.UserTotalRecharge{
|
||||
42: {
|
||||
{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{
|
||||
SysOrigin: "LIKEI",
|
||||
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{
|
||||
SysOrigin: "LIKEI",
|
||||
Enabled: true,
|
||||
@ -228,25 +232,33 @@ CREATE TABLE props_activity_rule_config (
|
||||
}).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'),
|
||||
(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)
|
||||
if err := db.Create(&model.RechargeRewardGrantRecord{
|
||||
ID: 8001,
|
||||
ConfigID: 1001,
|
||||
LevelID: 7001,
|
||||
SysOrigin: "LIKEI",
|
||||
CycleKey: "20260521",
|
||||
RechargeDate: 202605,
|
||||
PeriodStartAt: time.Date(2026, 5, 21, 0, 0, 0, 0, time.UTC),
|
||||
PeriodEndAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||
UserID: 42,
|
||||
Account: "1000042",
|
||||
UserNickname: "Nina",
|
||||
CountryCode: "SA",
|
||||
CountryName: "Saudi Arabia",
|
||||
RechargeAmountCents: 15050,
|
||||
Level: 1,
|
||||
RechargeThresholdCents: 10000,
|
||||
RewardGroupID: &groupID,
|
||||
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)
|
||||
@ -302,6 +314,56 @@ func ptrFlexibleInt64(value int64) flexibleOptionalInt64 {
|
||||
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 {
|
||||
monthly integration.DecimalString
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var currentRechargeCents int64
|
||||
if periodActive {
|
||||
currentRechargeCents, err = s.loadCurrentRechargeCents(ctx, sysOrigin, user.UserID, currentPeriod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
accumulatedRechargeCents, err := s.loadAccumulatedRechargeCents(ctx, user.UserID)
|
||||
if err != nil {
|
||||
@ -43,9 +47,9 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo
|
||||
Enabled: bundle.Config != nil && bundle.Config.Enabled,
|
||||
SysOrigin: sysOrigin,
|
||||
Timezone: location.String(),
|
||||
PeriodStartAt: formatDateTime(periodStart),
|
||||
PeriodEndAt: formatDateTime(periodEnd),
|
||||
CountdownMillis: maxInt64(0, periodEnd.UnixMilli()-now.UnixMilli()),
|
||||
PeriodStartAt: formatDateTime(period.StartAt),
|
||||
PeriodEndAt: formatDateTime(period.EndAt),
|
||||
CountdownMillis: maxInt64(0, period.EndAt.UnixMilli()-now.UnixMilli()),
|
||||
UserID: user.UserID,
|
||||
CurrentRechargeAmount: formatAmountCents(currentRechargeCents),
|
||||
CurrentRechargeAmountCents: currentRechargeCents,
|
||||
@ -88,14 +92,6 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo
|
||||
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) {
|
||||
recharges, err := s.java.MapUserTotalRecharge(ctx, []int64{userID})
|
||||
if err != nil {
|
||||
@ -104,12 +100,6 @@ func (s *Service) loadAccumulatedRechargeCents(ctx context.Context, userID int64
|
||||
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 {
|
||||
location, err := time.LoadLocation(normalizeRechargeRewardTimezone(timezone))
|
||||
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 用户态数据。
|
||||
type Service struct {
|
||||
cfg config.Config
|
||||
db rechargeRewardDB
|
||||
java rechargeRewardJavaGateway
|
||||
location *time.Location
|
||||
cfg config.Config
|
||||
db rechargeRewardDB
|
||||
java rechargeRewardJavaGateway
|
||||
location *time.Location
|
||||
storageLocation *time.Location
|
||||
}
|
||||
|
||||
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 {
|
||||
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 是前后台共用的充值奖励档位读模型。
|
||||
|
||||
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