Vodiči

Laravel CRM: Enrich Leads with Website Tech Stack Summaries

Laravel CRM: Obogatite potencijalne klijente sažecima tehnološkog stoga web-stranica

Web-mjesto potencijalnog klijenta često otkriva više od slobodnog polja „industrija”. Sažet pregled tehnološkog stoga poput „WordPress 6.5, WooCommerce, Cloudflare” može pomoći agenciji da usmjeri prilike, pripremi uvodne pozive i prepozna vjerojatne potrebe održavanja prije nego što itko otvori razvojne alate.

Korisna verzija ove značajke nije sinkroni API poziv skriven unutar zahtjeva za stranicu. Analiza web-mjesta može biti spora, ograničena stopom zahtjeva ili privremeno nedostupna. Produkcijski CRM trebao bi staviti posao u red čekanja, validirati nesiguran vanjski odgovor na jednoj granici, sačuvati korisne dokaze i prikazati čitljiv rezultat bez narušavanja stabilnosti zaslona potencijalnog klijenta.

Ovaj vodič izrađuje taj potpuni tijek u Laravelu i PHP-u 8.3 ili novijem.

Preduvjeti i dovršena arhitektura

Započnite s postojećim Laravel CRM-om koji sadrži model Lead i stupac website_url. Također su vam potrebni PHP 8.3 ili noviji, konfigurirana baza podataka, funkcionalan Laravel red čekanja i dijeljena predmemorija ako aplikacija radi na više poslužitelja.

Integracija ima pet malih odgovornosti:

  • Kontroler autorizira skeniranje i šalje pozadinski zadatak.
  • Zadatak upravlja stanjem životnog ciklusa i ponašanjem ponovnih pokušaja reda čekanja.
  • Namjenska HTTP usluga poziva detektor s ograničenim vremenskim ograničenjima.
  • Domenski objekt validira detekcije, pouzdanost, dokaze, verzije i informacije o preusmjeravanju.
  • Potencijalni klijent pohranjuje i čitljiv sažetak i strukturirane dokaze za kasniji pregled.

Ovo je namjerno skromna arhitektura. Vanjska granica zaslužuje izolaciju, ali malom CRM-u ne treba sabirnica događaja ni zasebna mikroservisna usluga za jednu operaciju obogaćivanja.

Pribavite pristup prije pisanja integracijskog koda

  1. Izradite račun na https://ai.mihajlo.mk/register ili se prijavite na https://ai.mihajlo.mk/login.
  2. Otvorite stranicu usluge Website Technology Detector.
  3. Odaberite dostupni Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
  4. Otvorite službenu dokumentaciju usluge.
  5. Pronađite ploču Service token i kopirajte token ograničen na uslugu.

Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Dajte prednost zaglavlju Bearer: parametri upita vjerojatnije će se pojaviti u zapisnicima proxyja, preglednika i pristupa.

Ponovno generiranje tokena usluge opoziva prethodno aktivan token. Rotaciju tretirajte kao promjenu pri implementaciji: ažurirajte tajnu aplikacije, ponovno pokrenite workere, provjerite zahtjev i tek tada smatrajte rotaciju dovršenom.

Potvrdite točan endpoint

API poziv je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Prihvaća JSON objekt koji sadrži url. Napravite jedan minimalan zahtjev prije integracije Laravela:

curl --request POST \
  'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
  --header 'Authorization: Bearer YOUR_SERVICE_TOKEN' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://example.com"}'

Ne predajte token u repozitorij. Stavite ga u okruženje implementacije ili u nepraćenu datoteku .env projekta:

WEBSITE_TECH_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_TECH_ENDPOINT=https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
QUEUE_CONNECTION=database

Izložite te vrijednosti kroz config/services.php. Aplikacijski kod treba čitati konfiguraciju, a nikada izravno pozivati env():

<?php

return [
    // Existing services...

    'website_technology_detector' => [
        'token' => env('WEBSITE_TECH_TOKEN'),
        'endpoint' => env(
            'WEBSITE_TECH_ENDPOINT',
            'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies'
        ),
    ],
];

Dodajte stanje obogaćivanja potencijalnim klijentima

Izradite integracijske klase i migraciju:

