Логистический стартап в 2023: что делать, чтобы не пожалеть

Как запустить логистический стартап на примере pet-проекта: какие технологии использовать, как настроить маршрутизацию и выстроить процессы.

2К открытий2К показов

Логистика за последние 5 лет растет быстрыми темпами и на этом останавливаться не собирается. Это как раз та категория, которая сильно модернизировалась во времена пандемии, когда доставка до дома стала жизненно необходимой историей.

Давайте рассмотрим, какой же рынок грузоперевозок сейчас в России. Официальные источники утверждают, что объем рынка в 2019 году достиг 900 млрд руб., они, конечно же не учитывают объем «серого» рынка, когда оплата происходит наличными. Хотя, введение самозанятости у водителей сильно обеляет рынок, но оплата «переводом на карту водителю» остается существенной и достигает 30%-40% от всего рынка.

Запуская свой стартап, мы ориентировались на рынок last mile, который, считается, что занимает до 70% от рынка грузоперевозок. Как принято говорить, это большой красный океан, в котором хотелось найти голубой ручеек.

Особенности запуска проекта

Для начала пришлось разобраться, как работает логистика на стороне клиента. Оговорюсь сразу, мы пошли в сегмент b2b и не хотели заниматься квартирными переездами, где пришлось бы конкурировать с небезызвестными крупными игроками, которые имеют огромные бюджеты на продвижение.

Итак, как же работали логисты в крупных и не очень компаниях. Мы назвали это «метод коленки», когда каждый вечер, в 18-00 садятся два менеджера-логиста и начинают раскидывать адреса доставки в excel так, чтобы успеть развести всем клиентам. Это упражнение они делают исходя из своего опыта, ни о каких технологиях и речи нет. Уверена, часть компаний так и остались в этой мезозойской эре.

На то есть причины: невозможно внедрять что-то новое в логистике, когда у тебя складская программа/бухгалтерия/crm написаны 10 лет назад группой программистов на аутсорсе, которых уже нет. И никто уже не знает, как работает их API, а программист на саппорте уныло разводит руками.

Для нас такой процесс оказался вызовом, и мы предложили маршрутизацию.

Выбор солвера для маршрутизации

Сейчас на рынке существует множество универсальных опенсорсных солверов Google OR-Tools, Choco-solver. В каждом есть функционал, который можно настроить для применения различных бизнес правил: вместимость авто, время подачи/загрузки, в нашем случае, даже была задача от клиента, что точку в Кремле можно было давать только одному водителю, т.к он уже прошел миллион проверок и по понятным причинам менять каждый день водителя никто не хотел. Ограничения от клиентов могут быть абсолютно не типовые.

Итак, свой выбор мы остановили на Google OR-tools, хотя был большой интерес еще к платному Gurobi, но на этапе стартапа сначала ты экономишь и тестируешь. Нам не хотелось лезть в промышленные солверы за большие деньги, тк был план разобраться сначала с тем, что дают бесплатно.

Чтобы «приготовить» OR-tools так, как нужно, пришлось разбить первоначальную задачу на подзадачи, чтобы соответствовать опциям и ограничениям от заказчиков. Процесс был не супер быстрый, солвер частенько тупил и выдавал «решение не найдено».  Как мы с этим справлялись?

Идея такая, что сначала мы получаем первое решение, а затем итеративно гоняем на ней модель, иногда возвращая на место ограничения, которых не было в первой итерации, и за счет этого находим хорошее решение, которое сразу бы не нашлось

			private def getFirstSolution(data: DRSourceDataHelper): Option[AssignmentWithSourceObjects] = {
    val copy = data.copy()
    val reducers = generateLimitReducers(copy)
    val seq = mergeReducerSequence(reducers, 3)
    runSolver(data, "as is")
      .orElse(trySolveWithReducerSeq(copy, seq))
      .orElse({ data.withSimplifiedCarCost(runSolver(data, "simplified")) })
      .orElse(runSolver(data, "synthetic all-in-one", Some(generateIndicesAllInOne(data))))
      .orElse(runSolver(data, "synthetic mappingwise-stupid", Some(generateIndicesMappingStupid(data))))
      .orElse(runSolver(data, "synthetic mappingwise-stupid-fifth", Some(generateIndicesMappingStupid(data, data.deliveries.size / 5))))
      .orElse(runSolver(data, "synthetic mappingwise-stupid-half", Some(generateIndicesMappingStupid(data, data.deliveries.size / 2))))
      .orElse(generateIndicesWithoutWeight(data).flatMap(x => runSolver(data, "alter-based no-weight", Some(x))))
      .orElse(generateIndicesWithoutTimeSlots(data).flatMap(x => runSolver(data, "alter-based no-time-slots", Some(x))))
      .orElse(generateIndicesWithoutTimeLimit(data).flatMap(x => runSolver(data, "alter-based no-time-limit", Some(x))))
      .orElse(generateIndicesWithoutFlags(data).flatMap(x => runSolver(data, "alter-based no-flags", Some(x))))
      .orElse(runSolver(data, "indices mappingwise-stupid-even", Some(generateIndicesMappingStupidEvenlyDistributed(data))))
  }

  private def runSolver(data: DRSourceDataHelper, datasetName: String, prevAssignment: Option[Array[Array[Long]]] = None): Option[AssignmentWithSourceObjects] = {
    val (indexManager, routingModel, solver) = DeliveryRoutingModelBuilder.createModel(data)

    val searchParameters = main.defaultRoutingSearchParameters.toBuilder
      .setTimeLimit(Duration.newBuilder.setSeconds(60).build)
      .setSolutionLimit(10)
      .setFirstSolutionStrategy(FirstSolutionStrategy.Value.AUTOMATIC)
      .setLogSearch(true)
      .setUseDepthFirstSearch(true)
      .setUseFullPropagation(true)
      .setLnsTimeLimit(Duration.newBuilder.setSeconds(10).build)
      .setLocalSearchMetaheuristic(LocalSearchMetaheuristic.Value.GREEDY_DESCENT)
      .setLocalSearchOperators(LocalSearchNeighborhoodOperators.getDefaultInstance.toBuilder.build())
      .build

    val solution = prevAssignment match {
      case None => routingModel.solveWithParameters(searchParameters)
      case Some(prevIndices) =>
        val parsedAssignemnt = routingModel.readAssignmentFromRoutes(prevIndices, false)
        routingModel.solveFromAssignmentWithParameters(parsedAssignemnt, searchParameters)
    }

    solution.map(solution => Some(AssignmentWithSourceObjects(solution, indexManager, routingModel, solver)))
  }
		

