feat: 迁移注册充值明细和筛选导出

This commit is contained in:
Yvan
2026-11-04 00:32:49 +08:00
parent f5b7ce21b9
commit 6703a66f0d
34 changed files with 1964 additions and 9 deletions
+1 -1
View File
@@ -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)
+239
View File
@@ -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)
}
+92
View File
@@ -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("原游戏选项的特殊批量上限丢失")
}
}