Files
intaleq/backend/bot/cron_dashboard_snapshot.php
T
Hamza-AyedandClaude Opus 5 92dc6b3641 chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:10:29 +03:00

351 lines
15 KiB
PHP

<?php
/**
* cron_dashboard_snapshot.php
* يجمع بيانات التحليلات من Redis وقاعدة البيانات ويخزنها كملفات JSON مؤرشفة.
*
* الجدولة المقترحة:
* - لقطة سريعة (ساعي): 0 * * * * php cron_dashboard_snapshot.php hourly
* - لقطة يومية: 0 3 * * * php cron_dashboard_snapshot.php daily
* - لقطة أسبوعية: 0 4 * * 1 php cron_dashboard_snapshot.php weekly
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
set_time_limit(180);
ini_set('memory_limit', '256M');
$mode = $argv[1] ?? 'hourly';
$today = date('Y-m-d');
$now = date('Y-m-d H:i:s');
$cacheBase = __DIR__ . '/../cache/analytics';
$todayDir = "$cacheBase/$today";
if (!is_dir($todayDir)) {
mkdir($todayDir, 0755, true);
}
try {
$con = Database::get('main');
$conRide = Database::get('ride');
$redis = getRedisConnection();
} catch (Exception $e) {
die("[DashboardSnapshot] Connection failed: " . $e->getMessage() . "\n");
}
echo "[DashboardSnapshot] Mode: $mode | $now\n";
// ─────────────────────────────────────────────────────────────
// الوظائف المساعدة
// ─────────────────────────────────────────────────────────────
function saveSnapshot(string $filename, array $data, string $dir): void {
$data['_generated_at'] = date('Y-m-d H:i:s');
$data['_mode'] = $GLOBALS['mode'];
$path = "$dir/$filename";
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo " ✓ Saved: $path\n";
}
function safeQuery(PDO $db, string $sql, array $params = []): array {
$stmt = $db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function safeScalar(PDO $db, string $sql, array $params = []) {
$stmt = $db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchColumn();
}
// ─────────────────────────────────────────────────────────────
// 1. لقطة اللحظة الحالية (كل ساعة)
// ─────────────────────────────────────────────────────────────
if (in_array($mode, ['hourly', 'daily', 'weekly'])) {
echo "[Snapshot] Realtime stats...\n";
$activeRides = (int)safeScalar($conRide, "SELECT COUNT(*) FROM ride WHERE status IN ('wait','started','arrived')");
$onlineDrivers = (int)safeScalar($con, "SELECT COUNT(*) FROM car_locations WHERE status = 'on'");
$revenueToday = (float)safeScalar($conRide, "SELECT IFNULL(SUM(price_for_passenger),0) FROM ride WHERE status='Finished' AND DATE(created_at)=CURDATE()");
$revenueYesterday = (float)safeScalar($conRide, "SELECT IFNULL(SUM(price_for_passenger),0) FROM ride WHERE status='Finished' AND DATE(created_at)=DATE_SUB(CURDATE(), INTERVAL 1 DAY)");
$openComplaints = (int)safeScalar($con, "SELECT COUNT(*) FROM complaint WHERE statusComplaint='Open'");
$expiringLicenses = (int)safeScalar($con, "SELECT COUNT(*) FROM driver WHERE expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 15 DAY)");
$hour = date('H');
saveSnapshot("realtime_{$hour}.json", [
'active_rides' => $activeRides,
'online_drivers' => $onlineDrivers,
'revenue_today' => $revenueToday,
'revenue_yesterday' => $revenueYesterday,
'open_complaints' => $openComplaints,
'expiring_licenses' => $expiringLicenses,
], $todayDir);
// ─── فجوة العرض والطلب (Supply-Demand Gap) ───
echo "[Snapshot] Supply-Demand gap...\n";
$GRID = 0.01;
$demandRaw = safeQuery($conRide, "
SELECT
ROUND(SUBSTRING_INDEX(start_location,',',1) / $GRID) * $GRID AS lat,
ROUND(SUBSTRING_INDEX(start_location,',',-1) / $GRID) * $GRID AS lng,
COUNT(*) AS demand
FROM ride
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
AND start_location IS NOT NULL AND start_location != ''
GROUP BY lat, lng
HAVING demand >= 1
");
$supplyRaw = safeQuery($con, "
SELECT
ROUND(latitude / $GRID) * $GRID AS lat,
ROUND(longitude / $GRID) * $GRID AS lng,
COUNT(*) AS supply
FROM car_locations
WHERE status = 'on'
AND latitude != 0 AND longitude != 0
GROUP BY lat, lng
");
$supplyMap = [];
foreach ($supplyRaw as $s) {
$key = $s['lat'] . '_' . $s['lng'];
$supplyMap[$key] = (int)$s['supply'];
}
$gapCells = [];
foreach ($demandRaw as $d) {
$key = $d['lat'] . '_' . $d['lng'];
$supply = $supplyMap[$key] ?? 0;
$demand = (int)$d['demand'];
$gap = $demand - $supply;
$gapCells[] = [
'lat' => (float)$d['lat'],
'lng' => (float)$d['lng'],
'demand' => $demand,
'supply' => $supply,
'gap' => $gap,
'ratio' => $supply > 0 ? round($demand / $supply, 2) : ($demand > 0 ? 99.0 : 0),
];
}
usort($gapCells, fn($a, $b) => $b['gap'] <=> $a['gap']);
$gapCells = array_slice($gapCells, 0, 200);
saveSnapshot("supply_demand_gap.json", [
'cells' => $gapCells,
'total_demand_cells' => count($demandRaw),
'total_supply_cells' => count($supplyRaw),
], $todayDir);
// ─── كاش الخريطة الحرارية من Redis ───
echo "[Snapshot] Heatmap cache from Redis...\n";
$heatmapRedis = $redis ? $redis->get('siro:cache:heatmap:data') : null;
if ($heatmapRedis) {
$heatmapData = json_decode($heatmapRedis, true);
saveSnapshot("heatmap.json", $heatmapData ?: [], $todayDir);
}
// ─── كاش التسعير من Redis ───
echo "[Snapshot] Pricing cache from Redis...\n";
$pricingRedis = $redis ? $redis->get('siro:cache:pricing:grids') : null;
if ($pricingRedis) {
$pricingData = json_decode($pricingRedis, true);
saveSnapshot("pricing_grids.json", $pricingData ?: [], $todayDir);
}
// ─── بيانات الكرون Predictive Demand من Redis ───
$predictiveRedis = $redis ? $redis->get('siro:predictive_demand:latest') : null;
if ($predictiveRedis) {
saveSnapshot("predictive_demand.json", json_decode($predictiveRedis, true) ?: [], $todayDir);
}
}
// ─────────────────────────────────────────────────────────────
// 2. لقطة يومية (تحليلات ثقيلة)
// ─────────────────────────────────────────────────────────────
if (in_array($mode, ['daily', 'weekly'])) {
echo "[Snapshot] Daily analytics...\n";
// ─── الإيرادات اليومية (30 يوم) ───
$revenueDaily = safeQuery($conRide, "
SELECT DATE(created_at) as date,
SUM(price) as total_revenue,
SUM(price - price_for_driver) as company_profit,
COUNT(*) as total_rides,
AVG(price) as avg_fare
FROM ride
WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
");
saveSnapshot("revenue_30d.json", ['daily' => $revenueDaily], $todayDir);
// ─── النمو (ركاب + كباتن) ───
$passengerGrowth = safeQuery($con, "
SELECT DATE(created_at) as date, COUNT(*) as count
FROM passengers
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at) ORDER BY date ASC
");
$driverGrowth = safeQuery($con, "
SELECT DATE(created_at) as date, COUNT(*) as count
FROM driver
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(created_at) ORDER BY date ASC
");
$totalPassengers = (int)safeScalar($con, "SELECT COUNT(*) FROM passengers");
$totalDrivers = (int)safeScalar($con, "SELECT COUNT(*) FROM driver");
saveSnapshot("growth_30d.json", [
'passengers' => $passengerGrowth,
'drivers' => $driverGrowth,
'totals' => ['passengers' => $totalPassengers, 'drivers' => $totalDrivers],
], $todayDir);
// ─── توزيع الرحلات حسب الساعة (لاكتشاف الأنماط) ───
$hourlyPattern = safeQuery($conRide, "
SELECT HOUR(created_at) as hour, COUNT(*) as rides,
AVG(price) as avg_price
FROM ride
WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY HOUR(created_at) ORDER BY hour
");
saveSnapshot("hourly_pattern.json", ['hours' => $hourlyPattern], $todayDir);
// ─── توزيع حالات الرحلات (Funnel) ───
$rideFunnel = safeQuery($conRide, "
SELECT status, COUNT(*) as count
FROM ride
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY status
");
saveSnapshot("ride_funnel.json", ['statuses' => $rideFunnel], $todayDir);
// ─── أسعار المنافسين (تاريخي ٢٤ ساعة) ───
$competitorHourly = safeQuery($con, "
SELECT DATE_FORMAT(created_at,'%Y-%m-%d %H:00:00') AS hour_bucket,
competitor_name,
AVG(price_per_km) AS avg_price,
COUNT(*) AS samples
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY hour_bucket, competitor_name
ORDER BY hour_bucket ASC
");
saveSnapshot("competitor_prices_24h.json", ['hourly' => $competitorHourly], $todayDir);
// ─── صحة السوق (market health) ───
$countries = ['JO', 'SY', 'EG', 'IQ'];
$marketHealth = [];
foreach ($countries as $cc) {
$rows = safeQuery($con, "
SELECT report_date, average_pci, market_share_percent, total_anomalies
FROM market_health_reports
WHERE country_code = :cc
ORDER BY report_date DESC LIMIT 12
", [':cc' => $cc]);
if (!empty($rows)) {
$marketHealth[$cc] = array_reverse($rows);
}
}
saveSnapshot("market_health.json", ['countries' => $marketHealth], $todayDir);
// ─── شكاوى مفتوحة ───
$complaints = safeQuery($con, "
SELECT complaint_type, COUNT(*) as count
FROM complaint
WHERE statusComplaint = 'Open'
GROUP BY complaint_type
ORDER BY count DESC
");
saveSnapshot("complaints_open.json", ['by_type' => $complaints], $todayDir);
}
// ─────────────────────────────────────────────────────────────
// 3. لقطة أسبوعية (مقارنات وتجميعات)
// ─────────────────────────────────────────────────────────────
if ($mode === 'weekly') {
echo "[Snapshot] Weekly deep analytics...\n";
// ─── مقارنة أسبوع بأسبوع ───
$weeklyComparison = safeQuery($conRide, "
SELECT
YEARWEEK(created_at, 1) as yw,
MIN(DATE(created_at)) as week_start,
COUNT(*) as rides,
SUM(price) as revenue,
SUM(price - price_for_driver) as profit,
COUNT(DISTINCT driver_id) as active_drivers,
COUNT(DISTINCT passenger_id) as active_passengers
FROM ride
WHERE status = 'Finished' AND created_at >= DATE_SUB(CURDATE(), INTERVAL 12 WEEK)
GROUP BY yw ORDER BY yw ASC
");
saveSnapshot("weekly_comparison.json", ['weeks' => $weeklyComparison], $todayDir);
// ─── أعلى مناطق (أكثر 20 خلية نشاطاً) ───
$topZones = safeQuery($conRide, "
SELECT
ROUND(SUBSTRING_INDEX(start_location,',',1) / 0.01) * 0.01 AS lat,
ROUND(SUBSTRING_INDEX(start_location,',',-1) / 0.01) * 0.01 AS lng,
COUNT(*) AS rides,
AVG(price) AS avg_price
FROM ride
WHERE status = 'Finished'
AND created_at >= DATE_SUB(CURDATE(), INTERVAL 4 WEEK)
AND start_location IS NOT NULL AND start_location != ''
GROUP BY lat, lng
ORDER BY rides DESC
LIMIT 20
");
saveSnapshot("top_zones.json", ['zones' => $topZones], $todayDir);
// ─── احتفاظ الركاب (Retention) ───
$retention = safeQuery($conRide, "
SELECT
weeks_since_signup,
COUNT(DISTINCT passenger_id) as active_passengers
FROM (
SELECT r.passenger_id,
FLOOR(DATEDIFF(r.created_at, p.created_at) / 7) as weeks_since_signup
FROM ride r
JOIN passengers p ON r.passenger_id = p.id
WHERE r.status = 'Finished'
AND r.created_at >= DATE_SUB(CURDATE(), INTERVAL 12 WEEK)
) sub
GROUP BY weeks_since_signup
ORDER BY weeks_since_signup
");
saveSnapshot("retention_cohort.json", ['cohorts' => $retention], $todayDir);
}
// ─────────────────────────────────────────────────────────────
// 4. إنشاء manifest يسهّل على الداشبورد معرفة الملفات المتاحة
// ─────────────────────────────────────────────────────────────
$files = glob("$todayDir/*.json");
$manifest = [];
foreach ($files as $f) {
$name = basename($f, '.json');
if ($name === 'manifest') continue;
$manifest[$name] = [
'file' => basename($f),
'size' => filesize($f),
'updated' => date('Y-m-d H:i:s', filemtime($f)),
];
}
saveSnapshot("manifest.json", ['snapshots' => $manifest, 'date' => $today], $todayDir);
// ─── فهرس الأيام المتاحة (لتسهيل التصفح التاريخي) ───
$days = array_map('basename', glob("$cacheBase/20*", GLOB_ONLYDIR));
rsort($days);
file_put_contents("$cacheBase/index.json", json_encode([
'available_dates' => $days,
'latest' => $days[0] ?? $today,
'updated' => $now,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo "[DashboardSnapshot] Done! Files in $todayDir\n";