php artisan make:migration add_technology_enrichment_to_leads_table
php artisan make:job EnrichLeadTechnology
php artisan make:controller LeadTechnologyController
php artisan make:test LeadTechnologyEnrichmentTest

Migracija drži sažetak namijenjen korisnicima odvojenim od strukturiranih podataka detektora. Stupci statusa i pogreške čine asinkrone neuspjehe vidljivima bez preopterećivanja zapisnika aplikacije.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::table('leads', function (Blueprint $table): void {
            $table->string('technology_status', 24)->default('not_scanned');
            $table->text('technology_summary')->nullable();
            $table->json('technology_report')->nullable();
            $table->text('technology_error')->nullable();
            $table->timestamp('technology_checked_at')->nullable();
            $table->index('technology_status');
        });
    }

    public function down(): void
    {
        Schema::table('leads', function (Blueprint $table): void {
            $table->dropIndex(['technology_status']);
            $table->dropColumn([
                'technology_status',
                'technology_summary',
                'technology_report',
                'technology_error',
                'technology_checked_at',
            ]);
        });
    }
};

Dodajte nove atribute postojećoj politici masovnog dodjeljivanja i pretvorbama modela:

protected $fillable = [
    // Existing lead fields...
    'technology_status',
    'technology_summary',
    'technology_report',
    'technology_error',
    'technology_checked_at',
];

protected function casts(): array
{
    return [
        'technology_report' => 'array',
        'technology_checked_at' => 'immutable_datetime',
    ];
}

Validirajte odgovor na granici

Vanjski JSON je nepouzdan ulaz čak i kada je usluga pouzdana. Detektor vraća tehnologije s ocjenom pouzdanosti, zajedno s dokazima, verzijama i informacijama o preusmjeravanju, ali aplikacija i dalje mora odbaciti neispravne oblike te tolerirati neupotrebljive pojedinačne unose.

Izradite app/Domain/Technology/DetectionReport.php:

<?php

namespace App\Domain\Technology;

use UnexpectedValueException;

final readonly class DetectionReport
{
    public function __construct(
        public array $detections,
        public array $redirect,
    ) {}

    public static function fromApi(array $payload): self
    {
        if (!isset($payload['detections']) || !is_array($payload['detections'])) {
            throw new UnexpectedValueException('Missing detections array.');
        }

        $detections = [];

        foreach ($payload['detections'] as $item) {
            if (!is_array($item)) {
                continue;
            }

            $name = $item['name'] ?? null;
            $confidence = $item['confidence'] ?? null;

            if (!is_string($name) || $name === '' || !is_numeric($confidence)) {
                continue;
            }

            $versions = $item['versions'] ?? [];
            $evidence = $item['evidence'] ?? [];

            $detections[] = [
                'name' => $name,
                'confidence' => (float) $confidence,
                'versions' => is_array($versions)
                    ? array_values(array_filter($versions, 'is_string'))
                    : [],
                'evidence' => is_array($evidence) ? $evidence : [],
            ];
        }

        $redirect = $payload['redirect'] ?? [];

        return new self(
            detections: $detections,
            redirect: is_array($redirect) ? $redirect : [],
        );
    }

    public function summary(): string
    {
        if ($this->detections === []) {
            return 'No technologies identified.';
        }

        $items = $this->detections;
        usort(
            $items,
            fn (array $a, array $b): int =>
                $b['confidence'] <=> $a['confidence']
        );

        return implode(', ', array_map(function (array $item): string {
            $version = $item['versions'][0] ?? null;

            return $version ? "{$item['name']} {$version}" : $item['name'];
        }, $items));
    }

    public function toArray(): array
    {
        return [
            'detections' => $this->detections,
            'redirect' => $this->redirect,
        ];
    }
}

Aplikacija ne nameće prag pouzdanosti jer dostavljeni ugovor ne definira koriste li se ocjene na ljestvici od nula do jedan ili u postocima. Sortira ih numerički, zadržava ocjenu i politiku praga ostavlja za dokumentiranu poslovnu odluku.

Izradite ograničeni Laravel HTTP klijent

Izradite app/Services/TechnologyDetector.php. Usluga dopušta jedan neposredan ponovni pokušaj za neuspjele veze i pogreške poslužitelja. Ne pokušava ponovno autentikaciju, validaciju ni odgovore ograničenja stope unutar istog pokušaja workera.

<?php

