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 }