Laravel CRM: Додајте технолошки увиди за веб-страници на потенцијални клиенти со API за AI Detector
Веб-страницата на потенцијален клиент често открива повеќе отколку што открива оскудна контакт-форма. Нејзините јавно видливи технолошки избори може да укажат на работа што една агенција разумно би можела да ја предложи: надградба на framework, средување на аналитиката, подобрувања на перформансите или миграција на CMS.
Овој туторијал го додава тој контекст во мал Laravel CRM. Кога се создава потенцијален клиент или експлицитно повторно се скенира, Laravel поставува задача во заднина во редица, го повикува API-то Website Technology Detector, го валидира одговорот на границата на апликацијата и зачувува концизно резиме како „Laravel, Nginx и Google Analytics“ покрај потенцијалниот клиент.
Дизајнот намерно го задржува далечинското откривање надвор од патеката барање-одговор. Бавната upstream услуга треба да го одложи збогатувањето, а не да го спречи продавачот да зачува потенцијален клиент.
Предуслови и облик на проектот
Ќе ви треба PHP 8.3 или понов, поддржана Laravel апликација со нејзината вообичаена инфраструктура за редици и база на податоци, како и модел Lead што содржи јавна URL-адреса на веб-страница. Примерите претпоставуваат дека тој атрибут е именуван website_url.
Интеграцијата има четири одговорности:
- Laravel HTTP клиентот автентицира, применува строги временски ограничувања и ги класифицира upstream неуспесите.
- Мапер на доменот ги претвора откривањата со оценета доверба, верзиите, доказите и пренасочувањата во контролирани податоци на апликацијата.
- Задача во редица го извршува збогатувањето без да го забавува создавањето потенцијални клиенти.
- Базата на податоци ги зачувува читливото резиме и оперативната состојба, но не и целосниот upstream payload.
Ова е намерно скромна архитектура. Посебен микросервис би додал трошоци за распоредување и следење без да помогне на типичен CRM за агенција. Посветен API клиент и мапер ја обезбедуваат потребната граница, а истовремено остануваат лесни за тестирање.
Добијте пристап пред да пишувате код за интеграција
- Отворете ја страницата за регистрација и создајте сметка, или користете ја страницата за најава ако веќе имате сметка.
- Отворете ја страницата на услугата Website Technology Detector.
- Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го таму прикажаниот токен ограничен на услугата.
Повторното генерирање на тој токен го поништува претходно активниот токен. Третирајте го повторното генерирање како ротација на ингеренции: веднаш ажурирајте ја секоја распоредена околина, па рестартирајте ги долготрајните worker процеси за повторно да ја вчитаат конфигурацијата.
Точната API операција е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Таа прифаќа JSON што содржи url. Автентикацијата може да користи Bearer токен, заглавие X-API-Token или параметар за пребарување token. Овој проект го користи Bearer обликот бидејќи ги задржува ингеренциите надвор од URL-адресите, дневниците за пристап и копираните линкови за барања.
Потврдете го пристапот со едно минимално барање:
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"}'
Сега поставете го токенот во датотеката .env на проектот. Никогаш не ја верзионирајте таа датотека и не копирајте вистински токен во fixtures, слики од екран, дневници или изворен код.
WEBSITE_TECH_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_TECH_CONNECT_TIMEOUT=3
WEBSITE_TECH_TIMEOUT=12
Изложете ги тие вредности преку config/services.php. Читањето на env() само од конфигурацијата ја одржува интеграцијата компатибилна со Laravel кешот на конфигурацијата.
'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),
],
Зачувајте корисна состојба, не нетранспарентен одговор
Додадете полиња за резултатот читлив за луѓе, информациите за пренасочување, состојбата на обработка, класификацијата на неуспехот и времето на скенирање. Чувањето на целосниот одговор обично е непотребно и може да ги зголеми ризиците од задржување и поврзаност со шемата.
<?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',
]);
});
}
};
Додадете technology_redirects како cast array и technology_scanned_at како cast datetime во Lead. Извршете php artisan migrate откако ќе го прегледате генерираниот SQL за вашата база на податоци.
Мапирајте го одговорот на границата на доменот
Далечинскиот JSON е недоверлив влез дури и кога доаѓа од услуга што ја контролирате. Маперот подолу бара низа detections, прифаќа записи за откривање со клучеви или во облик на листа, ги игнорира неупотребливите записи, ги ограничува прикажаните докази и зачувува само скаларни детали за пренасочувања.
<?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));
}
}
Вредностите на доверба се нормализираат без оглед дали се претставени како оценки од нула до еден или како проценти. Уште поважно, неочекуваните структури никогаш не протекуваат директно во CRM прегледите или колоните на базата на податоци.
Изградете API клиент свесен за неуспеси
Клиентот ги разликува трајните грешки на повикувачот од upstream состојбите што може повторно да се обидат. Неуспесите при автентикација и валидација не се повторуваат слепо. Мрежните грешки и грешките на серверот добиваат еден краток непосреден повторен обид; подоцнежното опоравување ѝ припаѓа на редицата, каде што чекањето не зафаќа веб-барање.
<?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);
}
}
Збогатувајте потенцијални клиенти во задача во редица
Валидирајте ги URL-адресите пред да зачувате потенцијални клиенти со Laravel правилото url и дозволете само јавни одредишта http или https според политиката на вашиот CRM. Детекторот е наменет за јавни веб-страници; не го користете како пат до внатрешни хостови, loopback адреси или cloud metadata endpoints.
Задачата не извршува трансакција во база на податоци околу мрежниот повик. Таа исто така запишува идентификатори и класификации наместо токени, тела на одговори или целосни URL-адреси.
<?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();
}
}
Испратете ја задачата по успешно внесување на потенцијален клиент, по можност откако ќе се потврди околната трансакција во базата на податоци:
$lead = Lead::create($validated);
DetectLeadTechnologies::dispatch($lead->id)->afterCommit();
За експлицитна акција за повторно скенирање, заштитете ја рутата со автентикација и ограничување на барањата:
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']);
Во поголема апликација, преместете го ова затворање во авторизирана акција на контролер. Ограничувањето на барања штити од случајни повторени кликови, додека авторизацијата мора да осигури дека тековниот корисник смее да го измени тој потенцијален клиент.
Тестирајте успех и неуспех без повикување на услугата
Http::fake() ги прави тестовите детерминистички и ја потврдува вистинската граница на барањето. Користете го конфигурираниот endpoint за подоцнежна промена на URL-адресата да не може тивко да ја поништи проверката.
<?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);
});
Додадете придружни случаи за malformed JSON, отсутна низа detections, празни откривања, HTTP 429, грешки на серверот и исклучоци при поврзување. Тие тестови се поважни од уште еден fixture за успешен пат бидејќи го штитат капацитетот на редицата и семантиката на неуспесите.
Распоредување, набљудливост и вообичаени неуспеси
Поставете ги вредностите за токенот и временските ограничувања во секоја околина за распоредување, извршете миграции, повторно изградете ја конфигурацијата со php artisan config:cache и рестартирајте ги worker процесите на редицата со php artisan queue:restart. Осигурете се дека worker обработува редицата што ја користи задачата.
Следете ги полињата за завршување, неуспех, повторен обид, времетраење и број на откривања од структурираните дневници. Алармирајте при трајни неуспеси на автентикацијата бидејќи тие обично укажуваат на недостасувачки, поништен или застарен токен. Следете го ограничувањето на стапката одделно; тоа може да укажува на нагло зголемување што бара темпирање на редицата или преглед на капацитетот на планот.
Вообичаените проблеми имаат тенденција да бидат оперативни наместо алгоритамски:
- Секое скенирање останува во состојба pending: потврдете дека worker процесот на редицата работи и дека продукцијата не користи ненамерна конекција за редица.
- Непосредни неуспеси на автентикацијата: проверете го токенот ограничен на услугата, освежете ја кешираната конфигурација и рестартирајте ги worker процесите. Ако токенот бил повторно генериран, претходната вредност веќе не работи.
- Неуспеси при валидација: потврдете дека телото е JSON и содржи
urlсо целосна јавна URL-адреса. - Повторено ограничување на стапката: почитувајте го ограниченото доцнење за повторен обид, намалете ја паралелноста и прегледајте го активираниот план наместо да создавате агресивна јамка за повторни обиди.
- Неуспеси поради невалиден payload: споредете го одговорот со официјалната документација, а потоа приспособете го само маперот. Не распространувајте претпоставки за далечинскиот одговор низ контролерите и прегледите.
- Неочекувано бавни worker процеси: задржете ги ограничените временски ограничувања за поврзување и вкупно време и испитајте ја upstream латентноста пред да ги зголемите.
Конечна контролна листа за проверка
- Точниот endpoint на детекторот прима JSON
POSTшто содржиurl. - Bearer токенот доаѓа од конфигурација поддржана од околината и никогаш не влегува во дневници или контрола на изворен код.
- Создавањето потенцијален клиент успева дури и кога збогатувањето не е достапно.
- Успешните скенирања зачувуваат читливо резиме, пренасочувања, состојба на завршување и временска ознака.
- Грешките при автентикација и валидација не се повторуваат.
- Ограничувањата на стапката, мрежните неуспеси и грешките на серверот користат ограничени повторни обиди.
- Тестовите го лажираат секој надворешен повик и ги покриваат и мапирањето и класификацијата на неуспесите.
- Продукциските worker процеси беа рестартирани по распоредувањето на конфигурацијата.
Корисен CRM не само што собира полиња; тој ги претвора достапните сигнали во контекст врз кој луѓето можат да дејствуваат. Со тоа што откривањето останува асинхроно, ингеренциите изолирани, одговорите дефанзивно мапирани и неуспесите видливи, оваа интеграција го додава тој контекст без да го направи процесот за потенцијални клиенти зависен од далечинска услуга. Резимето е видливата функционалност, но внимателно дизајнираната граница е она што ја прави подготвена за продукција.