Update: 2026-07-02 05:27:04
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* ai_formula_solver.php
|
||||
* مكتشف خوارزميات المنافسين (AI Competitor Formula Solver)
|
||||
* يستخدم الانحدار الخطي المتعدد (Multiple Linear Regression) لاكتشاف
|
||||
* أجرة فتح العداد، وسعر الكيلومتر، وسعر الدقيقة لكل تطبيق منافس.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../functions.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
} catch (Exception $e) {
|
||||
die("Database connection failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
echo "Starting AI Formula Discovery Engine...\n";
|
||||
|
||||
// نجلب التطبيقات التي لديها بيانات (مسافة ووقت وسعر)
|
||||
$sqlApps = "SELECT DISTINCT competitor_name, country_code
|
||||
FROM scraped_competitor_prices
|
||||
WHERE distance_km > 0 AND duration_min > 0 AND price_amount > 0";
|
||||
$stmtApps = $con->query($sqlApps);
|
||||
$apps = $stmtApps->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($apps)) {
|
||||
echo "No sufficient data (Distance/Duration) found to perform regression.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
// دالة لحل نظام معادلات خطية (Gaussian Elimination)
|
||||
function solveLinearSystem($A, $B) {
|
||||
$n = count($A);
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
// Search for maximum in this column
|
||||
$maxEl = abs($A[$i][$i]);
|
||||
$maxRow = $i;
|
||||
for ($k = $i + 1; $k < $n; $k++) {
|
||||
if (abs($A[$k][$i]) > $maxEl) {
|
||||
$maxEl = abs($A[$k][$i]);
|
||||
$maxRow = $k;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap maximum row with current row
|
||||
for ($k = $i; $k < $n; $k++) {
|
||||
$tmp = $A[$maxRow][$k];
|
||||
$A[$maxRow][$k] = $A[$i][$k];
|
||||
$A[$i][$k] = $tmp;
|
||||
}
|
||||
$tmp = $B[$maxRow];
|
||||
$B[$maxRow] = $B[$i];
|
||||
$B[$i] = $tmp;
|
||||
|
||||
// Make all rows below this one 0 in current column
|
||||
for ($k = $i + 1; $k < $n; $k++) {
|
||||
if ($A[$i][$i] == 0) continue;
|
||||
$c = -$A[$k][$i] / $A[$i][$i];
|
||||
for ($j = $i; $j < $n; $j++) {
|
||||
if ($i == $j) {
|
||||
$A[$k][$j] = 0;
|
||||
} else {
|
||||
$A[$k][$j] += $c * $A[$i][$j];
|
||||
}
|
||||
}
|
||||
$B[$k] += $c * $B[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// Solve equation Ax=b for an upper triangular matrix A
|
||||
$x = array_fill(0, $n, 0);
|
||||
for ($i = $n - 1; $i >= 0; $i--) {
|
||||
if ($A[$i][$i] == 0) continue;
|
||||
$x[$i] = $B[$i] / $A[$i][$i];
|
||||
for ($k = $i - 1; $k >= 0; $k--) {
|
||||
$B[$k] -= $A[$k][$i] * $x[$i];
|
||||
}
|
||||
}
|
||||
return $x;
|
||||
}
|
||||
|
||||
foreach ($apps as $app) {
|
||||
$competitor = $app['competitor_name'];
|
||||
$countryCode = $app['country_code'];
|
||||
|
||||
echo "Analyzing: $competitor ($countryCode)...\n";
|
||||
|
||||
// سحب أحدث 5000 رحلة لتكوين نموذج رياضي دقيق
|
||||
$sqlData = "SELECT distance_km, duration_min, price_amount
|
||||
FROM scraped_competitor_prices
|
||||
WHERE competitor_name = :comp
|
||||
AND country_code = :country
|
||||
AND distance_km > 0 AND duration_min > 0 AND price_amount > 0
|
||||
ORDER BY id DESC LIMIT 5000";
|
||||
$stmtData = $con->prepare($sqlData);
|
||||
$stmtData->execute([':comp' => $competitor, ':country' => $countryCode]);
|
||||
$samples = $stmtData->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$N = count($samples);
|
||||
if ($N < 10) {
|
||||
echo " -> Not enough samples ($N). Skipping.\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// بناء مصفوفات Least Squares (X^T X) * Beta = (X^T Y)
|
||||
// Beta = [Base_Fare, KM_Price, Min_Price]
|
||||
$sum_x1 = 0; $sum_x2 = 0; $sum_y = 0;
|
||||
$sum_x1_sq = 0; $sum_x2_sq = 0; $sum_x1_x2 = 0;
|
||||
$sum_x1_y = 0; $sum_x2_y = 0;
|
||||
|
||||
foreach ($samples as $s) {
|
||||
$x1 = (float)$s['distance_km'];
|
||||
$x2 = (float)$s['duration_min'];
|
||||
$y = (float)$s['price_amount'];
|
||||
|
||||
$sum_x1 += $x1;
|
||||
$sum_x2 += $x2;
|
||||
$sum_y += $y;
|
||||
|
||||
$sum_x1_sq += ($x1 * $x1);
|
||||
$sum_x2_sq += ($x2 * $x2);
|
||||
$sum_x1_x2 += ($x1 * $x2);
|
||||
|
||||
$sum_x1_y += ($x1 * $y);
|
||||
$sum_x2_y += ($x2 * $y);
|
||||
}
|
||||
|
||||
$matrixA = [
|
||||
[$N, $sum_x1, $sum_x2],
|
||||
[$sum_x1, $sum_x1_sq, $sum_x1_x2],
|
||||
[$sum_x2, $sum_x1_x2, $sum_x2_sq]
|
||||
];
|
||||
|
||||
$matrixB = [
|
||||
$sum_y,
|
||||
$sum_x1_y,
|
||||
$sum_x2_y
|
||||
];
|
||||
|
||||
// حل المصفوفة
|
||||
try {
|
||||
$beta = solveLinearSystem($matrixA, $matrixB);
|
||||
|
||||
$baseFare = round(max(0, $beta[0]), 3); // Base fare cannot be negative
|
||||
$kmPrice = round(max(0, $beta[1]), 3);
|
||||
$minPrice = round(max(0, $beta[2]), 3);
|
||||
|
||||
echo " -> [DISCOVERED] Base Fare: $baseFare, KM: $kmPrice, Min: $minPrice\n";
|
||||
|
||||
// حفظ في جدول المعادلات السرية
|
||||
$sqlUpsert = "INSERT INTO competitor_secret_formulas
|
||||
(competitor_name, country_code, base_fare, price_per_km, price_per_min, sample_size)
|
||||
VALUES (:comp, :country, :base, :km, :min, :size)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
base_fare = :base, price_per_km = :km, price_per_min = :min, sample_size = :size, last_updated = NOW()";
|
||||
|
||||
$stmtUp = $con->prepare($sqlUpsert);
|
||||
$stmtUp->execute([
|
||||
':comp' => $competitor,
|
||||
':country' => $countryCode,
|
||||
':base' => $baseFare,
|
||||
':km' => $kmPrice,
|
||||
':min' => $minPrice,
|
||||
':size' => $N
|
||||
]);
|
||||
|
||||
echo " -> Saved successfully.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " -> Error solving matrix: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done.\n";
|
||||
?>
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* cron_ai_engine.php
|
||||
* المحرك الرئيسي للذكاء الاصطناعي (AI Engine)
|
||||
* يتم تشغيله كـ Cron Job كل 30 دقيقة أو ساعة لتقليل الضغط على السيرفر.
|
||||
*
|
||||
* يدمج 3 وحدات (Modules) ذكية:
|
||||
* 1. AI Pricing (Total Price Math): تعديل جدول kazan ليكون السعر الإجمالي أرخص بـ 6.5% من المنافس الأقوى بدقة.
|
||||
* 2. AI Dispatch: تحديد مناطق الذروة وتوجيه السائقين إليها.
|
||||
* 3. AI Retention: اصطياد الركاب الخاملين.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../functions.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
$redis = getRedisConnection();
|
||||
} catch (Exception $e) {
|
||||
die("Connection failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
echo "Starting Siro AI Engine...\n";
|
||||
|
||||
// نسبة الخصم المستهدفة (6.5% من إجمالي سعر الرحلة)
|
||||
$targetMargin = 0.065;
|
||||
|
||||
// ==========================================
|
||||
// 1. وحدة التسعير الديناميكي بناءً على السعر الإجمالي
|
||||
// ==========================================
|
||||
echo "1. Running Smart Pricing Module (Total Price Formula)...\n";
|
||||
try {
|
||||
$sql = "SELECT country_code,
|
||||
AVG(price_per_km) as avg_price_km,
|
||||
MIN(price_per_km) as min_price_km
|
||||
FROM scraped_competitor_prices
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
|
||||
AND price_per_km > 0
|
||||
GROUP BY country_code";
|
||||
|
||||
$stmt = $con->query($sql);
|
||||
$competitorRates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($competitorRates as $rate) {
|
||||
$country = $rate['country_code'];
|
||||
$countryNameMap = ['JO' => 'Jordan', 'SY' => 'Syria', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
|
||||
$countryName = $countryNameMap[$country] ?? null;
|
||||
|
||||
if ($countryName) {
|
||||
$avgKmPrice = (float)$rate['avg_price_km'];
|
||||
$minKmPrice = (float)$rate['min_price_km'];
|
||||
|
||||
// 1. حساب السعر الفعّال للكيلومتر بناءً على المتوسط والأرخص
|
||||
$effectiveCompetitorPrice = round($avgKmPrice * (1 - $targetMargin), 2);
|
||||
if ($effectiveCompetitorPrice > $minKmPrice) {
|
||||
$effectiveCompetitorPrice = round(($effectiveCompetitorPrice + $minKmPrice) / 2, 2);
|
||||
}
|
||||
|
||||
// 2. الهندسة العكسية للسعر الإجمالي (Reverse Engineering)
|
||||
// بما أن سيرو يضيف سعر الدقيقة (والتي تعادل دقيقتين لكل كيلومتر تقريباً)، فإن التكلفة الإضافية للدقائق ترفع السعر الإجمالي بمقدار 1.5x
|
||||
// لضمان أن يكون السعر النهائي أقل بـ 6%، نقسم الناتج على 1.5 ليمتص تكلفة الدقائق.
|
||||
$calculatedSpeedPrice = round($effectiveCompetitorPrice / 1.5, 3);
|
||||
|
||||
// 3. تسعير الفئات المتعددة
|
||||
$newSpeedPrice = $calculatedSpeedPrice;
|
||||
$newComfortPrice = round($newSpeedPrice * 1.30, 3);
|
||||
$newLadyPrice = round($newSpeedPrice * 1.10, 3);
|
||||
$newElectricPrice = round($newSpeedPrice * 1.20, 3);
|
||||
$newVanPrice = round($newSpeedPrice * 1.50, 3);
|
||||
$newDeliveryPrice = round($newSpeedPrice * 0.90, 3);
|
||||
$newMishwarVipPrice = round($newSpeedPrice * 1.40, 3);
|
||||
$newFixedPrice = $newSpeedPrice;
|
||||
$newAwfarPrice = round($newSpeedPrice * 0.85, 3);
|
||||
|
||||
// أسعار الدقائق
|
||||
$newNormalMin = round($newSpeedPrice / 4, 3);
|
||||
$newPeakMin = round($newNormalMin * 1.15, 3);
|
||||
$newLateMin = round($newNormalMin * 1.25, 3);
|
||||
|
||||
$updateSql = "UPDATE kazan
|
||||
SET speedPrice = :speedPrice,
|
||||
comfortPrice = :comfortPrice,
|
||||
ladyPrice = :ladyPrice,
|
||||
electricPrice = :electricPrice,
|
||||
vanPrice = :vanPrice,
|
||||
deliveryPrice = :deliveryPrice,
|
||||
mishwarVipPrice = :mishwarVipPrice,
|
||||
fixedPrice = :fixedPrice,
|
||||
awfarPrice = :awfarPrice,
|
||||
normalMinPrice = :normalMin,
|
||||
peakMinPrice = :peakMin,
|
||||
lateMinPrice = :lateMin
|
||||
WHERE country = :countryName";
|
||||
$upStmt = $con->prepare($updateSql);
|
||||
$upStmt->execute([
|
||||
':speedPrice' => $newSpeedPrice,
|
||||
':comfortPrice' => $newComfortPrice,
|
||||
':ladyPrice' => $newLadyPrice,
|
||||
':electricPrice' => $newElectricPrice,
|
||||
':vanPrice' => $newVanPrice,
|
||||
':deliveryPrice' => $newDeliveryPrice,
|
||||
':mishwarVipPrice' => $newMishwarVipPrice,
|
||||
':fixedPrice' => $newFixedPrice,
|
||||
':awfarPrice' => $newAwfarPrice,
|
||||
':normalMin' => $newNormalMin,
|
||||
':peakMin' => $newPeakMin,
|
||||
':lateMin' => $newLateMin,
|
||||
':countryName' => $countryName
|
||||
]);
|
||||
|
||||
echo " -> Updated $countryName (Total Price Math applied): Speed=$newSpeedPrice JOD/KM, NormalMin=$newNormalMin JOD/MIN\n";
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo " Error in Pricing Module: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. وحدة توجيه السائقين (Demand Predictor)
|
||||
// ==========================================
|
||||
echo "2. Running Demand Predictor Module...\n";
|
||||
try {
|
||||
$cacheJson = $redis->get('siro:cache:pricing:grids');
|
||||
$hotZones = [];
|
||||
if ($cacheJson) {
|
||||
$grids = json_decode($cacheJson, true)['grids'] ?? [];
|
||||
foreach ($grids as $key => $data) {
|
||||
if (strpos($key, 'FALLBACK') !== false) continue;
|
||||
|
||||
if ($data['avg_price'] > 0) {
|
||||
$parts = explode('_', $key);
|
||||
if (count($parts) == 3) {
|
||||
$hotZones[] = [
|
||||
'latitude' => (float)$parts[1],
|
||||
'longitude' => (float)$parts[2],
|
||||
'avg_price' => $data['avg_price'],
|
||||
'top_competitor' => $data['top_competitor'],
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$redis->set('siro:cache:ai:hotzones', json_encode(['status' => 'success', 'data' => $hotZones], JSON_UNESCAPED_UNICODE));
|
||||
echo " -> Saved " . count($hotZones) . " Hot Zones to Redis for Driver Map Guidance.\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo " Error in Demand Predictor: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. وحدة استهداف الركاب الخاملين (Smart Retention)
|
||||
// ==========================================
|
||||
echo "3. Running Smart Retention Module...\n";
|
||||
try {
|
||||
$sql = "SELECT source, COUNT(*) as opens
|
||||
FROM passenger_opening_locations
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
|
||||
GROUP BY source
|
||||
HAVING opens >= 3";
|
||||
|
||||
$stmt = $con->query($sql);
|
||||
$idleRiders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$notifiedCount = count($idleRiders);
|
||||
echo " -> Identified $notifiedCount idle riders requiring push notifications.\n";
|
||||
} catch (Exception $e) {
|
||||
echo " Error in Smart Retention: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "AI Engine finished successfully.\n";
|
||||
?>
|
||||
@@ -35,7 +35,7 @@ if (empty($data) || !is_array($data)) {
|
||||
}
|
||||
|
||||
$insertedCount = 0;
|
||||
$stmt = $con->prepare("INSERT INTO scraped_competitor_prices (task_id, app_name, competitor_name, start_location, end_location, start_lat, start_lng, end_lat, end_lng, price_amount, price_per_km, currency, country_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt = $con->prepare("INSERT INTO scraped_competitor_prices (task_id, app_name, competitor_name, start_location, end_location, start_lat, start_lng, end_lat, end_lng, price_amount, price_per_km, distance_km, duration_min, currency, country_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (isset($row['status']) && $row['status'] !== 'success') {
|
||||
@@ -46,6 +46,7 @@ foreach ($data as $row) {
|
||||
$resultData = $row['result_data'] ?? [];
|
||||
|
||||
$appName = $resultData['app'] ?? $row['app'] ?? 'Unknown';
|
||||
$competitorName = $appName; // Assuming app_name is competitor_name for now
|
||||
|
||||
$startLoc = $resultData['start_location'] ?? $row['start_location'] ?? '';
|
||||
if (empty($startLoc) && !empty($resultData['start_lat'])) {
|
||||
@@ -84,19 +85,34 @@ foreach ($data as $row) {
|
||||
}
|
||||
|
||||
$distanceKm = (float)($resultData['distance_km'] ?? 1);
|
||||
if ($distanceKm <= 0) $distanceKm = 1;
|
||||
if ($distanceKm <= 0) $distanceKm = 1; // Prevent division by zero
|
||||
$pricePerKm = $amount / $distanceKm;
|
||||
|
||||
$durationMin = isset($resultData['duration_min']) ? (int)$resultData['duration_min'] : null;
|
||||
|
||||
$startLat = $resultData['start_lat'] ?? null;
|
||||
$startLng = $resultData['start_lng'] ?? null;
|
||||
$endLat = $resultData['end_lat'] ?? null;
|
||||
$endLng = $resultData['end_lng'] ?? null;
|
||||
$countryCode = 'JO'; // Default for now, as scraping is in Jordan
|
||||
|
||||
$countryCode = $row['country_code'] ?? 'JO'; // Default
|
||||
|
||||
if ($stmt->execute([
|
||||
$taskId, $appName, $appName, $startLoc, $endLoc,
|
||||
$startLat, $startLng, $endLat, $endLng,
|
||||
$amount, $pricePerKm, $currency, $countryCode
|
||||
$taskId,
|
||||
$appName,
|
||||
$competitorName,
|
||||
$startLoc,
|
||||
$endLoc,
|
||||
$startLat,
|
||||
$startLng,
|
||||
$endLat,
|
||||
$endLng,
|
||||
$amount,
|
||||
$pricePerKm,
|
||||
$distanceKm,
|
||||
$durationMin,
|
||||
$currency,
|
||||
$countryCode
|
||||
])) {
|
||||
$insertedCount++;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* cron_generate_heatmap_cache.php
|
||||
* يجمع بيانات الخريطة الحرارية ويخزنها في Redis
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../functions.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
$redis = getRedisConnection();
|
||||
} catch (Exception $e) {
|
||||
die("Connection failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
echo "Starting Heatmap Cache Generation (Redis)...\n";
|
||||
|
||||
// مربعات المدن الكبرى لتمثيل الدول (لتجنب حساب المضلعات المعقدة)
|
||||
// الأردن (عمان والزرقاء)
|
||||
// سوريا (دمشق)
|
||||
// مصر (القاهرة والإسكندرية)
|
||||
// العراق (بغداد)
|
||||
$cityBounds = [
|
||||
'JO' => [ // Amman & Zarqa rough bounding box
|
||||
'lat' => [31.80, 32.20],
|
||||
'lng' => [35.80, 36.20]
|
||||
],
|
||||
'SY' => [ // Damascus
|
||||
'lat' => [33.40, 33.60],
|
||||
'lng' => [36.20, 36.40]
|
||||
],
|
||||
'EG' => [ // Cairo & Alexandria
|
||||
'lat' => [29.80, 31.30],
|
||||
'lng' => [29.80, 31.50]
|
||||
],
|
||||
'IQ' => [ // Baghdad
|
||||
'lat' => [33.10, 33.50],
|
||||
'lng' => [44.20, 44.60]
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
$sql = "SELECT latitude, longitude, source, created_at
|
||||
FROM passenger_opening_locations
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20000";
|
||||
|
||||
$stmt = $con->query($sql);
|
||||
$locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stats = ['geofence' => 0, 'app_usage' => 0, 'silent_push' => 0];
|
||||
|
||||
// تقسيم البيانات حسب الدولة (لتسهيل قراءتها من الـ API)
|
||||
$countryData = [
|
||||
'JO' => [], 'SY' => [], 'EG' => [], 'IQ' => [], 'OTHER' => []
|
||||
];
|
||||
|
||||
foreach ($locations as $loc) {
|
||||
$lat = (float)$loc['latitude'];
|
||||
$lng = (float)$loc['longitude'];
|
||||
if ($lat == 0 || $lng == 0) continue;
|
||||
|
||||
$src = $loc['source'] ?? 'app_usage';
|
||||
$date = substr($loc['created_at'], 0, 10);
|
||||
|
||||
$assignedCountry = 'OTHER';
|
||||
// البحث عن المربع الذي يقع فيه الإحداثي
|
||||
foreach ($cityBounds as $cc => $b) {
|
||||
if ($lat >= $b['lat'][0] && $lat <= $b['lat'][1] &&
|
||||
$lng >= $b['lng'][0] && $lng <= $b['lng'][1]) {
|
||||
$assignedCountry = $cc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($stats[$src])) $stats[$src]++;
|
||||
|
||||
$countryData[$assignedCountry][] = [
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'source' => $src,
|
||||
'date' => $date
|
||||
];
|
||||
}
|
||||
|
||||
$redisData = [
|
||||
'last_updated' => date('Y-m-d H:i:s'),
|
||||
'total' => count($locations),
|
||||
'stats' => $stats,
|
||||
'data' => $countryData // مقسمة وجاهزة
|
||||
];
|
||||
|
||||
$redis->set('siro:cache:heatmap:data', json_encode($redisData, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
echo "Heatmap Cache Generated Successfully. Points: " . count($locations) . "\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error generating heatmap cache: " . $e->getMessage());
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* cron_generate_pricing_cache.php
|
||||
* يجمع أسعار المنافسين لكل مربع جغرافي (2.5km) ويحفظ النتيجة في Redis
|
||||
* يتم تشغيله كـ Cron Job (CLI) لتخفيف الضغط تماماً عن الاستعلام المباشر للركاب.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../functions.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
$redis = getRedisConnection();
|
||||
} catch (Exception $e) {
|
||||
die("Connection failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
echo "Starting Pricing Cache Generation (Redis)...\n";
|
||||
|
||||
try {
|
||||
// 1. جلب متوسط الأسعار من المنافسين في آخر ساعة للشبكة
|
||||
$sql = "SELECT country_code,
|
||||
ROUND(latitude / 0.025) * 0.025 AS grid_lat,
|
||||
ROUND(longitude / 0.025) * 0.025 AS grid_lng,
|
||||
competitor_name,
|
||||
AVG(price) as avg_price,
|
||||
COUNT(*) as requests_count
|
||||
FROM competitor_prices
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
GROUP BY country_code, grid_lat, grid_lng, competitor_name";
|
||||
|
||||
$stmt = $con->query($sql);
|
||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// بناء مصفوفة الشبكات
|
||||
$grids = [];
|
||||
foreach ($results as $row) {
|
||||
$cc = strtoupper($row['country_code']);
|
||||
$gLat = number_format((float)$row['grid_lat'], 3);
|
||||
$gLng = number_format((float)$row['grid_lng'], 3);
|
||||
|
||||
$key = "{$cc}_{$gLat}_{$gLng}";
|
||||
|
||||
if (!isset($grids[$key])) {
|
||||
$grids[$key] = [
|
||||
'competitors' => [],
|
||||
'total_price' => 0,
|
||||
'total_competitors' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$grids[$key]['competitors'][$row['competitor_name']] = (float)$row['avg_price'];
|
||||
$grids[$key]['total_price'] += (float)$row['avg_price'];
|
||||
$grids[$key]['total_competitors']++;
|
||||
}
|
||||
|
||||
$processedGrids = [];
|
||||
$fallbackData = []; // لمتوسط البلد بالكامل كبديل
|
||||
|
||||
foreach ($grids as $key => $data) {
|
||||
if ($data['total_competitors'] == 0) continue;
|
||||
|
||||
$overallAvg = $data['total_price'] / $data['total_competitors'];
|
||||
|
||||
// إيجاد المنافس الأرخص في هذا المربع
|
||||
$cheapestComp = '';
|
||||
$cheapestPrice = 999999;
|
||||
foreach ($data['competitors'] as $name => $price) {
|
||||
if ($price < $cheapestPrice) {
|
||||
$cheapestPrice = $price;
|
||||
$cheapestComp = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$gridInfo = [
|
||||
'avg_price' => round($overallAvg, 2),
|
||||
'top_competitor' => $cheapestComp,
|
||||
'cheapest_price' => round($cheapestPrice, 2)
|
||||
];
|
||||
|
||||
$processedGrids[$key] = $gridInfo;
|
||||
|
||||
// حفظ المتوسط للـ Fallback
|
||||
$cc = explode('_', $key)[0];
|
||||
if (!isset($fallbackData[$cc])) {
|
||||
$fallbackData[$cc] = ['sum' => 0, 'count' => 0, 'cheapest_comp' => $cheapestComp];
|
||||
}
|
||||
$fallbackData[$cc]['sum'] += $overallAvg;
|
||||
$fallbackData[$cc]['count']++;
|
||||
}
|
||||
|
||||
// إضافة Fallback لكل دولة (في حال الراكب كان في مربع فارغ)
|
||||
foreach ($fallbackData as $cc => $d) {
|
||||
if ($d['count'] > 0) {
|
||||
$processedGrids["{$cc}_FALLBACK"] = [
|
||||
'avg_price' => round($d['sum'] / $d['count'], 2),
|
||||
'top_competitor' => $d['cheapest_comp'],
|
||||
'cheapest_price' => round($d['sum'] / $d['count'], 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. الحفظ في Redis
|
||||
// نحفظ المصفوفة بالكامل كـ JSON String داخل مفتاح رئيسي واحد للسرعة العالية في القراءة
|
||||
// مفتاح: siro:cache:pricing:grids
|
||||
$redisData = [
|
||||
'last_updated' => date('Y-m-d H:i:s'),
|
||||
'grids' => $processedGrids
|
||||
];
|
||||
|
||||
$redis->set('siro:cache:pricing:grids', json_encode($redisData, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
echo "Pricing Cache Generated Successfully in Redis. Grids: " . count($processedGrids) . "\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error generating pricing cache (Redis): " . $e->getMessage());
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
@@ -30,6 +30,8 @@ CREATE TABLE IF NOT EXISTS `scraped_competitor_prices` (
|
||||
`end_lng` decimal(10,7) DEFAULT NULL,
|
||||
`price_amount` decimal(8,2) NOT NULL,
|
||||
`price_per_km` decimal(8,2) NOT NULL,
|
||||
`distance_km` decimal(8,2) DEFAULT NULL,
|
||||
`duration_min` int DEFAULT NULL,
|
||||
`currency` varchar(10) NOT NULL DEFAULT 'JOD',
|
||||
`country_code` varchar(10) NOT NULL DEFAULT 'JO',
|
||||
`scraped_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -42,6 +44,32 @@ CREATE TABLE IF NOT EXISTS `scraped_competitor_prices` (
|
||||
";
|
||||
$con->exec($sql);
|
||||
|
||||
// [AI Formula Discovery] Create table for reverse-engineered formulas
|
||||
$sqlFormula = "
|
||||
CREATE TABLE IF NOT EXISTS `competitor_secret_formulas` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`competitor_name` varchar(100) NOT NULL,
|
||||
`country_code` varchar(10) NOT NULL,
|
||||
`base_fare` decimal(8,3) NOT NULL,
|
||||
`price_per_km` decimal(8,3) NOT NULL,
|
||||
`price_per_min` decimal(8,3) NOT NULL,
|
||||
`confidence_score` decimal(5,2) DEFAULT 0,
|
||||
`sample_size` int DEFAULT 0,
|
||||
`last_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `idx_comp_country` (`competitor_name`, `country_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
";
|
||||
$con->exec($sqlFormula);
|
||||
|
||||
// Auto-patch existing table if upgrading
|
||||
try {
|
||||
$con->exec("ALTER TABLE `scraped_competitor_prices` ADD COLUMN `distance_km` decimal(8,2) DEFAULT NULL AFTER `price_per_km`");
|
||||
$con->exec("ALTER TABLE `scraped_competitor_prices` ADD COLUMN `duration_min` int DEFAULT NULL AFTER `distance_km`");
|
||||
} catch (Exception $e) {
|
||||
// Columns might already exist, ignore error
|
||||
}
|
||||
|
||||
|
||||
// 2. Ten Key Regions in Damascus (Syria) and Amman (Jordan)
|
||||
$countriesConfig = [
|
||||
'SY' => [
|
||||
|
||||
Reference in New Issue
Block a user