Laravel CRM: Збогатете ги потенцијалните клиенти со резимеа на технолошкиот стек на веб-страниците
Веб-страницата на потенцијалниот клиент често открива повеќе отколку слободно внесено поле „индустрија“. Краток преглед на стекот, како „WordPress 6.5, WooCommerce, Cloudflare“, може да ѝ помогне на агенцијата да ги насочува можностите, да се подготви за воведни разговори и да идентификува веројатна работа за одржување пред некој воопшто да отвори развојни алатки.
Корисната верзија на оваа функционалност не е синхрон API-повик скриен во барање за страница. Анализата на веб-страница може да биде бавна, ограничена по стапка или привремено недостапна. Продукциски CRM треба да ја стави работата во ред, да валидира неизвесен надворешен одговор на една граница, да зачува корисни докази и да прикаже читлив резултат без да го направи екранот за потенцијалниот клиент кревок.
Ова упатство го гради целиот тој тек во Laravel и PHP 8.3 или понова верзија.
Предуслови и завршената архитектура
Почнете со постоечки Laravel CRM што содржи модел Lead и колона website_url. Потребни ви се и PHP 8.3 или понова верзија, конфигурирана база на податоци, функционален Laravel ред и споделен кеш ако апликацијата работи на повеќе сервери.
Интеграцијата има пет мали одговорности:
- Контролерот авторизира скенирање и испраќа задача во заднина.
- Задачата управува со состојбата на животниот циклус и однесувањето при повторување во редот.
- Наменска HTTP-услуга го повикува детекторот со ограничени истекувања на време.
- Доменски објект ги валидира детекциите, довербата, доказите, верзиите и информациите за пренасочување.
- Потенцијалниот клиент чува и читлив преглед и структурирани докази за подоцнежна проверка.
Ова е намерно скромна архитектура. Надворешната граница заслужува изолација, но на мал CRM не му треба магистрала за настани или посебен микросервис за една операција на збогатување.
Добијте пристап пред да пишувате код за интеграција
- Создадете сметка на https://ai.mihajlo.mk/register, или најавете се на https://ai.mihajlo.mk/login.
- Отворете ја страницата на услугата Website Technology Detector.
- Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го токенот ограничен на услугата.
Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување token. Претпочитајте го заглавието Bearer: параметрите за пребарување поверојатно се појавуваат во дневниците на проксијата, прелистувачот и пристапот.
Повторното генерирање на токенот за услугата го отповикува претходно активниот токен. Третирајте ја ротацијата како промена при распоредување: ажурирајте ја тајната на апликацијата, рестартирајте ги worker-процесите, потврдете едно барање и дури тогаш сметајте ја ротацијата за завршена.
Потврдете ја точната крајна точка
API-повикот е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Прифаќа JSON-објект што содржи url. Направете едно минимално барање пред да го интегрирате Laravel:
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"}'
Не го зачувувајте токенот во комити. Ставете го во околината за распоредување или во непратената .env датотека на проектот:
WEBSITE_TECH_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_TECH_ENDPOINT=https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
QUEUE_CONNECTION=database
Изложете ги тие вредности преку config/services.php. Кодот на апликацијата треба да чита конфигурација, а никогаш директно да не повикува 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'
),
],
];
Додајте состојба на збогатување кај потенцијалните клиенти
Создадете ги класите за интеграција и миграција:
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
Миграцијата го чува прегледот наменет за луѓе одвоен од структурираните податоци на детекторот. Колоните за статус и грешка ги прават асинхроните неуспеси видливи без преоптоварување на дневниците на апликацијата.
<?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',
]);
});
}
};
Додајте ги новите атрибути во постоечката политика за масовно доделување и преобразби на моделот:
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',
];
}
Валидирајте го одговорот на границата
Надворешниот JSON е недоверлив влез дури и кога услугата е сигурна. Детекторот враќа технологии со оценка на доверба, заедно со докази, верзии и информации за пренасочување, но апликацијата сепак мора да отфрли неправилни структури и да толерира неупотребливи поединечни записи.
Создадете 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,
];
}
}
Апликацијата не наметнува праг на доверба бидејќи доставениот договор не дефинира дали оценките користат скала од нула до еден или процентуална скала. Таа ги подредува нумерички, ја задржува оценката и политиката за праг ја остава за документирана деловна одлука.
Изградете ограничен Laravel HTTP-клиент
Создадете app/Services/TechnologyDetector.php. Услугата дозволува едно непосредно повторување за неуспеси во поврзувањето и грешки на серверот. Не повторува одговори за автентикација, валидација или ограничување на стапката во рамките на истиот обид на worker-процесот.
<?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);
}
}
Телото на одговорот и акредитивот никогаш не влегуваат во исклучоци или дневници. 401, 403 или друга грешка од клиентот е трајна за тој обид на задачата; слепото повторување троши квота и ги прикрива грешките во конфигурацијата.
Извршувајте збогатување надвор од патеката на барањето
Создадете 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.',
]);
}
}
Непосредните HTTP-повторувања и повторувањата во редот служат за различни неуспеси. Краток втор обид ублажува прекината врска; одложените обиди во редот се справуваат со подолги прекини и ограничувања на стапката. Горенаведените ограничувања ограничуваат траен неуспех на серверот на шест HTTP-повици, а траен 429 на три.
Авторизирајте и испраќајте скенирања
Создадете 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.');
}
}
Регистрирајте ја заштитената рута во 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');
Стандардниот middleware web обезбедува CSRF-заштита. Авторизацијата спречува една сметка да ги скенира потенцијалните клиенти на друга сметка, додека ограничувањето ја намалува можноста за случајно постојано притискање на копчето. Заклучувањето на единствената задача додава уште една заштита; користете споделен двигател за кеш кога повеќе јазли на апликацијата го трошат редот.
Тестирајте успех и траен неуспех
Laravel-овиот Http::fake() ги прави тестовите детерминистички и докажува дека не се потребни вистински акредитиви или мрежна врска. Додајте ги овие случаи во 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);
}
}
Распоредете, набљудувајте и решавајте проблеми
Извршете ги миграцијата и тестовите, кеширајте ја продукциската конфигурација и стартувајте надгледуван 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
Користете systemd, Supervisor или менаџерот на процеси што го обезбедува хостинг-платформата за да го рестартирате worker-процесот по неуспех и при распоредување. Worker-процесите на редот се долготрајни: по промена на токенот или конфигурацијата, повторно изградете го кешот за конфигурација и рестартирајте ги со php artisan queue:restart.
Следете го бројот и староста на потенцијалните клиенти со статус queued, scanning, complete и failed. Поставете предупредување за растечки ред за интеграција или трајно зголемување на неуспесите. Дневниците треба да вклучуваат ID на потенцијалниот клиент, обиди, категории на HTTP-статус и траења, но никогаш заглавија за авторизација, целосни тела на одговори или токени за услугата.
Вообичаени шеми на неуспех
- Секое барање враќа 401 или 403: потврдете ја активацијата на планот, токенот ограничен на услугата и заглавието Bearer. Повторно генериран токен веднаш го поништува претходно активниот токен.
- Барањата остануваат во ред: проверете дали worker-процесот го троши редот
integrationsи ја користи истата конфигурација на редот како веб-процесот. - Промените во конфигурацијата немаат ефект: повторно изградете го кешот за конфигурација на Laravel и рестартирајте ги долготрајните worker-процеси.
- Одговорите не успеваат при мапирање: споредете го товарот со официјалната документација. Ажурирајте го само маперот на границата; контролерите, задачите и зачуваните доменски податоци треба да останат стабилни.
- Ограничувањата на стапката се повторуваат: намалете ја фреквенцијата на скенирање, избегнувајте автоматско повторно скенирање при секое уредување на потенцијалниот клиент и прегледајте го активниот план наместо да ги зголемувате непосредните повторувања.
- Дупликати на задачи се појавуваат меѓу сервери: конфигурирајте двигател за кеш споделен од секој јазол, за
ShouldBeUniqueда користи едно складиште за заклучување.
Конечна контролна листа за потврда
- Токенот постои само во конфигурација поддржана од околината и складиште за тајни.
- Апликацијата испраќа барања
POSTдо точната крајна точка на детекторот со JSONurl. - Само авторизирани CRM-корисници можат да ставаат скенирања во ред.
- Истекувањата на времето за HTTP-поврзување и одговор се ограничени.
- Неуспесите при автентикација и валидација не се повторуваат слепо.
- Ограничувањата на стапката и привремените неуспеси на серверот минуваат низ ограничени повторувања во редот.
- Детекциите, довербата, верзиите, доказите и информациите за пренасочување се валидираат пред складирање.
- Екранот за потенцијалниот клиент може да прикаже
technology_summaryбез да го разбира надворешниот товар. - Worker-процесите се рестартираат по распоредувања и ротација на тајни.
- Тестовите користат
Http::fake()и не содржат продукциски акредитиви.
Трајната поука е поголема од детекцијата на технологии: збогатувањето треба да подобри CRM без да стане нова точка на неуспех. Чувајте го одговорот на добавувачот на границата, животниот циклус на редот експлицитен, а резултатот наменет за потенцијалниот клиент пријатно едноставен. Тогаш „WordPress 6.5, Cloudflare“ станува корисен оперативен контекст наместо уште едно ненадежно поле во облик на API.