Реализация приоритизации поставщиков (цена/наличие) при автонаполнении

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация приоритизации поставщиков (цена/наличие) при автонаполнении
Сложная
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация приоритизации поставщиков (цена/наличие) при автонаполнении

Когда один и тот же товар есть у нескольких поставщиков, нужны правила: у кого заказать, чья цена ляжет в карточку, чьи данные считать основными. Без явных правил приоритизации каталог превращается в хаос — цены прыгают, фото меняются при каждом импорте, нет предсказуемости для покупателя.

Уровни приоритетов

Приоритизация работает на нескольких уровнях одновременно:

Уровень Что определяет Пример
Контент Чьё название, описание, фото использовать Поставщик A имеет лучший контент
Цена Какую цену показывать покупателю Минимум среди поставщиков с наличием
Заказ У кого фактически размещать заказ Самый дешёвый, потом резерв
Наличие Как считать суммарный сток Сумма, или только основной поставщик

Модель конфигурации приоритетов

CREATE TABLE supplier_priority_rules (
    id              BIGSERIAL PRIMARY KEY,
    name            VARCHAR(255) NOT NULL,
    scope_type      VARCHAR(20) NOT NULL,   -- 'global', 'category', 'brand', 'product'
    scope_id        BIGINT,                  -- NULL для global
    price_strategy  VARCHAR(30) NOT NULL,    -- 'min', 'primary', 'markup'
    content_mode    VARCHAR(20) NOT NULL,    -- 'primary_first', 'best_score'
    order_mode      VARCHAR(20) NOT NULL,    -- 'cheapest', 'priority_rank', 'round_robin'
    stock_mode      VARCHAR(20) NOT NULL,    -- 'sum', 'primary_only', 'max'
    is_active       BOOLEAN DEFAULT TRUE,
    priority        INT DEFAULT 0            -- приоритет правила (выше = важнее)
);

-- Ранги поставщиков в контексте правила
CREATE TABLE supplier_rule_ranks (
    rule_id         BIGINT REFERENCES supplier_priority_rules(id),
    supplier_id     INT REFERENCES suppliers(id),
    rank            SMALLINT NOT NULL,       -- 1 = высший приоритет
    markup_pct      NUMERIC(5,2) DEFAULT 0, -- наценка к цене поставщика
    is_content_src  BOOLEAN DEFAULT FALSE,   -- источник контента
    PRIMARY KEY (rule_id, supplier_id)
);

Стратегии ценообразования

enum PriceStrategy: string
{
    case Min       = 'min';       // Минимальная цена среди поставщиков с наличием
    case Primary   = 'primary';   // Цена основного поставщика
    case Markup    = 'markup';    // Базовая цена + наценка из правила
}

class PriceResolver
{
    public function resolve(Product $product, PriorityRule $rule): ?float
    {
        $offers = $product->offers()
            ->where('stock', '>', 0)
            ->with('supplier')
            ->get();

        return match ($rule->price_strategy) {
            PriceStrategy::Min->value => $this->resolveMin($offers, $rule),
            PriceStrategy::Primary->value => $this->resolvePrimary($offers, $rule),
            PriceStrategy::Markup->value => $this->resolveWithMarkup($offers, $rule),
        };
    }

    private function resolveMin(Collection $offers, PriorityRule $rule): ?float
    {
        // Учитываем наценку каждого поставщика при вычислении минимума
        return $offers->map(function ($offer) use ($rule) {
            $rank = $rule->ranks->firstWhere('supplier_id', $offer->supplier_id);
            $markup = $rank?->markup_pct ?? 0;
            return $offer->price * (1 + $markup / 100);
        })->min();
    }

    private function resolvePrimary(Collection $offers, PriorityRule $rule): ?float
    {
        // Основной поставщик — первый по рангу с наличием
        $rankedOffers = $offers->sortBy(function ($offer) use ($rule) {
            $rank = $rule->ranks->firstWhere('supplier_id', $offer->supplier_id);
            return $rank?->rank ?? PHP_INT_MAX;
        });

        $primaryOffer = $rankedOffers->first();
        if (!$primaryOffer) return null;

        $rank = $rule->ranks->firstWhere('supplier_id', $primaryOffer->supplier_id);
        return $primaryOffer->price * (1 + ($rank?->markup_pct ?? 0) / 100);
    }
}

