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, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase)
|
||||
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, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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, []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 := filter.StartDate, filter.EndDate
|
||||
if begin == nil || end == nil {
|
||||
now := uc.now().In(portraitLocation)
|
||||
yesterday := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, portraitLocation)
|
||||
first := yesterday.AddDate(0, 0, -6)
|
||||
begin, end = &first, &yesterday
|
||||
}
|
||||
if end.Before(*begin) {
|
||||
return scope, filter, ErrLedgerInput
|
||||
}
|
||||
filter.BeginUnix = begin.Unix()
|
||||
filter.EndUnix = end.AddDate(0, 0, 1).Unix() - 1
|
||||
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 "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) {
|
||||
if _, _, err := uc.accounts.actor(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 10000
|
||||
}
|
||||
return uc.repo.Games(ctx, sources, strings.TrimSpace(keyword), limit)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ledgerRepoFake struct {
|
||||
LedgerRepo
|
||||
scope LedgerScope
|
||||
filter LedgerFilter
|
||||
options LedgerListOptions
|
||||
total int64
|
||||
available bool
|
||||
gameLimit int
|
||||
}
|
||||
|
||||
func (r *ledgerRepoFake) Registrations(_ context.Context, s LedgerScope, f LedgerFilter, o LedgerListOptions) (*RegistrationPage, error) {
|
||||
r.scope = s
|
||||
r.filter = f
|
||||
r.options = o
|
||||
return &RegistrationPage{Rows: []*Registration{}, Total: r.total, Available: r.available}, nil
|
||||
}
|
||||
func (r *ledgerRepoFake) Spends(_ context.Context, s LedgerScope, f LedgerFilter, o LedgerListOptions) (*SpendPage, error) {
|
||||
r.scope = s
|
||||
r.filter = f
|
||||
r.options = o
|
||||
return &SpendPage{Rows: []*Spend{}, Total: r.total, Available: r.available, TotalPayAmount: stringPointer("0")}, nil
|
||||
}
|
||||
func (r *ledgerRepoFake) Games(_ context.Context, _ []string, _ string, limit int) ([]*GameOption, error) {
|
||||
r.gameLimit = limit
|
||||
return []*GameOption{}, nil
|
||||
}
|
||||
func ledgerFixture() (*LedgerUsecase, *ledgerRepoFake, *channelPreferenceFake) {
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1, Level: 0}, 7: {ID: 7, Level: 1, IDPath: "0"}, 9: {ID: 9, Level: 3, IDPath: "0,7"}}}
|
||||
r := &ledgerRepoFake{available: true}
|
||||
p := &channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}
|
||||
uc := NewLedgerUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(p))
|
||||
uc.now = func() time.Time { return time.Date(2026, 11, 4, 14, 0, 0, 0, portraitLocation) }
|
||||
return uc, r, p
|
||||
}
|
||||
func TestLedgerDateDefaultsAndBusinessScope(t *testing.T) {
|
||||
uc, r, p := ledgerFixture()
|
||||
if _, err := uc.Registrations(accountContext(7, 901), LedgerFilter{SortField: "payAmount"}, LedgerListOptions{Limit: 10}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if time.Unix(r.filter.BeginUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-10-28 00:00:00" || time.Unix(r.filter.EndUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-11-03 23:59:59" {
|
||||
t.Fatal("未传日期应取截至昨日的七天")
|
||||
}
|
||||
if r.scope.Unrestricted || !r.scope.Viewer.IncludeSelf || r.filter.SortField != "totalPayAmount" {
|
||||
t.Fatal("商务范围或排序别名错误")
|
||||
}
|
||||
p.pref = UserPreference{IncludeSubPromote: false, IDCardDedup: false}
|
||||
uc.Spends(accountContext(9, 903), LedgerFilter{}, LedgerListOptions{Limit: 10})
|
||||
if !r.scope.Viewer.OnlySelf || r.scope.IncludeSub {
|
||||
t.Fatal("三级本人范围或下级开关丢失")
|
||||
}
|
||||
uc.Spends(accountContext(1, 888), LedgerFilter{}, LedgerListOptions{Limit: 10})
|
||||
if !r.scope.Unrestricted {
|
||||
t.Fatal("超管默认全平台")
|
||||
}
|
||||
uc.Spends(accountContext(1, 888), LedgerFilter{BusierAccount: "商务"}, LedgerListOptions{Limit: 10})
|
||||
if r.scope.Unrestricted {
|
||||
t.Fatal("超管指定商务后必须收窄")
|
||||
}
|
||||
}
|
||||
func TestLedgerExportLimitAndDisplayFallback(t *testing.T) {
|
||||
uc, r, _ := ledgerFixture()
|
||||
r.total = 10001
|
||||
if _, err := uc.ExportRegistrations(accountContext(7, 901), LedgerFilter{}); err != ErrLedgerExportLimit || r.options.RejectAbove != 10000 {
|
||||
t.Fatal("导出没有执行一万条上限")
|
||||
}
|
||||
r.total = 0
|
||||
r.available = false
|
||||
if _, err := uc.ExportSpends(accountContext(7, 901), LedgerFilter{}); err != ErrLedgerUnavailable {
|
||||
t.Fatal("缺数据不能导出空文件冒充成功")
|
||||
}
|
||||
zero, unknown := int64(0), int64(99)
|
||||
for _, tc := range []struct {
|
||||
names LedgerNames
|
||||
want string
|
||||
}{{LedgerNames{PromoteID: &zero}, "官方渠道"}, {LedgerNames{PromoteID: &unknown}, "未知渠道(ID:99)"}, {LedgerNames{}, "未知渠道"}, {LedgerNames{PromoteID: &zero, PromoteAccount: "历史账号"}, "历史账号"}} {
|
||||
if tc.names.DisplayPromote() != tc.want {
|
||||
t.Fatal("渠道展示回退错误")
|
||||
}
|
||||
}
|
||||
uc.Games(accountContext(1, 888), nil, "", 51)
|
||||
if r.gameLimit != 10000 {
|
||||
t.Fatal("原游戏选项的特殊批量上限丢失")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user