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("原游戏选项的特殊批量上限丢失")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
type ledgerRepo struct{ data Provider }
|
||||
|
||||
func NewLedgerRepo(data Provider) business.LedgerRepo { return &ledgerRepo{data: data} }
|
||||
func ledgerBasePromotes(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter, alias string) *gorm.DB {
|
||||
q := db.Table("tab_promote " + alias)
|
||||
if len(filter.Sources) > 0 {
|
||||
q = q.Where(alias+".source_db IN ?", filter.Sources)
|
||||
}
|
||||
if !scope.IncludeSub {
|
||||
q = q.Where(alias + ".promote_level = 1")
|
||||
}
|
||||
if !scope.Unrestricted {
|
||||
bindings := channelBindingQuery(db, scope.Viewer, business.ChannelFilter{Sources: filter.Sources, BusierAccount: filter.BusierAccount}).Select("1").Where("b.source_db=" + alias + ".source_db AND b.platform_busier_id=" + alias + ".busier_id")
|
||||
q = q.Where("EXISTS (?)", bindings)
|
||||
}
|
||||
return q
|
||||
}
|
||||
func ledgerPromotes(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter, alias string) *gorm.DB {
|
||||
q := ledgerBasePromotes(db, scope, filter, alias)
|
||||
if filter.PromoteAccount == "" {
|
||||
return q
|
||||
}
|
||||
hits := ledgerBasePromotes(db, scope, filter, "hit")
|
||||
exact := "TRIM(hit.account) = ? COLLATE BINARY"
|
||||
if db.Dialector.Name() == "mysql" {
|
||||
exact = "BINARY TRIM(hit.account) = ?"
|
||||
}
|
||||
hits = hits.Where(exact, filter.PromoteAccount).Where("hit.source_db=" + alias + ".source_db")
|
||||
if scope.IncludeSub {
|
||||
root := "CASE WHEN hit.promote_level=1 THEN hit.id WHEN hit.top_promote_id>0 THEN hit.top_promote_id ELSE hit.id END"
|
||||
hits = hits.Where("(" + alias + ".id = (" + root + ") OR " + alias + ".top_promote_id = (" + root + "))")
|
||||
} else {
|
||||
hits = hits.Where("hit.id=" + alias + ".id")
|
||||
}
|
||||
return q.Where("EXISTS (?)", hits.Select("1"))
|
||||
}
|
||||
func prepareLedger(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter) (empty, available bool, err error) {
|
||||
if !scope.Unrestricted {
|
||||
var bindings []struct{ UserID uint }
|
||||
if err := channelBindingQuery(db, scope.Viewer, business.ChannelFilter{Sources: filter.Sources, BusierAccount: filter.BusierAccount}).Select("b.user_id").Limit(1).Scan(&bindings).Error; err != nil {
|
||||
return false, false, business.ErrStorage
|
||||
}
|
||||
if len(bindings) == 0 {
|
||||
return true, true, nil
|
||||
}
|
||||
}
|
||||
available, err = businessTableAvailable(db, "tab_promote")
|
||||
if err != nil || !available {
|
||||
return false, available, err
|
||||
}
|
||||
var rows []struct{ ID uint64 }
|
||||
if err := ledgerPromotes(db, scope, filter, "p").Select("p.id").Limit(1).Scan(&rows).Error; err != nil {
|
||||
return false, false, business.ErrStorage
|
||||
}
|
||||
return len(rows) == 0, true, nil
|
||||
}
|
||||
func ledgerScopeQuery(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter, table string) *gorm.DB {
|
||||
q := db.Table(table + " r")
|
||||
if len(filter.Sources) > 0 {
|
||||
q = q.Where("r.source_db IN ?", filter.Sources)
|
||||
}
|
||||
if scope.Unrestricted && scope.IncludeSub && filter.PromoteAccount == "" {
|
||||
return q
|
||||
}
|
||||
return q.Where("EXISTS (?)", ledgerPromotes(db, scope, filter, "p").Select("1").Where("p.source_db=r.source_db AND p.id=r.promote_id"))
|
||||
}
|
||||
|
||||
type registrationPO struct {
|
||||
ID uint64
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
RegisterTime *string
|
||||
FirstGameID *int64 `gorm:"column:fgame_id"`
|
||||
FirstGameName string `gorm:"column:fgame_name"`
|
||||
RegisterIP string `gorm:"column:register_ip"`
|
||||
DeviceName string
|
||||
PromoteID *int64
|
||||
}
|
||||
|
||||
func registrationQuery(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter) *gorm.DB {
|
||||
cast := "CAST(r.register_time AS INTEGER)"
|
||||
if db.Dialector.Name() == "mysql" {
|
||||
cast = "CAST(r.register_time AS UNSIGNED)"
|
||||
}
|
||||
q := ledgerScopeQuery(db, scope, filter, "tab_user").Where("r.puid = 0").Where(cast+" BETWEEN ? AND ?", filter.BeginUnix, filter.EndUnix)
|
||||
if filter.UserID != nil {
|
||||
q = q.Where("r.id = ?", *filter.UserID)
|
||||
}
|
||||
if len(filter.GameNames) > 0 {
|
||||
q = q.Where("EXISTS (?)", db.Table("tab_game g").Select("1").Where("g.source_db=r.source_db AND g.id=r.fgame_id AND g.game_name IN ?", filter.GameNames))
|
||||
}
|
||||
return q
|
||||
}
|
||||
func (r *ledgerRepo) Registrations(ctx context.Context, scope business.LedgerScope, filter business.LedgerFilter, options business.LedgerListOptions) (*business.RegistrationPage, error) {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
page := &business.RegistrationPage{Rows: []*business.Registration{}, Available: true}
|
||||
empty, available, err := prepareLedger(db, scope, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Available = available
|
||||
if empty || !available {
|
||||
return page, nil
|
||||
}
|
||||
for _, table := range append([]string{"tab_user"}, func() []string {
|
||||
if len(filter.GameNames) > 0 {
|
||||
return []string{"tab_game"}
|
||||
}
|
||||
return nil
|
||||
}()...) {
|
||||
available, err := businessTableAvailable(db, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !available {
|
||||
page.Available = false
|
||||
return page, nil
|
||||
}
|
||||
}
|
||||
payAvailable, err := businessTableAvailable(db, "tab_spend")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter.SortField == "totalPayAmount" && !payAvailable {
|
||||
page.Available = false
|
||||
return page, nil
|
||||
}
|
||||
q := registrationQuery(db, scope, filter)
|
||||
if err := q.Count(&page.Total).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
if options.RejectAbove > 0 && page.Total > options.RejectAbove {
|
||||
return page, nil
|
||||
}
|
||||
direction := " DESC"
|
||||
if filter.Ascending {
|
||||
direction = " ASC"
|
||||
}
|
||||
order := "CAST(r.register_time AS INTEGER)"
|
||||
if db.Dialector.Name() == "mysql" {
|
||||
order = "CAST(r.register_time AS UNSIGNED)"
|
||||
}
|
||||
if filter.SortField == "totalPayAmount" {
|
||||
order = "(SELECT COALESCE(SUM(s.pay_amount),0) FROM tab_spend s WHERE s.source_db=r.source_db AND s.user_id=r.id AND s.pay_status=1)"
|
||||
}
|
||||
var pos []registrationPO
|
||||
if err := q.Select("r.id,r.source_db,r.register_time,r.fgame_id,r.fgame_name,r.register_ip,r.device_name,r.promote_id").Order(order + direction).Order("r.id DESC,r.source_db ASC").Offset(options.Offset).Limit(options.Limit).Scan(&pos).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
meta := []ledgerMetaTarget{}
|
||||
keys := []business.ChannelKey{}
|
||||
for _, po := range pos {
|
||||
row := &business.Registration{ChannelKey: business.ChannelKey{SourceDB: po.SourceDB, ID: po.ID}, LedgerNames: business.LedgerNames{PromoteID: po.PromoteID}, FirstGameID: po.FirstGameID, FirstGameName: po.FirstGameName, RegisterIP: po.RegisterIP, DeviceName: po.DeviceName}
|
||||
if po.RegisterTime != nil {
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(*po.RegisterTime), 10, 64)
|
||||
if err == nil {
|
||||
row.RegisterTime = &value
|
||||
}
|
||||
}
|
||||
if payAvailable {
|
||||
zero := "0"
|
||||
row.TotalPayAmount = &zero
|
||||
}
|
||||
page.Rows = append(page.Rows, row)
|
||||
meta = append(meta, ledgerMetaTarget{Source: row.SourceDB, Names: &row.LedgerNames})
|
||||
keys = append(keys, row.ChannelKey)
|
||||
}
|
||||
if err := fillLedgerNames(db, scope, filter, meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payAvailable && len(keys) > 0 {
|
||||
amounts := map[business.ChannelKey]string{}
|
||||
for start := 0; start < len(keys); start += 300 {
|
||||
var values []struct {
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
UserID uint64
|
||||
Amount string
|
||||
}
|
||||
if err := db.Table("tab_spend").Select("source_db,user_id,COALESCE(SUM(pay_amount),0) AS amount").Where("pay_status=1 AND (source_db,user_id) IN ?", channelPairs(keys[start:min(start+300, len(keys))])).Group("source_db,user_id").Scan(&values).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, value := range values {
|
||||
amounts[business.ChannelKey{SourceDB: value.SourceDB, ID: value.UserID}] = value.Amount
|
||||
}
|
||||
}
|
||||
for _, row := range page.Rows {
|
||||
if amount, ok := amounts[row.ChannelKey]; ok {
|
||||
row.TotalPayAmount = &amount
|
||||
}
|
||||
}
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
type spendPO struct {
|
||||
ID uint64
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
UserID, GameID, PromoteID *int64
|
||||
PayOrderNumber, GameName, ServerID, ServerName, GamePlayerName, SpendIP, PromoteAccount string
|
||||
PayAmount *string
|
||||
PayTime *int64
|
||||
PayWay, PayStatus *int
|
||||
}
|
||||
|
||||
func spendQuery(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter, successOnly bool) *gorm.DB {
|
||||
q := ledgerScopeQuery(db, scope, filter, "tab_spend").Where("r.pay_time BETWEEN ? AND ?", filter.BeginUnix, filter.EndUnix)
|
||||
if successOnly {
|
||||
q = q.Where("r.pay_status = 1")
|
||||
} else if filter.PayStatus != nil {
|
||||
q = q.Where("r.pay_status = ?", *filter.PayStatus)
|
||||
}
|
||||
if len(filter.GameNames) > 0 {
|
||||
q = q.Where("r.game_name IN ?", filter.GameNames)
|
||||
}
|
||||
if filter.UserID != nil {
|
||||
q = q.Where("r.user_id = ?", *filter.UserID)
|
||||
}
|
||||
if filter.PayWay != nil {
|
||||
q = q.Where("r.pay_way = ?", *filter.PayWay)
|
||||
}
|
||||
if filter.PlayerName != "" {
|
||||
q = q.Where("r.game_player_name LIKE ?", "%"+filter.PlayerName+"%")
|
||||
}
|
||||
if filter.OrderNumber != "" {
|
||||
q = q.Where("r.pay_order_number LIKE ?", "%"+filter.OrderNumber+"%")
|
||||
}
|
||||
return q
|
||||
}
|
||||
func (r *ledgerRepo) Spends(ctx context.Context, scope business.LedgerScope, filter business.LedgerFilter, options business.LedgerListOptions) (*business.SpendPage, error) {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
zero := "0"
|
||||
page := &business.SpendPage{Rows: []*business.Spend{}, Available: true, TotalPayAmount: &zero}
|
||||
empty, available, err := prepareLedger(db, scope, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Available = available
|
||||
if !available {
|
||||
page.TotalPayAmount = nil
|
||||
}
|
||||
if empty || !available {
|
||||
return page, nil
|
||||
}
|
||||
available, err = businessTableAvailable(db, "tab_spend")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !available {
|
||||
page.Available = false
|
||||
page.TotalPayAmount = nil
|
||||
return page, nil
|
||||
}
|
||||
q := spendQuery(db, scope, filter, false)
|
||||
if err := q.Count(&page.Total).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
if options.RejectAbove > 0 && page.Total > options.RejectAbove {
|
||||
return page, nil
|
||||
}
|
||||
if err := spendQuery(db, scope, filter, true).Select("COALESCE(SUM(r.pay_amount),0)").Scan(page.TotalPayAmount).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
order := "r.pay_time"
|
||||
if filter.SortField == "payAmount" {
|
||||
order = "r.pay_amount"
|
||||
}
|
||||
if filter.Ascending {
|
||||
order += " ASC"
|
||||
} else {
|
||||
order += " DESC"
|
||||
}
|
||||
var pos []spendPO
|
||||
if err := q.Select("r.id,r.source_db,r.user_id,r.pay_order_number,r.game_id,r.game_name,r.server_id,r.server_name,r.game_player_name,r.pay_amount,r.pay_time,r.pay_way,r.pay_status,r.spend_ip,r.promote_id,r.promote_account").Order(order).Order("r.id DESC,r.source_db ASC").Offset(options.Offset).Limit(options.Limit).Scan(&pos).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
meta := []ledgerMetaTarget{}
|
||||
for _, po := range pos {
|
||||
row := &business.Spend{ChannelKey: business.ChannelKey{SourceDB: po.SourceDB, ID: po.ID}, LedgerNames: business.LedgerNames{PromoteID: po.PromoteID, PromoteAccount: po.PromoteAccount}, UserID: po.UserID, GameID: po.GameID, OrderNumber: po.PayOrderNumber, GameName: po.GameName, ServerID: po.ServerID, ServerName: po.ServerName, PlayerName: po.GamePlayerName, PayAmount: po.PayAmount, PayTime: po.PayTime, PayWay: po.PayWay, PayStatus: po.PayStatus, IP: po.SpendIP}
|
||||
page.Rows = append(page.Rows, row)
|
||||
meta = append(meta, ledgerMetaTarget{Source: row.SourceDB, Names: &row.LedgerNames})
|
||||
}
|
||||
if err := fillLedgerNames(db, scope, filter, meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
type ledgerMetaTarget struct {
|
||||
Source string
|
||||
Names *business.LedgerNames
|
||||
}
|
||||
|
||||
func fillLedgerNames(db *gorm.DB, scope business.LedgerScope, filter business.LedgerFilter, targets []ledgerMetaTarget) error {
|
||||
keys := []business.ChannelKey{}
|
||||
seen := map[business.ChannelKey]bool{}
|
||||
for _, target := range targets {
|
||||
if target.Names.PromoteID == nil || *target.Names.PromoteID < 0 {
|
||||
continue
|
||||
}
|
||||
key := business.ChannelKey{SourceDB: target.Source, ID: uint64(*target.Names.PromoteID)}
|
||||
if !seen[key] {
|
||||
keys = append(keys, key)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
channels := map[business.ChannelKey]channelPO{}
|
||||
pairs := []business.PlatformKey{}
|
||||
pairSeen := map[business.PlatformKey]bool{}
|
||||
for start := 0; start < len(keys); start += 300 {
|
||||
var rows []channelPO
|
||||
if err := ledgerPromotes(db, scope, filter, "p").Select("p.id,p.source_db,p.account,p.busier_id").Where("(p.source_db,p.id) IN ?", channelPairs(keys[start:min(start+300, len(keys))])).Scan(&rows).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
for _, row := range rows {
|
||||
channels[business.ChannelKey{SourceDB: row.SourceDB, ID: row.ID}] = row
|
||||
key := business.PlatformKey{SourceDB: row.SourceDB, BusierID: row.BusierID}
|
||||
if !pairSeen[key] {
|
||||
pairs = append(pairs, key)
|
||||
pairSeen[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
type bindName struct {
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
PlatformBusierID uint64
|
||||
PlatformBusierAccount, Username string
|
||||
}
|
||||
names := map[business.PlatformKey]bindName{}
|
||||
for start := 0; start < len(pairs); start += 300 {
|
||||
var rows []bindName
|
||||
q := channelBindingQuery(db, scope.Viewer, business.ChannelFilter{}).Select("b.source_db,b.platform_busier_id,b.platform_busier_account,u.username").Where("(b.source_db,b.platform_busier_id) IN ?", portraitBindingPairs(pairs[start:min(start+300, len(pairs))])).Order("b.user_id")
|
||||
if err := q.Scan(&rows).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
for _, row := range rows {
|
||||
key := business.PlatformKey{SourceDB: row.SourceDB, BusierID: row.PlatformBusierID}
|
||||
if _, ok := names[key]; !ok {
|
||||
names[key] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, target := range targets {
|
||||
n := target.Names
|
||||
if n.PromoteID == nil || *n.PromoteID < 0 {
|
||||
continue
|
||||
}
|
||||
if c, ok := channels[business.ChannelKey{SourceDB: target.Source, ID: uint64(*n.PromoteID)}]; ok {
|
||||
if strings.TrimSpace(n.PromoteAccount) == "" {
|
||||
n.PromoteAccount = c.Account
|
||||
}
|
||||
if binding, ok := names[business.PlatformKey{SourceDB: c.SourceDB, BusierID: c.BusierID}]; ok {
|
||||
n.BindingAccount = binding.PlatformBusierAccount
|
||||
n.BindingUserName = binding.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *ledgerRepo) Games(ctx context.Context, sources []string, keyword string, limit int) ([]*business.GameOption, error) {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
available, err := businessTableAvailable(db, "tab_game")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !available {
|
||||
return nil, business.ErrGameOptionsUnavailable
|
||||
}
|
||||
q := db.Table("tab_game")
|
||||
if len(sources) > 0 {
|
||||
q = q.Where("source_db IN ?", sources)
|
||||
}
|
||||
if keyword != "" {
|
||||
q = q.Where("game_name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
var rows []struct {
|
||||
ID uint64
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
GameName string
|
||||
}
|
||||
if err := q.Select("id,source_db,game_name").Order("game_name,id,source_db").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
result := make([]*business.GameOption, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
result = append(result, &business.GameOption{ID: row.ID, SourceDB: row.SourceDB, Name: row.GameName})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
func prepareLedgerData(t *testing.T) (*gorm.DB, business.LedgerRepo, business.LedgerScope, business.LedgerFilter) {
|
||||
t.Helper()
|
||||
db, _ := prepareChannels(t)
|
||||
for _, sql := range []string{
|
||||
"DROP TABLE tab_user", "DROP TABLE tab_spend",
|
||||
"CREATE TABLE tab_user(id INTEGER,source_db TEXT,register_time TEXT,fgame_id INTEGER,fgame_name TEXT,register_ip TEXT,device_name TEXT,promote_id INTEGER,puid INTEGER)",
|
||||
"INSERT INTO tab_user VALUES(1,'lx','1500',1,'历史游戏名','192.0.2.1','机型甲',100,0),(2,'lx','1500',1,'历史游戏名','','',101,NULL),(3,'lx','1500',1,'历史游戏名','','',102,0),(4,'lx','1500',1,'历史游戏名','','',101,1),(5,'lx','1500',1,'历史游戏名','','',103,0),(6,'lx','1500',1,'历史游戏名','','',0,0),(7,'lx','1500',1,'历史游戏名','','',999,0),(1,'hw','1500',1,'其他平台','','',100,0)",
|
||||
"CREATE TABLE tab_spend(id INTEGER,source_db TEXT,user_id INTEGER,pay_order_number TEXT,game_id INTEGER,game_name TEXT,server_id TEXT,server_name TEXT,game_player_name TEXT,pay_amount DECIMAL(20,2),pay_time INTEGER,pay_way INTEGER,pay_status INTEGER,spend_ip TEXT,promote_id INTEGER,promote_account TEXT)",
|
||||
"INSERT INTO tab_spend VALUES(901,'lx',1,'00001',1,'游戏甲','s1','区服甲','角色甲',10.10,1500,3,1,'192.0.2.2',100,''),(902,'lx',3,'00002',1,'游戏甲','s1','区服甲','角色乙',30.30,1501,4,1,'',102,''),(903,'lx',5,'00003',1,'游戏甲','s1','区服甲','角色丙',40.40,1502,3,1,'',103,''),(904,'lx',1,'failed',1,'游戏甲','s1','区服甲','角色甲',999,1503,3,0,'',100,''),(905,'lx',1,'outside',1,'游戏甲','s1','区服甲','角色甲',700,500,3,1,'',999,'历史渠道'),(906,'hw',1,'other',1,'同号其他游戏','s1','其他','角色甲',9999,1500,3,1,'',100,''),(907,'lx',6,'official',1,'游戏甲','s1','官方','角色甲',5,1500,3,1,'',0,''),(908,'lx',7,'orphan',1,'游戏甲','s1','未知','角色甲',6,1500,3,1,'',999,''),(909,'lx',1,'pending',1,'游戏甲','s1','区服甲','角色甲',8,1504,3,2,'',100,'旧渠道名')",
|
||||
} {
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
scope := business.LedgerScope{Viewer: channelTestScope(), IncludeSub: true}
|
||||
filter := business.LedgerFilter{Sources: []string{"lx"}, BeginUnix: 1000, EndUnix: 2000, SortField: "registerTime"}
|
||||
return db, NewLedgerRepo(testData{db}), scope, filter
|
||||
}
|
||||
func TestRegistrationScopePuidAndUnboundedPlayerPay(t *testing.T) {
|
||||
_, repo, scope, filter := prepareLedgerData(t)
|
||||
ctx := context.Background()
|
||||
page, err := repo.Registrations(ctx, scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 3 {
|
||||
t.Fatalf("注册范围或 puid=0 规则错误: %+v %v", page, err)
|
||||
}
|
||||
var first *business.Registration
|
||||
for _, row := range page.Rows {
|
||||
if row.ID == 1 {
|
||||
first = row
|
||||
}
|
||||
}
|
||||
if first == nil || first.TotalPayAmount == nil || portraitTestMoney(*first.TotalPayAmount) != "710.10" || first.FirstGameName != "历史游戏名" || first.DisplayBusiness() != "lx-business(leaf)" {
|
||||
t.Fatalf("累计充值不能受本次时间/渠道限制,且需保留游戏快照: %+v", first)
|
||||
}
|
||||
filter.SortField = "totalPayAmount"
|
||||
page, err = repo.Registrations(ctx, scope, filter, business.LedgerListOptions{Limit: 2})
|
||||
if err != nil || page.Rows[0].ID != 1 || page.Rows[1].ID != 5 {
|
||||
t.Fatal("累计金额排序关联到了错误的支付 ID 或平台")
|
||||
}
|
||||
filter.GameNames = []string{"同号其他游戏"}
|
||||
page, err = repo.Registrations(ctx, scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 0 {
|
||||
t.Fatal("游戏名称筛选串到了另一个平台")
|
||||
}
|
||||
filter.GameNames = []string{"游戏甲"}
|
||||
filter.PromoteAccount = "child-channel"
|
||||
page, err = repo.Registrations(ctx, scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 2 {
|
||||
t.Fatal("精确子渠道账号应扩到同一一级树内的可见渠道")
|
||||
}
|
||||
filter.PromoteAccount = "CHILD-channel"
|
||||
page, err = repo.Registrations(ctx, scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 0 {
|
||||
t.Fatal("渠道账号应保持区分大小写的精确匹配")
|
||||
}
|
||||
}
|
||||
func TestSpendSummaryIgnoresSelectedFailureStatusAndKeepsSnapshot(t *testing.T) {
|
||||
_, repo, scope, filter := prepareLedgerData(t)
|
||||
status := 0
|
||||
filter.PayStatus = &status
|
||||
page, err := repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 1 || page.Rows[0].ID != 904 || portraitTestMoney(*page.TotalPayAmount) != "80.80" {
|
||||
t.Fatalf("失败列表与成功支付合计不能使用同一个状态条件: %+v %v", page, err)
|
||||
}
|
||||
status = 2
|
||||
page, err = repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Rows[0].PromoteAccount != "旧渠道名" {
|
||||
t.Fatal("支付明细不应覆盖历史渠道账号")
|
||||
}
|
||||
filter.PayStatus = nil
|
||||
scope.Unrestricted = true
|
||||
scope.Viewer = business.UserScope{All: true}
|
||||
page, err = repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 7 || portraitTestMoney(*page.TotalPayAmount) != "91.80" {
|
||||
t.Fatalf("超管应包含官方和失去当前渠道资料的历史订单: %+v %v", page, err)
|
||||
}
|
||||
unknown := map[uint64]string{}
|
||||
for _, row := range page.Rows {
|
||||
unknown[row.ID] = row.DisplayPromote()
|
||||
}
|
||||
if unknown[907] != "官方渠道" || unknown[908] != "未知渠道(ID:999)" {
|
||||
t.Fatal("渠道资料缺失时显示不明确")
|
||||
}
|
||||
scope.IncludeSub = false
|
||||
page, err = repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 3 {
|
||||
t.Fatal("不含下级时超管只保留真实一级渠道")
|
||||
}
|
||||
}
|
||||
func TestLedgerLargeTreeFilterDoesNotFallBackToAll(t *testing.T) {
|
||||
db, repo, scope, filter := prepareLedgerData(t)
|
||||
for i := 0; i < 205; i++ {
|
||||
if err := db.Exec("INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id) VALUES(?,'lx',?,3,100,100,14)", 1000+i, fmt.Sprintf("child-%d", i)).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
scope.Unrestricted = true
|
||||
scope.Viewer = business.UserScope{All: true}
|
||||
filter.PromoteAccount = "root-channel"
|
||||
page, err := repo.Registrations(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Total != 2 {
|
||||
t.Fatalf("超过200渠道后筛选被放宽到全平台: %+v %v", page, err)
|
||||
}
|
||||
filter.PromoteAccount = ""
|
||||
page, err = repo.Registrations(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20, RejectAbove: 1})
|
||||
if err != nil || page.Total != 5 || len(page.Rows) != 0 {
|
||||
t.Fatal("导出超过上限应在读取完整结果前停止")
|
||||
}
|
||||
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page, err = repo.Registrations(context.Background(), scope, filter, business.LedgerListOptions{Limit: 20})
|
||||
if err != nil || page.Available {
|
||||
t.Fatal("缺渠道范围表不能把限制解释为全量")
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ var ProviderSet = wire.NewSet(
|
||||
databusiness.NewChannelWriteRepo,
|
||||
databusiness.NewBusinessPortraitRepo,
|
||||
databusiness.NewChannelPortraitRepo,
|
||||
databusiness.NewLedgerRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
func Definition() module.Definition {
|
||||
return module.Definition{Name: "business", Migrations: databusiness.Migrations(), Surface: module.Surface{
|
||||
Menus: []module.Menu{
|
||||
{Name: "sw_DataAnalysis", Path: "sw-DataAnalysis", Component: "view/routerHolder.vue", Title: "数据分析", Icon: "data-analysis", Sort: 3},
|
||||
{Name: "sw_RegistrationDetails", Path: "RegistrationDetails", ParentName: "sw_DataAnalysis", Component: "view/sw/RegistrationDetails.vue", Title: "注册明细", Icon: "user", Sort: 3},
|
||||
{Name: "sw_RechargeDetails", Path: "RechargeDetails", ParentName: "sw_DataAnalysis", Component: "view/sw/RechargeDetails.vue", Title: "充值明细", Icon: "money", Sort: 2},
|
||||
{Name: "sw_ChannelList", Path: "ChannelList", ParentName: "sw_BusMgmt", Component: "view/sw/ChannelList.vue", Title: "渠道列表", Icon: "company", Sort: 2},
|
||||
{Name: "sw_ChannelActivationList", Path: "ChannelActivationList", ParentName: "sw_BusMgmt", Component: "view/sw/ChannelActivationList.vue", Title: "渠道激活", Icon: "company", Sort: 3},
|
||||
{Name: "sw_AlertList", Path: "AlertList", ParentName: "sw_AlertCenter", Component: "view/sw/AlertList.vue", Title: "预警列表", Icon: "warn", Sort: 1},
|
||||
|
||||
@@ -33,8 +33,8 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 11 || apis != 53 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 11/53", menus, apis)
|
||||
if menus != 14 || apis != 58 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 14/58", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,11 @@ type routeValue struct {
|
||||
// Gin router files still bind handlers; tests keep those registrations aligned
|
||||
// with this catalog while public, audit and API metadata live only here.
|
||||
var routes = map[string]routeValue{
|
||||
"GET /sw/register/list": {group: "注册明细", description: "查询注册明细"},
|
||||
"POST /sw/register/export": {group: "注册明细", description: "导出注册明细", audit: true},
|
||||
"GET /sw/spend/list": {group: "充值明细", description: "查询充值明细与成功支付合计"},
|
||||
"POST /sw/spend/export": {group: "充值明细", description: "导出充值明细", audit: true},
|
||||
"GET /sw/serverAnalysis/games": {group: "数据分析", description: "查询平台游戏选项"},
|
||||
"GET /sw/promote/portrait": {group: "渠道画像", description: "查询渠道画像和玩家付费分层"},
|
||||
"GET /sw/business/:userId/portrait": {group: "商务画像", description: "查询商务画像"},
|
||||
"GET /sw/business/bindPlatform/portrait": {group: "商务画像", description: "查询 SDK 商务画像"},
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewGinEngine(runtime *config.Store, access *systemservice.AccessControlServ
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Ledger: &handler.Ledger{},
|
||||
ChannelPortrait: &handler.ChannelPortrait{},
|
||||
BusinessPortrait: &handler.BusinessPortrait{},
|
||||
Channel: &handler.Channel{},
|
||||
@@ -105,7 +106,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":236`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":241`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type Ledger struct{ service *business.LedgerService }
|
||||
|
||||
func NewLedger(service *business.LedgerService) *Ledger { return &Ledger{service: service} }
|
||||
func (h *Ledger) Registrations(c *gin.Context) {
|
||||
var req dto.LedgerQuery
|
||||
if c.ShouldBindQuery(&req) != nil {
|
||||
Fail(c, "明细查询参数不正确")
|
||||
return
|
||||
}
|
||||
data, err := h.service.Registrations(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, data)
|
||||
}
|
||||
func (h *Ledger) Spends(c *gin.Context) {
|
||||
var req dto.LedgerQuery
|
||||
if c.ShouldBindQuery(&req) != nil {
|
||||
Fail(c, "明细查询参数不正确")
|
||||
return
|
||||
}
|
||||
data, err := h.service.Spends(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, data)
|
||||
}
|
||||
func (h *Ledger) Games(c *gin.Context) {
|
||||
var req dto.GameOptionQuery
|
||||
if c.ShouldBindQuery(&req) != nil {
|
||||
Fail(c, "游戏查询参数不正确")
|
||||
return
|
||||
}
|
||||
data, err := h.service.Games(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, data)
|
||||
}
|
||||
func (h *Ledger) Export(spend bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20)
|
||||
var req dto.LedgerQuery
|
||||
if c.ShouldBind(&req) != nil {
|
||||
c.Header("msg", url.PathEscape("明细查询参数不正确"))
|
||||
Fail(c, "明细查询参数不正确")
|
||||
return
|
||||
}
|
||||
var data []byte
|
||||
var err error
|
||||
name := "注册明细.xlsx"
|
||||
if spend {
|
||||
name = "充值明细.xlsx"
|
||||
data, err = h.service.ExportSpends(c.Request.Context(), &req)
|
||||
} else {
|
||||
data, err = h.service.ExportRegistrations(c.Request.Context(), &req)
|
||||
}
|
||||
if err != nil {
|
||||
message := errors.FromError(err).Message
|
||||
c.Header("msg", url.PathEscape(message))
|
||||
Fail(c, message)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": name}))
|
||||
c.Header("success", "true")
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewChannel,
|
||||
NewBusinessPortrait,
|
||||
NewChannelPortrait,
|
||||
NewLedger,
|
||||
NewAuthority, NewMenu, NewAPI, NewPermission, NewOrganization,
|
||||
NewAnnouncement, NewEmail, NewPayment, NewTask, NewMedia, NewAudit,
|
||||
NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
Ledger *Ledger
|
||||
ChannelPortrait *ChannelPortrait
|
||||
BusinessPortrait *BusinessPortrait
|
||||
Channel *Channel
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func RegisterLedger(private *gin.RouterGroup, h *handler.Ledger) {
|
||||
private.GET("/sw/register/list", h.Registrations)
|
||||
private.POST("/sw/register/export", h.Export(false))
|
||||
private.GET("/sw/spend/list", h.Spends)
|
||||
private.POST("/sw/spend/export", h.Export(true))
|
||||
private.GET("/sw/serverAnalysis/games", h.Games)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() { RegisterLedger(private, r.handlers.Ledger) },
|
||||
func() { private.GET("/sw/promote/portrait", r.handlers.ChannelPortrait.Portrait) },
|
||||
func() {
|
||||
private.GET("/sw/business/:userId/portrait", r.handlers.BusinessPortrait.ForUser)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LedgerUsecase interface {
|
||||
Registrations(context.Context, business.LedgerFilter, business.LedgerListOptions) (*business.RegistrationPage, error)
|
||||
Spends(context.Context, business.LedgerFilter, business.LedgerListOptions) (*business.SpendPage, error)
|
||||
ExportRegistrations(context.Context, business.LedgerFilter) ([]*business.Registration, error)
|
||||
ExportSpends(context.Context, business.LedgerFilter) ([]*business.Spend, error)
|
||||
Games(context.Context, []string, string, int) ([]*business.GameOption, error)
|
||||
}
|
||||
type LedgerService struct{ uc LedgerUsecase }
|
||||
|
||||
func NewLedgerService(uc *business.LedgerUsecase) *LedgerService { return &LedgerService{uc: uc} }
|
||||
func ledgerRequest(req *dto.LedgerQuery, spend bool) (business.LedgerFilter, business.LedgerListOptions, error) {
|
||||
if req == nil {
|
||||
return business.LedgerFilter{}, business.LedgerListOptions{}, business.ErrLedgerInput
|
||||
}
|
||||
sources, err := normalizePlatforms(req.Sources)
|
||||
if err != nil {
|
||||
return business.LedgerFilter{}, business.LedgerListOptions{}, err
|
||||
}
|
||||
f := business.LedgerFilter{Sources: sources, GameNames: splitQueryValues(req.GameNames), UserID: req.UserID, PromoteAccount: strings.TrimSpace(req.PromoteAccount), BusierAccount: strings.TrimSpace(req.BusierAccount), PlayerName: strings.TrimSpace(req.PlayerName), OrderNumber: strings.TrimSpace(req.OrderNumber), PayStatus: req.PayStatus, PayWay: req.PayWay, SortField: strings.TrimSpace(req.SortField), Ascending: strings.EqualFold(strings.TrimSpace(req.SortOrder), "asc") || strings.EqualFold(strings.TrimSpace(req.SortOrder), "ascending")}
|
||||
begin, end := req.BeginRegisterTime, req.EndRegisterTime
|
||||
if spend {
|
||||
begin, end = req.BeginPayTime, req.EndPayTime
|
||||
}
|
||||
if strings.TrimSpace(begin) != "" && strings.TrimSpace(end) != "" {
|
||||
location := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
b, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(begin), location)
|
||||
if err != nil {
|
||||
return f, business.LedgerListOptions{}, business.ErrLedgerInput
|
||||
}
|
||||
e, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(end), location)
|
||||
if err != nil {
|
||||
return f, business.LedgerListOptions{}, business.ErrLedgerInput
|
||||
}
|
||||
f.StartDate = &b
|
||||
f.EndDate = &e
|
||||
}
|
||||
page, size := req.Page, req.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
if page > int(^uint(0)>>1)/size {
|
||||
return f, business.LedgerListOptions{}, business.ErrLedgerInput
|
||||
}
|
||||
return f, business.LedgerListOptions{Offset: (page - 1) * size, Limit: size}, nil
|
||||
}
|
||||
func registrationResponse(r *business.Registration) *dto.RegistrationResponse {
|
||||
return &dto.RegistrationResponse{SourceDB: r.SourceDB, SourceDBLabel: business.PlatformName(r.SourceDB), ID: r.ID, UserID: r.ID, RegisterTime: r.RegisterTime, FirstGameID: r.FirstGameID, FirstGameName: r.FirstGameName, TotalPayAmount: r.TotalPayAmount, RegisterIP: r.RegisterIP, DeviceName: r.DeviceName, PromoteID: r.PromoteID, PromoteAccount: r.PromoteAccount, BusierName: r.DisplayBusiness()}
|
||||
}
|
||||
func spendResponse(r *business.Spend) *dto.SpendResponse {
|
||||
return &dto.SpendResponse{SourceDB: r.SourceDB, SourceDBLabel: business.PlatformName(r.SourceDB), ID: r.ID, UserID: r.UserID, OrderNumber: r.OrderNumber, GameID: r.GameID, GameName: r.GameName, ServerID: r.ServerID, ServerName: r.ServerName, PlayerName: r.PlayerName, PayAmount: r.PayAmount, PayTime: r.PayTime, PayWay: r.PayWay, PayStatus: r.PayStatus, IP: r.IP, PromoteID: r.PromoteID, PromoteAccount: r.PromoteAccount, BusierName: r.DisplayBusiness()}
|
||||
}
|
||||
func (s *LedgerService) Registrations(ctx context.Context, req *dto.LedgerQuery) (*dto.RegistrationPage, error) {
|
||||
f, o, err := ledgerRequest(req, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := s.uc.Registrations(ctx, f, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.RegistrationPage{Rows: []*dto.RegistrationResponse{}, Total: page.Total, Available: page.Available}
|
||||
for _, row := range page.Rows {
|
||||
out.Rows = append(out.Rows, registrationResponse(row))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *LedgerService) Spends(ctx context.Context, req *dto.LedgerQuery) (*dto.SpendPage, error) {
|
||||
f, o, err := ledgerRequest(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := s.uc.Spends(ctx, f, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.SpendPage{Rows: []*dto.SpendResponse{}, Total: page.Total, TotalPayAmount: page.TotalPayAmount, Available: page.Available}
|
||||
for _, row := range page.Rows {
|
||||
out.Rows = append(out.Rows, spendResponse(row))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *LedgerService) Games(ctx context.Context, req *dto.GameOptionQuery) ([]*dto.GameOptionResponse, error) {
|
||||
if req == nil {
|
||||
return nil, business.ErrLedgerInput
|
||||
}
|
||||
sources, err := normalizePlatforms(req.Sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.uc.Games(ctx, sources, req.Keyword, req.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*dto.GameOptionResponse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
result = append(result, &dto.GameOptionResponse{SourceDB: row.SourceDB, SourceDBLabel: business.PlatformName(row.SourceDB), GameID: row.ID, GameName: row.Name})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type ledgerCell struct {
|
||||
value string
|
||||
numeric bool
|
||||
}
|
||||
|
||||
func textCell(value string) ledgerCell { return ledgerCell{value: value} }
|
||||
func numericCell(value string) ledgerCell { return ledgerCell{value: value, numeric: true} }
|
||||
func optionalNumber[T ~int | ~int64](value *T) ledgerCell {
|
||||
if value == nil {
|
||||
return textCell("")
|
||||
}
|
||||
return numericCell(strconv.FormatInt(int64(*value), 10))
|
||||
}
|
||||
func optionalMoney(value *string) ledgerCell {
|
||||
if value == nil {
|
||||
return textCell("")
|
||||
}
|
||||
return numericCell(*value)
|
||||
}
|
||||
func ledgerWorkbook(sheet string, headers []string, rows [][]ledgerCell) ([]byte, error) {
|
||||
file := excelize.NewFile()
|
||||
defer file.Close()
|
||||
if err := file.SetSheetName(file.GetSheetName(0), sheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, label := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
if err := file.SetCellStr(sheet, cell, label); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for i, row := range rows {
|
||||
for j, value := range row {
|
||||
cell, _ := excelize.CoordinatesToCellName(j+1, i+2)
|
||||
var err error
|
||||
// 金额直接写入数值 XML,避免先转 float64;文本始终作为文本,保留订单号前导零。
|
||||
if value.numeric {
|
||||
err = file.SetCellDefault(sheet, cell, value.value)
|
||||
} else {
|
||||
err = file.SetCellStr(sheet, cell, value.value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
last, _ := excelize.ColumnNumberToName(len(headers))
|
||||
if err := file.SetColWidth(sheet, "A", last, 20); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := file.SetPanes(sheet, &excelize.Panes{Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer, err := file.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
func (s *LedgerService) ExportRegistrations(ctx context.Context, req *dto.LedgerQuery) ([]byte, error) {
|
||||
f, _, err := ledgerRequest(req, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := s.uc.ExportRegistrations(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([][]ledgerCell, 0, len(items))
|
||||
for _, item := range items {
|
||||
r := registrationResponse(item)
|
||||
rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), numericCell(strconv.FormatUint(r.ID, 10)), numericCell(strconv.FormatUint(r.UserID, 10)), optionalNumber(r.RegisterTime), optionalNumber(r.FirstGameID), textCell(r.FirstGameName), optionalMoney(r.TotalPayAmount), textCell(r.RegisterIP), textCell(r.DeviceName), textCell(r.PromoteAccount), optionalNumber(r.PromoteID), textCell(r.BusierName)})
|
||||
}
|
||||
return ledgerWorkbook("注册明细", []string{"sourceDb", "所属平台", "ID", "玩家ID", "注册时间", "fgameId", "游戏", "累计充值", "注册IP", "机型", "渠道", "promoteId", "所属商务"}, rows)
|
||||
}
|
||||
func (s *LedgerService) ExportSpends(ctx context.Context, req *dto.LedgerQuery) ([]byte, error) {
|
||||
f, _, err := ledgerRequest(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := s.uc.ExportSpends(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([][]ledgerCell, 0, len(items))
|
||||
for _, item := range items {
|
||||
r := spendResponse(item)
|
||||
rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), numericCell(strconv.FormatUint(r.ID, 10)), optionalNumber(r.UserID), textCell(r.OrderNumber), optionalNumber(r.GameID), textCell(r.GameName), textCell(r.ServerID), textCell(r.ServerName), textCell(r.PlayerName), optionalMoney(r.PayAmount), optionalNumber(r.PayTime), optionalNumber(r.PayWay), optionalNumber(r.PayStatus), textCell(r.IP), textCell(r.PromoteAccount), optionalNumber(r.PromoteID), textCell(r.BusierName)})
|
||||
}
|
||||
return ledgerWorkbook("充值明细", []string{"sourceDb", "所属平台", "ID", "玩家ID", "订单号", "gameId", "游戏", "serverId", "区服", "角色", "实付金额", "完成时间", "方式", "状态", "IP", "渠道", "promoteId", "所属商务"}, rows)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type ledgerExportFake struct{ LedgerUsecase }
|
||||
|
||||
func (ledgerExportFake) ExportRegistrations(context.Context, business.LedgerFilter) ([]*business.Registration, error) {
|
||||
amount := "9007199254740993.10"
|
||||
stamp := int64(1793577600)
|
||||
return []*business.Registration{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 7}, RegisterTime: &stamp, FirstGameName: "游戏", TotalPayAmount: &amount, LedgerNames: business.LedgerNames{PromoteAccount: "=1+1"}}}, nil
|
||||
}
|
||||
func (ledgerExportFake) ExportSpends(context.Context, business.LedgerFilter) ([]*business.Spend, error) {
|
||||
amount := "123.45"
|
||||
return []*business.Spend{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 8}, OrderNumber: "000000000000000001", PayAmount: &amount}}, nil
|
||||
}
|
||||
func TestLedgerWorkbookPreservesNumbersAndText(t *testing.T) {
|
||||
s := &LedgerService{uc: ledgerExportFake{}}
|
||||
data, err := s.ExportRegistrations(context.Background(), &dto.LedgerQuery{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
amount, _ := f.GetCellValue("注册明细", "H2", excelize.Options{RawCellValue: true})
|
||||
formula, _ := f.GetCellFormula("注册明细", "K2")
|
||||
text, _ := f.GetCellValue("注册明细", "K2")
|
||||
if amount != "9007199254740993.10" || formula != "" || text != "=1+1" {
|
||||
t.Fatalf("导出丢失金额精度或把账号当成公式: %s %s %s", amount, formula, text)
|
||||
}
|
||||
rows, err := f.GetRows("注册明细")
|
||||
if err != nil || len(rows) != 2 || rows[0][0] != "sourceDb" || rows[0][12] != "所属商务" {
|
||||
t.Fatal("原导出字段与顺序丢失")
|
||||
}
|
||||
data, err = s.ExportSpends(context.Background(), &dto.LedgerQuery{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pay, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pay.Close()
|
||||
order, _ := pay.GetCellValue("充值明细", "E2")
|
||||
if order != "000000000000000001" {
|
||||
t.Fatal("订单号前导零丢失")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package dto
|
||||
|
||||
type LedgerQuery struct {
|
||||
Page int `form:"pageNum" json:"pageNum"`
|
||||
PageSize int `form:"pageSize" json:"pageSize"`
|
||||
Sources []string `form:"sourceDb" json:"sourceDb"`
|
||||
GameNames []string `form:"gameName" json:"gameName"`
|
||||
UserID *int64 `form:"userId" json:"userId"`
|
||||
PromoteAccount string `form:"promoteAccount" json:"promoteAccount"`
|
||||
BusierAccount string `form:"busierAccount" json:"busierAccount"`
|
||||
PlayerName string `form:"gamePlayerName" json:"gamePlayerName"`
|
||||
OrderNumber string `form:"payOrderNumber" json:"payOrderNumber"`
|
||||
PayStatus *int `form:"payStatus" json:"payStatus"`
|
||||
PayWay *int `form:"payWay" json:"payWay"`
|
||||
BeginRegisterTime string `form:"beginRegisterTime" json:"beginRegisterTime"`
|
||||
EndRegisterTime string `form:"endRegisterTime" json:"endRegisterTime"`
|
||||
BeginPayTime string `form:"beginPayTime" json:"beginPayTime"`
|
||||
EndPayTime string `form:"endPayTime" json:"endPayTime"`
|
||||
SortField string `form:"sortField" json:"sortField"`
|
||||
SortOrder string `form:"sortOrder" json:"sortOrder"`
|
||||
}
|
||||
type RegistrationResponse struct {
|
||||
SourceDB string `json:"sourceDb"`
|
||||
SourceDBLabel string `json:"sourceDbLabel"`
|
||||
ID uint64 `json:"id"`
|
||||
UserID uint64 `json:"userId"`
|
||||
RegisterTime *int64 `json:"registerTime"`
|
||||
FirstGameID *int64 `json:"fgameId"`
|
||||
FirstGameName string `json:"fgameName"`
|
||||
TotalPayAmount *string `json:"totalPayAmount"`
|
||||
RegisterIP string `json:"registerIp"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
PromoteAccount string `json:"promoteAccount"`
|
||||
PromoteID *int64 `json:"promoteId"`
|
||||
BusierName string `json:"busierName"`
|
||||
}
|
||||
type SpendResponse struct {
|
||||
SourceDB string `json:"sourceDb"`
|
||||
SourceDBLabel string `json:"sourceDbLabel"`
|
||||
ID uint64 `json:"id"`
|
||||
UserID *int64 `json:"userId"`
|
||||
OrderNumber string `json:"payOrderNumber"`
|
||||
GameID *int64 `json:"gameId"`
|
||||
GameName string `json:"gameName"`
|
||||
ServerID string `json:"serverId"`
|
||||
ServerName string `json:"serverName"`
|
||||
PlayerName string `json:"gamePlayerName"`
|
||||
PayAmount *string `json:"payAmount"`
|
||||
PayTime *int64 `json:"payTime"`
|
||||
PayWay *int `json:"payWay"`
|
||||
PayStatus *int `json:"payStatus"`
|
||||
IP string `json:"spendIp"`
|
||||
PromoteAccount string `json:"promoteAccount"`
|
||||
PromoteID *int64 `json:"promoteId"`
|
||||
BusierName string `json:"busierName"`
|
||||
}
|
||||
type RegistrationPage struct {
|
||||
Rows []*RegistrationResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
type SpendPage struct {
|
||||
Rows []*SpendResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPayAmount *string `json:"totalPayAmount"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
type GameOptionQuery struct {
|
||||
Sources []string `form:"sourceDb"`
|
||||
Keyword string `form:"keyword"`
|
||||
Limit int `form:"limit"`
|
||||
}
|
||||
type GameOptionResponse struct {
|
||||
SourceDB string `json:"sourceDb"`
|
||||
SourceDBLabel string `json:"sourceDbLabel"`
|
||||
GameID uint64 `json:"gameId"`
|
||||
GameName string `json:"gameName"`
|
||||
}
|
||||
@@ -23,6 +23,7 @@ var ProviderSet = wire.NewSet(
|
||||
businessservice.NewChannelService,
|
||||
businessservice.NewBusinessPortraitService,
|
||||
businessservice.NewChannelPortraitService,
|
||||
businessservice.NewLedgerService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
Reference in New Issue
Block a user