Стратегии выбора источника контента

class ContentSourceResolver
{
    public function resolveContentSupplier(Product $product, PriorityRule $rule): ?int
    {
        return match ($rule->content_mode) {
            'primary_first' => $this->primaryFirst($product, $rule),
            'best_score'    => $this->bestScore($product, $rule),
            default         => null,
        };
    }

    private function primaryFirst(Product $product, PriorityRule $rule): ?int
    {
        // Берём поставщика с is_content_src = true, если у него есть оффер
        $contentSupplierIds = $rule->ranks
            ->where('is_content_src', true)
            ->sortBy('rank')
            ->pluck('supplier_id');

        foreach ($contentSupplierIds as $supplierId) {
            if ($product->offers->firstWhere('supplier_id', $supplierId)) {
                return $supplierId;
            }
        }

        // Fallback: первый по рангу с наличием
        return $product->offers
            ->sortBy(fn($o) => $rule->ranks->firstWhere('supplier_id', $o->supplier_id)?->rank ?? 999)
            ->first()?->supplier_id;
    }

    private function bestScore(Product $product, PriorityRule $rule): ?int
    {
        // Скоринг полноты контента поставщика
        return $product->offers->sortByDesc(function ($offer) {
            $sp = SupplierProduct::where([
                'supplier_id' => $offer->supplier_id,
                'external_id' => $offer->supplier_sku,
            ])->first();

            if (!$sp) return 0;

            $score = 0;
            if (!empty($sp->attributes['description'])) $score += 30;
            if (!empty($sp->attributes['images']))      $score += 25;
            if (!empty($sp->attributes['brand']))       $score += 15;
            if (mb_strlen($sp->name) > 50)              $score += 10;
            if (!empty($sp->attributes['specs']))       $score += 20;

            return $score;
        })->first()?->supplier_id;
    }
}

Применение правил

class ProductSyncService
{
    public function syncProduct(Product $product): void
    {
        $rule = $this->ruleResolver->findApplicableRule($product);

        if (!$rule) return;

        // Цена
        $newPrice = $this->priceResolver->resolve($product, $rule);

        // Наличие
        $newStock = match ($rule->stock_mode) {
            'sum'          => $product->offers->sum('stock'),
            'primary_only' => $this->getPrimaryOffer($product, $rule)?->stock ?? 0,
            'max'          => $product->offers->max('stock'),
        };

        // Контент
        $contentSupplierId = $this->contentResolver->resolveContentSupplier($product, $rule);

        $product->update([
            'price'             => $newPrice,
            'stock'             => $newStock,
            'content_supplier'  => $contentSupplierId,
        ]);

        if ($contentSupplierId) {
            $this->applySupplierContent($product, $contentSupplierId);
        }
    }
}

Разрешение конфликтов при одинаковых ценах

Если несколько поставщиков дают одинаковую цену — порядок предпочтения:

  1. Поставщик с наименьшим сроком доставки (lead_time_days)
  2. Поставщик с большим остатком
  3. Поставщик с наибольшим рейтингом надёжности (процент успешных заказов)
  4. Ранг в таблице supplier_rule_ranks

Интерфейс управления правилами

В административной панели правила должны быть настраиваемы без деплоя:

  • Выбор области действия (глобально, по категории, по бренду)
  • Drag-and-drop ранжирование поставщиков
  • Переключатели стратегий (min/primary/markup)
  • Поле наценки для каждого поставщика
  • Тестирование правила на конкретном товаре

Сроки реализации

  • Схема данных + модели: 1 день
  • PriceResolver + ContentSourceResolver: 1–2 дня
  • ProductSyncService + Observer-триггеры: 1 день
  • Интерфейс управления правилами в админке: 2 дня
  • Тесты + документация для оператора: 1 день

Итого: 6–7 рабочих дней.