package business import ( "context" "strconv" "strings" "time" "github.com/go-kratos/kratos/v3/errors" ) var ( ErrLedgerInput = errors.BadRequest("SW_LEDGER_INPUT", "明细查询参数不正确") ErrLedgerUnavailable = errors.BadRequest("SW_LEDGER_UNAVAILABLE", "明细数据尚未接入,无法导出") ErrLedgerExportLimit = errors.BadRequest("SW_LEDGER_EXPORT_LIMIT", "导出数据超过10000条,请缩小时间范围或筛选条件") ErrGameOptionsUnavailable = errors.BadRequest("SW_GAME_OPTIONS_UNAVAILABLE", "游戏数据尚未接入") ) type LedgerFilter struct { Sources, GameNames []string UserID *int64 PromoteAccount, BusierAccount, PlayerName, OrderNumber string PayStatus, PayWay *int StartDate, EndDate *time.Time BeginUnix, EndUnix int64 SortField string Ascending bool } type LedgerScope struct { Viewer UserScope IncludeSub, Unrestricted bool } type LedgerListOptions struct { Offset, Limit int RejectAbove int64 } type LedgerNames struct { PromoteID *int64 PromoteAccount, BindingAccount, BindingUserName string BusinessPath []string } func (n *LedgerNames) DisplayPromote() string { if strings.TrimSpace(n.PromoteAccount) != "" { return n.PromoteAccount } if n.PromoteID == nil { return "未知渠道" } if *n.PromoteID == 0 { return "官方渠道" } return "未知渠道(ID:" + strconv.FormatInt(*n.PromoteID, 10) + ")" } func (n *LedgerNames) DisplayBusiness() string { if len(n.BusinessPath) > 0 { return strings.Join(n.BusinessPath, "→") } return (&Channel{BindingAccount: n.BindingAccount, BindingUserName: n.BindingUserName}).BusinessDisplayName() } type Registration struct { ChannelKey LedgerNames RegisterTime *int64 FirstGameID *int64 FirstGameName, RegisterIP, DeviceName string TotalPayAmount *string } type Spend struct { ChannelKey LedgerNames UserID, GameID *int64 OrderNumber, GameName, ServerID, ServerName, PlayerName, IP string PayAmount *string PayTime *int64 PayWay, PayStatus *int } type RegistrationPage struct { Rows []*Registration Total int64 Available bool } type SpendPage struct { Rows []*Spend Total int64 TotalPayAmount *string Available bool } type GameOption struct { SourceDB string ID uint64 Name string } type LedgerRepo interface { Registrations(context.Context, LedgerScope, LedgerFilter, LedgerListOptions) (*RegistrationPage, error) Spends(context.Context, LedgerScope, LedgerFilter, LedgerListOptions) (*SpendPage, error) Games(context.Context, UserScope, []string, string, int) ([]*GameOption, error) } type LedgerUsecase struct { repo LedgerRepo accounts *AccountUsecase preferences *UserPreferenceUsecase now func() time.Time } func NewLedgerUsecase(repo LedgerRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *LedgerUsecase { return &LedgerUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now} } func (uc *LedgerUsecase) prepare(ctx context.Context, filter LedgerFilter) (LedgerScope, LedgerFilter, error) { actor, a, err := uc.accounts.actor(ctx) if err != nil { return LedgerScope{}, filter, err } pref, err := uc.preferences.Current(ctx) if err != nil { return LedgerScope{}, filter, err } viewer := portraitUserScope(a) viewer.All = actor.AuthorityID == 888 scope := LedgerScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, Unrestricted: viewer.All && strings.TrimSpace(filter.BusierAccount) == ""} begin, end := reportDateDefaults(filter.StartDate, filter.EndDate, 30, uc.now()) if end.Before(*begin) { return scope, filter, ErrLedgerInput } filter.BeginUnix = begin.Unix() filter.EndUnix = end.AddDate(0, 0, 1).Unix() - 1 filter.StartDate, filter.EndDate = begin, end return scope, filter, nil } func registrationSort(field string) string { switch field { case "totalPayAmount", "total_pay_amount", "totalPay", "payAmount": return "totalPayAmount" } return "registerTime" } func spendSort(field string) string { switch field { case "id": return "id" case "payAmount", "pay_amount", "amount": return "payAmount" } return "payTime" } func (uc *LedgerUsecase) Registrations(ctx context.Context, filter LedgerFilter, options LedgerListOptions) (*RegistrationPage, error) { scope, filter, err := uc.prepare(ctx, filter) if err != nil { return nil, err } filter.SortField = registrationSort(filter.SortField) page, err := uc.repo.Registrations(ctx, scope, filter, options) if err != nil { return nil, err } for _, row := range page.Rows { row.PromoteAccount = row.DisplayPromote() if row.TotalPayAmount != nil { n, err := portraitNumber(*row.TotalPayAmount) if err != nil { return nil, err } row.TotalPayAmount = stringPointer(portraitDecimal(n, 2)) } } return page, nil } func (uc *LedgerUsecase) Spends(ctx context.Context, filter LedgerFilter, options LedgerListOptions) (*SpendPage, error) { scope, filter, err := uc.prepare(ctx, filter) if err != nil { return nil, err } filter.SortField = spendSort(filter.SortField) page, err := uc.repo.Spends(ctx, scope, filter, options) if err != nil { return nil, err } for _, row := range page.Rows { row.PromoteAccount = row.DisplayPromote() if row.PayAmount != nil { n, err := portraitNumber(*row.PayAmount) if err != nil { return nil, err } row.PayAmount = stringPointer(portraitDecimal(n, 2)) } } if page.TotalPayAmount != nil { n, err := portraitNumber(*page.TotalPayAmount) if err != nil { return nil, err } page.TotalPayAmount = stringPointer(portraitDecimal(n, 2)) } return page, nil } func (uc *LedgerUsecase) ExportRegistrations(ctx context.Context, filter LedgerFilter) ([]*Registration, error) { page, err := uc.Registrations(ctx, filter, LedgerListOptions{Limit: 10000, RejectAbove: 10000}) if err != nil { return nil, err } if !page.Available { return nil, ErrLedgerUnavailable } if page.Total > 10000 { return nil, ErrLedgerExportLimit } for _, row := range page.Rows { if row.TotalPayAmount == nil { return nil, ErrLedgerUnavailable } } return page.Rows, nil } func (uc *LedgerUsecase) ExportSpends(ctx context.Context, filter LedgerFilter) ([]*Spend, error) { page, err := uc.Spends(ctx, filter, LedgerListOptions{Limit: 10000, RejectAbove: 10000}) if err != nil { return nil, err } if !page.Available { return nil, ErrLedgerUnavailable } if page.Total > 10000 { return nil, ErrLedgerExportLimit } return page.Rows, nil } func (uc *LedgerUsecase) Games(ctx context.Context, sources []string, keyword string, limit int) ([]*GameOption, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return nil, err } viewer := portraitUserScope(account) viewer.All = actor.AuthorityID == 888 if limit <= 0 { limit = 20 } if limit > 50 { limit = 10000 } return uc.repo.Games(ctx, viewer, sources, strings.TrimSpace(keyword), limit) }