В конечном счете мы смогли докрутить все параметры так, чтобы работало как часы.

Упаковка в продукт 

Маршрутизация отдельно от всего, может быть прекрасным продуктом, но, если клиент не может это «поженить» со своими внутренними программами, если водители игнорируют использование приложения, нужно придумывать кастомные интеграции или делать продукт настолько полезным, что клиент сам побежит менять свою внутрянку так, чтобы можно было нормально синхронизироваться со всем остальным. Даже если это будет болезненно.

Как заставить клиента менять привычный метод «на коленке»? Мы добавили водителей.

Представим себе логиста, его задача не просто построить маршрут и сделать так, чтобы доставка доехала до клиента, но и решать кучу сопутствующих проблем, таких как: где найти авто, когда пиковая нагрузка и своих не хватает, как заменить и перегрузить груз, если поломка, это все «оффлайн» проблемы, которые можно решать имея Uber модель в перевозках.

Таким образом, мы предложили full package услуг, маршрутизация плюс авто, логисту остается нажать несколько кнопок, маршрут построится, водитель найдется, отчеты придут, форс мажоры решатся.

Какие технологии мы использовали для создания такого продукта?

UI построили с помощью Typescript и кастомного UI фреймворка; этот подход упростил разработку мобильного приложения для водителей с использованием Apache Cordova. Бэкэнд реализовали на Scala и NodeJS (последний для более микросервисного подхода).

У нас используется множество внешних сервисов, такие как Firebase (для отправки push-уведомлений в Android-приложение); Контур помогает надежно обмениваться юридическими документами с B2B клиентами, Voximplant для организации управления звонками и подмены телефонных номеров; Smsaero для отправки уведомлений о выполнении заказов, с Банком 131 сделана API интеграция по массовым автоматическим выплатам исполнителям.

Все это позволяет нам к минимуму сводить человеческий труд.

Про водителей или без чего не будет успеха

Бизнесы по модели маркетплейса это всегда весы, когда нужно искать баланс. Мало заказов — водители моментально уходят и перестают использовать предложение, много заказов- водители находят в других приложения более выгодные/удобные по геолокации заказы и могут в последний момент отказываться от уже взятого.

Работа с водителями это большой продукт, которым нужно заниматься, продумывать систему обучения, мотивации и штрафов для водителей. Срыв поставки для B2B клиента это, как правило, потеря денег, репутации и клиент скорее всего к вам не вернется. Поэтому, от того, насколько лояльные водители будут работать в вашем сервисе зависит 80% успеха.

В завершении хочется сказать, что, собирая команду на логистический стартап, важно, чтобы у вас были не только айтишники, но и ребята с опытом в хардкорных классических ТК (транспортных компаниях).

Мы когда начинали, считали, что опыт таких сотрудников только помешает развитию бизнеса и мы будем делать те же ошибки, что и обычные компании без какого-либо софта, с диспетчером на телефоне. Оказалось, не совсем так.

В грузоперевозках работа с автопарками ГАЗелей в нашей модели, была несущественная, мы работаем с водителями напрямую. Это стратегическое решение, которое помогает держать цены ниже рынка. Когда у тебя 2000 водителей важно найти ключик, который поможет выяснить мотивацию, и, она, у ГАЗелистов не только денежная.

Что именно ими движет, как раз расскажут те, кто работал диспетчерами в компаниях со своим парком, с привлеченным транспортом, кто умеет сделать дисциплину водителей частью бизнеса процесса в компании. Так что, собирайте людей с разными скилами, общайтесь больше с клиентами и, возможно, вы сможете занять свою нишу в грузоперевозках.

Следите за новыми постами
Следите за новыми постами по любимым темам
2К открытий2К показов