371 lines
10 KiB
Go
371 lines
10 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"github.com/go-kratos/kratos/v3/errors"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrAnalyticsInput = errors.BadRequest("SW_ANALYTICS_INPUT", "统计查询参数不正确")
|
|
ErrAnalyticsDate = errors.BadRequest("SW_ANALYTICS_DATE", "统计周期格式错误,应为 yyyy-MM-dd")
|
|
ErrAnalyticsDateOrder = errors.BadRequest("SW_ANALYTICS_DATE_ORDER", "统计开始日期不能晚于结束日期")
|
|
)
|
|
|
|
type AnalyticsKind string
|
|
|
|
const (
|
|
ChannelAnalytics AnalyticsKind = "channel"
|
|
GameAnalytics AnalyticsKind = "game"
|
|
)
|
|
|
|
type AnalyticsKey struct {
|
|
SourceDB string
|
|
ID int64
|
|
}
|
|
type AnalyticsFilter struct {
|
|
HasRegister, HasPay *bool
|
|
Sources, GameNames []string
|
|
PromoteName string
|
|
PromoteType *int
|
|
StartDate, EndDate *time.Time
|
|
Begin, End, SortField string
|
|
Ascending bool
|
|
}
|
|
type AnalyticsScope struct {
|
|
Viewer UserScope
|
|
IncludeSub, IDCardDedup bool
|
|
}
|
|
type AnalyticsRow struct {
|
|
AnalyticsKey
|
|
Name string
|
|
PromoteType *int
|
|
BindingAccount, BindingUserName string
|
|
Counts PortraitCounts
|
|
Pay, PayRate, ARPU, ARPPU string
|
|
}
|
|
|
|
func (f AnalyticsFilter) MatchesMetrics(row *AnalyticsRow) (bool, error) {
|
|
if f.HasRegister != nil && (row.Counts.Register > 0) != *f.HasRegister {
|
|
return false, nil
|
|
}
|
|
if f.HasPay != nil {
|
|
amount, err := portraitNumber(row.Pay)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if (amount.Sign() > 0) != *f.HasPay {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Inactive channels match a negative activity filter, unless another selected
|
|
// metric explicitly requires activity.
|
|
func (f AnalyticsFilter) IncludesInactiveRows() bool {
|
|
return (f.HasRegister != nil || f.HasPay != nil) &&
|
|
(f.HasRegister == nil || !*f.HasRegister) && (f.HasPay == nil || !*f.HasPay)
|
|
}
|
|
|
|
func (r *AnalyticsRow) BusinessName() string {
|
|
if value := strings.TrimSpace(r.BindingAccount); value != "" {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(r.BindingUserName)
|
|
}
|
|
|
|
type AnalyticsData struct {
|
|
EffectiveCount int64
|
|
Rows []*AnalyticsRow
|
|
GlobalCounts *PortraitCounts
|
|
GlobalPay string
|
|
Available bool
|
|
}
|
|
type AnalyticsPage struct {
|
|
EffectiveCount int64
|
|
Rows []*AnalyticsRow
|
|
Total int64
|
|
Summary *AnalyticsRow
|
|
Begin, End string
|
|
Available bool
|
|
}
|
|
type AnalyticsRepo interface {
|
|
Aggregate(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter) (*AnalyticsData, error)
|
|
FillNames(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter, []*AnalyticsRow) error
|
|
}
|
|
type AnalyticsUsecase struct {
|
|
repo AnalyticsRepo
|
|
accounts *AccountUsecase
|
|
preferences *UserPreferenceUsecase
|
|
now func() time.Time
|
|
effectiveness *EffectivenessUsecase
|
|
}
|
|
|
|
func NewAnalyticsUsecase(repo AnalyticsRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase, effectiveness *EffectivenessUsecase) *AnalyticsUsecase {
|
|
return &AnalyticsUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now, effectiveness: effectiveness}
|
|
}
|
|
func analyticsCap(c PortraitCounts) PortraitCounts {
|
|
return PortraitCounts{Register: min(c.Register, 2147483647), Active: min(c.Active, 2147483647), Pay: min(c.Pay, 2147483647), NewPay: min(c.NewPay, 2147483647)}
|
|
}
|
|
func analyticsAdd(a, b PortraitCounts) PortraitCounts {
|
|
return PortraitCounts{Register: a.Register + b.Register, Active: a.Active + b.Active, Pay: a.Pay + b.Pay, NewPay: a.NewPay + b.NewPay}
|
|
}
|
|
func analyticsRatio(numerator *big.Rat, denominator int64, percent bool) string {
|
|
if denominator <= 0 {
|
|
return "0.00"
|
|
}
|
|
n := new(big.Rat).Quo(numerator, big.NewRat(denominator, 1))
|
|
if percent {
|
|
n.Mul(n, big.NewRat(100, 1))
|
|
}
|
|
return portraitDecimal(n, 2)
|
|
}
|
|
func analyticsValues(row *AnalyticsRow) error {
|
|
pay, err := portraitNumber(row.Pay)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row.Pay = portraitDecimal(pay, 2)
|
|
pay, _ = portraitNumber(row.Pay)
|
|
row.PayRate = analyticsRatio(big.NewRat(row.Counts.Pay, 1), row.Counts.Active, true)
|
|
row.ARPU = analyticsRatio(pay, row.Counts.Active, false)
|
|
row.ARPPU = analyticsRatio(pay, row.Counts.Pay, false)
|
|
return nil
|
|
}
|
|
func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter AnalyticsFilter, page, size int) (*AnalyticsPage, error) {
|
|
return uc.list(ctx, kind, filter, page, size, false)
|
|
}
|
|
func (uc *AnalyticsUsecase) ListAll(ctx context.Context, kind AnalyticsKind, filter AnalyticsFilter) (*AnalyticsPage, error) {
|
|
return uc.list(ctx, kind, filter, 1, 0, true)
|
|
}
|
|
func (uc *AnalyticsUsecase) list(ctx context.Context, kind AnalyticsKind, filter AnalyticsFilter, page, size int, all bool) (*AnalyticsPage, error) {
|
|
actor, account, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pref, err := uc.preferences.Current(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if kind != ChannelAnalytics && kind != GameAnalytics {
|
|
return nil, ErrAnalyticsInput
|
|
}
|
|
viewer := portraitUserScope(account)
|
|
viewer.All = actor.AuthorityID == 888
|
|
if kind == GameAnalytics {
|
|
filter.PromoteName = ""
|
|
filter.PromoteType = nil
|
|
filter.HasRegister, filter.HasPay = nil, nil
|
|
}
|
|
if kind == GameAnalytics {
|
|
filter.StartDate, filter.EndDate = reportDateDefaults(filter.StartDate, filter.EndDate, 1, uc.now())
|
|
} else if filter.EndDate == nil {
|
|
_, filter.EndDate = reportDateDefaults(nil, nil, 1, uc.now())
|
|
}
|
|
if filter.StartDate != nil && filter.EndDate.Before(*filter.StartDate) {
|
|
return nil, ErrAnalyticsDateOrder
|
|
}
|
|
filter.Begin = ""
|
|
if filter.StartDate != nil {
|
|
filter.Begin = filter.StartDate.Format("2006-01-02")
|
|
}
|
|
filter.End = filter.EndDate.Format("2006-01-02")
|
|
out := &AnalyticsPage{Rows: []*AnalyticsRow{}, Begin: filter.Begin, End: filter.End, Available: true}
|
|
scope := AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}
|
|
if kind == ChannelAnalytics && uc.effectiveness != nil {
|
|
if err := uc.effectiveness.Ensure(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
data, err := uc.repo.Aggregate(ctx, kind, scope, filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !data.Available {
|
|
out.Available = false
|
|
return out, nil
|
|
}
|
|
out.EffectiveCount = data.EffectiveCount
|
|
counts := PortraitCounts{}
|
|
pay := new(big.Rat)
|
|
for _, row := range data.Rows {
|
|
row.Counts = analyticsCap(row.Counts)
|
|
raw, err := portraitNumber(row.Pay)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if kind == ChannelAnalytics {
|
|
pay.Add(pay, raw)
|
|
}
|
|
if err := analyticsValues(row); err != nil {
|
|
return nil, err
|
|
}
|
|
if kind == GameAnalytics {
|
|
rounded, _ := portraitNumber(row.Pay)
|
|
pay.Add(pay, rounded)
|
|
}
|
|
counts = analyticsAdd(counts, row.Counts)
|
|
}
|
|
if kind == ChannelAnalytics {
|
|
counts = analyticsCap(counts)
|
|
if viewer.All && data.GlobalCounts != nil {
|
|
counts = analyticsCap(*data.GlobalCounts)
|
|
pay, err = portraitNumber(data.GlobalPay)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
label := "全部渠道"
|
|
if kind == GameAnalytics {
|
|
label = "全部游戏"
|
|
}
|
|
out.Summary = &AnalyticsRow{Name: label, Counts: counts, Pay: portraitDecimal(pay, 2)}
|
|
if err := analyticsValues(out.Summary); err != nil {
|
|
return nil, err
|
|
}
|
|
// 仅按渠道名称或类型排序时需要提前读取全部资料;数字指标排序
|
|
// 确定当前页后再补名称和商务信息,汇总仍使用全部可见统计行。
|
|
namesBeforeSort := kind == ChannelAnalytics && (filter.SortField == "promoteName" || filter.SortField == "promoteType")
|
|
fillNames := func(rows []*AnalyticsRow) error {
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
if err := uc.repo.FillNames(ctx, kind, scope, filter, rows); err != nil {
|
|
return err
|
|
}
|
|
if kind == ChannelAnalytics {
|
|
for _, row := range rows {
|
|
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if namesBeforeSort {
|
|
if err := fillNames(data.Rows); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := sortAnalytics(data.Rows, kind, filter.SortField, filter.Ascending); err != nil {
|
|
return nil, err
|
|
}
|
|
out.Total = int64(len(data.Rows))
|
|
if all {
|
|
if out.Total > 10000 {
|
|
return nil, ErrLedgerExportLimit
|
|
}
|
|
out.Rows = data.Rows
|
|
if !namesBeforeSort {
|
|
if err := fillNames(out.Rows); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if size <= 0 || size > 500 {
|
|
size = 500
|
|
}
|
|
if page > len(data.Rows)/size+1 {
|
|
return out, nil
|
|
}
|
|
from := (page - 1) * size
|
|
to := min(from+size, len(data.Rows))
|
|
if from < len(data.Rows) {
|
|
out.Rows = data.Rows[from:to]
|
|
}
|
|
if !namesBeforeSort {
|
|
if err := fillNames(out.Rows); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascending bool) error {
|
|
if field == "" {
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
if rows[i].SourceDB != rows[j].SourceDB {
|
|
return rows[i].SourceDB < rows[j].SourceDB
|
|
}
|
|
return rows[i].ID < rows[j].ID
|
|
})
|
|
return nil
|
|
}
|
|
allowed := map[string]bool{"newRegisterCount": true, "activeCount": true, "payUserCount": true, "newPayUserCount": true, "totalPay": true, "payRate": true}
|
|
if kind == GameAnalytics {
|
|
allowed["gameId"] = true
|
|
allowed["arpu"] = true
|
|
allowed["arppu"] = true
|
|
} else {
|
|
allowed["promoteId"] = true
|
|
allowed["promoteName"] = true
|
|
allowed["promoteType"] = true
|
|
}
|
|
if !allowed[field] {
|
|
field = "totalPay"
|
|
}
|
|
values := map[*AnalyticsRow]*big.Rat{}
|
|
for _, row := range rows {
|
|
var n *big.Rat
|
|
var err error
|
|
switch field {
|
|
case "newRegisterCount":
|
|
n = big.NewRat(row.Counts.Register, 1)
|
|
case "activeCount":
|
|
n = big.NewRat(row.Counts.Active, 1)
|
|
case "payUserCount":
|
|
n = big.NewRat(row.Counts.Pay, 1)
|
|
case "newPayUserCount":
|
|
n = big.NewRat(row.Counts.NewPay, 1)
|
|
case "promoteId", "gameId":
|
|
n = big.NewRat(row.ID, 1)
|
|
case "promoteType":
|
|
value := int64(0)
|
|
if row.PromoteType != nil {
|
|
value = int64(*row.PromoteType)
|
|
}
|
|
n = big.NewRat(value, 1)
|
|
case "payRate":
|
|
n, err = portraitNumber(row.PayRate)
|
|
case "arpu":
|
|
n, err = portraitNumber(row.ARPU)
|
|
case "arppu":
|
|
n, err = portraitNumber(row.ARPPU)
|
|
case "promoteName":
|
|
continue
|
|
default:
|
|
n, err = portraitNumber(row.Pay)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
values[row] = n
|
|
}
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
a, b := rows[i], rows[j]
|
|
comparison := 0
|
|
if field == "promoteName" {
|
|
comparison = strings.Compare(a.Name, b.Name)
|
|
} else {
|
|
comparison = values[a].Cmp(values[b])
|
|
}
|
|
if comparison != 0 {
|
|
if ascending {
|
|
return comparison < 0
|
|
}
|
|
return comparison > 0
|
|
}
|
|
if a.SourceDB != b.SourceDB {
|
|
return a.SourceDB < b.SourceDB
|
|
}
|
|
return a.ID < b.ID
|
|
})
|
|
return nil
|
|
}
|