E-ticaret ve talep üzerine teslimat alanında, bir kullanıcının “Sipariş Ver” butonuna basmasıyla bir endişe döngüsü başlar. Siparişim nerede? Ne zaman arrive edecek? Bu belirsizlik dönüşümleri öldürüyor.
<p>Kasım 2020 ile Temmuz 2021 arasında Viaduct'ta Full-Stack Geliştirici olarak çalıştım ve 5,000'den fazla aktif kullanıcıya hizmet veren bir platform için gerçek zamanlı bir teslimat izleme sistemi tasarladım. Platform, kullanıcıları yerel satıcılarla aynı gün teslimat için bir araya getiriyordu. Benim katılmamdan önce, kullanıcılar teslimat durumuna dair hiçbir görünürlük olmadığı için alışveriş sepetlerini terk ediyor ve siparişleri iptal ediyorlardı.</p>
<p>Bu yazı, izleme sistemini nasıl tasarlayıp oluşturduğumu detaylandırmaktadır — Google Haritalar entegrasyonu, WebSocket destekli canlı güncellemeler, Vue.js reaktif arayüzü ve bunları bir araya getiren Laravel arka uç. Sonuç: %20 artışla ödeme dönüşüm oranı.</p>
<hr/>
<h2>
<a name="the-problem-delivery-opacity-was-killing-revenue" href="#the-problem-delivery-opacity-was-killing-revenue">
</a>
Sorun: Teslimat Şeffaflığı Geliri Yok Ediyordu
</h2>
<p>Viaduct'a katıldığımda, teslimat platformu işlevsel ancak şeffaf değildi. Kullanıcılar sipariş veriyor, onay e-postası alıyor ve bekliyordu. Tek güncellemeler, bir sürücü yola çıktığında "teslimata çıktı" olarak işaretleyen ve geri döndüğünde "teslim edildi" olarak raporlayan manuel güncellemelerdi.</p>
<p>Sonuçları ölçülebilir:</p>
<p><strong>Sepet terk etme:</strong> Kullanıcıların %32'si ödeme sırasında sepetlerini terk etti. Çıkış anketleri, bunun en büyük nedeninin "belirsiz teslimat zamanı" olduğunu gösterdi.</p>
<p><strong>Sipariş sonrası iptaller:</strong> Tamamlanan ödemelerin %15'i, çoğu "teslimat güncellemeleri yok" diyerek, 30 dakika içinde iptal edildi.</p>
<p><strong>Destek yükü:</strong> Günde 200'den fazla "siparişim nerede?" sorusu ile arama yapılmaktaydı — her biri ortalama 4 dakika sürerek, günde toplam 13 kişi-saat zaman kaybı yaşandı.</p>
<p><strong>Sürücü hesap verebilirliği açıkları:</strong> GPS takibi olmadan, teslimat sürelerini doğrulamak, güzergah verimsizliklerini belirlemek ya da anlaşmazlıkları çözmek mümkün değildi.</p>
<hr/>
<h2>
<a name="my-role-fullstack-architect-of-the-tracking-system" href="#my-role-fullstack-architect-of-the-tracking-system">
</a>
Rolüm: İzleme Sisteminin Full-Stack Mimarisi
</h2>
<p>Gerçek zamanlı izleme sisteminin tamamını — Laravel arka uç altyapısı, Google Haritalar API entegrasyonu, WebSocket iletişim katmanı ve Vue.js ön uç izleme arayüzü — tasarladım ve inşa ettim. İlk teknik tasarımdan üretim dağıtımına ve sonrasında optimizasyona kadar her şeyin sahibi oldum.</p>
<hr/>
<h2>
<a name="technical-deep-dive-building-the-realtime-tracking-infrastructure" href="#technical-deep-dive-building-the-realtime-tracking-infrastructure">
</a>
Teknik Derin Dalma: Gerçek Zamanlı İzleme Altyapısını İnşa Etmek
</h2>
<h3>
<a name="1-driver-location-collection-architecture" href="#1-driver-location-collection-architecture">
</a>
1. Sürücü Konum Toplama Mimarisi
</h3>
<p>Gerçek zamanlı takibin temeli güvenilir konum verileridir. Sürücülerin cihazlarında yer alan bir konum raporlama servisi inşa ettim; bu servis, GPS koordinatlarını belirli aralıklarla iletmekteydi.<br/></p>
<div class="highlight js-code-highlight">class DriverLocationController extends Controller
{
public function updateLocation(UpdateLocationRequest $request): JsonResponse
{
$driver = $request->user();
$validated = $request->validated();
<span class="nv">$location</span> <span class="o">=</span> <span class="nc">DriverLocation</span><span class="o">::</span><span class="nf">create</span><span class="p">([</span>
<span class="s1">'driver_id'</span> <span class="o">=></span> <span class="nv">$driver</span><span class="o">-></span><span class="n">id</span><span class="p">,</span>
<span class="s1>'latitude'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'latitude'</span><span class="p">],</span>
<span class="s1>'longitude'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'longitude'</span><span class="p">],</span>
<span class="s1>'accuracy'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'accuracy'</span><span class="p">],</span>
<span class="s1>'speed'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'speed'</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">,</span>
<span class="s1>'heading'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'heading'</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">,</span>
<span class="s1>'recorded_at'</span> <span class="o">=></span> <span class="nc">Carbon</span><span class="o">::</span><span class="nf">createFromTimestamp</span><span class="p">(</span><span class="nv">$validated</span><span class="p">[</span><span class="s1>'timestamp'</span><span class="p">]),</span>
<span class="p">]);</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$activeDelivery</span> <span class="o">=</span> <span class="nv">$driver</span><span class="o">-></span><span class="n">activeDelivery</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">broadcastLocationUpdate</span><span class="p">(</span><span class="nv">$activeDelivery</span><span class="p">,</span> <span class="nv">$location</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">updateEstimatedArrival</span><span class="p">(</span><span class="nv">$activeDelivery</span><span class="p">,</span> <span class="nv">$location</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nf">response</span><span class="p">()</span><span class="o">-></span><span class="nf">json</span><span class="p">([</span><span class="s1>'status'</span> <span class="o">=></span> <span class="s1>'recorded'</span><span class="p">],</span> <span class="mi">200</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">broadcastLocationUpdate</span><span class="p">(</span><span class="kt">Delivery</span> <span class="nv">$delivery</span><span class="p">,</span> <span class="kt">DriverLocation</span> <span class="nv">$location</span><span class="p">):</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nc">Redis</span><span class="o">::</span><span class="nf">publish</span><span class="p">(</span><span class="s2>"delivery:</span><span class="si">{</span><span class="nv">$delivery</span><span class="o">-></span><span class="n">id</span><span class="si">}</span><span class="s2>:location"</span><span class="p">,</span> <span class="nb">json_encode</span><span class="p">([</span>
<span class="s1>'delivery_id'</span> <span class="o">=></span> <span class="nv">$delivery</span><span class="o">-></span><span class="n">id</span><span class="p">,</span>
<span class="s1>'driver'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'latitude'</span> <span class="o">=></span> <span class="nv">$location</span><span class="o">-></span><span class="n">latitude</span><span class="p">,</span>
<span class="s1>'longitude'</span> <span class="o">=></span> <span class="nv">$location</span><span class="o">-></span><span class="n">longitude</span><span class="p">,</span>
<span class="s1>'heading'</span> <span class="o">=></span> <span class="nv">$location</span><span class="o">-></span><span class="n">heading</span><span class="p">,</span>
<span class="p">],</span>
<span class="s1>'timestamp'</span> <span class="o">=></span> <span class="nv">$location</span><span class="o">-></span><span class="n">recorded_at</span><span class="o">-></span><span class="nf">toIso8601String</span><span class="p">(),</span>
<span class="p">]));</span>
<span class="p">}</span>
}
<p>Konum güncellemeleri Redis kanallarında yayınlandı, Laravel arka uç ile WebSocket sunucusu arasında köprü işlevi görerek. Bu, konum alımını (yüksek frekanslı HTTP POST'lar sürücülerden) konum yayınlamasından (müşterilere kalıcı WebSocket bağlantıları) ayırarak, her birinin bağımsız bir şekilde ölçeklenmesini sağladı.</p>
<h3>
<a name="2-google-maps-integration-for-eta-calculation" href="#2-google-maps-integration-for-eta-calculation">
</a>
2. ETA Hesaplama için Google Haritalar Entegrasyonu
</h3>
<p>Doğru ETAlar kritik öneme sahipti. Canlı trafik verileriyle Google Haritalar Yönlendirme API'sini entegre ettim:<br/></p>
<div class="highlight js-code-highlight">class GoogleMapsService
{
public static function getDirections(array $origin, array $destination): ?DirectionsResult
{
$response = Http::get(, [
=> $origin[]}$origin[]},
=> $destination[]}$destination[]},
=> ,
=> ,
=> ,
=> config(),
]);
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$response</span><span class="o">-></span><span class="nf">successful</span><span class="p">()</span> <span class="o">||</span> <span class="nv">$response</span><span class="o">-></span><span class="nf">json</span><span class="p">(</span><span class="s1>'status'</span><span class="p">)</span> <span class="o">!==</span> <span class="s1>'OK'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$leg</span> <span class="o">=</span> <span class="nv">$response</span><span class="o">-></span><span class="nf">json</span><span class="p">(</span><span class="s1>'routes.0.legs.0'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nc">DirectionsResult</span><span class="p">(</span>
<span class="n'>durationMinutes</span><span class="o">:</span> <span class="p">(</span><span class="n'>int</span><span class="p">)</span><span class="nb">ceil</span><span class="p">(</span><span class="nv>$leg</span><span class="p">[</span><span class="s1>'duration_in_traffic'</span><span class="p">][</span><span class="s1>'value'</span><span class="p">]</span> <span class="o">/</span> <span class="mi">60</span><span class="p">),</span>
<span class="n'>distanceKm</span><span class="o">:</span> <span class="nb">round</span><span class="p">(</span><span class="nv>$leg</span><span class="p">[</span><span class="s1>'distance'</span><span class="p">][</span><span class="s1>'value'</span><span class="p">]</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
<span class="n'>polyline</span><span class="o">:</span> <span class="nv>$response</span><span class="o">-></span><span class="nf">json</span><span class="p">(</span><span class="s1>'routes.0.overview_polyline.points'</span><span class="p">),</span>
<span class="p">);</span>
<span class="p">}</span>
}


