Laravel CRM: Dodajte uvide u tehnologiju web-stranica potencijalnim klijentima pomoću API-ja AI Detector
Web-mjesto potencijalnog klijenta često otkriva više nego oskudan obrazac za kontakt. Njegovi javno vidljivi tehnološki odabiri mogu ukazati na posao koji bi agencija razumno mogla predložiti: nadogradnju radnog okvira, čišćenje analitike, poboljšanja performansi ili migraciju CMS-a.
Ovaj vodič dodaje taj kontekst malom Laravel CRM-u. Kada se potencijalni klijent stvori ili izričito ponovno skenira, Laravel stavlja pozadinski zadatak u red čekanja, poziva API Website Technology Detector, provjerava odgovor na granici aplikacije i uz potencijalnog klijenta pohranjuje sažetak poput „Laravel, Nginx i Google Analytics”.
Dizajn namjerno zadržava udaljeno otkrivanje izvan putanje zahtjev–odgovor. Spora uzvodna usluga trebala bi odgoditi obogaćivanje, a ne spriječiti prodavača da spremi potencijalnog klijenta.
Preduvjeti i oblik projekta
Trebat će vam PHP 8.3 ili noviji, podržana Laravel aplikacija s uobičajenom infrastrukturom reda čekanja i baze podataka te model Lead koji sadrži javni URL web-mjesta. Primjeri pretpostavljaju da je taj atribut nazvan website_url.
Integracija ima četiri odgovornosti:
- Laravelov HTTP klijent autentificira se, primjenjuje stroga vremenska ograničenja i klasificira uzvodne neuspjehe.
- Mapper domene pretvara otkrivanja s ocjenom pouzdanosti, verzije, dokaze i preusmjeravanja u kontrolirane podatke aplikacije.
- Zadatak reda čekanja obavlja obogaćivanje bez usporavanja stvaranja potencijalnog klijenta.
- Baza podataka pohranjuje čitljiv sažetak i operativno stanje, ali ne i cjelokupni uzvodni sadržaj odgovora.
Ovo je namjerno skromna arhitektura. Zaseban mikroservis dodao bi troškove implementacije i praćenja bez koristi za tipičan agencijski CRM. Namjenski API klijent i mapper pružaju potrebnu granicu, a pritom ostaju jednostavni za testiranje.
Prije pisanja integracijskog koda pribavite pristup
- Otvorite stranicu za registraciju i izradite račun ili upotrijebite stranicu za prijavu ako ga već imate.
- Otvorite stranicu usluge Website Technology Detector.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte ondje prikazani token za tu uslugu.
Ponovno generiranje tog tokena opoziva prethodno aktivni token. Ponovno generiranje tretirajte kao rotaciju vjerodajnice: odmah ažurirajte svako implementirano okruženje, a zatim ponovno pokrenite dugotrajne workere kako bi ponovno učitali konfiguraciju.
Točna API operacija je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Prihvaća JSON koji sadrži url. Autentifikacija može koristiti Bearer token, zaglavlje X-API-Token ili parametar upita token. Ovaj projekt koristi oblik Bearer jer vjerodajnicu zadržava izvan URL-ova, zapisnika pristupa i kopiranih poveznica zahtjeva.
Provjerite pristup jednim minimalnim zahtjevom:
curl --request POST \
--url 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"}'
Sada stavite token u datoteku .env projekta. Nikada ne predajte tu datoteku u repozitorij niti kopirajte stvarni token u fixtureove, snimke zaslona, zapisnike ili izvorni kôd.
WEBSITE_TECH_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_TECH_CONNECT_TIMEOUT=3
WEBSITE_TECH_TIMEOUT=12
Izložite te vrijednosti putem config/services.php. Čitanje env() samo iz konfiguracije održava integraciju kompatibilnom s Laravelovom predmemorijom konfiguracije.
'website_technology_detector' => [
'endpoint' => 'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies',
'token' => env('WEBSITE_TECH_DETECTOR_TOKEN'),
'connect_timeout' => (int) env('WEBSITE_TECH_CONNECT_TIMEOUT', 3),
'timeout' => (int) env('WEBSITE_TECH_TIMEOUT', 12),
],
Pohranite korisno stanje, a ne neproziran odgovor
Dodajte polja za ljudima čitljiv rezultat, informacije o preusmjeravanju, stanje obrade, klasifikaciju neuspjeha i vrijeme skeniranja. Čuvanje cijelog odgovora obično nije potrebno te može povećati rizike zadržavanja podataka i povezanosti sa shemom.
<?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->text('technology_summary')->nullable();
$table->json('technology_redirects')->nullable();
$table->string('technology_scan_status', 24)->default('pending');
$table->string('technology_scan_error', 64)->nullable();
$table->timestamp('technology_scanned_at')->nullable();
});
}
public function down(): void
{
Schema::table('leads', function (Blueprint $table): void {
$table->dropColumn([
'technology_summary',
'technology_redirects',
'technology_scan_status',
'technology_scan_error',
'technology_scanned_at',
]);
});
}
};
Dodajte technology_redirects kao pretvorbu array i technology_scanned_at kao pretvorbu datetime u modelu Lead. Pokrenite php artisan migrate nakon pregleda generiranog SQL-a za svoju bazu podataka.
Mapirajte odgovor na granici domene
Udaljeni JSON je nepouzdan ulaz čak i kada dolazi iz usluge kojom upravljate. Mapper u nastavku zahtijeva polje detections, prihvaća zapise otkrivanja u obliku ključa ili popisa, zanemaruje neupotrebljive unose, ograničava prikazane dokaze i čuva samo skalarne pojedinosti preusmjeravanja.
<?php
namespace App\Domain\Leads;
use UnexpectedValueException;
final readonly class TechnologyReport
{
public function __construct(
public string $summary,
public array $redirects,
public int $detectionCount,
) {}
public static function fromPayload(array $payload): self
{
$detections = $payload['detections'] ?? null;
if (! is_array($detections)) {
throw new UnexpectedValueException('Missing detections array.');
}
$parts = [];
foreach ($detections as $key => $detection) {
if (is_string($detection)) {
$parts[] = trim($detection);
continue;
}
if (! is_array($detection)) {
continue;
}
$name = is_string($detection['name'] ?? null)
? trim($detection['name'])
: (is_string($key) ? trim($key) : '');
if ($name === '') {
continue;
}
$details = [];
if (is_numeric($detection['confidence'] ?? null)) {
$score = (float) $detection['confidence'];
$percentage = $score <= 1 ? $score * 100 : $score;
$details[] = round(max(0, min(100, $percentage))).'% confidence';
}
$versions = self::scalarStrings($detection['versions'] ?? []);
if ($versions !== []) {
$details[] = 'version '.implode(', ', array_slice($versions, 0, 3));
}
$evidence = self::scalarStrings($detection['evidence'] ?? []);
if ($evidence !== []) {
$details[] = 'evidence: '.implode(', ', array_slice($evidence, 0, 3));
}
$parts[] = $details === []
? $name
: $name.' ('.implode('; ', $details).')';
}
$redirects = self::scalarStrings($payload['redirects'] ?? []);
return new self(
summary: $parts === []
? 'No public website technologies were detected.'
: implode('; ', $parts),
redirects: array_slice($redirects, 0, 20),
detectionCount: count($parts),
);
}
private static function scalarStrings(mixed $value): array
{
if (is_scalar($value)) {
$text = trim((string) $value);
return $text === '' ? [] : [mb_substr($text, 0, 300)];
}
if (! is_array($value)) {
return [];
}
$result = [];
array_walk_recursive($value, function (mixed $item) use (&$result): void {
if (is_scalar($item)) {
$text = trim((string) $item);
if ($text !== '') {
$result[] = mb_substr($text, 0, 300);
}
}
});
return array_values(array_unique($result));
}
}
Vrijednosti pouzdanosti normaliziraju se bez obzira na to jesu li predstavljene kao rezultati od nula do jedan ili kao postoci. Još važnije, neočekivane strukture nikada ne prolaze izravno u CRM prikaze ili stupce baze podataka.
Izradite API klijent svjestan neuspjeha
Klijent razlikuje trajne pogreške pozivatelja od uzvodnih uvjeta koji se mogu ponoviti. Neuspjesi autentifikacije i validacije ne pokušavaju se naslijepo ponovno. Mrežne pogreške i pogreške poslužitelja dobivaju jedan kratki trenutačni ponovni pokušaj; kasniji oporavak pripada redu čekanja, gdje čekanje ne zauzima web-zahtjev.
<?php
namespace App\Services;
use App\Domain\Leads\TechnologyReport;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Throwable;
final class WebsiteTechnologyDetector
{
public function detect(string $url): DetectionOutcome
{
$token = config('services.website_technology_detector.token');
if (! is_string($token) || $token === '') {
return DetectionOutcome::failure('configuration', false);
}
for ($attempt = 1; $attempt <= 2; $attempt++) {
try {
$response = Http::acceptJson()
->withToken($token)
->connectTimeout(config('services.website_technology_detector.connect_timeout'))
->timeout(config('services.website_technology_detector.timeout'))
->post(
config('services.website_technology_detector.endpoint'),
['url' => $url],
);
} catch (ConnectionException) {
if ($attempt === 1) {
usleep(200_000);
continue;
}
return DetectionOutcome::failure('connection', true);
}
if (in_array($response->status(), [401, 403], true)) {
return DetectionOutcome::failure('authentication', false, $response->status());
}
if ($response->status() === 422) {
return DetectionOutcome::failure('invalid_request', false, 422);
}
if ($response->status() === 429) {
$delay = filter_var($response->header('Retry-After'), FILTER_VALIDATE_INT);
return DetectionOutcome::failure(
'rate_limited',
true,
429,
max(10, min(300, $delay ?: 60)),
);
}
if ($response->serverError()) {
if ($attempt === 1) {
usleep(500_000);
continue;
}
return DetectionOutcome::failure('upstream', true, $response->status());
}
if ($response->clientError()) {
return DetectionOutcome::failure('request_rejected', false, $response->status());
}
try {
$payload = $response->json();
if (! is_array($payload)) {
return DetectionOutcome::failure('invalid_payload', false);
}
return DetectionOutcome::success(
TechnologyReport::fromPayload($payload),
);
} catch (Throwable) {
return DetectionOutcome::failure('invalid_payload', false);
}
}
return DetectionOutcome::failure('unknown', true);
}
}
final readonly class DetectionOutcome
{
private function __construct(
public ?TechnologyReport $report,
public ?string $error,
public bool $retryable,
public ?int $status,
public int $retryAfter,
) {}
public static function success(TechnologyReport $report): self
{
return new self($report, null, false, 200, 0);
}
public static function failure(
string $error,
bool $retryable,
?int $status = null,
int $retryAfter = 30,
): self {
return new self(null, $error, $retryable, $status, $retryAfter);
}
}
Obogatite potencijalne klijente zadatkom reda čekanja
Prije spremanja potencijalnih klijenata validirajte URL-ove Laravelovim pravilom url i dopustite samo javna odredišta http ili https u skladu s pravilima svojeg CRM-a. Detektor je namijenjen javnim web-mjestima; nemojte ga koristiti kao put prema internim hostovima, loopback adresama ili krajnjim točkama metapodataka u oblaku.
Zadatak ne izvodi transakciju baze podataka oko mrežnog poziva. Također zapisuje identifikatore i klasifikacije, a ne tokene, tijela odgovora ili potpune URL-ove.
<?php
namespace App\Jobs;
use App\Models\Lead;
use App\Services\WebsiteTechnologyDetector;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
final class DetectLeadTechnologies implements ShouldQueue
{
use Queueable;
public int $tries = 4;
public function __construct(public readonly int $leadId) {}
public function handle(WebsiteTechnologyDetector $detector): void
{
$lead = Lead::find($this->leadId);
if (! $lead || ! is_string($lead->website_url) || $lead->website_url === '') {
return;
}
$started = hrtime(true);
$outcome = $detector->detect($lead->website_url);
if ($outcome->report) {
$lead->forceFill([
'technology_summary' => $outcome->report->summary,
'technology_redirects' => $outcome->report->redirects,
'technology_scan_status' => 'complete',
'technology_scan_error' => null,
'technology_scanned_at' => now(),
])->save();
Log::info('Lead technology scan completed', [
'lead_id' => $lead->id,
'detections' => $outcome->report->detectionCount,
'duration_ms' => (int) ((hrtime(true) - $started) / 1_000_000),
]);
return;
}
if ($outcome->retryable && $this->attempts() < $this->tries) {
Log::warning('Lead technology scan will retry', [
'lead_id' => $lead->id,
'failure' => $outcome->error,
'http_status' => $outcome->status,
]);
$this->release($outcome->retryAfter);
return;
}
$lead->forceFill([
'technology_scan_status' => 'failed',
'technology_scan_error' => $outcome->error,
'technology_scanned_at' => now(),
])->save();
}
}
Pošaljite zadatak nakon uspješnog umetanja potencijalnog klijenta, po mogućnosti nakon potvrde okolne transakcije baze podataka:
$lead = Lead::create($validated);
DetectLeadTechnologies::dispatch($lead->id)->afterCommit();
Za izričitu radnju ponovnog skeniranja zaštitite rutu autentifikacijom i ograničavanjem broja zahtjeva:
Route::post('/leads/{lead}/technology-scan', function (Lead $lead) {
$lead->update([
'technology_scan_status' => 'pending',
'technology_scan_error' => null,
]);
DetectLeadTechnologies::dispatch($lead->id)->afterCommit();
return back();
})->middleware(['auth', 'throttle:10,1']);
U većoj aplikaciji premjestite ovo zatvaranje u radnju autoriziranog kontrolera. Ograničavanje zahtjeva štiti od slučajnih ponovljenih klikova, dok autorizacija mora osigurati da trenutačni korisnik smije izmijeniti tog potencijalnog klijenta.
Testirajte uspjeh i neuspjeh bez pozivanja usluge
Http::fake() čini testove determinističkima i provjerava stvarnu granicu zahtjeva. Upotrijebite konfiguriranu krajnju točku kako kasnija promjena URL-a ne bi tiho poništila tvrdnju.
<?php
use App\Jobs\DetectLeadTechnologies;
use App\Models\Lead;
use Illuminate\Support\Facades\Http;
it('stores a readable technology report', function () {
config(['services.website_technology_detector.token' => 'test-token']);
Http::fake([
config('services.website_technology_detector.endpoint') => Http::response([
'detections' => [
[
'name' => 'Laravel',
'confidence' => 0.98,
'versions' => ['11'],
'evidence' => ['response signature'],
],
],
'redirects' => ['https://www.example.com'],
], 200),
]);
$lead = Lead::factory()->create([
'website_url' => 'https://example.com',
]);
app(DetectLeadTechnologies::class, ['leadId' => $lead->id])
->handle(app(\App\Services\WebsiteTechnologyDetector::class));
expect($lead->fresh()->technology_summary)
->toContain('Laravel')
->and($lead->fresh()->technology_scan_status)->toBe('complete');
Http::assertSent(fn ($request) =>
$request->method() === 'POST'
&& $request->url() === config('services.website_technology_detector.endpoint')
&& $request['url'] === 'https://example.com'
&& $request->hasHeader('Authorization', 'Bearer test-token')
);
});
it('classifies authentication failures without retrying', function () {
config(['services.website_technology_detector.token' => 'expired-token']);
Http::fake([
'*' => Http::response([], 401),
]);
$outcome = app(\App\Services\WebsiteTechnologyDetector::class)
->detect('https://example.com');
expect($outcome->error)->toBe('authentication')
->and($outcome->retryable)->toBeFalse();
Http::assertSentCount(1);
});
Dodajte popratne slučajeve za neispravan JSON, nedostajuće polje detections, prazna otkrivanja, HTTP 429, pogreške poslužitelja i iznimke veze. Ti su testovi važniji od još jednog fixturea za sretan put jer štite kapacitet reda čekanja i semantiku neuspjeha.
Implementacija, vidljivost i uobičajeni neuspjesi
Postavite vrijednosti tokena i vremenskog ograničenja u svakom implementacijskom okruženju, pokrenite migracije, ponovno izgradite konfiguraciju pomoću php artisan config:cache i ponovno pokrenite workere reda čekanja pomoću php artisan queue:restart. Osigurajte da worker obrađuje red čekanja koji koristi zadatak.
Pratite polja dovršetka, neuspjeha, ponovnog pokušaja, trajanja i broja otkrivanja iz strukturiranih zapisnika. Upozorite na trajne neuspjehe autentifikacije jer oni obično ukazuju na nedostajući, opozvani ili zastarjeli token. Ograničavanje broja zahtjeva pratite zasebno; ono može ukazivati na nalet koji zahtijeva tempo reda čekanja ili pregled kapaciteta plana.
Uobičajeni problemi obično su operativni, a ne algoritamski:
- Svako skeniranje ostaje na čekanju: potvrdite da worker reda čekanja radi i da produkcija ne koristi nenamjernu vezu reda čekanja.
- Trenutačni neuspjesi autentifikacije: provjerite token za konkretnu uslugu, osvježite predmemoriranu konfiguraciju i ponovno pokrenite workere. Ako je token ponovno generiran, prethodna vrijednost više ne radi.
- Neuspjesi validacije: potvrdite da je tijelo JSON i da sadrži
urls potpunim javnim URL-om. - Ponovljeno ograničavanje broja zahtjeva: poštujte ograničeno odgađanje ponovnog pokušaja, smanjite konkurentnost i pregledajte aktivirani plan umjesto stvaranja agresivne petlje ponovnih pokušaja.
- Neuspjesi zbog neispravnog sadržaja odgovora: usporedite odgovor sa službenom dokumentacijom, a zatim prilagodite samo mapper. Nemojte širiti pretpostavke o udaljenom odgovoru kroz kontrolere i prikaze.
- Neočekivano spori workeri: zadržite ograničena vremenska ograničenja veze i ukupnog trajanja te ispitajte uzvodnu latenciju prije njihova povećanja.
Završni kontrolni popis za provjeru
- Točna krajnja točka detektora prima JSON
POSTkoji sadržiurl. - Bearer token dolazi iz konfiguracije podržane varijablama okruženja i nikada ne ulazi u zapisnike ili kontrolu izvornog koda.
- Stvaranje potencijalnog klijenta uspijeva čak i kada obogaćivanje nije dostupno.
- Uspješna skeniranja pohranjuju čitljiv sažetak, preusmjeravanja, stanje dovršenosti i vremensku oznaku.
- Pogreške autentifikacije i validacije ne pokušavaju se ponovno.
- Ograničenja broja zahtjeva, mrežni neuspjesi i pogreške poslužitelja koriste ograničene ponovne pokušaje.
- Testovi lažiraju svaki vanjski poziv i pokrivaju mapiranje i klasifikaciju neuspjeha.
- Produkcijski workeri ponovno su pokrenuti nakon implementacije konfiguracije.
Korisni CRM ne prikuplja samo polja; dostupne signale pretvara u kontekst prema kojem ljudi mogu djelovati. Zadržavanjem asinkronog otkrivanja, izoliranih vjerodajnica, obrambenog mapiranja odgovora i vidljivih neuspjeha, ova integracija dodaje taj kontekst bez stvaranja ovisnosti tijeka rada s potencijalnim klijentima o udaljenoj usluzi. Sažetak je vidljiva značajka, ali pažljivo dizajnirana granica ono je što je čini spremnom za produkciju.