178 lines
6.2 KiB
Go
178 lines
6.2 KiB
Go
package business
|
|
|
|
import (
|
|
"database/sql"
|
|
"math/big"
|
|
"sort"
|
|
|
|
"gorm.io/gorm"
|
|
"kra/internal/biz/business"
|
|
)
|
|
|
|
// Build the same per-channel membership as Channel.IncludesStat. UNION removes
|
|
// descendants reached through both parent_id and top_promote_id.
|
|
func channelMetricMembership(db, roots *gorm.DB, includeSub bool) *gorm.DB {
|
|
self := roots.Session(&gorm.Session{}).Select("p.source_db,p.id AS root_id,p.id AS leaf_id")
|
|
if !includeSub {
|
|
return db.Table("(?) cm", self)
|
|
}
|
|
direct := roots.Session(&gorm.Session{}).Select("p.source_db,p.id AS root_id,leaf.id AS leaf_id").
|
|
Joins("JOIN tab_promote leaf ON leaf.source_db=p.source_db AND leaf.parent_id=p.id").
|
|
Where("p.promote_level<3 AND (p.promote_level<>2 OR leaf.promote_level=3)")
|
|
top := roots.Session(&gorm.Session{}).Select("p.source_db,p.id AS root_id,leaf.id AS leaf_id").
|
|
Joins("JOIN tab_promote leaf ON leaf.source_db=p.source_db AND leaf.top_promote_id=p.id").
|
|
Where("p.promote_level<3 AND p.promote_level<>2")
|
|
return db.Table("(? UNION ? UNION ?) cm", self, direct, top)
|
|
}
|
|
|
|
// Only keys and scores for visible rows are retained. Identity rows are streamed
|
|
// one channel at a time, using the same normalization as the displayed count.
|
|
func channelMetricPage(db, roots *gorm.DB, f business.ChannelFilter, o business.ListOptions) ([]business.ChannelKey, error) {
|
|
var channels []channelPO
|
|
if err := roots.Session(&gorm.Session{}).Select("p.id,p.source_db,p.create_time").Scan(&channels).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
if len(channels) == 0 {
|
|
return nil, nil
|
|
}
|
|
table := "tab_user"
|
|
if f.SortField == "totalPayAmount" {
|
|
table = "tab_spend"
|
|
}
|
|
available, err := businessTableAvailable(db, table)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !available {
|
|
return nil, business.ErrChannelUnavailable
|
|
}
|
|
scores := map[business.ChannelKey]*big.Rat{}
|
|
q := channelMetricMembership(db, roots, f.IncludeSub)
|
|
// MySQL 5.7 may choose the narrow pay_status index and then fetch millions
|
|
// of full records. Prefer the existing covering index for this full scan.
|
|
metricTable := table
|
|
index := "idx_sw_user_report_identity"
|
|
if table == "tab_spend" {
|
|
index = "idx_sw_spend_promote_summary"
|
|
} else if !f.IDCardDedup {
|
|
// Its source/channel prefix supports both filtering and grouped counting,
|
|
// without sorting the much wider identity index into a temporary table.
|
|
index = "idx_alert_user_window"
|
|
}
|
|
if db.Dialector.Name() == "mysql" && db.Migrator().HasIndex(table, index) {
|
|
metricTable += " FORCE INDEX (" + index + ")"
|
|
}
|
|
leaves := db.Table(metricTable)
|
|
if len(f.Sources) > 0 {
|
|
leaves = leaves.Where("source_db IN ?", f.Sources)
|
|
}
|
|
if f.SortField == "totalPayAmount" {
|
|
leaves = leaves.Where("pay_status=1")
|
|
} else {
|
|
leaves = leaves.Where("puid=0 OR puid IS NULL")
|
|
}
|
|
if f.SortField == "playerCount" && f.IDCardDedup {
|
|
join := "STRAIGHT_JOIN"
|
|
if db.Dialector.Name() == "sqlite" {
|
|
join = "JOIN"
|
|
}
|
|
// Stream the covering user index once, then look up its channel roots.
|
|
// Grouping raw identity strings first creates a large disk temporary table
|
|
// and still cannot apply the domain's exact whitespace/X normalization.
|
|
leaves = leaves.Select("source_db,promote_id,idcard")
|
|
members := q.Select("cm.source_db,cm.root_id,cm.leaf_id")
|
|
rows, err := db.Table("(?) u", leaves).Joins(join+" (?) cm ON cm.source_db=u.source_db AND cm.leaf_id=u.promote_id", members).
|
|
Select("cm.source_db,cm.root_id,u.idcard").Order("cm.source_db,cm.root_id").Rows()
|
|
if err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
defer rows.Close()
|
|
var previous business.ChannelKey
|
|
identities := map[string]struct{}{}
|
|
flush := func() { scores[previous] = big.NewRat(int64(len(identities)), 1) }
|
|
for rows.Next() {
|
|
var key business.ChannelKey
|
|
var card sql.NullString
|
|
if err := rows.Scan(&key.SourceDB, &key.ID, &card); err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
if key != previous {
|
|
flush()
|
|
identities = map[string]struct{}{}
|
|
previous = key
|
|
}
|
|
if identity := business.ChannelIdentity(card.String); identity != "" {
|
|
identities[identity] = struct{}{}
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
flush()
|
|
} else {
|
|
var values []struct {
|
|
SourceDB string
|
|
RootID uint64
|
|
Score string
|
|
}
|
|
if f.SortField == "totalPayAmount" {
|
|
leaves = leaves.Select("source_db,promote_id AS root_id,promote_id,COALESCE(SUM(pay_amount),0) AS score")
|
|
} else {
|
|
leaves = leaves.Select("source_db,promote_id AS root_id,promote_id,COUNT(*) AS score")
|
|
}
|
|
leaves = leaves.Group("source_db,promote_id")
|
|
if f.IncludeSub {
|
|
q = q.Joins("JOIN (?) leaf ON leaf.source_db=cm.source_db AND leaf.promote_id=cm.leaf_id", leaves).
|
|
Select("cm.source_db,cm.root_id,SUM(leaf.score) AS score").Group("cm.source_db,cm.root_id")
|
|
} else {
|
|
// A channel's own score needs no membership join or second grouping.
|
|
// Scores remain internal; pagination below only returns visible roots.
|
|
q = leaves
|
|
}
|
|
if err := q.Scan(&values).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
for _, value := range values {
|
|
score, ok := new(big.Rat).SetString(value.Score)
|
|
if !ok {
|
|
return nil, business.ErrStorage
|
|
}
|
|
scores[business.ChannelKey{SourceDB: value.SourceDB, ID: value.RootID}] = score
|
|
}
|
|
}
|
|
zero := new(big.Rat)
|
|
score := func(c channelPO) *big.Rat {
|
|
if n := scores[business.ChannelKey{SourceDB: c.SourceDB, ID: c.ID}]; n != nil {
|
|
return n
|
|
}
|
|
return zero
|
|
}
|
|
sort.Slice(channels, func(i, j int) bool {
|
|
a, b := channels[i], channels[j]
|
|
if compared := score(a).Cmp(score(b)); compared != 0 {
|
|
return (compared < 0) == f.Ascending
|
|
}
|
|
if a.CreateTime == nil || b.CreateTime == nil {
|
|
if (a.CreateTime == nil) != (b.CreateTime == nil) {
|
|
return a.CreateTime != nil
|
|
}
|
|
} else if *a.CreateTime != *b.CreateTime {
|
|
return *a.CreateTime > *b.CreateTime
|
|
}
|
|
if a.ID != b.ID {
|
|
return a.ID > b.ID
|
|
}
|
|
return a.SourceDB < b.SourceDB
|
|
})
|
|
begin := min(max(0, o.Offset), len(channels))
|
|
end := len(channels)
|
|
if o.Limit > 0 && o.Limit < end-begin {
|
|
end = begin + o.Limit
|
|
}
|
|
keys := make([]business.ChannelKey, 0, end-begin)
|
|
for _, row := range channels[begin:end] {
|
|
keys = append(keys, business.ChannelKey{SourceDB: row.SourceDB, ID: row.ID})
|
|
}
|
|
return keys, nil
|
|
}
|