fix invite calendar month reset
This commit is contained in:
parent
362069e2d4
commit
7c1ecb804e
@ -134,7 +134,7 @@ func (s *InviteService) bindCode(ctx context.Context, user AuthUser, clientIP st
|
||||
|
||||
// 在锁内判定风控资格并写入关系、进度两张表。
|
||||
bindTime := time.Now()
|
||||
periodKey, _, _ = thirtyDayPeriodInfoAt(bindTime, normalizeTimezone(bundle.Config.Timezone), bundle.Config.CreateTime)
|
||||
periodKey, _, _, _ = calendarMonthPeriodInfoAt(bindTime, normalizeTimezone(bundle.Config.Timezone))
|
||||
if bundle.Config.AntiCheatSameIPOnce && strings.TrimSpace(clientIP) != "" {
|
||||
exists, existsErr := s.existsEligibleRelationByIP(ctx, user.SysOrigin, clientIP)
|
||||
if existsErr != nil {
|
||||
|
||||
@ -20,10 +20,10 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 首页需要同时返回当前 30 天周期进度、累计统计、邀请码和任务配置。
|
||||
// 首页需要同时返回当前自然月周期进度、累计统计、邀请码和任务配置。
|
||||
now := time.Now()
|
||||
timezone := normalizeTimezone(bundle.Config.Timezone)
|
||||
periodKey, periodEndAt, secondsToReset := thirtyDayPeriodInfoAt(now, timezone, bundle.Config.CreateTime)
|
||||
periodKey, periodEndAt, secondsToReset, resetPeriodDays := calendarMonthPeriodInfoAt(now, timezone)
|
||||
monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, periodKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -58,7 +58,7 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
|
||||
ServerTimeMs: now.UnixMilli(),
|
||||
PeriodEndAtMs: periodEndAt.UnixMilli(),
|
||||
SecondsToReset: secondsToReset,
|
||||
ResetPeriodDays: inviteTaskPeriodDays,
|
||||
ResetPeriodDays: resetPeriodDays,
|
||||
ValidUserThreshold: effectiveValidThreshold(inviteeRechargeRules),
|
||||
InviterBindReward: rewardFromRule(inviterBindRule),
|
||||
InviteeBindReward: rewardFromRule(inviteeBindRule),
|
||||
|
||||
@ -115,13 +115,12 @@ func seedInviteCampaign(t *testing.T, db *gorm.DB) time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
func TestThirtyDayPeriodInfoAtReturnsNextPeriodStart(t *testing.T) {
|
||||
anchor := time.Date(2026, 4, 1, 9, 30, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
func TestCalendarMonthPeriodInfoAtReturnsNextMonthStart(t *testing.T) {
|
||||
now := time.Date(2026, 4, 30, 23, 59, 30, 0, time.FixedZone("CST", 8*3600))
|
||||
periodKey, periodEnd, seconds := thirtyDayPeriodInfoAt(now, "Asia/Shanghai", anchor)
|
||||
periodKey, periodEnd, seconds, periodDays := calendarMonthPeriodInfoAt(now, "Asia/Shanghai")
|
||||
|
||||
if periodKey != "20260401" {
|
||||
t.Fatalf("periodKey = %s, want 20260401", periodKey)
|
||||
if periodKey != "202604" {
|
||||
t.Fatalf("periodKey = %s, want 202604", periodKey)
|
||||
}
|
||||
if got := periodEnd.In(time.FixedZone("CST", 8*3600)).Format(time.RFC3339); got != "2026-05-01T00:00:00+08:00" {
|
||||
t.Fatalf("periodEnd = %s, want 2026-05-01T00:00:00+08:00", got)
|
||||
@ -129,15 +128,18 @@ func TestThirtyDayPeriodInfoAtReturnsNextPeriodStart(t *testing.T) {
|
||||
if seconds != 30 {
|
||||
t.Fatalf("seconds = %d, want 30", seconds)
|
||||
}
|
||||
if periodDays != 30 {
|
||||
t.Fatalf("periodDays = %d, want 30", periodDays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHomeUsesValidUserCountForValidCountTasks(t *testing.T) {
|
||||
service, db, cleanup := newTestInviteService(t, nil)
|
||||
defer cleanup()
|
||||
configTime := seedInviteCampaign(t, db)
|
||||
seedInviteCampaign(t, db)
|
||||
|
||||
now := time.Now()
|
||||
periodKey, _, _ := thirtyDayPeriodInfoAt(now, "Asia/Shanghai", configTime)
|
||||
periodKey, _, _, _ := calendarMonthPeriodInfoAt(now, "Asia/Shanghai")
|
||||
if err := db.Create(&model.InviteCodeMapping{UserID: 1001, InviteCode: "INV1001", Account: "u1001"}).Error; err != nil {
|
||||
t.Fatalf("seed invite code: %v", err)
|
||||
}
|
||||
@ -187,10 +189,10 @@ func TestClaimTaskUsesValidUserCountForValidCountTasks(t *testing.T) {
|
||||
gateway := &inviteTestGateway{}
|
||||
service, db, cleanup := newTestInviteService(t, gateway)
|
||||
defer cleanup()
|
||||
configTime := seedInviteCampaign(t, db)
|
||||
seedInviteCampaign(t, db)
|
||||
|
||||
now := time.Now()
|
||||
periodKey, _, _ := thirtyDayPeriodInfoAt(now, "Asia/Shanghai", configTime)
|
||||
periodKey, _, _, _ := calendarMonthPeriodInfoAt(now, "Asia/Shanghai")
|
||||
if err := db.Create(&model.InviteCampaignRewardRule{
|
||||
ID: 2002,
|
||||
SysOrigin: "LIKEI",
|
||||
@ -417,7 +419,7 @@ func TestRechargeMarksValidUserOnlyAfterEightyThousandCoins(t *testing.T) {
|
||||
configTime := seedInviteCampaign(t, db)
|
||||
|
||||
payTime := configTime.Add(time.Hour)
|
||||
periodKey, _, _ := thirtyDayPeriodInfoAt(payTime, "Asia/Shanghai", configTime)
|
||||
periodKey, _, _, _ := calendarMonthPeriodInfoAt(payTime, "Asia/Shanghai")
|
||||
rows := []any{
|
||||
&model.InviteCampaignRewardRule{
|
||||
ID: 2101,
|
||||
|
||||
@ -35,26 +35,18 @@ func inviteLocation(timezone string) *time.Location {
|
||||
return location
|
||||
}
|
||||
|
||||
// thirtyDayPeriodInfoAt 返回指定时间所在 30 天任务周期的 key、结束时间和剩余秒数。
|
||||
func thirtyDayPeriodInfoAt(t time.Time, timezone string, anchor time.Time) (string, time.Time, int64) {
|
||||
// calendarMonthPeriodInfoAt 返回指定时间所在自然月任务周期的 key、结束时间、剩余秒数和周期天数。
|
||||
func calendarMonthPeriodInfoAt(t time.Time, timezone string) (string, time.Time, int64, int) {
|
||||
location := inviteLocation(timezone)
|
||||
localTime := t.In(location)
|
||||
anchorLocal := anchor.In(location)
|
||||
if anchor.IsZero() {
|
||||
anchorLocal = time.Date(1970, 1, 1, 0, 0, 0, 0, location)
|
||||
}
|
||||
anchorStart := time.Date(anchorLocal.Year(), anchorLocal.Month(), anchorLocal.Day(), 0, 0, 0, 0, location)
|
||||
elapsedDays := int(localTime.Sub(anchorStart) / (24 * time.Hour))
|
||||
if elapsedDays < 0 {
|
||||
elapsedDays = 0
|
||||
}
|
||||
periodStart := anchorStart.AddDate(0, 0, elapsedDays/inviteTaskPeriodDays*inviteTaskPeriodDays)
|
||||
periodEnd := periodStart.AddDate(0, 0, inviteTaskPeriodDays)
|
||||
periodStart := time.Date(localTime.Year(), localTime.Month(), 1, 0, 0, 0, 0, location)
|
||||
periodEnd := periodStart.AddDate(0, 1, 0)
|
||||
seconds := int64(periodEnd.Sub(localTime).Seconds())
|
||||
if seconds < 0 {
|
||||
seconds = 0
|
||||
}
|
||||
return periodStart.Format("20060102"), periodEnd, seconds
|
||||
periodDays := int(periodEnd.Sub(periodStart) / (24 * time.Hour))
|
||||
return periodStart.Format("200601"), periodEnd, seconds, periodDays
|
||||
}
|
||||
|
||||
// digitsOnly 判断字符串是否只包含数字。
|
||||
|
||||
@ -41,7 +41,7 @@ func (s *InviteService) HandleRechargeSuccess(ctx context.Context, req RechargeS
|
||||
if err != nil {
|
||||
return nil, NewAppError(400, "invalid_pay_time", err.Error())
|
||||
}
|
||||
periodKey, _, _ := thirtyDayPeriodInfoAt(payTime, normalizeTimezone(bundle.Config.Timezone), bundle.Config.CreateTime)
|
||||
periodKey, _, _, _ := calendarMonthPeriodInfoAt(payTime, normalizeTimezone(bundle.Config.Timezone))
|
||||
inviteeRechargeRules := rulesByType(bundle.Rules, ruleTypeInviteeRecharge)
|
||||
validThreshold := effectiveValidThreshold(inviteeRechargeRules)
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ func (s *InviteService) ClaimTask(ctx context.Context, user AuthUser, req TaskCl
|
||||
return nil, NewAppError(400, "invalid_rule_type", "rule cannot be claimed manually")
|
||||
}
|
||||
|
||||
periodKey, _, _ := thirtyDayPeriodInfoAt(time.Now(), normalizeTimezone(bundle.Config.Timezone), bundle.Config.CreateTime)
|
||||
periodKey, _, _, _ := calendarMonthPeriodInfoAt(time.Now(), normalizeTimezone(bundle.Config.Timezone))
|
||||
claimed, err := s.hasTaskClaim(ctx, user.SysOrigin, user.UserID, periodKey, rule.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -36,7 +36,6 @@ const (
|
||||
ruleTypeInviterTotalRecharge = "INVITER_TOTAL_RECHARGE"
|
||||
defaultInviteRankLimit = 50
|
||||
maxInviteRankLimit = 100
|
||||
inviteTaskPeriodDays = 30
|
||||
)
|
||||
|
||||
var errAlreadyProcessed = errors.New("invite recharge already processed")
|
||||
|
||||
94
migrations/016_invite_campaign_calendar_month_period.sql
Normal file
94
migrations/016_invite_campaign_calendar_month_period.sql
Normal file
@ -0,0 +1,94 @@
|
||||
CREATE TEMPORARY TABLE tmp_invite_campaign_monthly_progress_merge AS
|
||||
SELECT
|
||||
MIN(id) AS id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
LEFT(month_key, 6) AS month_key,
|
||||
SUM(invite_count) AS invite_count,
|
||||
SUM(valid_user_count) AS valid_user_count,
|
||||
SUM(total_recharge_coins) AS total_recharge_coins,
|
||||
SUM(reward_gold_coins) AS reward_gold_coins,
|
||||
MIN(create_time) AS create_time,
|
||||
MAX(update_time) AS update_time
|
||||
FROM invite_campaign_monthly_progress
|
||||
WHERE CHAR_LENGTH(month_key) = 8
|
||||
GROUP BY sys_origin, inviter_user_id, LEFT(month_key, 6);
|
||||
|
||||
DELETE FROM invite_campaign_monthly_progress
|
||||
WHERE CHAR_LENGTH(month_key) = 8;
|
||||
|
||||
INSERT INTO invite_campaign_monthly_progress (
|
||||
id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
month_key,
|
||||
invite_count,
|
||||
valid_user_count,
|
||||
total_recharge_coins,
|
||||
reward_gold_coins,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
month_key,
|
||||
invite_count,
|
||||
valid_user_count,
|
||||
total_recharge_coins,
|
||||
reward_gold_coins,
|
||||
create_time,
|
||||
update_time
|
||||
FROM tmp_invite_campaign_monthly_progress_merge
|
||||
ON DUPLICATE KEY UPDATE
|
||||
invite_count = invite_campaign_monthly_progress.invite_count + VALUES(invite_count),
|
||||
valid_user_count = invite_campaign_monthly_progress.valid_user_count + VALUES(valid_user_count),
|
||||
total_recharge_coins = invite_campaign_monthly_progress.total_recharge_coins + VALUES(total_recharge_coins),
|
||||
reward_gold_coins = invite_campaign_monthly_progress.reward_gold_coins + VALUES(reward_gold_coins),
|
||||
update_time = GREATEST(invite_campaign_monthly_progress.update_time, VALUES(update_time));
|
||||
|
||||
DROP TEMPORARY TABLE tmp_invite_campaign_monthly_progress_merge;
|
||||
|
||||
CREATE TEMPORARY TABLE tmp_invite_campaign_task_claim_merge AS
|
||||
SELECT
|
||||
MIN(id) AS id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
LEFT(month_key, 6) AS month_key,
|
||||
rule_id,
|
||||
MIN(claim_time) AS claim_time,
|
||||
MIN(create_time) AS create_time,
|
||||
MAX(update_time) AS update_time
|
||||
FROM invite_campaign_task_claim
|
||||
WHERE CHAR_LENGTH(month_key) = 8
|
||||
GROUP BY sys_origin, inviter_user_id, LEFT(month_key, 6), rule_id;
|
||||
|
||||
DELETE FROM invite_campaign_task_claim
|
||||
WHERE CHAR_LENGTH(month_key) = 8;
|
||||
|
||||
INSERT INTO invite_campaign_task_claim (
|
||||
id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
month_key,
|
||||
rule_id,
|
||||
claim_time,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
sys_origin,
|
||||
inviter_user_id,
|
||||
month_key,
|
||||
rule_id,
|
||||
claim_time,
|
||||
create_time,
|
||||
update_time
|
||||
FROM tmp_invite_campaign_task_claim_merge
|
||||
ON DUPLICATE KEY UPDATE
|
||||
claim_time = LEAST(invite_campaign_task_claim.claim_time, VALUES(claim_time)),
|
||||
update_time = GREATEST(invite_campaign_task_claim.update_time, VALUES(update_time));
|
||||
|
||||
DROP TEMPORARY TABLE tmp_invite_campaign_task_claim_merge;
|
||||
Loading…
x
Reference in New Issue
Block a user