Files
kra-new/internal/biz/business/report_build.go
T

308 lines
8.2 KiB
Go

package business
import (
"context"
"kra/pkg/task"
"math/big"
"sort"
"strconv"
"strings"
"time"
"github.com/go-kratos/kratos/v3/errors"
)
var ErrReportBuildUnavailable = errors.BadRequest("SW_REPORT_BUILD_UNAVAILABLE", "原始数据或汇总表尚未接入,不能重建报表")
type ReportBuildKind string
const (
DailyReport ReportBuildKind = "day"
ServerReport ReportBuildKind = "server"
)
type ReportEvent struct {
SourceDB string
UserID, PromoteID, GameID *int64
ServerNum string
Amount string
}
type ReportUserKey struct {
SourceDB string
ID int64
}
type ReportUser struct {
ReportUserKey
ParentID, PromoteID *int64
IDCard string
}
type ReportInput struct {
Registers, Actives, Payments []ReportEvent
Users map[ReportUserKey]ReportUser
Games map[AnalyticsKey]string
}
type ReportGroup struct {
SourceDB string
PromoteID, GameID int64
ServerNum string
}
type ReportBuiltRow struct {
ReportGroup
Mode int
GameName, Pay string
Counts PortraitCounts
Keys map[int][]string
}
type ReportBuildRepo interface {
Rebuild(context.Context, ReportBuildKind, time.Time, func(*ReportInput) ([]*ReportBuiltRow, error)) error
}
type ReportBuildUsecase struct {
repo ReportBuildRepo
now func() time.Time
pause func(context.Context) error
}
func NewReportBuildUsecase(repo ReportBuildRepo) *ReportBuildUsecase {
return &ReportBuildUsecase{repo: repo, now: time.Now, pause: func(ctx context.Context) error {
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}}
}
func (uc *ReportBuildUsecase) Run(ctx context.Context, kind ReportBuildKind, period, start, end string) error {
if kind != DailyReport && kind != ServerReport {
return ErrAnalyticsInput
}
w := workbenchWindow(uc.now())
begin, finish := w.Yesterday, w.Yesterday
switch period {
case "today":
begin, finish = w.Today, w.Today
case "yesterday":
case "backfill":
var err error
if strings.TrimSpace(start) != "" {
begin, err = time.ParseInLocation("2006-01-02", strings.TrimSpace(start), portraitLocation)
if err != nil {
return ErrAnalyticsDate
}
}
if strings.TrimSpace(end) != "" {
finish, err = time.ParseInLocation("2006-01-02", strings.TrimSpace(end), portraitLocation)
if err != nil {
return ErrAnalyticsDate
}
}
default:
return ErrAnalyticsInput
}
if finish.Before(begin) {
return ErrAnalyticsDateOrder
}
task.Logf(ctx, "报表类型=%s;日期区间=%s 至 %s", kind, begin.Format("2006-01-02"), finish.Format("2006-01-02"))
completed, totalRows, totalKeys := 0, 0, 0
defer func() {
task.Logf(ctx, "已提交日期=%d;汇总行=%d;身份键=%d", completed, totalRows, totalKeys)
}()
for day := begin; !day.After(finish); day = day.AddDate(0, 0, 1) {
if err := ctx.Err(); err != nil {
return err
}
started := time.Now()
registers, actives, payments, rowCount, keyCount := 0, 0, 0, 0, 0
if err := uc.repo.Rebuild(ctx, kind, day, func(input *ReportInput) ([]*ReportBuiltRow, error) {
registers, actives, payments = len(input.Registers), len(input.Actives), len(input.Payments)
rows, err := compileReport(kind, input)
rowCount = len(rows)
for _, row := range rows {
for _, keys := range row.Keys {
keyCount += len(keys)
}
}
return rows, err
}); err != nil {
task.Logf(ctx, "日期=%s 未提交:%v", day.Format("2006-01-02"), err)
return err
}
completed++
totalRows += rowCount
totalKeys += keyCount
task.Logf(ctx, "日期=%s 已提交;原始注册=%d,活跃=%d,充值=%d;汇总行=%d,身份键=%d;耗时=%d ms", day.Format("2006-01-02"), registers, actives, payments, rowCount, keyCount, time.Since(started).Milliseconds())
if day.Before(finish) {
if err := uc.pause(ctx); err != nil {
return err
}
}
}
return nil
}
type reportAggregate struct {
users map[int]map[int64]bool
pay *big.Rat
}
func compileReport(kind ReportBuildKind, input *ReportInput) ([]*ReportBuiltRow, error) {
groups := map[ReportGroup]*reportAggregate{}
number := func(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
group := func(event ReportEvent) ReportGroup {
return ReportGroup{SourceDB: event.SourceDB, PromoteID: number(event.PromoteID), GameID: number(event.GameID), ServerNum: func() string {
if kind == ServerReport {
return strings.TrimSpace(event.ServerNum)
}
return ""
}()}
}
get := func(key ReportGroup) *reportAggregate {
if groups[key] == nil {
groups[key] = &reportAggregate{pay: new(big.Rat), users: map[int]map[int64]bool{1: {}, 2: {}, 3: {}, 4: {}}}
}
return groups[key]
}
// 无平台登录仅在全库唯一主账号匹配时归属,提前建立索引避免逐条扫描账号。
unique := map[int64]*ReportUser{}
ambiguous := map[int64]bool{}
for _, user := range input.Users {
if user.ParentID == nil || *user.ParentID != 0 {
continue
}
if _, ok := unique[user.ID]; ok {
ambiguous[user.ID] = true
}
copy := user
unique[user.ID] = &copy
}
for _, event := range input.Registers {
if event.UserID == nil {
continue
}
if kind == ServerReport && (strings.TrimSpace(event.ServerNum) == "" || event.GameID == nil) {
continue
}
get(group(event)).users[1][*event.UserID] = true
}
for _, event := range input.Actives {
if event.UserID == nil {
continue
}
if kind == ServerReport {
if strings.TrimSpace(event.ServerNum) == "" || event.GameID == nil {
continue
}
} else {
var user *ReportUser
if strings.TrimSpace(event.SourceDB) == "" {
if ambiguous[*event.UserID] {
continue
}
user = unique[*event.UserID]
if user != nil {
event.SourceDB = user.SourceDB
}
} else {
if value, ok := input.Users[ReportUserKey{SourceDB: event.SourceDB, ID: *event.UserID}]; ok {
user = &value
}
}
if user == nil || user.ParentID == nil || *user.ParentID != 0 {
continue
}
if event.PromoteID == nil {
event.PromoteID = user.PromoteID
}
}
get(group(event)).users[2][*event.UserID] = true
}
for _, event := range input.Payments {
if kind == ServerReport && strings.TrimSpace(event.ServerNum) == "" {
continue
}
a := get(group(event))
if event.UserID != nil {
a.users[3][*event.UserID] = true
}
amount, err := portraitNumber(event.Amount)
if err != nil {
return nil, err
}
a.pay.Add(a.pay, amount)
}
keys := make([]ReportGroup, 0, len(groups))
for key := range groups {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
a, b := keys[i], keys[j]
if a.SourceDB != b.SourceDB {
return a.SourceDB < b.SourceDB
}
if a.PromoteID != b.PromoteID {
return a.PromoteID < b.PromoteID
}
if a.GameID != b.GameID {
return a.GameID < b.GameID
}
return a.ServerNum < b.ServerNum
})
out := []*ReportBuiltRow{}
for _, key := range keys {
a := groups[key]
if len(a.users[1])+len(a.users[2])+len(a.users[3]) == 0 && a.pay.Sign() == 0 {
continue
}
if kind == DailyReport {
for id := range a.users[3] {
if a.users[1][id] {
a.users[4][id] = true
}
}
}
for mode := 0; mode <= 1; mode++ {
row := &ReportBuiltRow{ReportGroup: key, Mode: mode, GameName: input.Games[AnalyticsKey{SourceDB: key.SourceDB, ID: key.GameID}], Pay: portraitDecimal(a.pay, 2), Keys: map[int][]string{}}
for metric, ids := range a.users {
if metric == 4 && kind == ServerReport {
continue
}
identities := map[string]bool{}
for id := range ids {
identity := strconv.FormatInt(id, 10)
if mode == 1 {
identity = normalizeIDCard(input.Users[ReportUserKey{SourceDB: key.SourceDB, ID: id}].IDCard)
}
if identity != "" {
identities[identity] = true
}
}
values := make([]string, 0, len(identities))
for value := range identities {
values = append(values, value)
}
sort.Strings(values)
row.Keys[metric] = values
switch metric {
case 1:
row.Counts.Register = int64(len(values))
case 2:
row.Counts.Active = int64(len(values))
case 3:
row.Counts.Pay = int64(len(values))
case 4:
row.Counts.NewPay = int64(len(values))
}
}
out = append(out, row)
}
}
return out, nil
}