308 lines
8.2 KiB
Go
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] = ©
|
|
}
|
|
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
|
|
}
|