package business import ( "context" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "kra/internal/biz/business" ) type effectivenessRepo struct{ data Provider } func NewEffectivenessRepo(data Provider) business.EffectivenessRepo { return &effectivenessRepo{data: data} } func (r *effectivenessRepo) State(ctx context.Context) (*business.EffectivenessState, error) { var po effectivenessStatePO if err := r.data.DB().WithContext(ctx).First(&po, "id=1").Error; err != nil { return nil, business.ErrStorage } s := &business.EffectivenessState{RuleKey: po.RuleKey, Revision: po.Revision} if po.BuiltAt != nil { s.BuiltAt = *po.BuiltAt } return s, nil } func (r *effectivenessRepo) Candidates(ctx context.Context, key string, duration, now int64, full bool) ([]*business.EffectivenessCandidate, error) { db := r.data.DB().WithContext(ctx) available, err := businessTablesAvailable(db, "tab_promote", "tab_spend") if err != nil { return nil, err } if !available { return nil, business.ErrEffectivenessUnavailable } q := db.Table("tab_promote p").Joins("LEFT JOIN sys_business_channel_effectiveness e ON e.rule_key=? AND e.source_db=p.source_db AND e.promote_id=p.id", key).Where("p.source_db IN ?", []string{"lx", "hw", "yq"}) if !full { q = q.Where("e.promote_id IS NULL OR e.complete=?", false) } var pos []struct { ID uint64 SourceDB string `gorm:"column:source_db"` CreateTime, QualifiedAt *int64 WindowPay string } if err := q.Select("p.id,p.source_db,p.create_time,e.qualified_at,e.window_pay").Order("p.id,p.source_db").Scan(&pos).Error; err != nil { return nil, business.ErrStorage } rows := make([]*business.EffectivenessCandidate, 0, len(pos)) for _, po := range pos { rows = append(rows, &business.EffectivenessCandidate{ChannelKey: business.ChannelKey{ID: po.ID, SourceDB: po.SourceDB}, CreatedAt: po.CreateTime, PreviousAt: po.QualifiedAt, PreviousPay: po.WindowPay}) } return rows, nil } func (r *effectivenessRepo) Charges(ctx context.Context, candidates []*business.EffectivenessCandidate, duration, now int64, full bool, consume func(business.EffectivenessCharge) error) error { db := r.data.DB().WithContext(ctx) // Bound by now as future charges are excluded. Avoid unsigned subtraction // on legacy pay_time columns and overflow for very large configured windows. windowSeconds := min(duration, now+1) // The full-history scan is reserved for explicit rebuilds. Minute jobs // must keep their candidate restriction in SQL, even with many open windows. if full && len(candidates) > 3000 { return r.scanAllCharges(db, candidates, windowSeconds, now, consume) } for start := 0; start < len(candidates); start += 300 { keys := [][]any{} ids := []uint64{} for _, c := range candidates[start:min(start+300, len(candidates))] { if c.CreatedAt != nil && *c.CreatedAt > 0 && *c.CreatedAt <= now { keys = append(keys, []any{c.SourceDB, c.ID}) ids = append(ids, c.ID) } } if len(keys) == 0 { continue } q := db.Table("tab_spend s").Joins("JOIN tab_promote p ON p.source_db=s.source_db AND p.id=s.promote_id").Where("s.promote_id IN ? AND (s.source_db,s.promote_id) IN ? AND s.pay_status=1", ids, keys).Where("s.pay_time>=p.create_time AND s.pay_time<=? AND s.pay_time