Update: 2026-07-06 19:17:19
This commit is contained in:
@@ -51,7 +51,7 @@ try {
|
|||||||
// 1. Economy tier (الأكثر تنافسية)
|
// 1. Economy tier (الأكثر تنافسية)
|
||||||
// 2. Standard tier (إذا ما في Economy)
|
// 2. Standard tier (إذا ما في Economy)
|
||||||
// 3. أي tier ثاني بأعلى R²
|
// 3. أي tier ثاني بأعلى R²
|
||||||
$tierPriority = ['economy', 'standard', 'premium'];
|
$tierPriority = ['standard', 'economy', 'premium'];
|
||||||
$bestFormula = null;
|
$bestFormula = null;
|
||||||
$bestTierIdx = 999;
|
$bestTierIdx = 999;
|
||||||
|
|
||||||
@@ -131,7 +131,8 @@ try {
|
|||||||
awfarPrice = :awfarPrice,
|
awfarPrice = :awfarPrice,
|
||||||
normalMinPrice = :normalMin,
|
normalMinPrice = :normalMin,
|
||||||
peakMinPrice = :peakMin,
|
peakMinPrice = :peakMin,
|
||||||
lateMinPrice = :lateMin
|
lateMinPrice = :lateMin,
|
||||||
|
startPrice = :startPrice
|
||||||
WHERE country = :countryName";
|
WHERE country = :countryName";
|
||||||
|
|
||||||
$upStmt = $con->prepare($updateSql);
|
$upStmt = $con->prepare($updateSql);
|
||||||
@@ -148,6 +149,7 @@ try {
|
|||||||
':normalMin' => $normalMin,
|
':normalMin' => $normalMin,
|
||||||
':peakMin' => $peakMin,
|
':peakMin' => $peakMin,
|
||||||
':lateMin' => $lateMin,
|
':lateMin' => $lateMin,
|
||||||
|
':startPrice' => $ourBase,
|
||||||
':countryName' => $countryName
|
':countryName' => $countryName
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -230,4 +232,33 @@ try {
|
|||||||
echo " ❌ Error in Smart Retention: " . $e->getMessage() . "\n";
|
echo " ❌ Error in Smart Retention: " . $e->getMessage() . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. Export AI Hotzones to Redis for Captains
|
||||||
|
// ==========================================
|
||||||
|
echo "4. Exporting AI Hotzones to Redis...\n";
|
||||||
|
try {
|
||||||
|
$sql = "SELECT latitude, longitude, competitor_name AS top_competitor, avg_ppk AS avg_price
|
||||||
|
FROM competitor_surge_zones
|
||||||
|
WHERE detected_at >= DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||||||
|
AND surge_multiplier > 1.1";
|
||||||
|
|
||||||
|
$stmt = $con->query($sql);
|
||||||
|
$zones = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($zones)) {
|
||||||
|
$redisData = [
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $zones
|
||||||
|
];
|
||||||
|
$redis->setex('siro:cache:ai:hotzones', 3600, json_encode($redisData));
|
||||||
|
echo " 🗺️ Exported " . count($zones) . " hot zones to Redis (siro:cache:ai:hotzones).\n";
|
||||||
|
} else {
|
||||||
|
// Clear if no surge to avoid stale data
|
||||||
|
$redis->del('siro:cache:ai:hotzones');
|
||||||
|
echo " ℹ️ No active hot zones to export.\n";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo " ❌ Error in Hotzones Export: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
echo "AI Engine v2 finished successfully.\n";
|
echo "AI Engine v2 finished successfully.\n";
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Migration: Create competitor_surge_zones table for heatmap integration
|
||||||
|
-- Purpose: Bridge the geographic surge anomalies from Node.js pricing engine to PHP heatmap
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `competitor_surge_zones` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`competitor_name` VARCHAR(100) NOT NULL,
|
||||||
|
`country_code` VARCHAR(5) NOT NULL,
|
||||||
|
`zone_key` VARCHAR(50) NOT NULL,
|
||||||
|
`latitude` DECIMAL(10,6) NOT NULL,
|
||||||
|
`longitude` DECIMAL(10,6) NOT NULL,
|
||||||
|
`avg_ppk` DECIMAL(10,3) NOT NULL,
|
||||||
|
`sample_count` INT NOT NULL,
|
||||||
|
`surge_multiplier` DECIMAL(5,3) NOT NULL DEFAULT 1.000,
|
||||||
|
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `unique_zone` (`competitor_name`, `country_code`, `zone_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@@ -164,6 +164,43 @@ export async function saveSurgeInsights(
|
|||||||
await pool.execute(sql, flatParams);
|
await pool.execute(sql, flatParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveSurgeZones(
|
||||||
|
pool: mysql.Pool,
|
||||||
|
zones: Array<{
|
||||||
|
competitorName: string;
|
||||||
|
countryCode: string;
|
||||||
|
zoneKey: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
avgPpk: number;
|
||||||
|
sampleCount: number;
|
||||||
|
surgeMultiplier: number;
|
||||||
|
}>
|
||||||
|
): Promise<void> {
|
||||||
|
if (zones.length === 0) return;
|
||||||
|
|
||||||
|
const values = zones.map(() => `(?, ?, ?, ?, ?, ?, ?, ?, NOW())`).join(',');
|
||||||
|
const flatParams: (string | number)[] = [];
|
||||||
|
|
||||||
|
for (const z of zones) {
|
||||||
|
flatParams.push(
|
||||||
|
z.competitorName, z.countryCode, z.zoneKey,
|
||||||
|
z.latitude, z.longitude, z.avgPpk, z.sampleCount, z.surgeMultiplier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `INSERT INTO competitor_surge_zones
|
||||||
|
(competitor_name, country_code, zone_key, latitude, longitude, avg_ppk, sample_count, surge_multiplier, detected_at)
|
||||||
|
VALUES ${values}
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
avg_ppk = VALUES(avg_ppk),
|
||||||
|
sample_count = VALUES(sample_count),
|
||||||
|
surge_multiplier = VALUES(surge_multiplier),
|
||||||
|
detected_at = NOW()`;
|
||||||
|
|
||||||
|
await pool.execute(sql, flatParams);
|
||||||
|
}
|
||||||
|
|
||||||
export async function closeConnections(): Promise<void> {
|
export async function closeConnections(): Promise<void> {
|
||||||
if (mysqlPool) {
|
if (mysqlPool) {
|
||||||
await mysqlPool.end();
|
await mysqlPool.end();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Cron integration: see crontab examples in package.json scripts
|
* Cron integration: see crontab examples in package.json scripts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMySQL, fetchSamples, saveFormulas, saveSurgeInsights, closeConnections } from './db/connection';
|
import { getMySQL, fetchSamples, saveFormulas, saveSurgeInsights, saveSurgeZones, closeConnections } from './db/connection';
|
||||||
import { runAnalysis } from './analysis/engine';
|
import { runAnalysis } from './analysis/engine';
|
||||||
import { Pool, RowDataPacket } from 'mysql2/promise';
|
import { Pool, RowDataPacket } from 'mysql2/promise';
|
||||||
|
|
||||||
@@ -255,6 +255,31 @@ async function processCompetitor(
|
|||||||
await saveSurgeInsights(pool, surgeInsights);
|
await saveSurgeInsights(pool, surgeInsights);
|
||||||
console.log(` ✅ Saved surge insight: avg ${avgMultiplier.toFixed(3)}x, hours ${peakStart}:00-${peakEnd}:00`);
|
console.log(` ✅ Saved surge insight: avg ${avgMultiplier.toFixed(3)}x, hours ${peakStart}:00-${peakEnd}:00`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Save surge zones ---
|
||||||
|
if (opts.mode !== 'report' && report.zones.length > 0) {
|
||||||
|
// Find the standard tier formula to use as a baseline for calculating surge multipliers
|
||||||
|
const standardTier = formulas.find(f => f.tier === 'standard') || formulas[0];
|
||||||
|
const baselinePpk = standardTier ? standardTier.kmRate : 0.350;
|
||||||
|
|
||||||
|
const surgeZones = report.zones
|
||||||
|
.filter(z => z.avgPpk > baselinePpk * 1.1) // Only keep zones with > 10% surge
|
||||||
|
.map(z => ({
|
||||||
|
competitorName: comp.competitor_name,
|
||||||
|
countryCode: comp.country_code,
|
||||||
|
zoneKey: z.zoneKey,
|
||||||
|
latitude: z.centerLat,
|
||||||
|
longitude: z.centerLng,
|
||||||
|
avgPpk: z.avgPpk,
|
||||||
|
sampleCount: z.samples.length,
|
||||||
|
surgeMultiplier: parseFloat((z.avgPpk / baselinePpk).toFixed(3)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (surgeZones.length > 0) {
|
||||||
|
await saveSurgeZones(pool, surgeZones);
|
||||||
|
console.log(` ✅ Saved ${surgeZones.length} surge zones for heatmap`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCompetitors(
|
async function fetchCompetitors(
|
||||||
|
|||||||
Reference in New Issue
Block a user