namespace App\Services;

use App\Domain\Technology\DetectionReport;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Throwable;
use UnexpectedValueException;

final class TechnologyDetectorException extends \RuntimeException
{
    public function __construct(
        string $message,
        public readonly bool $retryable,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}

final class TechnologyDetector
{
    public function detect(string $url): DetectionReport
    {
        $parts = parse_url($url);
        $scheme = strtolower((string) ($parts['scheme'] ?? ''));

        if (
            !filter_var($url, FILTER_VALIDATE_URL)
            || !in_array($scheme, ['http', 'https'], true)
            || empty($parts['host'])
            || isset($parts['user'])
            || isset($parts['pass'])
        ) {
            throw new TechnologyDetectorException(
                'The lead website URL is invalid.',
                false
            );
        }

        $token = (string) config('services.website_technology_detector.token');
        $endpoint = (string) config('services.website_technology_detector.endpoint');

        if ($token === '' || $endpoint === '') {
            throw new TechnologyDetectorException(
                'Detector configuration is missing.',
                false
            );
        }

        for ($attempt = 1; $attempt <= 2; $attempt++) {
            try {
                $response = Http::acceptJson()
                    ->asJson()
                    ->withToken($token)
                    ->connectTimeout(3)
                    ->timeout(12)
                    ->post($endpoint, ['url' => $url]);
            } catch (ConnectionException $exception) {
                if ($attempt === 1) {
                    usleep(250_000);
                    continue;
                }

                throw new TechnologyDetectorException(
                    'Detector connection failed.',
                    true,
                    $exception
                );
            }

            if ($response->successful()) {
                $payload = $response->json();

                if (!is_array($payload)) {
                    throw new TechnologyDetectorException(
                        'Detector returned invalid JSON.',
                        false
                    );
                }

                try {
                    return DetectionReport::fromApi($payload);
                } catch (UnexpectedValueException $exception) {
                    throw new TechnologyDetectorException(
                        'Detector response did not match its contract.',
                        false,
                        $exception
                    );
                }
            }

            if ($response->status() === 429) {
                throw new TechnologyDetectorException(
                    'Detector rate limit reached.',
                    true
                );
            }

            if ($response->serverError()) {
                if ($attempt === 1) {
                    usleep(250_000);
                    continue;
                }

                throw new TechnologyDetectorException(
                    'Detector server error.',
                    true
                );
            }

            throw new TechnologyDetectorException(
                "Detector rejected the request with HTTP {$response->status()}.",
                false
            );
        }

        throw new TechnologyDetectorException('Detector request failed.', true);
    }
}

Tijelo odgovora i vjerodajnica nikada ne ulaze u iznimke ni zapisnike. Pogreška 401, 403 ili druga klijentska pogreška trajna je za taj pokušaj zadatka; slijepo ponovno pokušavanje troši kvotu i prikriva konfiguracijske pogreške.

Pokrenite obogaćivanje izvan putanje zahtjeva

Izradite app/Jobs/EnrichLeadTechnology.php:

<?php

namespace App\Jobs;

use App\Models\Lead;
use App\Services\TechnologyDetector;
use App\Services\TechnologyDetectorException;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Throwable;

final class EnrichLeadTechnology implements ShouldQueue, ShouldBeUnique
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 30;
    public int $uniqueFor = 600;
    public array $backoff = [60, 300];

    public function __construct(public readonly int $leadId) {}

    public function uniqueId(): string
    {
        return (string) $this->leadId;
    }

    public function handle(TechnologyDetector $detector): void
    {
        $lead = Lead::query()->findOrFail($this->leadId);
        $lead->update([
            'technology_status' => 'scanning',
            'technology_error' => null,
        ]);

        try {
            $report = $detector->detect($lead->website_url);
        } catch (TechnologyDetectorException $exception) {
            if ($exception->retryable) {
                Log::warning('Lead technology enrichment will retry.', [
                    'lead_id' => $lead->id,
                    'attempt' => $this->attempts(),
                ]);

                throw $exception;
            }

            $lead->update([
                'technology_status' => 'failed',
                'technology_error' => $exception->getMessage(),
            ]);

            return;
        }

        $lead->update([
            'technology_status' => 'complete',
            'technology_summary' => $report->summary(),
            'technology_report' => $report->toArray(),
            'technology_error' => null,
            'technology_checked_at' => now(),
        ]);
    }

