package business import ( "context" "testing" "time" ) type channelRepoFake struct { ChannelRepo roots, descendants []*Channel metrics *ChannelMetricData scope UserScope bindings []*Binding gameScope ChannelGameScope } func (r *channelRepoFake) List(_ context.Context, scope UserScope, _ ChannelFilter, _ ...ListOption) (*ChannelPage, error) { r.scope = scope rows := []*Channel{} for _, root := range r.roots { copy := *root rows = append(rows, ©) } return &ChannelPage{Rows: rows, Available: true}, nil } func (r *channelRepoFake) StatChannels(context.Context, []*Channel) ([]*Channel, error) { return r.descendants, nil } func (r *channelRepoFake) Metrics(context.Context, []ChannelKey, []ChannelKey, bool) (*ChannelMetricData, error) { return r.metrics, nil } func (r *channelRepoFake) Bindings(_ context.Context, scope UserScope, _ string) ([]*Binding, error) { r.scope = scope return r.bindings, nil } func (r *channelRepoFake) Find(context.Context, UserScope, ChannelKey) (*Channel, error) { return r.roots[0], nil } func (r *channelRepoFake) Games(_ context.Context, scope ChannelGameScope) ([]*ChannelGame, error) { r.gameScope = scope return []*ChannelGame{{ID: 2, PayAmount: "10.004"}, {ID: 1, PayAmount: "10.003"}}, nil } type channelWriteFake struct{ input *ChannelWrite } func (w *channelWriteFake) Create(_ context.Context, input *ChannelWrite) (uint64, error) { w.input = input return 123, nil } type channelEncoderFake struct{ received string } func (e *channelEncoderFake) Encode(raw string) (string, error) { e.received = raw return "encoded", nil } type channelPreferenceFake struct{ pref UserPreference } func (p *channelPreferenceFake) Find(context.Context, uint) (*UserPreference, error) { copy := p.pref return ©, nil } func (p *channelPreferenceFake) Save(context.Context, uint, UserPreference) error { return nil } func channelFixture() (*ChannelUsecase, *channelRepoFake, *channelPreferenceFake, *channelWriteFake, *channelEncoderFake) { key := func(id uint64) ChannelKey { return ChannelKey{SourceDB: "lx", ID: id} } r := &channelRepoFake{roots: []*Channel{{ChannelKey: key(100), Level: intPointer(1)}}, descendants: []*Channel{{ChannelKey: key(101), Level: intPointer(2), ParentID: 100, TopPromoteID: 100}, {ChannelKey: key(102), Level: intPointer(3), ParentID: 101, TopPromoteID: 100}, {ChannelKey: key(103), Level: intPointer(3), ParentID: 101}, {ChannelKey: ChannelKey{SourceDB: "hw", ID: 100}, Level: intPointer(1)}}, bindings: []*Binding{{UserID: 9, PlatformKey: PlatformKey{SourceDB: "lx", BusierID: 14}, Status: "0"}}} r.metrics = &ChannelMetricData{GamesAvailable: true, PlayersAvailable: true, PayAvailable: true, Games: map[ChannelKey]int64{key(100): 2, key(101): 1}, Players: map[ChannelKey]int64{key(100): 1, key(101): 1, key(102): 1, key(103): 1}, Pay: map[ChannelKey]string{key(100): "10.10", key(101): "20.20", key(102): "30.30", key(103): "40.40"}, PlayerCards: []ChannelPlayer{{ChannelKey: key(100), UserID: 1, IDCard: " abcX "}, {ChannelKey: key(101), UserID: 2, IDCard: "abcx"}, {ChannelKey: key(102), UserID: 3, IDCard: ""}, {ChannelKey: key(103), UserID: 4, IDCard: "third"}}} p := &channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}} w, e := &channelWriteFake{}, &channelEncoderFake{} a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1}, 7: {ID: 7, Level: 1, IDPath: "0"}, 8: {ID: 8, Level: 2, IDPath: "0,7"}, 9: {ID: 9, Level: 3, IDPath: "0,7,8"}}} uc := NewChannelUsecase(r, w, NewAccountUsecase(a), NewUserPreferenceUsecase(p), e, nil) uc.now = func() time.Time { return time.Date(2026, 11, 3, 12, 0, 0, 0, time.UTC) } return uc, r, p, w, e } func TestChannelMetricsHonorBothPreferencesWithoutChangingGameCount(t *testing.T) { uc, _, pref, _, _ := channelFixture() for _, tc := range []struct { include, dedup bool count int64 pay string }{{true, true, 1, "60.60"}, {true, false, 3, "60.60"}, {false, true, 1, "10.10"}, {false, false, 1, "10.10"}} { pref.pref = UserPreference{IncludeSubPromote: tc.include, IDCardDedup: tc.dedup} page, err := uc.List(accountContext(8, 902), ChannelFilter{}) if err != nil { t.Fatal(err) } row := page.Rows[0] if *row.GameCount != 2 || *row.PlayerCount != tc.count || *row.TotalPayAmount != tc.pay { t.Fatalf("偏好统计错误 include=%v dedup=%v row=%+v", tc.include, tc.dedup, row) } } } func TestChannelLevelTwoScopeAndUnavailableMetrics(t *testing.T) { uc, r, _, _, _ := channelFixture() r.roots = []*Channel{r.descendants[0]} page, err := uc.List(accountContext(9, 903), ChannelFilter{}) if err != nil || *page.Rows[0].PlayerCount != 2 || *page.Rows[0].TotalPayAmount != "90.90" || !r.scope.OnlySelf { t.Fatal("二级渠道仅聚合直属三级或商务范围错误") } r.metrics.PlayersAvailable = false r.metrics.PayAvailable = false page, err = uc.List(accountContext(9, 903), ChannelFilter{}) if err != nil || page.Rows[0].PlayerCount != nil || page.Rows[0].TotalPayAmount != nil || page.Rows[0].GameCount == nil { t.Fatal("未接入指标不能伪造零,已接入指标应保留") } } func TestChannelCreateOwnsHashAndSettlementDefaults(t *testing.T) { uc, r, _, writer, encoder := channelFixture() input := &ChannelCreate{SourceDB: " LX ", Account: " 中文渠道 ", Password: " pass123 ", BusierID: 14, RegisterType: 1} for _, actor := range [][2]uint{{7, 901}, {8, 902}} { if _, err := uc.Create(accountContext(actor[0], actor[1]), input); err != ErrChannelCreateScope { t.Fatal("一级和二级商务不能添加渠道") } } result, err := uc.Create(accountContext(9, 903), input) if err != nil || result.ID != 123 || result.SourceDB != "lx" { t.Fatalf("创建失败: %+v %v", result, err) } if writer.input.Account != "中文渠道" || writer.input.Password != "encoded" || encoder.received != " pass123 " || writer.input.NextCountUnix-writer.input.CreatedUnix != 604800 || writer.input.Status != -3 || writer.input.Level != 1 || writer.input.SettlementDays != 7 || !r.scope.IncludeSelf { t.Fatalf("创建参数或结算周期错误: %+v", writer.input) } input.BusierID = 999 if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelBinding { t.Fatal("选择了范围外的绑定") } input.BusierID = 14 input.Account = "bad_underscore" if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelAccount { t.Fatal("渠道账号字符限制丢失") } input.Account = "valid123" input.Password = "12345" if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelPassword { t.Fatal("密码长度未校验") } } func TestChannelGamesUseShanghaiDateAndStableMoneyOrder(t *testing.T) { uc, r, _, _, _ := channelFixture() uc.now = func() time.Time { return time.Date(2026, 11, 3, 17, 0, 0, 0, time.UTC) } games, err := uc.Games(accountContext(9, 903), ChannelKey{SourceDB: "lx", ID: 100}) if err != nil || r.gameScope.EndDate != "2026-11-04" || !r.gameScope.IDCardDedup || games[0].ID != 1 { t.Fatal("累计日期、口径或并列排序错误") } }