package business import ( "context" "gorm.io/gorm" "kra/internal/biz/business" "math/big" "sort" "time" ) func (r *activationRepo) applyTransferEnd(ctx context.Context, row *business.Activation) error { if row.BindAt == nil || row.PromoteID == nil || (row.Status != "reached" && row.ReachAt == nil) { return nil } db, err := r.databases.Database(row.PlatformName) if err != nil || db == nil { return nil } db = db.WithContext(ctx) ok, err := businessTableAvailable(db, "tab_promote_business_event") if err != nil { return err } if !ok { return nil } begin := *row.BindAt if row.ReachAt != nil && row.ReachAt.After(begin) { begin = *row.ReachAt } q := db.Table("tab_promote_business_event").Where("kind='transfer' AND promote_id=? AND occurred_at>=?", *row.PromoteID, begin) if row.BusierID > 0 { q = q.Where("old_busier_id=?", row.BusierID) } else { q = q.Where("LOWER(old_busier_account)=LOWER(?)", row.BusierAccount) } var rows []struct{ OccurredAt time.Time } if err := q.Select("occurred_at").Order("occurred_at,id").Limit(1).Find(&rows).Error; err != nil { return business.ErrActivationSource } if len(rows) > 0 { row.EndOnTransfer(rows[0].OccurredAt) } return nil } func activationMetricQuery(row *business.Activation) business.ActivationSpendQuery { q := business.ActivationSpendQuery{PlatformName: row.PlatformName, Channels: row.Channels, Start: row.BindAt.Unix()} if row.PromoteID != nil { q.PromoteID = *row.PromoteID } if end := row.EndAt(); end != nil { n := end.Unix() q.End = &n } return q } func sumActivationMoney(values []string) (string, error) { total := new(big.Rat) for _, v := range values { n, ok := new(big.Rat).SetString(v) if !ok { return "", business.ErrStorage } total.Add(total, n) } return total.FloatString(2), nil } // 激活数据与 OA 使用同一 SDK 数据源,避免商务报表副本延迟/测试副本不同导致金额不一致。 func activationRoots(q business.ActivationSpendQuery) map[string][]uint64 { roots := map[string][]uint64{} keys := q.Channels if len(keys) == 0 { keys = []business.ChannelKey{{SourceDB: q.PlatformName, ID: q.PromoteID}} } for _, key := range keys { if key.ID > 0 { roots[key.SourceDB] = append(roots[key.SourceDB], key.ID) } } return roots } func (r *activationRepo) activationMetricsAvailable(ctx context.Context, q business.ActivationSpendQuery) (bool, error) { for source := range activationRoots(q) { db, err := r.databases.Database(source) if err != nil || db == nil { return false, nil } ok, err := businessTablesAvailable(db.WithContext(ctx), "tab_promote", "tab_spend") if err != nil { return false, err } if !ok { return false, nil } } return true, nil } func nativeActivationTree(db *gorm.DB, roots []uint64) *gorm.DB { return db.Table("tab_promote p").Where("p.id IN ? OR p.top_promote_id IN ? OR p.parent_id IN ?", roots, roots, roots) } func (r *activationRepo) Spend(ctx context.Context, q business.ActivationSpendQuery) ([]*business.ActivationSpendItem, error) { out := []*business.ActivationSpendItem{} for source, roots := range activationRoots(q) { db, err := r.databases.Database(source) if err != nil || db == nil { return nil, business.ErrActivationMetrics } db = db.WithContext(ctx) tree := nativeActivationTree(db, roots) var n int64 if err := tree.Count(&n).Error; err != nil { return nil, business.ErrActivationMetrics } if n == 0 { continue } query := db.Table("tab_spend s").Where("s.pay_status=1 AND s.pay_time>=?", q.Start).Where("EXISTS (?)", tree.Select("1").Where("p.id=s.promote_id")) if q.End != nil { query = query.Where("s.pay_time<=?", *q.End) } var rows []struct { GameID uint64 GameName, Amount string } if err := query.Select("s.game_id,COALESCE(MAX(s.game_name),'') AS game_name,COALESCE(SUM(s.pay_amount),0) AS amount").Group("s.game_id").Order("s.game_id").Scan(&rows).Error; err != nil { return nil, business.ErrActivationMetrics } for _, row := range rows { out = append(out, &business.ActivationSpendItem{PlatformName: source, GameName: row.GameName, Amount: row.Amount}) } } sort.SliceStable(out, func(i, j int) bool { a, okA := new(big.Rat).SetString(out[i].Amount) b, okB := new(big.Rat).SetString(out[j].Amount) if okA && okB && a.Cmp(b) != 0 { return a.Cmp(b) > 0 } if out[i].PlatformName != out[j].PlatformName { return out[i].PlatformName < out[j].PlatformName } return out[i].GameName < out[j].GameName }) return out, nil } func (r *activationRepo) registered(ctx context.Context, q business.ActivationSpendQuery, dedup bool) (*int64, error) { total := int64(0) identities := map[string]bool{} for source, roots := range activationRoots(q) { db, err := r.databases.Database(source) if err != nil || db == nil { return nil, nil } db = db.WithContext(ctx) ok, err := businessTablesAvailable(db, "tab_user", "tab_promote") if err != nil { return nil, err } if !ok { return nil, nil } cast := "CAST(r.register_time AS INTEGER)" if db.Dialector.Name() == "mysql" { cast = "CAST(r.register_time AS UNSIGNED)" } query := db.Table("tab_user r").Where("r.puid=0 AND "+cast+">=?", q.Start).Where("EXISTS (?)", nativeActivationTree(db, roots).Select("1").Where("p.id=r.promote_id")) if q.End != nil { query = query.Where(cast+"<=?", *q.End) } if dedup { var cards []string if err := query.Where("TRIM(COALESCE(r.idcard,''))<>''").Distinct("TRIM(r.idcard)").Pluck("TRIM(r.idcard)", &cards).Error; err != nil { return nil, business.ErrActivationMetrics } for _, card := range cards { identities[card] = true } } else { var count int64 if err := query.Distinct("r.id").Count(&count).Error; err != nil { return nil, business.ErrActivationMetrics } total += count } } if dedup { total = int64(len(identities)) } return &total, nil }