package business import ( "context" "errors" "sort" "strings" "time" "gorm.io/gorm" "kra/internal/biz/business" ) type channelPO struct { QualifiedAt *int64 `gorm:"->;-:migration"` ID uint64 SourceDB string `gorm:"column:source_db"` Account, Nickname, ParentName, RealName string PromoteLevel, RegisterType, Status, LockStatus *int ParentID, TopPromoteID, BusierID uint64 CreateTime *int64 LastLoginTime *int64 `gorm:"column:last_login_time"` MobilePhone string SettlementDayPeriod *int } func (channelPO) TableName() string { return "tab_promote" } func toBizChannel(p channelPO) *business.Channel { return &business.Channel{ChannelKey: business.ChannelKey{SourceDB: p.SourceDB, ID: p.ID}, Account: p.Account, Nickname: p.Nickname, ParentName: p.ParentName, RealName: p.RealName, Level: p.PromoteLevel, RegisterType: p.RegisterType, Status: p.Status, LockStatus: p.LockStatus, ParentID: p.ParentID, TopPromoteID: p.TopPromoteID, BusierID: p.BusierID, CreatedUnix: p.CreateTime, LastLoginUnix: p.LastLoginTime, QualifiedAt: p.QualifiedAt, Phone: p.MobilePhone, SettlementDays: p.SettlementDayPeriod} } type channelRepo struct{ data Provider } func NewChannelRepo(data Provider) business.ChannelRepo { return &channelRepo{data: data} } func channelBindingQuery(db *gorm.DB, scope business.UserScope, filter business.ChannelFilter) *gorm.DB { q := db.Table("sys_user_bind_platform b").Joins("LEFT JOIN sys_users u ON u.id=b.user_id").Where("b.status = ?", "0") if !scope.All { q = applyUserScope(q.Where("u.deleted_at IS NULL"), scope, "u.") } if filter.BusierUserID != nil { q = q.Where("b.user_id = ?", *filter.BusierUserID) } if len(filter.Sources) > 0 { q = q.Where("b.source_db IN ?", filter.Sources) } if keyword := strings.ToLower(strings.TrimSpace(filter.BusierAccount)); keyword != "" { q = q.Where("(INSTR(LOWER(b.platform_busier_account),?) > 0 OR INSTR(LOWER(u.username),?) > 0)", keyword, keyword) } return q } func (r *channelRepo) Bindings(ctx context.Context, scope business.UserScope, source string) ([]*business.Binding, error) { var pos []bindingPO if err := channelBindingQuery(r.data.DB().WithContext(ctx), scope, business.ChannelFilter{Sources: []string{source}}).Select("b.*").Order("b.user_id,b.source_db,b.platform_busier_id").Scan(&pos).Error; err != nil { return nil, business.ErrStorage } rows := make([]*business.Binding, 0, len(pos)) for _, po := range pos { rows = append(rows, toBizBinding(po)) } return rows, nil } func (r *channelRepo) List(ctx context.Context, scope business.UserScope, filter business.ChannelFilter, options ...business.ListOption) (*business.ChannelPage, error) { db := r.data.DB().WithContext(ctx) page := &business.ChannelPage{Rows: []*business.Channel{}, Available: true, StatusCounts: &business.ChannelStatusCounts{}} bindings := channelBindingQuery(db, scope, filter) constrain := !scope.All || filter.BusierUserID != nil || filter.BusierAccount != "" if constrain { var count int64 if err := bindings.Count(&count).Error; err != nil { return nil, business.ErrStorage } if count == 0 { return page, nil } } available, err := businessTableAvailable(db, "tab_promote") if err != nil { return nil, err } if !available { page.Available = false return page, nil } q := db.Table("tab_promote p") if constrain { q = q.Where("EXISTS (?)", bindings.Select("1").Where("b.source_db=p.source_db AND b.platform_busier_id=p.busier_id")) } if len(filter.Sources) > 0 { q = q.Where("p.source_db IN ?", filter.Sources) } if filter.Keyword != "" { keyword := strings.ToLower(strings.TrimSpace(filter.Keyword)) q = q.Where("INSTR(LOWER(p.account),?)>0 OR INSTR(LOWER(p.nickname),?)>0 OR INSTR(LOWER(p.real_name),?)>0", keyword, keyword, keyword) } if filter.Phone != "" { q = q.Where("INSTR(p.mobile_phone,?) > 0", filter.Phone) } if filter.SettlementDays != nil { q = q.Where("p.settlement_day_period = ?", *filter.SettlementDays) } for _, period := range []struct { column string begin, end *time.Time }{ {"p.create_time", filter.CreatedBegin, filter.CreatedEnd}, {"p.last_login_time", filter.LoginBegin, filter.LoginEnd}, } { if period.begin != nil { q = q.Where(period.column+" >= ?", period.begin.Unix()) } if period.end != nil { q = q.Where(period.column+" < ?", period.end.AddDate(0, 0, 1).Unix()) } } for _, field := range []struct { column string value *int }{{"promote_level", filter.Level}, {"register_type", filter.RegisterType}, {"status", filter.Status}, {"lock_status", filter.LockStatus}} { if field.value != nil { q = q.Where("p."+field.column+" = ?", *field.value) } } if filter.Effective != nil { q = joinEffectiveness(q) if *filter.Effective { q = q.Where("e.qualified_at IS NOT NULL") } else { q = q.Where("e.qualified_at IS NULL") } } var counts struct{ TotalCount, NormalCount, LockedCount, TerminatedCount int64 } if err := q.Session(&gorm.Session{}).Select("COUNT(*) AS total_count,COALESCE(SUM(CASE WHEN COALESCE(p.lock_status,0)=0 THEN 1 ELSE 0 END),0) AS normal_count,COALESCE(SUM(CASE WHEN COALESCE(p.lock_status,0) NOT IN (0,-2) THEN 1 ELSE 0 END),0) AS locked_count,COALESCE(SUM(CASE WHEN p.lock_status=-2 THEN 1 ELSE 0 END),0) AS terminated_count").Scan(&counts).Error; err != nil { return nil, business.ErrStorage } page.Total = counts.TotalCount page.StatusCounts = &business.ChannelStatusCounts{Normal: counts.NormalCount, Locked: counts.LockedCount, Terminated: counts.TerminatedCount} var pos []channelPO o := business.NewListOptions(options...) order := "p.create_time DESC,p.id DESC,p.source_db ASC" if filter.SortField == "createdAt" && filter.Ascending { order = "p.create_time ASC,p.id ASC,p.source_db ASC" } var pageQuery *gorm.DB var metricKeys []business.ChannelKey metricSort := filter.SortField == "playerCount" || filter.SortField == "totalPayAmount" if metricSort { metricKeys, err = channelMetricPage(db, q, filter, o) if err != nil { return nil, err } if len(metricKeys) == 0 { return page, nil } pageQuery = joinEffectiveness(db.Table("tab_promote p").Where("(p.source_db,p.id) IN ?", channelPairs(metricKeys))) } else { keysQuery := q.Select("p.id,p.source_db").Order(order).Offset(o.Offset).Limit(o.Limit) pageQuery = joinEffectiveness(db.Table("tab_promote p").Joins("JOIN (?) page_ids ON page_ids.id=p.id AND page_ids.source_db=p.source_db", keysQuery)).Order(order) } if err := pageQuery.Select("p.*,e.qualified_at").Scan(&pos).Error; err != nil { return nil, business.ErrStorage } if metricSort { ranks := map[business.ChannelKey]int{} for i, key := range metricKeys { ranks[key] = i } sort.Slice(pos, func(i, j int) bool { return ranks[business.ChannelKey{SourceDB: pos[i].SourceDB, ID: pos[i].ID}] < ranks[business.ChannelKey{SourceDB: pos[j].SourceDB, ID: pos[j].ID}] }) } if len(pos) == 0 { return page, nil } pairs := make([][]any, 0, len(pos)) for _, po := range pos { pairs = append(pairs, []any{po.SourceDB, po.BusierID}) } var bound []struct { SourceDB string `gorm:"column:source_db"` PlatformBusierID uint64 UserID uint PlatformBusierAccount, UserName string IDPath string } if err := channelBindingQuery(db, scope, filter).Select("b.source_db,b.platform_busier_id,b.user_id,b.platform_busier_account,u.username AS user_name,u.id_path").Where("(b.source_db,b.platform_busier_id) IN ?", pairs).Order("b.user_id").Scan(&bound).Error; err != nil { return nil, business.ErrStorage } ownerPaths := map[uint]string{} for _, b := range bound { ownerPaths[b.UserID] = b.IDPath } paths, err := businessDisplayPaths(db, ownerPaths) if err != nil { return nil, err } byKey := map[business.PlatformKey]int{} for i, b := range bound { key := business.PlatformKey{SourceDB: b.SourceDB, BusierID: b.PlatformBusierID} if _, ok := byKey[key]; !ok { byKey[key] = i } } for _, po := range pos { c := toBizChannel(po) if i, ok := byKey[business.PlatformKey{SourceDB: po.SourceDB, BusierID: po.BusierID}]; ok { b := bound[i] c.BusierUserID = &b.UserID c.BindingAccount = b.PlatformBusierAccount c.BindingUserName = b.UserName c.BusinessPath = paths[b.UserID] } page.Rows = append(page.Rows, c) } return page, nil } func (r *channelRepo) Find(ctx context.Context, scope business.UserScope, key business.ChannelKey) (*business.Channel, error) { db := r.data.DB().WithContext(ctx) available, err := businessTableAvailable(db, "tab_promote") if err != nil { return nil, err } if !available { return nil, business.ErrChannelUnavailable } var po channelPO if err := db.Where("source_db = ? AND id = ?", key.SourceDB, key.ID).Take(&po).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, business.ErrChannelNotFound } return nil, business.ErrStorage } if !scope.All { var count int64 if err := channelBindingQuery(db, scope, business.ChannelFilter{Sources: []string{key.SourceDB}}).Where("b.platform_busier_id = ?", po.BusierID).Count(&count).Error; err != nil { return nil, business.ErrStorage } if count == 0 { return nil, business.ErrChannelScope } } c := toBizChannel(po) var bindings []struct { UserID uint PlatformBusierAccount, UserName, IDPath string } if err := channelBindingQuery(db, scope, business.ChannelFilter{Sources: []string{key.SourceDB}}). Where("b.platform_busier_id = ?", po.BusierID).Order("b.user_id").Limit(1). Select("b.user_id,b.platform_busier_account,u.username AS user_name,u.id_path").Scan(&bindings).Error; err != nil { return nil, business.ErrStorage } if len(bindings) > 0 { b := bindings[0] paths, err := businessDisplayPaths(db, map[uint]string{b.UserID: b.IDPath}) if err != nil { return nil, err } c.BusierUserID, c.BindingAccount, c.BindingUserName = &b.UserID, b.PlatformBusierAccount, b.UserName c.BusinessPath = paths[b.UserID] } return c, nil } func channelPairs(keys []business.ChannelKey) [][]any { pairs := make([][]any, 0, len(keys)) for _, key := range keys { pairs = append(pairs, []any{key.SourceDB, key.ID}) } return pairs } func (r *channelRepo) StatChannels(ctx context.Context, roots []*business.Channel) ([]*business.Channel, error) { keys := make([]business.ChannelKey, 0, len(roots)) for _, root := range roots { if root.Level != nil && *root.Level < 3 { keys = append(keys, root.ChannelKey) } } rows := []*business.Channel{} seen := map[business.ChannelKey]bool{} for start := 0; start < len(keys); start += 200 { pairs := channelPairs(keys[start:min(start+200, len(keys))]) var pos []channelPO db := r.data.DB().WithContext(ctx) columns := "id,source_db,promote_level,parent_id,top_promote_id" parents := db.Table("tab_promote").Select(columns).Where("(source_db,parent_id) IN ?", pairs) tops := db.Table("tab_promote").Select(columns).Where("(source_db,top_promote_id) IN ?", pairs) // 分别走 parent_id / top_promote_id 索引,避免跨列 OR 退化为全表扫描。 // 同时命中两条关系的渠道继续由下面的 ChannelKey 集合去重。 err := db.Raw("? UNION ALL ?", parents, tops).Scan(&pos).Error if err != nil { return nil, business.ErrStorage } for _, po := range pos { c := toBizChannel(po) if !seen[c.ChannelKey] { rows = append(rows, c) seen[c.ChannelKey] = true } } } return rows, nil }