    public function failed(?Throwable $exception): void
    {
        Lead::query()->whereKey($this->leadId)->update([
            'technology_status' => 'failed',
            'technology_error' => 'Technology detection is temporarily unavailable.',
        ]);
    }
}

Neposredni HTTP ponovni pokušaji i ponovni pokušaji reda čekanja služe različitim neuspjesima. Kratak drugi pokušaj ublažava prekinutu vezu; odgođeni pokušaji reda čekanja rješavaju dulje prekide rada i ograničenja stope. Gornja ograničenja ograničavaju trajnu pogrešku poslužitelja na šest HTTP poziva, a trajni 429 na tri.

Autorizirajte i pošaljite skeniranja

Izradite app/Http/Controllers/LeadTechnologyController.php:

<?php

namespace App\Http\Controllers;

use App\Jobs\EnrichLeadTechnology;
use App\Models\Lead;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Gate;

final class LeadTechnologyController extends Controller
{
    public function store(Lead $lead): RedirectResponse
    {
        Gate::authorize('update', $lead);

        $lead->update([
            'technology_status' => 'queued',
            'technology_error' => null,
        ]);

        EnrichLeadTechnology::dispatch($lead->id)
            ->onQueue('integrations')
            ->afterCommit();

        return back()->with('status', 'Technology scan queued.');
    }
}

Registrirajte zaštićenu rutu u routes/web.php:

use App\Http\Controllers\LeadTechnologyController;
use Illuminate\Support\Facades\Route;

Route::post(
    '/leads/{lead}/technology-scan',
    [LeadTechnologyController::class, 'store']
)->middleware(['auth', 'throttle:20,1'])
 ->name('leads.technology.scan');

Uobičajeni međusoftver web pruža CSRF zaštitu. Autorizacija sprječava da jedan račun skenira potencijalne klijente drugog računa, dok ograničavanje sprječava slučajno opetovano klikanje gumba. Jedinstveno zaključavanje zadatka dodaje još jednu zaštitu; upotrijebite dijeljeni upravljački program predmemorije kada više aplikacijskih čvorova obrađuje red čekanja.

Testirajte uspjeh i trajni neuspjeh

Laravelov Http::fake() održava testove determinističkima i dokazuje da nisu potrebni stvarna vjerodajnica ni mrežna veza. Dodajte ove slučajeve u tests/Feature/LeadTechnologyEnrichmentTest.php:

<?php

namespace Tests\Feature;

use App\Jobs\EnrichLeadTechnology;
use App\Models\Lead;
use App\Services\TechnologyDetector;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

final class LeadTechnologyEnrichmentTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        config()->set(
            'services.website_technology_detector.token',
            'test-token'
        );
        config()->set(
            'services.website_technology_detector.endpoint',
            'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies'
        );
    }

    public function test_it_saves_a_readable_summary_and_evidence(): void
    {
        Http::fake([
            'https://ai.mihajlo.mk/*' => Http::response([
                'detections' => [
                    [
                        'name' => 'WordPress',
                        'confidence' => 0.98,
                        'versions' => ['6.5'],
                        'evidence' => ['generator metadata'],
                    ],
                    [
                        'name' => 'Cloudflare',
                        'confidence' => 0.91,
                        'versions' => [],
                        'evidence' => ['response headers'],
                    ],
                ],
                'redirect' => [],
            ], 200),
        ]);

        $lead = Lead::factory()->create([
            'website_url' => 'https://example.com',
        ]);

        (new EnrichLeadTechnology($lead->id))
            ->handle(app(TechnologyDetector::class));

        $lead->refresh();

        $this->assertSame('complete', $lead->technology_status);
        $this->assertSame(
            'WordPress 6.5, Cloudflare',
            $lead->technology_summary
        );
        $this->assertCount(
            2,
            $lead->technology_report['detections']
        );

        Http::assertSent(fn ($request): bool =>
            $request->hasHeader('Authorization', 'Bearer test-token')
            && $request['url'] === 'https://example.com'
        );
    }

    public function test_it_does_not_retry_an_authentication_failure(): void
    {
        Http::fake([
            'https://ai.mihajlo.mk/*' => Http::response([], 401),
        ]);

        $lead = Lead::factory()->create([
            'website_url' => 'https://example.com',
        ]);

        (new EnrichLeadTechnology($lead->id))
            ->handle(app(TechnologyDetector::class));

        $this->assertSame(
            'failed',
            $lead->fresh()->technology_status
        );
        Http::assertSentCount(1);
    }
}

