Update: 2026-08-02 22:51:18

This commit is contained in:
Hamza-Ayed
2026-08-02 22:51:18 +03:00
parent c9af364cbc
commit 2c962bf80a
39 changed files with 627 additions and 191 deletions
+12 -5
View File
@@ -33,15 +33,20 @@ SELECT
-- كانت تُنتج متوسط مدة سالباً.
(SELECT TIME_FORMAT(SEC_TO_TIME(AVG(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))), '%Hh %im') FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL AND TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish) > 0) AS driver_avg_duration,
(SELECT MAX(SEC_TO_TIME(TIMESTAMPDIFF(SECOND, rideTimeStart, rideTimeFinish))) FROM ride WHERE rideTimeStart IS NOT NULL AND rideTimeFinish IS NOT NULL) AS longest_duration,
(SELECT ROUND(SUM(distance),2) FROM ride) AS total_distance,
(SELECT ROUND(AVG(distance),2) FROM ride) AS average_distance,
(SELECT ROUND(MAX(distance),2) FROM ride) AS longest_distance,
-- المسافات تُحسب على الرحلات المكتملة فقط. حسابها على كل الصفوف كان
-- يُدخل الرحلات الملغاة و timeout (مسافتها 0 أو غير مضبوطة) في المتوسط
-- فيهبط average_distance بشكل مضلّل.
(SELECT ROUND(SUM(distance),2) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS total_distance,
(SELECT ROUND(AVG(distance),2) FROM ride WHERE LOWER(status) IN ('finished','completed') AND distance > 0) AS average_distance,
(SELECT ROUND(MAX(distance),2) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS longest_distance,
-- أرباح السائق والشركة
-- ملاحظة: خط الرحلات الحالي يكتب 'completed' بينما القديم يكتب 'Finished'،
-- والاكتفاء بالقديم كان يُرجع NULL للأرباح وصفراً للرحلات المكتملة/الملغاة.
(SELECT SUM(price_for_driver) FROM ride WHERE LOWER(status) IN ('finished','completed')) AS total_driver_earnings,
(SELECT ROUND(AVG(price_for_passenger),2) FROM ride) AS avg_passenger_price,
-- متوسط سعر الراكب على الرحلات المكتملة فقط — الرحلات الملغاة سعرها 0
-- وكانت تسحب المتوسط إلى أسفل.
(SELECT ROUND(AVG(price_for_passenger),2) FROM ride WHERE LOWER(status) IN ('finished','completed') AND price_for_passenger > 0) AS avg_passenger_price,
-- توزيع الرحلات حسب الوقت
(SELECT COUNT(*) FROM ride WHERE HOUR(created_at) BETWEEN 6 AND 11) AS morning_ride_count,
@@ -59,7 +64,9 @@ SELECT
(SELECT COUNT(*) FROM ride WHERE LOWER(status) LIKE 'cancel%' OR LOWER(status) IN ('timeout','refused')) AS cancelled_rides,
-- عدد السائقين الفريدين
(SELECT COUNT(*) FROM (SELECT driver_id FROM ride GROUP BY driver_id) AS sub) AS num_Driver,
-- cancel_ride_by_driver.php يضبط driver_id = 0 عند الإلغاء، وكان هذا
-- الصفر يُحتسب سائقاً وهمياً إضافياً في العدد.
(SELECT COUNT(DISTINCT driver_id) FROM ride WHERE driver_id > 0) AS num_Driver,
-- التحويلات البنكية
0 AS transfer_from_count
+8
View File
@@ -19,6 +19,14 @@ try {
die("Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// كل مخرجات هذا المحرّك تُكتب في Redis (مقارنة السائقين، ملخّص ذروة
// المنافسين، المناطق الساخنة)، فلا معنى لمتابعة التشغيل بدونه.
if (!$redis) {
error_log('[AI Engine] ABORT: Redis unavailable — no cache target to write to.');
die("Redis unavailable — aborting AI engine run.\n");
}
echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n";
// ==========================================
@@ -26,6 +26,14 @@ try {
die("Database connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// مفتاح مكافحة التكرار (TTL يوم كامل) يعيش في Redis؛ بدونه يُعاد إرسال
// نفس الحملة التسويقية كل ساعة.
if (!$redis) {
error_log('[AutoMarketingPusher] ABORT: Redis unavailable — anti-spam guard would fail open.');
die("Redis unavailable — aborting to avoid duplicate campaign pushes.\n");
}
echo "Starting Auto Marketing Pusher Engine...\n";
// 1. استخراج الركاب الخاملين في آخر ساعتين (الذين لم يقموا بأي طلب اليوم)
@@ -14,6 +14,13 @@ try {
die("Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// الهدف الوحيد لهذا السكربت هو الكتابة إلى مفتاح Redis للكاش.
if (!$redis) {
error_log('[HeatmapCache] ABORT: Redis unavailable — nothing to cache into.');
die("Redis unavailable — aborting heatmap cache generation.\n");
}
echo "Starting Heatmap Cache Generation (Redis)...\n";
// مربعات المدن الكبرى لتمثيل الدول (لتجنب حساب المضلعات المعقدة)
+8
View File
@@ -18,6 +18,14 @@ try {
die("Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() تُعيد المتغير العام $redis فقط، وbootstrap.php يتركه null
// عند فشل الاتصال — فهي لا ترمي استثناءً والـ try/catch أعلاه لا يلتقط شيئاً.
// بدون هذا الفحص كان السكربت ينهار فوراً بـ "call to a member function get() on null".
if (!$redis) {
error_log('[cron_kazan_adjuster] ABORT: Redis unavailable — Kazan discounts left untouched.');
die("Redis unavailable — aborting without touching Kazan discounts.\n");
}
$countries = ['SY', 'JO', 'EG', 'IQ'];
foreach ($countries as $country) {
@@ -28,6 +28,14 @@ try {
die("[ReEngagement] Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// مفتاح مكافحة التكرار يعيش في Redis؛ بدونه يفشل الفحص "مفتوحاً"
// فيتلقّى كل راكب نفس رسالة الاستعادة في كل تشغيل.
if (!$redis) {
error_log('[ReEngagement] ABORT: Redis unavailable — anti-spam guard would fail open.');
die("[ReEngagement] Redis unavailable — aborting to avoid duplicate pushes.\n");
}
echo "[ReEngagement] Starting Passenger Re-engagement Engine...\n";
$REDIS_TTL = 72 * 3600; // anti-spam: 72 ساعة
+8
View File
@@ -34,6 +34,14 @@ try {
die("[PredictiveDemand] Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// الإيقاف هنا مقصود: مفتاح مكافحة التكرار يعيش في Redis، وبدونه سيُعاد
// إرسال نفس الإشعارات للركاب في كل تشغيل.
if (!$redis) {
error_log('[PredictiveDemand] ABORT: Redis unavailable — anti-spam guard would fail open.');
die("[PredictiveDemand] Redis unavailable — aborting to avoid duplicate pushes.\n");
}
echo "[PredictiveDemand] Starting...\n";
$GRID_SIZE = 0.01; // حجم الخلية (~1.1km)
+7
View File
@@ -15,6 +15,13 @@ try {
die("Connection failed: " . $e->getMessage() . "\n");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// المُعاملات الموسمية تُنشر عبر Redis وحده ليقرأها محرّك التسعير.
if (!$redis) {
error_log('[SeasonalPricing] ABORT: Redis unavailable — seasonal multipliers not published.');
die("Redis unavailable — aborting seasonal pricing run.\n");
}
$countries = ['SY' => 'Asia/Damascus', 'JO' => 'Asia/Amman', 'EG' => 'Africa/Cairo', 'IQ' => 'Asia/Baghdad'];
// Simulated Calendar of Events
+42 -13
View File
@@ -23,7 +23,15 @@ if (!$SECRET_KEY) {
echo json_encode(['status' => 'failure', 'message' => 'Server configuration error: BOT_SECRET_KEY not set in environment']);
exit;
}
$ALLOWED_DEVICES = ['SHAM_CASH_BOT_01', 'PRICE_SCRAPER_BOT_01', '77a9528112ef7c2a'];
// أجهزة البوت المسموح بها — تُضبط عبر BOT_ALLOWED_DEVICES (مفصولة بفواصل)
// حتى لا يتطلب ضم جهاز جديد تعديل الكود ونشره.
$ALLOWED_DEVICES = array_values(array_filter(array_map(
'trim',
explode(',', (string)(getenv('BOT_ALLOWED_DEVICES') ?: ''))
)));
if (!$ALLOWED_DEVICES) {
$ALLOWED_DEVICES = ['SHAM_CASH_BOT_01', 'PRICE_SCRAPER_BOT_01', '77a9528112ef7c2a'];
}
$method = $_SERVER['REQUEST_METHOD'];
@@ -43,9 +51,12 @@ if ($method === 'GET') {
// ---------------------------------------------------------
// GET: Fetch Pending Task for the Bot
// ---------------------------------------------------------
$device_id = filterRequest('device_id');
$ts = filterRequest('ts');
$sig = filterRequest('sig');
// ملاحظة: filterRequest() تقرأ من POST أو JSON body فقط ولا تلمس $_GET
// (backend/core/helpers.php) — والبوت يرسل بارامترات التوقيع في الـ query
// string، لذا تُقرأ هنا من $_GET مباشرة.
$device_id = isset($_GET['device_id']) ? trim((string)$_GET['device_id']) : null;
$ts = isset($_GET['ts']) ? (int)$_GET['ts'] : null;
$sig = isset($_GET['sig']) ? trim((string)$_GET['sig']) : null;
if (!$device_id || !$ts || !$sig) {
jsonError("Missing security parameters");
@@ -64,6 +75,14 @@ if ($method === 'GET') {
// Check Redis Queue for tasks
// Queue Name: queue:bot:tasks (Using Main Redis)
// bootstrap.php يترك $redis = null إذا فشل الاتصال — بدون هذا الفحص
// كان الطلب ينهار بخطأ 500 غامض بدل رسالة واضحة.
if (!$redis) {
http_response_code(503);
echo json_encode(['status' => 'failure', 'message' => 'Task queue unavailable']);
exit;
}
$taskJson = $redis->rpop('queue:bot:tasks');
if ($taskJson) {
@@ -144,18 +163,28 @@ if ($method === 'GET') {
}
// ------------------------------------------------------
// العملة تتبع الدولة بدل تثبيتها على JOD — البوت يعمل على أكثر من سوق
$currencyByCountry = ['JO' => 'JOD', 'SY' => 'SYP', 'EG' => 'EGP'];
$currency = $currencyByCountry[strtoupper($country_code)] ?? 'JOD';
// 1. Save to MySQL
$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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$start_loc = "Lat: $start_lat, Lng: $start_lng";
$end_loc = "Lat: $end_lat, Lng: $end_lng";
$stmt->execute([$task_id, $app_name, $app_name, $start_loc, $end_loc, (string)$start_lat, (string)$start_lng, (string)$end_lat, (string)$end_lng, $price, $pricePerKm, $distance_km, $duration_min, 'JOD', $country_code]);
try {
$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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$start_loc = "Lat: $start_lat, Lng: $start_lng";
$end_loc = "Lat: $end_lat, Lng: $end_lng";
$stmt->execute([$task_id, $app_name, $app_name, $start_loc, $end_loc, (string)$start_lat, (string)$start_lng, (string)$end_lat, (string)$end_lng, $price, $pricePerKm, $distance_km, $duration_min, $currency, $country_code]);
} catch (PDOException $e) {
// لا نُسقط الطلب: نسجّل الخطأ ونكمل إلى Redis حتى لا تضيع العيّنة
// من محرّك التسعير بسبب عطل في الكتابة إلى MySQL.
error_log("[Bot Worker] MySQL insert failed for task $task_id: " . $e->getMessage());
}
// 2. Save to Redis (Calculate Price Per KM)
if ($distance_km > 0 && $price > 0) {
if ($redis && $distance_km > 0 && $price > 0) {
// Store in Redis (Main) to be used by Pricing Engine
// Store recent 50 prices for the app
$redis->lpush("competitor:price_history:$app_name", $pricePerKm);
@@ -0,0 +1,19 @@
-- ============================================================
-- scraped_competitor_prices: add distance_km / duration_min
-- ============================================================
-- backend/bot/worker.php computes both values via OSRM
-- (core/osrm_routing.php) and has always listed them in its
-- INSERT, but the columns were never created — so every
-- production bot submission failed with a PDOException.
--
-- Consumers currently derive distance as
-- (price_amount / price_per_km)
-- e.g. Admin/marketing/what_if_simulator.php and
-- Admin/marketing/trigger_campaign.php
-- Those keep working unchanged; the new columns are additive
-- and nullable, so historical rows stay valid.
-- ============================================================
ALTER TABLE `scraped_competitor_prices`
ADD COLUMN `distance_km` DECIMAL(8,3) DEFAULT NULL AFTER `price_per_km`,
ADD COLUMN `duration_min` DECIMAL(8,2) DEFAULT NULL AFTER `distance_km`;
+12 -12
View File
@@ -28,20 +28,20 @@ WITH RECURSIVE date_series AS (
FROM date_series
WHERE date < :end_date
)
SELECT
date_series.date AS day,
COALESCE(SUM(ride.status = 'Finished'), 0) AS totalRides,
(SELECT COUNT(*) FROM ride
WHERE ride.created_at >= :start_date_total
AND ride.created_at <= :end_date_total
AND ride.status = 'Finished') AS totalMonthly
SELECT
date_series.date AS day,
COALESCE(SUM(LOWER(ride.status) IN ('finished','completed')), 0) AS totalRides,
FROM
(SELECT COUNT(*) FROM ride
WHERE ride.created_at >= :start_date_total
AND ride.created_at <= :end_date_total
AND LOWER(ride.status) IN ('finished','completed')) AS totalMonthly
FROM
date_series
LEFT JOIN
ride ON DATE(ride.created_at) = date_series.date
AND ride.status = 'Finished'
LEFT JOIN
ride ON DATE(ride.created_at) = date_series.date
AND LOWER(ride.status) IN ('finished','completed')
WHERE
date_series.date >= :start_date_where
AND date_series.date <= :end_date_where