feat: 迁移渠道列表新增和游戏累计分布
This commit is contained in:
+1
-1
@@ -11,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase)
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrChannelNotFound = errors.NotFound("SW_CHANNEL_NOT_FOUND", "渠道不存在")
|
||||
ErrChannelScope = errors.Forbidden("SW_CHANNEL_SCOPE", "无权查看该渠道")
|
||||
ErrChannelInput = errors.BadRequest("SW_CHANNEL_INPUT", "渠道查询参数不正确")
|
||||
ErrChannelAccount = errors.BadRequest("SW_CHANNEL_ACCOUNT", "渠道账号需为中文/英文/数字,长度 4-30 位")
|
||||
ErrChannelPassword = errors.BadRequest("SW_CHANNEL_PASSWORD", "渠道密码长度需为 6-15 位")
|
||||
ErrChannelBinding = errors.BadRequest("SW_CHANNEL_BINDING", "请选择有效的游戏商务绑定")
|
||||
ErrChannelExists = errors.BadRequest("SW_CHANNEL_EXISTS", "渠道账号已存在")
|
||||
ErrChannelUnavailable = errors.BadRequest("SW_CHANNEL_UNAVAILABLE", "渠道数据尚未接入")
|
||||
ErrChannelReportsUnavailable = errors.BadRequest("SW_CHANNEL_REPORTS_UNAVAILABLE", "游戏日汇总数据尚未接入")
|
||||
ErrChannelBusy = errors.Conflict("SW_CHANNEL_BUSY", "该渠道正在创建,请稍后重试")
|
||||
ErrChannelPlatformOwner = errors.BadRequest("SW_CHANNEL_PLATFORM_OWNER", "游戏商务不存在或渠道关系更新失败")
|
||||
)
|
||||
|
||||
type ChannelKey struct {
|
||||
SourceDB string
|
||||
ID uint64
|
||||
}
|
||||
type Channel struct {
|
||||
ChannelKey
|
||||
Account, Nickname, ParentName, RealName string
|
||||
Level, RegisterType, Status, LockStatus *int
|
||||
ParentID, TopPromoteID, BusierID uint64
|
||||
BusierUserID *uint
|
||||
BindingAccount, BindingUserName string
|
||||
GameCount, PlayerCount *int64
|
||||
TotalPayAmount *string
|
||||
}
|
||||
|
||||
func (c *Channel) BusinessDisplayName() string {
|
||||
account, user := strings.TrimSpace(c.BindingAccount), strings.TrimSpace(c.BindingUserName)
|
||||
if account == "" {
|
||||
return user
|
||||
}
|
||||
if user == "" || account == user {
|
||||
return account
|
||||
}
|
||||
return account + "(" + user + ")"
|
||||
}
|
||||
func (c *Channel) IncludesStat(other *Channel, includeSub bool) bool {
|
||||
if c.SourceDB != other.SourceDB {
|
||||
return false
|
||||
}
|
||||
if c.ID == other.ID {
|
||||
return true
|
||||
}
|
||||
if !includeSub || c.Level == nil || *c.Level >= 3 {
|
||||
return false
|
||||
}
|
||||
if *c.Level == 2 {
|
||||
return other.ParentID == c.ID && other.Level != nil && *other.Level == 3
|
||||
}
|
||||
return other.ParentID == c.ID || other.TopPromoteID == c.ID
|
||||
}
|
||||
|
||||
type ChannelFilter struct {
|
||||
Sources []string
|
||||
BusierUserID *uint
|
||||
BusierAccount, Keyword string
|
||||
Level, RegisterType, Status, LockStatus *int
|
||||
}
|
||||
type ChannelPage struct {
|
||||
Rows []*Channel
|
||||
Total int64
|
||||
Available bool
|
||||
}
|
||||
type ChannelPlayer struct {
|
||||
ChannelKey
|
||||
UserID uint64
|
||||
IDCard string
|
||||
}
|
||||
type ChannelMetricData struct {
|
||||
Games map[ChannelKey]int64
|
||||
Players map[ChannelKey]int64
|
||||
Pay map[ChannelKey]string
|
||||
PlayerCards []ChannelPlayer
|
||||
GamesAvailable, PlayersAvailable, PayAvailable bool
|
||||
}
|
||||
type ChannelGame struct {
|
||||
ID uint64
|
||||
Name string
|
||||
RegisterCount int64
|
||||
PayAmount string
|
||||
}
|
||||
type ChannelGameScope struct {
|
||||
Root *Channel
|
||||
IncludeSub, IDCardDedup bool
|
||||
EndDate string
|
||||
}
|
||||
type ChannelRepo interface {
|
||||
List(context.Context, UserScope, ChannelFilter, ...ListOption) (*ChannelPage, error)
|
||||
Bindings(context.Context, UserScope, string) ([]*Binding, error)
|
||||
Find(context.Context, UserScope, ChannelKey) (*Channel, error)
|
||||
StatChannels(context.Context, []*Channel) ([]*Channel, error)
|
||||
Metrics(context.Context, []ChannelKey, []ChannelKey, bool) (*ChannelMetricData, error)
|
||||
Games(context.Context, ChannelGameScope) ([]*ChannelGame, error)
|
||||
}
|
||||
type ChannelCreate struct {
|
||||
SourceDB, Account, Password string
|
||||
BusierID uint64
|
||||
RegisterType int
|
||||
}
|
||||
type ChannelCreateResult struct {
|
||||
ID uint64
|
||||
SourceDB string
|
||||
}
|
||||
type ChannelWrite struct {
|
||||
ChannelCreate
|
||||
Status, Level, SettlementType, SettlementDays int
|
||||
ParentID, TopPromoteID uint64
|
||||
GameIDs2 string
|
||||
CreatedUnix, NextCountUnix int64
|
||||
}
|
||||
type ChannelWriteRepo interface {
|
||||
Create(context.Context, *ChannelWrite) (uint64, error)
|
||||
}
|
||||
type PlatformPasswordEncoder interface{ Encode(string) (string, error) }
|
||||
type ChannelUsecase struct {
|
||||
repo ChannelRepo
|
||||
writer ChannelWriteRepo
|
||||
accounts *AccountUsecase
|
||||
preferences *UserPreferenceUsecase
|
||||
passwords PlatformPasswordEncoder
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewChannelUsecase(repo ChannelRepo, writer ChannelWriteRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase, passwords PlatformPasswordEncoder) *ChannelUsecase {
|
||||
return &ChannelUsecase{repo: repo, writer: writer, accounts: accounts, preferences: preferences, passwords: passwords, now: time.Now}
|
||||
}
|
||||
func (uc *ChannelUsecase) scope(ctx context.Context) (UserScope, error) {
|
||||
actor, a, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return UserScope{}, err
|
||||
}
|
||||
if actor.AuthorityID == 888 {
|
||||
return UserScope{All: true}, nil
|
||||
}
|
||||
return UserScope{SelfID: a.ID, IncludeSelf: true, OnlySelf: a.Level >= 3, DescendantPath: descendantPath(a)}, nil
|
||||
}
|
||||
func normalizeIDCard(raw string) string {
|
||||
// Java isBlank 排除不换行空格;其余空白证件仍不计入去重人数。
|
||||
if strings.TrimFunc(raw, func(r rune) bool { return unicode.IsSpace(r) && r != '\u00a0' && r != '\u2007' && r != '\u202f' }) == "" {
|
||||
return ""
|
||||
}
|
||||
value := strings.TrimFunc(raw, func(r rune) bool { return r <= 32 })
|
||||
if strings.HasSuffix(value, "X") {
|
||||
value = strings.TrimSuffix(value, "X") + "x"
|
||||
}
|
||||
return value
|
||||
}
|
||||
func (uc *ChannelUsecase) List(ctx context.Context, filter ChannelFilter, options ...ListOption) (*ChannelPage, error) {
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := uc.repo.List(ctx, scope, filter, options...)
|
||||
if err != nil || len(page.Rows) == 0 {
|
||||
return page, err
|
||||
}
|
||||
pref, err := uc.preferences.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statChannels := page.Rows
|
||||
if pref.IncludeSubPromote {
|
||||
statChannels, err = uc.repo.StatChannels(ctx, page.Rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
roots, statKeys := []ChannelKey{}, []ChannelKey{}
|
||||
groups := map[ChannelKey][]ChannelKey{}
|
||||
seen := map[ChannelKey]bool{}
|
||||
for _, root := range page.Rows {
|
||||
roots = append(roots, root.ChannelKey)
|
||||
groupSeen := map[ChannelKey]bool{root.ChannelKey: true}
|
||||
groups[root.ChannelKey] = []ChannelKey{root.ChannelKey}
|
||||
for _, candidate := range statChannels {
|
||||
if root.IncludesStat(candidate, pref.IncludeSubPromote) && !groupSeen[candidate.ChannelKey] {
|
||||
groups[root.ChannelKey] = append(groups[root.ChannelKey], candidate.ChannelKey)
|
||||
groupSeen[candidate.ChannelKey] = true
|
||||
}
|
||||
}
|
||||
for _, key := range groups[root.ChannelKey] {
|
||||
if !seen[key] {
|
||||
statKeys = append(statKeys, key)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics, err := uc.repo.Metrics(ctx, roots, statKeys, pref.IDCardDedup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cards := map[ChannelKey][]ChannelPlayer{}
|
||||
for _, player := range metrics.PlayerCards {
|
||||
cards[player.ChannelKey] = append(cards[player.ChannelKey], player)
|
||||
}
|
||||
for _, root := range page.Rows {
|
||||
if metrics.GamesAvailable {
|
||||
count := metrics.Games[root.ChannelKey]
|
||||
root.GameCount = &count
|
||||
}
|
||||
count := int64(0)
|
||||
userCards := map[uint64]string{}
|
||||
pay := new(big.Rat)
|
||||
for _, key := range groups[root.ChannelKey] {
|
||||
count += metrics.Players[key]
|
||||
for _, player := range cards[key] {
|
||||
userCards[player.UserID] = normalizeIDCard(player.IDCard)
|
||||
}
|
||||
if amount := metrics.Pay[key]; amount != "" {
|
||||
n, ok := new(big.Rat).SetString(amount)
|
||||
if !ok {
|
||||
return nil, ErrStorage
|
||||
}
|
||||
pay.Add(pay, n)
|
||||
}
|
||||
}
|
||||
if metrics.PlayersAvailable {
|
||||
if pref.IDCardDedup {
|
||||
identities := map[string]bool{}
|
||||
for _, card := range userCards {
|
||||
if card != "" {
|
||||
identities[card] = true
|
||||
}
|
||||
}
|
||||
count = int64(len(identities))
|
||||
}
|
||||
root.PlayerCount = &count
|
||||
}
|
||||
if metrics.PayAvailable {
|
||||
amount := pay.FloatString(2)
|
||||
root.TotalPayAmount = &amount
|
||||
}
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
func (uc *ChannelUsecase) BindOptions(ctx context.Context, source string) ([]*Binding, error) {
|
||||
source, err := NormalizePlatform(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.Bindings(ctx, scope, source)
|
||||
}
|
||||
|
||||
var channelAccountPattern = regexp.MustCompile(`^[\x{4e00}-\x{9fa5}a-zA-Z0-9]{4,30}$`)
|
||||
|
||||
func (uc *ChannelUsecase) Create(ctx context.Context, input *ChannelCreate) (*ChannelCreateResult, error) {
|
||||
if input == nil {
|
||||
return nil, ErrChannelInput
|
||||
}
|
||||
account := strings.TrimSpace(input.Account)
|
||||
if !channelAccountPattern.MatchString(account) {
|
||||
return nil, ErrChannelAccount
|
||||
}
|
||||
size := len(utf16.Encode([]rune(input.Password)))
|
||||
if strings.TrimSpace(input.Password) == "" || size < 6 || size > 15 {
|
||||
return nil, ErrChannelPassword
|
||||
}
|
||||
bindings, err := uc.BindOptions(ctx, input.SourceDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valid := false
|
||||
for _, binding := range bindings {
|
||||
if binding.BusierID == input.BusierID {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return nil, ErrChannelBinding
|
||||
}
|
||||
source, _ := NormalizePlatform(input.SourceDB)
|
||||
password, err := uc.passwords.Encode(input.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := uc.now().Unix()
|
||||
write := &ChannelWrite{ChannelCreate: ChannelCreate{SourceDB: source, Account: account, Password: password, BusierID: input.BusierID, RegisterType: input.RegisterType}, Status: -3, Level: 1, SettlementType: 0, SettlementDays: 7, CreatedUnix: now, NextCountUnix: now + 7*24*60*60}
|
||||
id, err := uc.writer.Create(ctx, write)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ChannelCreateResult{ID: id, SourceDB: source}, nil
|
||||
}
|
||||
func (uc *ChannelUsecase) Games(ctx context.Context, key ChannelKey) ([]*ChannelGame, error) {
|
||||
source, err := NormalizePlatform(key.SourceDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.SourceDB = source
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := uc.repo.Find(ctx, scope, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pref, err := uc.preferences.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
day := uc.now().In(time.FixedZone("Asia/Shanghai", 8*3600)).Format("2006-01-02")
|
||||
games, err := uc.repo.Games(ctx, ChannelGameScope{Root: root, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup, EndDate: day})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
amounts := map[uint64]*big.Rat{}
|
||||
for _, game := range games {
|
||||
game.RegisterCount = min(game.RegisterCount, int64(2147483647))
|
||||
n, ok := new(big.Rat).SetString(game.PayAmount)
|
||||
if !ok {
|
||||
return nil, ErrStorage
|
||||
}
|
||||
game.PayAmount = n.FloatString(2)
|
||||
// 原画像先把金额保留两位再排序;同额时按游戏 ID 稳定排序。
|
||||
amounts[game.ID], _ = new(big.Rat).SetString(game.PayAmount)
|
||||
}
|
||||
sort.Slice(games, func(i, j int) bool {
|
||||
order := amounts[games[i].ID].Cmp(amounts[games[j].ID])
|
||||
if order != 0 {
|
||||
return order > 0
|
||||
}
|
||||
return games[i].ID < games[j].ID
|
||||
})
|
||||
return games, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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)
|
||||
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}
|
||||
result, err := uc.Create(accountContext(8, 902), 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(8, 902), input); err != ErrChannelBinding {
|
||||
t.Fatal("选择了范围外的绑定")
|
||||
}
|
||||
input.BusierID = 14
|
||||
input.Account = "bad_underscore"
|
||||
if _, err := uc.Create(accountContext(8, 902), input); err != ErrChannelAccount {
|
||||
t.Fatal("渠道账号字符限制丢失")
|
||||
}
|
||||
input.Account = "valid123"
|
||||
input.Password = "12345"
|
||||
if _, err := uc.Create(accountContext(8, 902), 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("累计日期、口径或并列排序错误")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user