fix: 补齐渠道全量合计筛选导出及有效端目标遗漏
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
type AnalyticsUsecase interface {
|
||||
List(context.Context, business.AnalyticsKind, business.AnalyticsFilter, int, int) (*business.AnalyticsPage, error)
|
||||
ListAll(context.Context, business.AnalyticsKind, business.AnalyticsFilter) (*business.AnalyticsPage, error)
|
||||
}
|
||||
type AnalyticsService struct{ uc AnalyticsUsecase }
|
||||
|
||||
@@ -24,22 +25,21 @@ func analyticsRequest(req *dto.AnalyticsQuery) (business.AnalyticsFilter, error)
|
||||
if err != nil {
|
||||
return business.AnalyticsFilter{}, err
|
||||
}
|
||||
f := business.AnalyticsFilter{Sources: sources, GameNames: splitQueryValues(req.GameNames), PromoteName: strings.TrimSpace(req.PromoteName), PromoteType: req.PromoteType, SortField: strings.TrimSpace(req.SortField), Ascending: strings.EqualFold(strings.TrimSpace(req.SortOrder), "asc")}
|
||||
f := business.AnalyticsFilter{Sources: sources, GameNames: splitQueryValues(req.GameNames), PromoteName: strings.TrimSpace(req.PromoteName), PromoteType: req.PromoteType, HasRegister: req.HasRegister, HasPay: req.HasPay, SortField: strings.TrimSpace(req.SortField), Ascending: strings.EqualFold(strings.TrimSpace(req.SortOrder), "asc")}
|
||||
begin, end := strings.TrimSpace(req.BeginTime), strings.TrimSpace(req.EndTime)
|
||||
if (begin == "") != (end == "") {
|
||||
return f, business.ErrAnalyticsDate
|
||||
}
|
||||
if begin != "" && end != "" {
|
||||
location := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
location := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
if begin != "" {
|
||||
b, err := time.ParseInLocation("2006-01-02", begin, location)
|
||||
if err != nil {
|
||||
return f, business.ErrAnalyticsDate
|
||||
}
|
||||
f.StartDate = &b
|
||||
}
|
||||
if end != "" {
|
||||
e, err := time.ParseInLocation("2006-01-02", end, location)
|
||||
if err != nil || e.Before(b) {
|
||||
if err != nil || f.StartDate != nil && e.Before(*f.StartDate) {
|
||||
return f, business.ErrAnalyticsDate
|
||||
}
|
||||
f.StartDate = &b
|
||||
f.EndDate = &e
|
||||
}
|
||||
return f, nil
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
func (s *AnalyticsService) Export(ctx context.Context, req *dto.AnalyticsQuery) ([]byte, error) {
|
||||
f, err := analyticsRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := s.uc.ListAll(ctx, business.ChannelAnalytics, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !page.Available {
|
||||
return nil, business.ErrChannelReportsUnavailable
|
||||
}
|
||||
headers := []string{"所属平台", "渠道名称", "归属商务", "渠道类型", "新增注册", "活跃玩家", "充值额", "付费玩家", "新增付费", "付费率"}
|
||||
rows := make([][]ledgerCell, 0, len(page.Rows)+1)
|
||||
appendRow := func(row *business.AnalyticsRow, summary bool) {
|
||||
platform, name, owner, kind := business.PlatformName(row.SourceDB), row.Name, row.BusinessName(), "—"
|
||||
if row.PromoteType != nil {
|
||||
if *row.PromoteType == 1 {
|
||||
kind = "公会"
|
||||
} else if *row.PromoteType == 0 {
|
||||
kind = "个人"
|
||||
}
|
||||
}
|
||||
if summary {
|
||||
platform, name, owner, kind = "全部", "合计(有效端 "+strconv.FormatInt(page.EffectiveCount, 10)+")", "—", "—"
|
||||
}
|
||||
rows = append(rows, []ledgerCell{textCell(platform), textCell(name), textCell(owner), textCell(kind), numericCell(strconv.FormatInt(row.Counts.Register, 10)), numericCell(strconv.FormatInt(row.Counts.Active, 10)), numericCell(row.Pay), numericCell(strconv.FormatInt(row.Counts.Pay, 10)), numericCell(strconv.FormatInt(row.Counts.NewPay, 10)), textCell(row.PayRate + "%")})
|
||||
}
|
||||
for _, row := range page.Rows {
|
||||
appendRow(row, false)
|
||||
}
|
||||
if page.Summary != nil {
|
||||
appendRow(page.Summary, true)
|
||||
}
|
||||
return ledgerWorkbook("渠道数据统计", headers, rows)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
@@ -42,13 +43,34 @@ func (s *ChannelService) List(ctx context.Context, req *dto.ChannelQuery) (*dto.
|
||||
return nil, business.ErrChannelInput
|
||||
}
|
||||
filter := business.ChannelFilter{Sources: sources, BusierUserID: req.BusierUserID, BusierAccount: strings.TrimSpace(req.BusierAccount), Keyword: strings.TrimSpace(req.Keyword), Level: req.Level, RegisterType: req.RegisterType, Status: req.Status, LockStatus: req.LockStatus, Effective: req.Effective}
|
||||
filter.Phone, filter.SettlementDays = strings.TrimSpace(req.Phone), req.SettlementDays
|
||||
filter.SortField, filter.Ascending = req.SortField, req.SortOrder == "asc"
|
||||
if filter.SortField != "" && filter.SortField != "createdAt" || filter.SettlementDays != nil && *filter.SettlementDays < 1 {
|
||||
return nil, business.ErrChannelInput
|
||||
}
|
||||
for _, pair := range []struct {
|
||||
begin, end string
|
||||
outBegin, outEnd **time.Time
|
||||
}{
|
||||
{req.CreatedBegin, req.CreatedEnd, &filter.CreatedBegin, &filter.CreatedEnd},
|
||||
{req.LoginBegin, req.LoginEnd, &filter.LoginBegin, &filter.LoginEnd},
|
||||
} {
|
||||
parsed, err := analyticsRequest(&dto.AnalyticsQuery{BeginTime: pair.begin, EndTime: pair.end})
|
||||
if err != nil {
|
||||
return nil, business.ErrChannelInput
|
||||
}
|
||||
*pair.outBegin, *pair.outEnd = parsed.StartDate, parsed.EndDate
|
||||
}
|
||||
result, err := s.uc.List(ctx, filter, business.ListOffset((page-1)*size), business.ListLimit(size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.ChannelPage{Rows: []*dto.ChannelResponse{}, Total: result.Total, Available: result.Available}
|
||||
if result.StatusCounts != nil {
|
||||
out.StatusCounts = &dto.ChannelStatusCounts{Normal: result.StatusCounts.Normal, Locked: result.StatusCounts.Locked, Terminated: result.StatusCounts.Terminated}
|
||||
}
|
||||
for _, c := range result.Rows {
|
||||
out.Rows = append(out.Rows, &dto.ChannelResponse{ID: c.ID, SourceDB: c.SourceDB, PlatformName: business.PlatformName(c.SourceDB), Account: c.Account, Nickname: c.Nickname, Level: c.Level, ParentID: c.ParentID, ParentName: c.ParentName, RealName: c.RealName, RegisterType: c.RegisterType, BusierID: c.BusierID, BusierUserID: c.BusierUserID, BusierDisplayName: c.BusinessDisplayName(), Status: c.Status, LockStatus: c.LockStatus, GameCount: c.GameCount, PlayerCount: c.PlayerCount, TotalPayAmount: c.TotalPayAmount, Effective: c.QualifiedAt != nil, QualifiedAt: c.QualifiedAt, CreatedAt: c.CreatedUnix})
|
||||
out.Rows = append(out.Rows, &dto.ChannelResponse{ID: c.ID, SourceDB: c.SourceDB, PlatformName: business.PlatformName(c.SourceDB), Account: c.Account, Nickname: c.Nickname, Level: c.Level, ParentID: c.ParentID, ParentName: c.ParentName, RealName: c.RealName, RegisterType: c.RegisterType, BusierID: c.BusierID, BusierUserID: c.BusierUserID, BusierDisplayName: c.BusinessDisplayName(), Status: c.Status, LockStatus: c.LockStatus, GameCount: c.GameCount, PlayerCount: c.PlayerCount, TotalPayAmount: c.TotalPayAmount, Effective: c.QualifiedAt != nil, QualifiedAt: c.QualifiedAt, CreatedAt: c.CreatedUnix, LastLoginAt: c.LastLoginUnix, Phone: c.Phone, SettlementDays: c.SettlementDays})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestRejectIncompleteAndReversedDateRanges(t *testing.T) {
|
||||
for _, dates := range [][2]string{{"2026-11-01", ""}, {"", "2026-11-01"}, {"2026-11-03", "2026-11-01"}, {"bad", "2026-11-01"}} {
|
||||
if _, err := analyticsRequest(&dto.AnalyticsQuery{BeginTime: dates[0], EndTime: dates[1]}); err == nil {
|
||||
if _, err := analyticsRequest(&dto.AnalyticsQuery{BeginTime: dates[0], EndTime: dates[1]}); dates[0] != "" && dates[1] != "" && err == nil {
|
||||
t.Errorf("analytics accepted %v", dates)
|
||||
}
|
||||
for _, spend := range []bool{false, true} {
|
||||
@@ -31,3 +31,18 @@ func TestRejectIncompleteAndReversedDateRanges(t *testing.T) {
|
||||
t.Fatal("account query silently ignored incomplete date range")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsOpenEndedDatesAndMetricFilters(t *testing.T) {
|
||||
no, yes := false, true
|
||||
for _, dates := range [][2]string{{"", ""}, {"2026-11-01", ""}, {"", "2026-11-03"}} {
|
||||
filter, err := analyticsRequest(&dto.AnalyticsQuery{BeginTime: dates[0], EndTime: dates[1], HasRegister: &no, HasPay: &yes})
|
||||
if err != nil || (filter.StartDate == nil) != (dates[0] == "") || (filter.EndDate == nil) != (dates[1] == "") || *filter.HasRegister || !*filter.HasPay {
|
||||
t.Fatalf("独立起止日期及是否有数据筛选必须原样传递: %+v %v", filter, err)
|
||||
}
|
||||
}
|
||||
for _, dates := range [][2]string{{"bad", ""}, {"", "bad"}, {"2026-11-03", "2026-11-01"}} {
|
||||
if _, err := analyticsRequest(&dto.AnalyticsQuery{BeginTime: dates[0], EndTime: dates[1]}); err == nil {
|
||||
t.Fatalf("invalid dates accepted: %v", dates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package dto
|
||||
|
||||
type AnalyticsQuery struct {
|
||||
Export bool `form:"export"`
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Sources []string `form:"sourceDb"`
|
||||
GameNames []string `form:"gameName"`
|
||||
PromoteType *int `form:"promoteType"`
|
||||
HasRegister *bool `form:"hasRegister"`
|
||||
HasPay *bool `form:"hasPay"`
|
||||
PromoteName string `form:"promoteName"`
|
||||
BeginTime string `form:"beginTime"`
|
||||
EndTime string `form:"endTime"`
|
||||
|
||||
@@ -3,22 +3,33 @@ package dto
|
||||
import "encoding/json"
|
||||
|
||||
type ChannelQuery struct {
|
||||
Effective *bool `form:"effective"`
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Sources []string `form:"sourceDb"`
|
||||
BusierUserID *uint `form:"busierUserId"`
|
||||
BusierAccount string `form:"busierAccount"`
|
||||
Keyword string `form:"keyword"`
|
||||
Level *int `form:"promoteLevel"`
|
||||
RegisterType *int `form:"registerType"`
|
||||
Status *int `form:"status"`
|
||||
LockStatus *int `form:"lockStatus"`
|
||||
Effective *bool `form:"effective"`
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Sources []string `form:"sourceDb"`
|
||||
BusierUserID *uint `form:"busierUserId"`
|
||||
BusierAccount string `form:"busierAccount"`
|
||||
Keyword string `form:"keyword"`
|
||||
Level *int `form:"promoteLevel"`
|
||||
RegisterType *int `form:"registerType"`
|
||||
Status *int `form:"status"`
|
||||
LockStatus *int `form:"lockStatus"`
|
||||
Phone string `form:"phone"`
|
||||
SettlementDays *int `form:"settlementDays"`
|
||||
CreatedBegin string `form:"createdBegin"`
|
||||
CreatedEnd string `form:"createdEnd"`
|
||||
LoginBegin string `form:"loginBegin"`
|
||||
LoginEnd string `form:"loginEnd"`
|
||||
SortField string `form:"sortField"`
|
||||
SortOrder string `form:"sortOrder"`
|
||||
}
|
||||
type ChannelResponse struct {
|
||||
Effective bool `json:"effective"`
|
||||
QualifiedAt *int64 `json:"qualifiedAt"`
|
||||
CreatedAt *int64 `json:"createdAt"`
|
||||
LastLoginAt *int64 `json:"lastLoginAt"`
|
||||
Phone string `json:"phone"`
|
||||
SettlementDays *int `json:"settlementDays"`
|
||||
ID uint64 `json:"id"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
PlatformName string `json:"platformName"`
|
||||
@@ -39,9 +50,15 @@ type ChannelResponse struct {
|
||||
TotalPayAmount *string `json:"totalPayAmount"`
|
||||
}
|
||||
type ChannelPage struct {
|
||||
Rows []*ChannelResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
Available bool `json:"available"`
|
||||
Rows []*ChannelResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
Available bool `json:"available"`
|
||||
StatusCounts *ChannelStatusCounts `json:"statusCounts"`
|
||||
}
|
||||
type ChannelStatusCounts struct {
|
||||
Normal int64 `json:"normal"`
|
||||
Locked int64 `json:"locked"`
|
||||
Terminated int64 `json:"terminated"`
|
||||
}
|
||||
type ChannelBindOption struct {
|
||||
SourceDB string `json:"sourceDb"`
|
||||
|
||||
Reference in New Issue
Block a user