Implementirajte, pratite i otklonite poteškoće

Pokrenite migraciju i testove, predmemorirajte produkcijsku konfiguraciju te pokrenite nadzirani worker:

php artisan migrate --force
php artisan test
php artisan config:cache
php artisan queue:work --queue=integrations --sleep=3 --tries=3 --timeout=40 --max-time=3600

Upotrijebite systemd, Supervisor ili upravitelj procesa koji pruža hosting platforma za ponovno pokretanje workera nakon neuspjeha i tijekom implementacije. Workeri reda čekanja dugotrajni su: nakon promjene tokena ili konfiguracije ponovno izgradite predmemoriju konfiguracije i ponovno ih pokrenite s php artisan queue:restart.

Pratite broj i starost potencijalnih klijenata sa statusima queued, scanning, complete i failed. Postavite upozorenje za rastući red integracija ili trajan porast neuspjeha. Zapisnici trebaju uključivati ID-ove potencijalnih klijenata, pokušaje, kategorije HTTP statusa i trajanja, ali nikada autorizacijska zaglavlja, potpuna tijela odgovora ni tokene usluge.

Uobičajeni obrasci neuspjeha

  • Svaki zahtjev vraća 401 ili 403: potvrdite aktivaciju plana, token ograničen na uslugu i Bearer zaglavlje. Ponovno generirani token odmah poništava prethodno aktivan token.
  • Zahtjevi ostaju u redu čekanja: provjerite obrađuje li worker red integrations i upotrebljava li istu konfiguraciju reda čekanja kao web proces.
  • Promjene konfiguracije nemaju učinka: ponovno izgradite Laravelovu predmemoriju konfiguracije i ponovno pokrenite dugotrajne workere.
  • Odgovori ne prolaze mapiranje: usporedite payload sa službenom dokumentacijom. Ažurirajte samo mapper na granici; kontroleri, zadaci i pohranjeni domenski podaci trebaju ostati stabilni.
  • Ograničenja stope se ponavljaju: smanjite učestalost skeniranja, izbjegavajte automatska ponovna skeniranja pri svakoj izmjeni potencijalnog klijenta i pregledajte aktivni plan umjesto povećavanja neposrednih ponovnih pokušaja.
  • Duplicirani zadaci pojavljuju se na više poslužitelja: konfigurirajte upravljački program predmemorije koji dijele svi čvorovi kako bi ShouldBeUnique upotrebljavao jedno spremište zaključavanja.

Završni kontrolni popis za provjeru

  • Token postoji samo u konfiguraciji podržanoj okruženjem i pohrani tajni.
  • Aplikacija šalje zahtjeve POST na točan endpoint detektora s JSON-om url.
  • Samo ovlašteni korisnici CRM-a mogu staviti skeniranja u red čekanja.
  • Vremenska ograničenja HTTP veze i odgovora su ograničena.
  • Neuspjesi autentikacije i validacije ne pokušavaju se slijepo ponovno.
  • Ograničenja stope i privremeni neuspjesi poslužitelja prolaze kroz ograničene ponovne pokušaje reda čekanja.
  • Detekcije, pouzdanost, verzije, dokazi i informacije o preusmjeravanju validiraju se prije pohrane.
  • Zaslon potencijalnog klijenta može prikazati technology_summary bez razumijevanja vanjskog payloada.
  • Workeri se ponovno pokreću nakon implementacija i rotacije tajni.
  • Testovi koriste Http::fake() i ne sadrže produkcijsku vjerodajnicu.

Trajna pouka veća je od detekcije tehnologije: obogaćivanje treba unaprijediti CRM bez toga da postane nova točka neuspjeha. Zadržite odgovor dobavljača na granici, životni ciklus reda čekanja eksplicitnim, a rezultat za potencijalnog klijenta ugodno jednostavnim. Tada „WordPress 6.5, Cloudflare” postaje koristan operativni kontekst, a ne još jedno nepouzdano polje oblikovano kao API.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.