package databi import ( "context" "testing" "time" luckygiftv1 "hyapp.local/api/proto/luckygift/v1" ) type fakeLuckyGiftDailyStatsSource struct { lastReq *luckygiftv1.ListLuckyGiftDailyStatsRequest stats []*luckygiftv1.LuckyGiftDailyStat } func (f *fakeLuckyGiftDailyStatsSource) ListLuckyGiftDailyStats(_ context.Context, req *luckygiftv1.ListLuckyGiftDailyStatsRequest) (*luckygiftv1.ListLuckyGiftDailyStatsResponse, error) { f.lastReq = req return &luckygiftv1.ListLuckyGiftDailyStatsResponse{Stats: f.stats}, nil } func TestRequirementsLuckyGiftV3Gating(t *testing.T) { for _, tc := range []struct { section string includes bool only bool }{ {"", true, false}, {"all", true, false}, {"lucky_gift_v3", true, true}, {" Lucky_Gift_V3 ", true, true}, {"revenue", false, false}, {"game", false, false}, } { if got := requirementsIncludesLuckyGiftV3(tc.section); got != tc.includes { t.Fatalf("includes(%q)=%v, want %v", tc.section, got, tc.includes) } if got := requirementsOnlyLuckyGiftV3(tc.section); got != tc.only { t.Fatalf("only(%q)=%v, want %v", tc.section, got, tc.only) } } } // 回归:section 组装——按北京日零填充整个窗口、比例按合计重算、v3 策略与区域过滤透传到 RPC。 func TestLuckyGiftV3Section(t *testing.T) { source := &fakeLuckyGiftDailyStatsSource{stats: []*luckygiftv1.LuckyGiftDailyStat{ {StatDay: "2026-07-11", DrawCount: 4, TurnoverCoin: 8_000, PayoutCoin: 6_000, BaseRewardCoin: 5_500}, }} service := &Service{luckyGiftStats: source, luckyGiftTimeout: time.Second} // 北京 2026-07-10 00:00 ~ 2026-07-12 00:00(排他)= 两个北京日。 startMS := time.Date(2026, 7, 9, 16, 0, 0, 0, time.UTC).UnixMilli() endMS := time.Date(2026, 7, 11, 16, 0, 0, 0, time.UTC).UnixMilli() section, err := service.luckyGiftV3Section(context.Background(), "lalu", RequirementsQuery{StartMS: startMS, EndMS: endMS}, []int64{7, 8}) if err != nil { t.Fatalf("build section: %v", err) } if source.lastReq.GetStrategyVersion() != "dynamic_v3" || source.lastReq.GetMeta().GetAppCode() != "lalu" { t.Fatalf("rpc request mismatch: %+v", source.lastReq) } if regions := source.lastReq.GetRegionIds(); len(regions) != 2 || regions[0] != 7 || regions[1] != 8 { t.Fatalf("rpc region ids mismatch: %v", regions) } if section["key"] != luckyGiftV3SectionKey { t.Fatalf("section key mismatch: %v", section["key"]) } rows, ok := section["daily_series"].([]map[string]any) if !ok || len(rows) != 2 { t.Fatalf("daily_series rows=%v, want 2 zero-filled days", section["daily_series"]) } if rows[0]["stat_day"] != "2026-07-10" || rows[0]["draw_count"] != float64(0) { t.Fatalf("zero-filled day mismatch: %+v", rows[0]) } if rows[0]["payout_rate"] != nil { t.Fatalf("zero turnover day must render nil ratio, got %v", rows[0]["payout_rate"]) } if rows[1]["stat_day"] != "2026-07-11" || rows[1]["turnover_coin"] != float64(8_000) || rows[1]["profit_coin"] != float64(2_000) { t.Fatalf("data day mismatch: %+v", rows[1]) } if rate := rows[1]["payout_rate"].(float64); rate != 0.75 { t.Fatalf("payout_rate=%v, want 0.75", rate) } total, ok := section["total"].(map[string]any) if !ok || total["draw_count"] != float64(4) || total["profit_coin"] != float64(2_000) { t.Fatalf("total mismatch: %+v", total) } average, ok := section["average"].(map[string]any) if !ok || average["draw_count"] != float64(2) || average["turnover_coin"] != float64(4_000) { t.Fatalf("average mismatch: %+v", average) } // 平均值行的比例 = 合计比例(4000/… 与 6000/8000 相同),不是逐日比例算术平均。 if rate := average["payout_rate"].(float64); rate != 0.75 { t.Fatalf("average payout_rate=%v, want 0.75", rate) } } func TestLuckyGiftV3StatDaysCrossesBeijingMidnight(t *testing.T) { // UTC 2026-07-10 15:00 ~ 17:00 跨北京 7-10/7-11 两日。 startMS := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC).UnixMilli() endMS := time.Date(2026, 7, 10, 17, 0, 0, 0, time.UTC).UnixMilli() days := luckyGiftV3StatDays(startMS, endMS) if len(days) != 2 || days[0] != "2026-07-10" || days[1] != "2026-07-11" { t.Fatalf("days=%v, want [2026-07-10 2026-07-11]", days) } if got := luckyGiftV3StatDays(endMS, startMS); got != nil { t.Fatalf("inverted range must return nil, got %v", got) } }