对齐 vivid a338edb / 7fb5c57 / 4f454b6: - model_configs.enabled 列删除:上架唯一门槛 = 绑定了启用渠道 + 全部档位 有价(视频另需时长有价)。/v1/models、/api/models、video-presets、 managed-models 全部改按 sellable 透出 - 保存渠道即全量同步上游模型(消失解绑/孤儿删除/失败告警不删库) - v1 job 持久化 upstream jobId,重启后维护循环按 jobId 续等不丢单 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
216 lines
7.9 KiB
Go
216 lines
7.9 KiB
Go
package repo
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"backend/internal/model"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type ChannelRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewChannelRepository(db *gorm.DB) *ChannelRepository {
|
|
return &ChannelRepository{db: db}
|
|
}
|
|
|
|
// ChannelWithUpstream pairs a channel row with the upstream model name a
|
|
// binding carries — the shape dispatch needs in one fetch.
|
|
type ChannelWithUpstream struct {
|
|
Channel model.Channel
|
|
UpstreamModel string
|
|
}
|
|
|
|
func (r *ChannelRepository) List(ctx context.Context) ([]model.Channel, error) {
|
|
var items []model.Channel
|
|
if err := r.db.WithContext(ctx).Order("created_at asc").Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (r *ChannelRepository) Get(ctx context.Context, id string) (*model.Channel, error) {
|
|
var item model.Channel
|
|
if err := r.db.WithContext(ctx).First(&item, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *ChannelRepository) Create(ctx context.Context, item *model.Channel) error {
|
|
return r.db.WithContext(ctx).Create(item).Error
|
|
}
|
|
|
|
func (r *ChannelRepository) Update(ctx context.Context, id string, patch map[string]any) (*model.Channel, error) {
|
|
patch["updated_at"] = time.Now()
|
|
if err := r.db.WithContext(ctx).Model(&model.Channel{}).Where("id = ?", id).Updates(patch).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Get(ctx, id)
|
|
}
|
|
|
|
func (r *ChannelRepository) Delete(ctx context.Context, id string) (int64, error) {
|
|
res := r.db.WithContext(ctx).Delete(&model.Channel{}, "id = ?", id)
|
|
return res.RowsAffected, res.Error
|
|
}
|
|
|
|
// Bindings returns all channel_models rows for a channel.
|
|
func (r *ChannelRepository) Bindings(ctx context.Context, channelID string) ([]model.ChannelModel, error) {
|
|
var items []model.ChannelModel
|
|
if err := r.db.WithContext(ctx).Where("channel_id = ?", channelID).Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// Bind upserts one channel↔model binding (upstream model name is refreshed on
|
|
// conflict so re-import can rename it).
|
|
func (r *ChannelRepository) Bind(ctx context.Context, channelID, modelID, upstreamModel string) error {
|
|
row := model.ChannelModel{ChannelID: channelID, ModelID: modelID, UpstreamModel: upstreamModel}
|
|
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "channel_id"}, {Name: "model_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"upstream_model", "updated_at"}),
|
|
}).Create(&row).Error
|
|
}
|
|
|
|
func (r *ChannelRepository) Unbind(ctx context.Context, channelID, modelID string) error {
|
|
return r.db.WithContext(ctx).Delete(&model.ChannelModel{}, "channel_id = ? AND model_id = ?", channelID, modelID).Error
|
|
}
|
|
|
|
// UnbindExcept removes every binding of a channel whose upstream model name is
|
|
// NOT in the keep list (upstream_model, falling back to model_id for legacy
|
|
// rows) — the "true sync" counterpart used after saving a channel.
|
|
func (r *ChannelRepository) UnbindExcept(ctx context.Context, channelID string, upstreamIDs []string) error {
|
|
return r.db.WithContext(ctx).
|
|
Where("channel_id = ? AND NOT (COALESCE(NULLIF(upstream_model, ''), model_id) IN ?)", channelID, upstreamIDs).
|
|
Delete(&model.ChannelModel{}).Error
|
|
}
|
|
|
|
func (r *ChannelRepository) DeleteBindingsByChannel(ctx context.Context, channelID string) error {
|
|
return r.db.WithContext(ctx).Delete(&model.ChannelModel{}, "channel_id = ?", channelID).Error
|
|
}
|
|
|
|
func (r *ChannelRepository) DeleteBindingsByModel(ctx context.Context, modelID string) error {
|
|
return r.db.WithContext(ctx).Delete(&model.ChannelModel{}, "model_id = ?", modelID).Error
|
|
}
|
|
|
|
// ForModel returns enabled channels bound to modelID (channel rows + the
|
|
// upstream model name each binding carries), stable-ordered for dispatch.
|
|
func (r *ChannelRepository) ForModel(ctx context.Context, modelID string) ([]ChannelWithUpstream, error) {
|
|
var rows []struct {
|
|
model.Channel
|
|
UpstreamModel string `gorm:"column:upstream_model"`
|
|
}
|
|
err := r.db.WithContext(ctx).
|
|
Table("channels").
|
|
Select("channels.*, channel_models.upstream_model").
|
|
Joins("JOIN channel_models ON channel_models.channel_id = channels.id").
|
|
Where("channel_models.model_id = ? AND channels.enabled = ?", modelID, true).
|
|
Order("channels.id asc").
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]ChannelWithUpstream, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, ChannelWithUpstream{Channel: row.Channel, UpstreamModel: row.UpstreamModel})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ForModelAnyState is ForModel without the enabled filter — used by admin
|
|
// channel-pinned tests which deliberately target one channel even when it's
|
|
// disabled for traffic.
|
|
func (r *ChannelRepository) ForModelAnyState(ctx context.Context, modelID string) ([]ChannelWithUpstream, error) {
|
|
var rows []struct {
|
|
model.Channel
|
|
UpstreamModel string `gorm:"column:upstream_model"`
|
|
}
|
|
err := r.db.WithContext(ctx).
|
|
Table("channels").
|
|
Select("channels.*, channel_models.upstream_model").
|
|
Joins("JOIN channel_models ON channel_models.channel_id = channels.id").
|
|
Where("channel_models.model_id = ?", modelID).
|
|
Order("channels.id asc").
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]ChannelWithUpstream, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, ChannelWithUpstream{Channel: row.Channel, UpstreamModel: row.UpstreamModel})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ModelIDsByChannel lists local model ids bound to a channel.
|
|
func (r *ChannelRepository) ModelIDsByChannel(ctx context.Context, channelID string) ([]string, error) {
|
|
var ids []string
|
|
err := r.db.WithContext(ctx).Model(&model.ChannelModel{}).
|
|
Where("channel_id = ?", channelID).Pluck("model_id", &ids).Error
|
|
return ids, err
|
|
}
|
|
|
|
// UpstreamModelMap returns model_id → upstream_model for a channel.
|
|
func (r *ChannelRepository) UpstreamModelMap(ctx context.Context, channelID string) (map[string]string, error) {
|
|
var rows []model.ChannelModel
|
|
if err := r.db.WithContext(ctx).Where("channel_id = ?", channelID).Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(map[string]string, len(rows))
|
|
for _, row := range rows {
|
|
out[row.ModelID] = row.UpstreamModel
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// RoutableModelIDs returns the set of local model ids bound to at least one
|
|
// ENABLED channel — the "can actually dispatch" set behind the sellable flag.
|
|
func (r *ChannelRepository) RoutableModelIDs(ctx context.Context) (map[string]struct{}, error) {
|
|
var ids []string
|
|
err := r.db.WithContext(ctx).
|
|
Table("channel_models").
|
|
Select("DISTINCT channel_models.model_id").
|
|
Joins("JOIN channels ON channels.id = channel_models.channel_id AND channels.enabled = ?", true).
|
|
Scan(&ids).Error
|
|
out := make(map[string]struct{}, len(ids))
|
|
for _, id := range ids {
|
|
out[id] = struct{}{}
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// ChannelIDsByModel lists channel ids bound to a model (any enabled state).
|
|
func (r *ChannelRepository) ChannelIDsByModel(ctx context.Context, modelID string) ([]string, error) {
|
|
var ids []string
|
|
err := r.db.WithContext(ctx).Model(&model.ChannelModel{}).
|
|
Where("model_id = ?", modelID).Pluck("channel_id", &ids).Error
|
|
return ids, err
|
|
}
|
|
|
|
// OrphanModelIDs returns local model ids that no channel binds anymore.
|
|
func (r *ChannelRepository) OrphanModelIDs(ctx context.Context) ([]string, error) {
|
|
var ids []string
|
|
err := r.db.WithContext(ctx).
|
|
Table("model_configs").
|
|
Select("model_configs.id").
|
|
Joins("LEFT JOIN channel_models ON channel_models.model_id = model_configs.id").
|
|
Where("channel_models.model_id IS NULL").
|
|
Scan(&ids).Error
|
|
return ids, err
|
|
}
|
|
|
|
// BumpSuccess records one successful upstream call (count + reported credits
|
|
// spent + last-used timestamp). Best-effort.
|
|
func (r *ChannelRepository) BumpSuccess(ctx context.Context, channelID string, credits float64) error {
|
|
now := time.Now()
|
|
return r.db.WithContext(ctx).Model(&model.Channel{}).Where("id = ?", channelID).Updates(map[string]any{
|
|
"success_total": gorm.Expr("success_total + 1"),
|
|
"used_credits": gorm.Expr("used_credits + ?", credits),
|
|
"last_used_at": now,
|
|
}).Error
|
|
}
|