// Package channel 渠道仓储:选择、加解密、健康过滤。 // M1 阶段实现最小选择逻辑(按优先级+权重取第一个健康启用的渠道), // 负载均衡/健康检查/故障转移在 M4 完善。 package channel import ( "errors" "github.com/openteam/server/internal/pkg/crypto" "github.com/openteam/server/internal/store" "gorm.io/gorm" ) var ErrNoChannel = errors.New("no available channel") type Service struct { db *gorm.DB enc *crypto.Encryptor } func NewService(db *gorm.DB, enc *crypto.Encryptor) *Service { return &Service{db: db, enc: enc} } // Select 选择处理请求的渠道:启用 + 健康,按 priority 升序、weight 降序。 func (s *Service) Select() (*store.Channel, error) { var chs []store.Channel if err := s.db.Where("enabled = ? AND health_status = ?", true, store.ChannelHealthHealthy). Order("priority ASC, weight DESC, id ASC").Find(&chs).Error; err != nil { return nil, err } if len(chs) == 0 { return nil, ErrNoChannel } return &chs[0], nil } // UpstreamKey 解密渠道上游密钥。 func (s *Service) UpstreamKey(ch *store.Channel) (string, error) { return s.enc.Decrypt(ch.APIKeyEnc) } // ResolveModel 按全局模型名找到绑定渠道;M1 简化:返回绑定该模型的第一个健康渠道。 func (s *Service) ResolveModel(modelName string) (*store.Channel, *store.ChannelModelBinding, error) { var m store.Model if err := s.db.Where("name = ? AND enabled = ?", modelName, true).First(&m).Error; err != nil { return nil, nil, err } var b store.ChannelModelBinding if err := s.db.Where("model_id = ?", m.ID). Joins("JOIN channels ON channels.id = channel_model_bindings.channel_id AND channels.enabled = ? AND channels.health_status = ?", true, store.ChannelHealthHealthy). Order("channel_model_bindings.weight DESC"). First(&b).Error; err != nil { return nil, nil, err } ch, err := s.Select() if err != nil { return nil, nil, err } // 用绑定里的渠道(如果健康),否则回退默认渠道 if b.ChannelID != ch.ID { var bound store.Channel if err := s.db.First(&bound, b.ChannelID).Error; err == nil && bound.Enabled && bound.HealthStatus == store.ChannelHealthHealthy { return &bound, &b, nil } } return ch, &b, nil }