feat: add dashboard recharge user metrics

This commit is contained in:
zhx 2026-05-27 21:33:56 +08:00
parent 422fab1bf3
commit fcc11122d4
12 changed files with 310 additions and 53 deletions

View File

@ -36,7 +36,7 @@ func (m *Mapper) Map(ctx context.Context, schema string, table string, action st
case "order_purchase_history":
return m.mapOfficialRecharge(ctx, schema, table, action, row)
case "order_user_purchase_pay":
return m.mapMifapayRecharge(ctx, schema, table, action, row)
return m.mapThirdPartyRecharge(ctx, schema, table, action, row)
case "game_lucky_gift_count":
return m.mapLuckyGift(ctx, schema, table, action, row)
case "game_lucky_box_count":
@ -96,6 +96,7 @@ func (m *Mapper) mapOfficialRecharge(ctx context.Context, schema string, table s
}
if payPlatform == "GOOGLE" {
c.GoogleRecharge = amount
c.UserRecharge = amount
}
c.NewUserRechargeCandidate = amount
c.RechargeDetail = rechargeDetail("OFFICIAL", rowPK(row), payPlatform, row, user, amount,
@ -103,19 +104,26 @@ func (m *Mapper) mapOfficialRecharge(ctx context.Context, schema string, table s
return []Contribution{c}, nil
}
func (m *Mapper) mapMifapayRecharge(ctx context.Context, schema string, table string, action string, row Row) ([]Contribution, error) {
if row.String("evn") != "PROD" || row.String("factory_code") != "MIFA_PAY" ||
func (m *Mapper) mapThirdPartyRecharge(ctx context.Context, schema string, table string, action string, row Row) ([]Contribution, error) {
if row.String("evn") != "PROD" ||
row.String("pay_status") != "SUCCESSFUL" || row.String("receipt_type") != "PAYMENT" ||
row.String("refund_status") != "NONE" {
return nil, nil
}
items, err := m.amountContribution(ctx, schema, table, action, row, row.Decimal("compute_usd_amount"), func(c *Contribution, amount decimal.Decimal) {
c.UserRecharge = amount
if row.String("factory_code") == "MIFA_PAY" {
c.MifapayRecharge = amount
}
})
if err != nil || len(items) == 0 {
return items, err
}
items[0].RechargeDetail = rechargeDetail("MIFAPAY", rowPK(row), row.String("factory_code"), row,
sourceType := "THIRD_PARTY"
if row.String("factory_code") == "MIFA_PAY" {
sourceType = "MIFAPAY"
}
items[0].RechargeDetail = rechargeDetail(sourceType, rowPK(row), row.String("factory_code"), row,
detailUser(items[0]), row.Decimal("compute_usd_amount"), decimal.Zero, "")
return items, nil
}
@ -126,6 +134,7 @@ func (m *Mapper) mapDealerRecharge(ctx context.Context, schema string, table str
}
items, err := m.amountContribution(ctx, schema, table, action, row, row.Decimal("amount"), func(c *Contribution, amount decimal.Decimal) {
c.DealerRecharge = amount
c.NewDealerUserRecharge = amount
})
if err != nil || len(items) == 0 {
return items, err

View File

@ -123,3 +123,74 @@ func TestUserCreateTimeDisplayOffsetIsApplied(t *testing.T) {
t.Fatalf("expected country new user contribution, got %d", contributions[0].CountryNewUser)
}
}
func TestGoogleRechargeContributesToUserRecharge(t *testing.T) {
mapper := NewMapper(walletMapperTestStore{users: map[int64]UserCountry{
1001: {
UserID: 1001,
SysOrigin: "LIKEI",
Country: Country{Code: "BD", Name: "Bangladesh"},
CreatedAt: time.Date(2026, 5, 27, 8, 0, 0, 0, time.UTC),
},
}}, []string{"LIKEI"}, "UNKNOWN", 0)
contributions, err := mapper.Map(context.Background(), "likei", "order_purchase_history", "insert", Row{
"id": int64(21),
"user_id": int64(1001),
"sys_origin": "LIKEI",
"evn": "PROD",
"status": "COMPLETE",
"is_trial_period": int64(0),
"unit_price": decimal.NewFromInt(99),
"pay_platform": "GOOGLE",
"create_time": time.Date(2026, 5, 27, 9, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if len(contributions) != 1 {
t.Fatalf("expected one contribution, got %d", len(contributions))
}
if !contributions[0].GoogleRecharge.Equal(decimal.NewFromInt(99)) {
t.Fatalf("expected google recharge 99, got %s", contributions[0].GoogleRecharge)
}
if !contributions[0].UserRecharge.Equal(decimal.NewFromInt(99)) {
t.Fatalf("expected user recharge 99, got %s", contributions[0].UserRecharge)
}
}
func TestThirdPartyRechargeContributesToUserRechargeForAnyFactory(t *testing.T) {
mapper := NewMapper(walletMapperTestStore{users: map[int64]UserCountry{
1001: {
UserID: 1001,
SysOrigin: "LIKEI",
Country: Country{Code: "BD", Name: "Bangladesh"},
CreatedAt: time.Date(2026, 5, 27, 8, 0, 0, 0, time.UTC),
},
}}, []string{"LIKEI"}, "UNKNOWN", 0)
contributions, err := mapper.Map(context.Background(), "likei", "order_user_purchase_pay", "insert", Row{
"id": int64(22),
"user_id": int64(1001),
"sys_origin": "LIKEI",
"evn": "PROD",
"factory_code": "OTHER_PAY",
"pay_status": "SUCCESSFUL",
"receipt_type": "PAYMENT",
"refund_status": "NONE",
"compute_usd_amount": decimal.NewFromInt(12),
"create_time": time.Date(2026, 5, 27, 9, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if len(contributions) != 1 {
t.Fatalf("expected one contribution, got %d", len(contributions))
}
if !contributions[0].UserRecharge.Equal(decimal.NewFromInt(12)) {
t.Fatalf("expected user recharge 12, got %s", contributions[0].UserRecharge)
}
if !contributions[0].MifapayRecharge.IsZero() {
t.Fatalf("expected non MifaPay recharge to keep mifapay zero, got %s", contributions[0].MifapayRecharge)
}
}

View File

@ -24,10 +24,16 @@ func (p *Projector) writeRealtimeRedis(ctx context.Context, contributions []Cont
c.Country.Code,
)
sign := decimal.NewFromInt(int64(c.DeltaSign))
newDealerUserRecharge := decimal.Zero
if SamePeriod(c.UserCreatedAt, c.EventTime, PeriodWindow{Type: PeriodDay}, p.storageZone) {
newDealerUserRecharge = c.NewDealerUserRecharge
}
hincrDecimal(pipe, ctx, key, "official_recharge", c.OfficialRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "mifapay_recharge", c.MifapayRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "google_recharge", c.GoogleRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "dealer_recharge", c.DealerRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "user_recharge", c.UserRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "new_dealer_user_recharge", newDealerUserRecharge.Mul(sign))
hincrDecimal(pipe, ctx, key, "salary_exchange", c.SalaryExchange.Mul(sign))
hincrDecimal(pipe, ctx, key, "salary_transfer", c.SalaryTransfer.Mul(sign))
hincrDecimal(pipe, ctx, key, "lucky_gift_total_flow", c.LuckyGiftTotalFlow.Mul(sign))

View File

@ -18,6 +18,7 @@ const (
MetricCountryNewUser = "COUNTRY_NEW_USER"
MetricLuckyGiftUser = "LUCKY_GIFT_USER"
MetricGameUser = "GAME_USER"
MetricRechargeUser = "RECHARGE_USER"
)
type Country struct {
@ -46,6 +47,8 @@ type Contribution struct {
MifapayRecharge decimal.Decimal
GoogleRecharge decimal.Decimal
DealerRecharge decimal.Decimal
UserRecharge decimal.Decimal
NewDealerUserRecharge decimal.Decimal
SalaryExchange decimal.Decimal
SalaryTransfer decimal.Decimal
GiftConsume decimal.Decimal
@ -82,6 +85,8 @@ func (c Contribution) Empty() bool {
c.MifapayRecharge.IsZero() &&
c.GoogleRecharge.IsZero() &&
c.DealerRecharge.IsZero() &&
c.UserRecharge.IsZero() &&
c.NewDealerUserRecharge.IsZero() &&
c.SalaryExchange.IsZero() &&
c.SalaryTransfer.IsZero() &&
c.GiftConsume.IsZero() &&

View File

@ -611,9 +611,9 @@ INSERT INTO country_dashboard_period_metric (
period_type, period_key, stat_timezone, period_name, period_start_date, period_end_date,
sys_origin, country_code, country_name,
country_new_user, daily_active_user, new_user_recharge, official_recharge, mifapay_recharge,
google_recharge, dealer_recharge, salary_exchange, salary_transfer, gift_consume,
google_recharge, dealer_recharge, user_recharge, new_dealer_user_recharge, salary_exchange, salary_transfer, gift_consume,
lucky_gift_total_flow, lucky_gift_user, lucky_gift_payout, lucky_gift_anchor_share,
game_total_flow, game_user, game_payout,
game_total_flow, daily_recharge_user, game_user, game_payout,
d1_retention_user, d1_retention_base_user, d7_retention_user, d7_retention_base_user,
d30_retention_user, d30_retention_base_user, refreshed_at
)
@ -626,6 +626,8 @@ SELECT 'ALL', 'ALL', ?, '全部', NULL, NULL,
IFNULL(SUM(d.mifapay_recharge), 0),
IFNULL(SUM(d.google_recharge), 0),
IFNULL(SUM(d.dealer_recharge), 0),
IFNULL(SUM(d.user_recharge), 0),
IFNULL(SUM(d.new_dealer_user_recharge), 0),
IFNULL(SUM(d.salary_exchange), 0),
IFNULL(SUM(d.salary_transfer), 0),
IFNULL(SUM(d.gift_consume), 0),
@ -634,6 +636,7 @@ SELECT 'ALL', 'ALL', ?, '全部', NULL, NULL,
IFNULL(SUM(d.lucky_gift_payout), 0),
IFNULL(SUM(d.lucky_gift_anchor_share), 0),
IFNULL(SUM(d.game_total_flow), 0),
IFNULL(MAX(u.daily_recharge_user), 0),
IFNULL(MAX(u.game_user), 0),
IFNULL(SUM(d.game_payout), 0),
IFNULL(SUM(d.d1_retention_user), 0),
@ -648,6 +651,7 @@ LEFT JOIN (
SELECT country_code,
SUM(metric_type = 'COUNTRY_NEW_USER') AS country_new_user,
SUM(metric_type = 'LUCKY_GIFT_USER') AS lucky_gift_user,
SUM(metric_type = 'RECHARGE_USER') AS daily_recharge_user,
SUM(metric_type = 'GAME_USER') AS game_user
FROM country_dashboard_period_user_metric
WHERE period_type = 'ALL' AND period_key = 'ALL' AND stat_timezone = ? AND sys_origin = ?

View File

@ -17,10 +17,12 @@ func (r *MySQLRepository) ApplyDailyContribution(ctx context.Context, tx *sql.Tx
}
day := dashboard.DailyDate(c.EventTime, zone)
newUserRecharge := decimal.Zero
newDealerUserRecharge := decimal.Zero
if dashboard.SamePeriod(c.UserCreatedAt, c.EventTime, dashboard.PeriodWindow{Type: dashboard.PeriodDay}, zone) {
newUserRecharge = c.NewUserRechargeCandidate
newDealerUserRecharge = c.NewDealerUserRecharge
}
if err := r.upsertDailyAmounts(ctx, tx, c, day, newUserRecharge); err != nil {
if err := r.upsertDailyAmounts(ctx, tx, c, day, newUserRecharge, newDealerUserRecharge); err != nil {
return err
}
if c.CountryNewUser > 0 {
@ -38,6 +40,11 @@ func (r *MySQLRepository) ApplyDailyContribution(ctx context.Context, tx *sql.Tx
return err
}
}
if c.UserRecharge.GreaterThan(decimal.Zero) {
if err := r.applyDailyUserMetric(ctx, tx, c, day, dashboard.MetricRechargeUser); err != nil {
return err
}
}
return nil
}
@ -48,10 +55,12 @@ func (r *MySQLRepository) ApplyPeriodContribution(ctx context.Context, tx *sql.T
}
for _, window := range dashboard.PeriodWindows(c.EventTime, zone) {
newUserRecharge := decimal.Zero
newDealerUserRecharge := decimal.Zero
if dashboard.SamePeriod(c.UserCreatedAt, c.EventTime, window, zone) {
newUserRecharge = c.NewUserRechargeCandidate
newDealerUserRecharge = c.NewDealerUserRecharge
}
if err := r.upsertPeriodAmounts(ctx, tx, c, window, statTimezone, newUserRecharge); err != nil {
if err := r.upsertPeriodAmounts(ctx, tx, c, window, statTimezone, newUserRecharge, newDealerUserRecharge); err != nil {
return err
}
if c.CountryNewUser > 0 {
@ -69,6 +78,11 @@ func (r *MySQLRepository) ApplyPeriodContribution(ctx context.Context, tx *sql.T
return err
}
}
if c.UserRecharge.GreaterThan(decimal.Zero) {
if err := r.applyPeriodUserMetric(ctx, tx, c, window, statTimezone, dashboard.MetricRechargeUser); err != nil {
return err
}
}
}
return nil
}
@ -94,15 +108,15 @@ func (r *MySQLRepository) ApplyGamePeriodContribution(ctx context.Context, tx *s
return nil
}
func (r *MySQLRepository) upsertDailyAmounts(ctx context.Context, tx *sql.Tx, c dashboard.Contribution, day time.Time, newUserRecharge decimal.Decimal) error {
func (r *MySQLRepository) upsertDailyAmounts(ctx context.Context, tx *sql.Tx, c dashboard.Contribution, day time.Time, newUserRecharge decimal.Decimal, newDealerUserRecharge decimal.Decimal) error {
sign := decimal.NewFromInt(int64(c.DeltaSign))
_, err := tx.ExecContext(ctx, `
INSERT INTO country_dashboard_daily_metric (
stat_date, date_number, sys_origin, country_code, country_name,
new_user_recharge, official_recharge, mifapay_recharge, google_recharge,
dealer_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
dealer_recharge, user_recharge, new_dealer_user_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
game_total_flow, game_payout, refreshed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
country_name = VALUES(country_name),
new_user_recharge = new_user_recharge + VALUES(new_user_recharge),
@ -110,6 +124,8 @@ ON DUPLICATE KEY UPDATE
mifapay_recharge = mifapay_recharge + VALUES(mifapay_recharge),
google_recharge = google_recharge + VALUES(google_recharge),
dealer_recharge = dealer_recharge + VALUES(dealer_recharge),
user_recharge = user_recharge + VALUES(user_recharge),
new_dealer_user_recharge = new_dealer_user_recharge + VALUES(new_dealer_user_recharge),
salary_exchange = salary_exchange + VALUES(salary_exchange),
salary_transfer = salary_transfer + VALUES(salary_transfer),
gift_consume = gift_consume + VALUES(gift_consume),
@ -120,20 +136,20 @@ ON DUPLICATE KEY UPDATE
refreshed_at = NOW()
`, day, dashboard.DateNumber(day), c.SysOrigin, c.Country.Code, c.Country.Name,
newUserRecharge.Mul(sign), c.OfficialRecharge.Mul(sign), c.MifapayRecharge.Mul(sign), c.GoogleRecharge.Mul(sign),
c.DealerRecharge.Mul(sign), c.SalaryExchange.Mul(sign), c.SalaryTransfer.Mul(sign), c.GiftConsume.Mul(sign), c.LuckyGiftTotalFlow.Mul(sign), c.LuckyGiftPayout.Mul(sign),
c.DealerRecharge.Mul(sign), c.UserRecharge.Mul(sign), newDealerUserRecharge.Mul(sign), c.SalaryExchange.Mul(sign), c.SalaryTransfer.Mul(sign), c.GiftConsume.Mul(sign), c.LuckyGiftTotalFlow.Mul(sign), c.LuckyGiftPayout.Mul(sign),
c.GameTotalFlow.Mul(sign), c.GamePayout.Mul(sign))
return err
}
func (r *MySQLRepository) upsertPeriodAmounts(ctx context.Context, tx *sql.Tx, c dashboard.Contribution, window dashboard.PeriodWindow, statTimezone string, newUserRecharge decimal.Decimal) error {
func (r *MySQLRepository) upsertPeriodAmounts(ctx context.Context, tx *sql.Tx, c dashboard.Contribution, window dashboard.PeriodWindow, statTimezone string, newUserRecharge decimal.Decimal, newDealerUserRecharge decimal.Decimal) error {
sign := decimal.NewFromInt(int64(c.DeltaSign))
_, err := tx.ExecContext(ctx, `
INSERT INTO country_dashboard_period_metric (
period_type, period_key, stat_timezone, period_name, period_start_date, period_end_date,
sys_origin, country_code, country_name, new_user_recharge, official_recharge, mifapay_recharge,
google_recharge, dealer_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
google_recharge, dealer_recharge, user_recharge, new_dealer_user_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
game_total_flow, game_payout, refreshed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
period_name = VALUES(period_name),
period_start_date = VALUES(period_start_date),
@ -144,6 +160,8 @@ ON DUPLICATE KEY UPDATE
mifapay_recharge = mifapay_recharge + VALUES(mifapay_recharge),
google_recharge = google_recharge + VALUES(google_recharge),
dealer_recharge = dealer_recharge + VALUES(dealer_recharge),
user_recharge = user_recharge + VALUES(user_recharge),
new_dealer_user_recharge = new_dealer_user_recharge + VALUES(new_dealer_user_recharge),
salary_exchange = salary_exchange + VALUES(salary_exchange),
salary_transfer = salary_transfer + VALUES(salary_transfer),
gift_consume = gift_consume + VALUES(gift_consume),
@ -154,7 +172,7 @@ ON DUPLICATE KEY UPDATE
refreshed_at = NOW()
`, window.Type, window.Key, statTimezone, window.Name, window.StartDate, window.EndDate,
c.SysOrigin, c.Country.Code, c.Country.Name, newUserRecharge.Mul(sign), c.OfficialRecharge.Mul(sign),
c.MifapayRecharge.Mul(sign), c.GoogleRecharge.Mul(sign), c.DealerRecharge.Mul(sign), c.SalaryExchange.Mul(sign),
c.MifapayRecharge.Mul(sign), c.GoogleRecharge.Mul(sign), c.DealerRecharge.Mul(sign), c.UserRecharge.Mul(sign), newDealerUserRecharge.Mul(sign), c.SalaryExchange.Mul(sign),
c.SalaryTransfer.Mul(sign), c.GiftConsume.Mul(sign), c.LuckyGiftTotalFlow.Mul(sign), c.LuckyGiftPayout.Mul(sign),
c.GameTotalFlow.Mul(sign), c.GamePayout.Mul(sign))
return err

View File

@ -19,6 +19,8 @@ type amountTotals struct {
mifapayRecharge decimal.Decimal
googleRecharge decimal.Decimal
dealerRecharge decimal.Decimal
userRecharge decimal.Decimal
newDealerUserRecharge decimal.Decimal
salaryExchange decimal.Decimal
salaryTransfer decimal.Decimal
giftConsume decimal.Decimal
@ -150,12 +152,14 @@ func (r *MySQLRepository) ApplyContributionBatch(ctx context.Context, tx *sql.Tx
}
day := dashboard.DailyDate(c.EventTime, storageZone)
newUserRecharge := decimal.Zero
newDealerUserRecharge := decimal.Zero
if dashboard.SamePeriod(c.UserCreatedAt, c.EventTime, dashboard.PeriodWindow{Type: dashboard.PeriodDay}, storageZone) {
newUserRecharge = c.NewUserRechargeCandidate
newDealerUserRecharge = c.NewDealerUserRecharge
}
dailyKey := dailyAmountKey{day: day, sysOrigin: c.SysOrigin, countryCode: c.Country.Code, countryName: c.Country.Name}
totals := dailyAmounts[dailyKey]
totals.add(c, newUserRecharge)
totals.add(c, newUserRecharge, newDealerUserRecharge)
dailyAmounts[dailyKey] = totals
addDailyUserGroups(dailyUsers, c, day)
@ -163,8 +167,10 @@ func (r *MySQLRepository) ApplyContributionBatch(ctx context.Context, tx *sql.Tx
for _, window := range dashboard.PeriodWindows(c.EventTime, zone) {
startDate, endDate := windowDates(window)
newUserRecharge = decimal.Zero
newDealerUserRecharge = decimal.Zero
if dashboard.SamePeriod(c.UserCreatedAt, c.EventTime, window, zone) {
newUserRecharge = c.NewUserRechargeCandidate
newDealerUserRecharge = c.NewDealerUserRecharge
}
periodKey := periodAmountKey{
periodType: window.Type, periodKey: window.Key, statTimezone: statTimezone,
@ -172,7 +178,7 @@ func (r *MySQLRepository) ApplyContributionBatch(ctx context.Context, tx *sql.Tx
sysOrigin: c.SysOrigin, countryCode: c.Country.Code, countryName: c.Country.Name,
}
periodTotal := periodAmounts[periodKey]
periodTotal.add(c, newUserRecharge)
periodTotal.add(c, newUserRecharge, newDealerUserRecharge)
periodAmounts[periodKey] = periodTotal
addPeriodUserGroups(periodUsers, c, window, statTimezone)
@ -250,13 +256,15 @@ func (r *MySQLRepository) ApplyContributionBatch(ctx context.Context, tx *sql.Tx
return nil
}
func (t *amountTotals) add(c dashboard.Contribution, newUserRecharge decimal.Decimal) {
func (t *amountTotals) add(c dashboard.Contribution, newUserRecharge decimal.Decimal, newDealerUserRecharge decimal.Decimal) {
sign := decimal.NewFromInt(int64(c.DeltaSign))
t.newUserRecharge = t.newUserRecharge.Add(newUserRecharge.Mul(sign))
t.officialRecharge = t.officialRecharge.Add(c.OfficialRecharge.Mul(sign))
t.mifapayRecharge = t.mifapayRecharge.Add(c.MifapayRecharge.Mul(sign))
t.googleRecharge = t.googleRecharge.Add(c.GoogleRecharge.Mul(sign))
t.dealerRecharge = t.dealerRecharge.Add(c.DealerRecharge.Mul(sign))
t.userRecharge = t.userRecharge.Add(c.UserRecharge.Mul(sign))
t.newDealerUserRecharge = t.newDealerUserRecharge.Add(newDealerUserRecharge.Mul(sign))
t.salaryExchange = t.salaryExchange.Add(c.SalaryExchange.Mul(sign))
t.salaryTransfer = t.salaryTransfer.Add(c.SalaryTransfer.Mul(sign))
t.giftConsume = t.giftConsume.Add(c.GiftConsume.Mul(sign))
@ -268,7 +276,8 @@ func (t *amountTotals) add(c dashboard.Contribution, newUserRecharge decimal.Dec
func (t amountTotals) empty() bool {
return t.newUserRecharge.IsZero() && t.officialRecharge.IsZero() && t.mifapayRecharge.IsZero() &&
t.googleRecharge.IsZero() && t.dealerRecharge.IsZero() && t.salaryExchange.IsZero() &&
t.googleRecharge.IsZero() && t.dealerRecharge.IsZero() && t.userRecharge.IsZero() &&
t.newDealerUserRecharge.IsZero() && t.salaryExchange.IsZero() &&
t.salaryTransfer.IsZero() &&
t.giftConsume.IsZero() && t.luckyGiftTotalFlow.IsZero() && t.luckyGiftPayout.IsZero() &&
t.gameTotalFlow.IsZero() && t.gamePayout.IsZero()
@ -299,6 +308,9 @@ func addDailyUserGroups(groups map[dailyUserKey]map[int64]struct{}, c dashboard.
if c.GameUser {
addDailyUser(groups, c, day, dashboard.MetricGameUser)
}
if c.UserRecharge.GreaterThan(decimal.Zero) {
addDailyUser(groups, c, day, dashboard.MetricRechargeUser)
}
}
func addPeriodUserGroups(groups map[periodUserKey]map[int64]struct{}, c dashboard.Contribution, window dashboard.PeriodWindow, statTimezone string) {
@ -314,6 +326,9 @@ func addPeriodUserGroups(groups map[periodUserKey]map[int64]struct{}, c dashboar
if c.GameUser {
addPeriodUser(groups, c, window, statTimezone, dashboard.MetricGameUser)
}
if c.UserRecharge.GreaterThan(decimal.Zero) {
addPeriodUser(groups, c, window, statTimezone, dashboard.MetricRechargeUser)
}
}
func addDailyUser(groups map[dailyUserKey]map[int64]struct{}, c dashboard.Contribution, day time.Time, metricType string) {

View File

@ -11,9 +11,9 @@ func (r *MySQLRepository) upsertDailyAmountTotals(ctx context.Context, tx *sql.T
INSERT INTO country_dashboard_daily_metric (
stat_date, date_number, sys_origin, country_code, country_name,
new_user_recharge, official_recharge, mifapay_recharge, google_recharge,
dealer_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
dealer_recharge, user_recharge, new_dealer_user_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
game_total_flow, game_payout, refreshed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
country_name = VALUES(country_name),
new_user_recharge = new_user_recharge + VALUES(new_user_recharge),
@ -21,6 +21,8 @@ ON DUPLICATE KEY UPDATE
mifapay_recharge = mifapay_recharge + VALUES(mifapay_recharge),
google_recharge = google_recharge + VALUES(google_recharge),
dealer_recharge = dealer_recharge + VALUES(dealer_recharge),
user_recharge = user_recharge + VALUES(user_recharge),
new_dealer_user_recharge = new_dealer_user_recharge + VALUES(new_dealer_user_recharge),
salary_exchange = salary_exchange + VALUES(salary_exchange),
salary_transfer = salary_transfer + VALUES(salary_transfer),
gift_consume = gift_consume + VALUES(gift_consume),
@ -31,7 +33,7 @@ ON DUPLICATE KEY UPDATE
refreshed_at = NOW()
`, key.day, dateNumber(key.day), key.sysOrigin, key.countryCode, key.countryName,
totals.newUserRecharge, totals.officialRecharge, totals.mifapayRecharge, totals.googleRecharge,
totals.dealerRecharge, totals.salaryExchange, totals.salaryTransfer, totals.giftConsume, totals.luckyGiftTotalFlow, totals.luckyGiftPayout,
totals.dealerRecharge, totals.userRecharge, totals.newDealerUserRecharge, totals.salaryExchange, totals.salaryTransfer, totals.giftConsume, totals.luckyGiftTotalFlow, totals.luckyGiftPayout,
totals.gameTotalFlow, totals.gamePayout)
return err
}
@ -41,9 +43,9 @@ func (r *MySQLRepository) upsertPeriodAmountTotals(ctx context.Context, tx *sql.
INSERT INTO country_dashboard_period_metric (
period_type, period_key, stat_timezone, period_name, period_start_date, period_end_date,
sys_origin, country_code, country_name, new_user_recharge, official_recharge, mifapay_recharge,
google_recharge, dealer_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
google_recharge, dealer_recharge, user_recharge, new_dealer_user_recharge, salary_exchange, salary_transfer, gift_consume, lucky_gift_total_flow, lucky_gift_payout,
game_total_flow, game_payout, refreshed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
period_name = VALUES(period_name),
period_start_date = VALUES(period_start_date),
@ -54,6 +56,8 @@ ON DUPLICATE KEY UPDATE
mifapay_recharge = mifapay_recharge + VALUES(mifapay_recharge),
google_recharge = google_recharge + VALUES(google_recharge),
dealer_recharge = dealer_recharge + VALUES(dealer_recharge),
user_recharge = user_recharge + VALUES(user_recharge),
new_dealer_user_recharge = new_dealer_user_recharge + VALUES(new_dealer_user_recharge),
salary_exchange = salary_exchange + VALUES(salary_exchange),
salary_transfer = salary_transfer + VALUES(salary_transfer),
gift_consume = gift_consume + VALUES(gift_consume),
@ -64,7 +68,7 @@ ON DUPLICATE KEY UPDATE
refreshed_at = NOW()
`, key.periodType, key.periodKey, key.statTimezone, key.periodName, ptrOrNil(key.startDate), ptrOrNil(key.endDate),
key.sysOrigin, key.countryCode, key.countryName, totals.newUserRecharge, totals.officialRecharge,
totals.mifapayRecharge, totals.googleRecharge, totals.dealerRecharge, totals.salaryExchange,
totals.mifapayRecharge, totals.googleRecharge, totals.dealerRecharge, totals.userRecharge, totals.newDealerUserRecharge, totals.salaryExchange,
totals.salaryTransfer, totals.giftConsume, totals.luckyGiftTotalFlow, totals.luckyGiftPayout,
totals.gameTotalFlow, totals.gamePayout)
return err

View File

@ -33,6 +33,7 @@ type mifapayRechargeRow struct {
id string
userID int64
sysOrigin string
factoryCode string
amount decimal.Decimal
eventTime time.Time
user dashboard.UserCountry
@ -79,6 +80,7 @@ func (r *MySQLRepository) BuildRechargeReconcileEvents(ctx context.Context, sinc
}
if row.payPlatform == "GOOGLE" {
contribution.GoogleRecharge = row.unitPrice
contribution.UserRecharge = row.unitPrice
}
events = append(events, dashboard.Event{
EventKey: rechargeReconcileEventKey(reconcileOfficialSource, row.id),
@ -102,9 +104,12 @@ func (r *MySQLRepository) BuildRechargeReconcileEvents(ctx context.Context, sinc
EventTime: row.eventTime,
UserCreatedAt: row.user.CreatedAt,
NewUserRechargeCandidate: row.amount,
MifapayRecharge: row.amount,
UserRecharge: row.amount,
RechargeDetail: mifapayRechargeDetail(row),
}
if row.factoryCode == "MIFA_PAY" {
contribution.MifapayRecharge = row.amount
}
events = append(events, dashboard.Event{
EventKey: rechargeReconcileEventKey(reconcileMifapaySource, row.id),
Source: reconcileMifapaySource,
@ -193,7 +198,7 @@ LIMIT ?`, r.cfg.Dashboard.UnknownCountryCode, r.cfg.Dashboard.UnknownCountryCode
func (r *MySQLRepository) listMifapayRechargeRows(ctx context.Context, since time.Time,
limit int) ([]mifapayRechargeRow, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT CAST(o.id AS CHAR), o.user_id, o.sys_origin, o.compute_usd_amount, o.create_time,
SELECT CAST(o.id AS CHAR), o.user_id, o.sys_origin, o.factory_code, o.compute_usd_amount, o.create_time,
u.id, u.origin_sys, IFNULL(NULLIF(u.country_code, ''), ?),
IFNULL(NULLIF(u.country_name, ''), IFNULL(NULLIF(u.country_code, ''), ?)),
IFNULL(u.is_del, 0), u.create_time, u.update_time
@ -201,7 +206,6 @@ FROM order_user_purchase_pay o
JOIN user_base_info u ON u.id = o.user_id
WHERE o.create_time >= ?
AND o.evn = 'PROD'
AND o.factory_code = 'MIFA_PAY'
AND o.pay_status = 'SUCCESSFUL'
AND o.receipt_type = 'PAYMENT'
AND o.refund_status = 'NONE'
@ -217,7 +221,7 @@ LIMIT ?`, r.cfg.Dashboard.UnknownCountryCode, r.cfg.Dashboard.UnknownCountryCode
var countryCode, countryName string
var isDeleted bool
var userCreatedAt, userUpdatedAt sql.NullTime
if err := rows.Scan(&item.id, &item.userID, &item.sysOrigin, &item.amount,
if err := rows.Scan(&item.id, &item.userID, &item.sysOrigin, &item.factoryCode, &item.amount,
&item.eventTime, &item.user.UserID, &item.user.SysOrigin, &countryCode,
&countryName, &isDeleted, &userCreatedAt, &userUpdatedAt); err != nil {
return nil, err
@ -272,10 +276,14 @@ func officialRechargeDetail(row officialRechargeRow) *dashboard.RechargeDetail {
}
func mifapayRechargeDetail(row mifapayRechargeRow) *dashboard.RechargeDetail {
sourceType := "THIRD_PARTY"
if row.factoryCode == "MIFA_PAY" {
sourceType = "MIFAPAY"
}
return &dashboard.RechargeDetail{
SourceType: "MIFAPAY",
SourceType: sourceType,
RecordID: row.id,
SourceChannel: "MIFA_PAY",
SourceChannel: row.factoryCode,
SysOrigin: row.user.SysOrigin,
UserID: row.userID,
Country: row.user.Country,

View File

@ -64,6 +64,8 @@ const createDailyMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_dai
mifapay_recharge decimal(20,2) NOT NULL DEFAULT 0,
google_recharge decimal(20,2) NOT NULL DEFAULT 0,
dealer_recharge decimal(20,2) NOT NULL DEFAULT 0,
user_recharge decimal(20,2) NOT NULL DEFAULT 0,
new_dealer_user_recharge decimal(20,2) NOT NULL DEFAULT 0,
salary_exchange decimal(20,2) NOT NULL DEFAULT 0,
salary_transfer decimal(20,2) NOT NULL DEFAULT 0,
gift_consume decimal(20,2) NOT NULL DEFAULT 0,
@ -71,6 +73,7 @@ const createDailyMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_dai
lucky_gift_user bigint NOT NULL DEFAULT 0,
lucky_gift_payout decimal(20,2) NOT NULL DEFAULT 0,
game_total_flow decimal(20,2) NOT NULL DEFAULT 0,
daily_recharge_user bigint NOT NULL DEFAULT 0,
game_user bigint NOT NULL DEFAULT 0,
game_payout decimal(20,2) NOT NULL DEFAULT 0,
refreshed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -110,6 +113,8 @@ const createPeriodMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_pe
mifapay_recharge decimal(20,2) NOT NULL DEFAULT 0,
google_recharge decimal(20,2) NOT NULL DEFAULT 0,
dealer_recharge decimal(20,2) NOT NULL DEFAULT 0,
user_recharge decimal(20,2) NOT NULL DEFAULT 0,
new_dealer_user_recharge decimal(20,2) NOT NULL DEFAULT 0,
salary_exchange decimal(20,2) NOT NULL DEFAULT 0,
salary_transfer decimal(20,2) NOT NULL DEFAULT 0,
gift_consume decimal(20,2) NOT NULL DEFAULT 0,
@ -117,6 +122,7 @@ const createPeriodMetricTable = `CREATE TABLE IF NOT EXISTS country_dashboard_pe
lucky_gift_user bigint NOT NULL DEFAULT 0,
lucky_gift_payout decimal(20,2) NOT NULL DEFAULT 0,
game_total_flow decimal(20,2) NOT NULL DEFAULT 0,
daily_recharge_user bigint NOT NULL DEFAULT 0,
game_user bigint NOT NULL DEFAULT 0,
game_payout decimal(20,2) NOT NULL DEFAULT 0,
refreshed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -134,6 +140,30 @@ func (r *MySQLRepository) ensureMetricColumns(ctx context.Context) error {
"ALTER TABLE country_dashboard_period_metric ADD COLUMN salary_transfer decimal(20,2) NOT NULL DEFAULT 0 AFTER salary_exchange"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_daily_metric", "user_recharge",
"ALTER TABLE country_dashboard_daily_metric ADD COLUMN user_recharge decimal(20,2) NOT NULL DEFAULT 0 AFTER dealer_recharge"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_period_metric", "user_recharge",
"ALTER TABLE country_dashboard_period_metric ADD COLUMN user_recharge decimal(20,2) NOT NULL DEFAULT 0 AFTER dealer_recharge"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_daily_metric", "new_dealer_user_recharge",
"ALTER TABLE country_dashboard_daily_metric ADD COLUMN new_dealer_user_recharge decimal(20,2) NOT NULL DEFAULT 0 AFTER user_recharge"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_period_metric", "new_dealer_user_recharge",
"ALTER TABLE country_dashboard_period_metric ADD COLUMN new_dealer_user_recharge decimal(20,2) NOT NULL DEFAULT 0 AFTER user_recharge"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_daily_metric", "daily_recharge_user",
"ALTER TABLE country_dashboard_daily_metric ADD COLUMN daily_recharge_user bigint NOT NULL DEFAULT 0 AFTER game_total_flow"); err != nil {
return err
}
if err := r.addColumnIfMissing(ctx, "country_dashboard_period_metric", "daily_recharge_user",
"ALTER TABLE country_dashboard_period_metric ADD COLUMN daily_recharge_user bigint NOT NULL DEFAULT 0 AFTER game_total_flow"); err != nil {
return err
}
return r.addIndexIfMissing(ctx, "dashboard_cdc_user_country", "idx_dashboard_cdc_user_country_origin_created",
"ALTER TABLE dashboard_cdc_user_country ADD INDEX idx_dashboard_cdc_user_country_origin_created (sys_origin, user_created_at, country_code)")
}

View File

@ -178,6 +178,8 @@ func dailyMetricColumn(metricType string) string {
return "lucky_gift_user"
case dashboard.MetricGameUser:
return "game_user"
case dashboard.MetricRechargeUser:
return "daily_recharge_user"
default:
return ""
}

View File

@ -0,0 +1,85 @@
SET @current_schema = DATABASE();
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'daily_recharge_user'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT ''当日充值用户数'' AFTER `country_new_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'daily_recharge_user'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `daily_recharge_user` bigint NOT NULL DEFAULT 0 COMMENT ''当日充值用户数'' AFTER `daily_active_user`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'user_recharge'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''用户充值'' AFTER `dealer_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'user_recharge'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''用户充值'' AFTER `dealer_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_daily_metric'
AND COLUMN_NAME = 'new_dealer_user_recharge'
),
'ALTER TABLE `country_dashboard_daily_metric` ADD COLUMN `new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''新增币商用户充值'' AFTER `user_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @current_schema
AND TABLE_NAME = 'country_dashboard_period_metric'
AND COLUMN_NAME = 'new_dealer_user_recharge'
),
'ALTER TABLE `country_dashboard_period_metric` ADD COLUMN `new_dealer_user_recharge` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT ''新增币商用户充值'' AFTER `user_recharge`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;