قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
393 lines
14 KiB
JavaScript
393 lines
14 KiB
JavaScript
const io = require('socket.io-client');
|
|
const axios = require('axios');
|
|
const { execSync } = require('child_process');
|
|
const yargs = require('yargs/yargs');
|
|
const { hideBin } = require('yargs/helpers');
|
|
|
|
const argv = yargs(hideBin(process.argv))
|
|
.option('trips', {
|
|
alias: 't',
|
|
type: 'number',
|
|
description: 'Number of concurrent trips to simulate',
|
|
default: 1
|
|
})
|
|
.option('base-url', {
|
|
alias: 'b',
|
|
type: 'string',
|
|
description: 'Base URL for backend APIs',
|
|
default: 'http://localhost/backend'
|
|
})
|
|
.option('socket-url', {
|
|
alias: 's',
|
|
type: 'string',
|
|
description: 'Location server socket URL',
|
|
default: 'http://localhost:2020'
|
|
})
|
|
.option('debug', {
|
|
alias: 'd',
|
|
type: 'boolean',
|
|
description: 'Enable debug logging',
|
|
default: false
|
|
})
|
|
.argv;
|
|
|
|
function debug(msg) {
|
|
if (argv.debug) console.log(` [DEBUG] ${msg}`);
|
|
}
|
|
|
|
// Helper to get JWT and FP for mock user
|
|
function getMockJwt(userId, role = 'driver') {
|
|
try {
|
|
const output = execSync(`php generate_mock_data.php ${userId} ${role}`, {
|
|
timeout: 5000
|
|
}).toString();
|
|
debug(`JWT output for ${role} ${userId}: ${output.substring(0, 80)}...`);
|
|
const jsonString = output.substring(output.indexOf('{'));
|
|
const parsed = JSON.parse(jsonString);
|
|
if (!parsed.jwt) {
|
|
console.error(`[Error] JWT generation returned empty for ${role} ${userId}: ${output}`);
|
|
return { jwt: '', fp: '' };
|
|
}
|
|
return {
|
|
jwt: parsed.jwt,
|
|
fp: parsed.fp || `mock_device_fp_${userId}`,
|
|
price_token: parsed.price_token || ''
|
|
};
|
|
} catch (err) {
|
|
console.error(`[Error] Failed to generate JWT for ${role} ${userId}:`, err.message);
|
|
return { jwt: '', fp: '' };
|
|
}
|
|
}
|
|
|
|
let metrics = {
|
|
successful: 0,
|
|
failed: 0,
|
|
connectionErrors: 0,
|
|
apiErrors: 0,
|
|
jwtErrors: 0,
|
|
totalTime: 0
|
|
};
|
|
|
|
async function simulateTrip(tripIndex) {
|
|
const driverId = 900000 + tripIndex;
|
|
const passengerId = 800000 + tripIndex;
|
|
const driverAuth = getMockJwt(driverId, 'driver');
|
|
const passengerAuth = getMockJwt(passengerId, 'passenger');
|
|
|
|
if (!driverAuth.jwt || !passengerAuth.jwt) {
|
|
console.error(`[Trip ${tripIndex}] Skipping — JWT generation failed.`);
|
|
metrics.jwtErrors++;
|
|
metrics.failed++;
|
|
return false;
|
|
}
|
|
|
|
console.log(`[Trip ${tripIndex}] Starting... Driver: ${driverId}, Passenger: ${passengerId}`);
|
|
const tripStartTime = Date.now();
|
|
|
|
// 1. Connect Driver to Socket
|
|
const socketUrl = argv.socketUrl;
|
|
debug(`Connecting to socket at: ${socketUrl}`);
|
|
|
|
const socket = io(socketUrl, {
|
|
query: {
|
|
driver_id: String(driverId),
|
|
platform: 'android',
|
|
jwt: driverAuth.jwt
|
|
},
|
|
reconnection: false,
|
|
timeout: 10000,
|
|
forceNew: true
|
|
});
|
|
|
|
return new Promise((resolve) => {
|
|
let rideId = null;
|
|
let resolved = false;
|
|
|
|
function finish(success) {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
try { socket.disconnect(); } catch(e) {}
|
|
resolve(success);
|
|
}
|
|
|
|
socket.on('connect_error', (err) => {
|
|
console.error(`[Trip ${tripIndex}] ❌ Socket Connection Error: ${err.message}`);
|
|
metrics.connectionErrors++;
|
|
metrics.failed++;
|
|
finish(false);
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error(`[Trip ${tripIndex}] ❌ Socket Error: ${err}`);
|
|
metrics.connectionErrors++;
|
|
metrics.failed++;
|
|
finish(false);
|
|
});
|
|
|
|
socket.on('connect', async () => {
|
|
console.log(`[Trip ${tripIndex}] ✅ Driver connected to socket.`);
|
|
|
|
// Register driver location in Redis available pool
|
|
socket.emit('update_location', {
|
|
lat: 31.95,
|
|
lng: 35.91,
|
|
heading: 90,
|
|
speed: 0,
|
|
status: 'off'
|
|
});
|
|
|
|
await new Promise(r => setTimeout(r, 750));
|
|
|
|
// 2. Passenger Requests Ride
|
|
try {
|
|
const addRideFormData = new URLSearchParams({
|
|
passenger_id: String(passengerId),
|
|
start_location: '31.95,35.91',
|
|
end_location: '31.96,35.92',
|
|
price: '5',
|
|
price_token: passengerAuth.price_token || 'dummy',
|
|
distance: '2',
|
|
carType: 'Economy',
|
|
passenger_name: 'Test Passenger',
|
|
passenger_phone: '0790000000',
|
|
start_name: 'Start',
|
|
end_name: 'End'
|
|
});
|
|
|
|
const reqStart = Date.now();
|
|
const res = await axios.post(
|
|
`${argv.baseUrl}/ride/rides/add_ride.php`,
|
|
addRideFormData.toString(),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Bearer ${passengerAuth.jwt}`,
|
|
'X-Device-FP': passengerAuth.fp
|
|
},
|
|
timeout: 10000
|
|
}
|
|
);
|
|
debug(`add_ride response: ${JSON.stringify(res.data).substring(0, 100)}`);
|
|
console.log(`[Trip ${tripIndex}] Passenger requested ride. (API: ${Date.now() - reqStart}ms)`);
|
|
} catch (err) {
|
|
console.error(`[Trip ${tripIndex}] ❌ API Error (add_ride): ${err.message}`);
|
|
if (err.response) debug(`Response: ${JSON.stringify(err.response.data).substring(0, 200)}`);
|
|
metrics.apiErrors++;
|
|
metrics.failed++;
|
|
finish(false);
|
|
return;
|
|
}
|
|
});
|
|
|
|
socket.on('market_new_ride', async (payload) => {
|
|
rideId = payload.id || payload[0]?.id;
|
|
console.log(`[Trip ${tripIndex}] 📨 Received ride offer #${rideId}. Accepting...`);
|
|
|
|
try {
|
|
const acceptFormData = new URLSearchParams({
|
|
id: String(rideId),
|
|
driver_id: String(driverId),
|
|
status: 'accepted'
|
|
});
|
|
|
|
const acceptStart = Date.now();
|
|
await axios.post(
|
|
`${argv.baseUrl}/ride/rides/acceptRide.php`,
|
|
acceptFormData.toString(),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Bearer ${driverAuth.jwt}`,
|
|
'X-Device-FP': driverAuth.fp
|
|
},
|
|
timeout: 10000
|
|
}
|
|
);
|
|
console.log(`[Trip ${tripIndex}] Accepted ride #${rideId}. (API: ${Date.now() - acceptStart}ms)`);
|
|
|
|
// 4. Start Ride API Call
|
|
try {
|
|
const startFormData = new URLSearchParams({ id: String(rideId), driver_id: String(driverId), status: 'Begin' });
|
|
await axios.post(
|
|
`${argv.baseUrl}/ride/rides/start_ride.php`,
|
|
startFormData.toString(),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Bearer ${driverAuth.jwt}`,
|
|
'X-Device-FP': driverAuth.fp
|
|
},
|
|
timeout: 10000
|
|
}
|
|
);
|
|
console.log(`[Trip ${tripIndex}] 🚗 Started ride #${rideId}`);
|
|
} catch (e) {
|
|
debug(`start_ride error: ${e.message}`);
|
|
}
|
|
|
|
// 5. Simulate Driving (4 Random GPS Location Updates)
|
|
let currentLat = 31.95;
|
|
let currentLng = 35.91;
|
|
let steps = 0;
|
|
|
|
const driveInterval = setInterval(async () => {
|
|
steps++;
|
|
currentLat += 0.0005 + (Math.random() * 0.0005);
|
|
currentLng += 0.0005 + (Math.random() * 0.0005);
|
|
|
|
socket.emit('update_location', {
|
|
lat: currentLat,
|
|
lng: currentLng,
|
|
heading: Math.floor(Math.random() * 360),
|
|
speed: 40 + Math.floor(Math.random() * 20),
|
|
status: 'on'
|
|
});
|
|
|
|
console.log(`[Trip ${tripIndex}] 📍 Driver GPS update #${steps}: (${currentLat.toFixed(4)}, ${currentLng.toFixed(4)})`);
|
|
|
|
if (steps >= 4) {
|
|
clearInterval(driveInterval);
|
|
|
|
// 6. Finish Ride API Call
|
|
try {
|
|
const finishFormData = new URLSearchParams({
|
|
rideId: String(rideId),
|
|
driver_id: String(driverId),
|
|
passengerId: String(passengerId),
|
|
status: 'Finished',
|
|
actualDistance: '2.5',
|
|
actualDuration: '10',
|
|
country_code: 'Jordan'
|
|
});
|
|
|
|
await axios.post(
|
|
`${argv.baseUrl}/ride/rides/finish_ride_updates.php`,
|
|
finishFormData.toString(),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Bearer ${driverAuth.jwt}`,
|
|
'X-Device-FP': driverAuth.fp
|
|
},
|
|
timeout: 10000
|
|
}
|
|
);
|
|
console.log(`[Trip ${tripIndex}] 🏁 Finished ride #${rideId} successfully.`);
|
|
} catch (e) {
|
|
debug(`finish_ride error: ${e.message}`);
|
|
}
|
|
|
|
metrics.successful++;
|
|
finish(true);
|
|
}
|
|
}, 500);
|
|
|
|
} catch (err) {
|
|
console.error(`[Trip ${tripIndex}] ❌ API Error (acceptRide): ${err.message}`);
|
|
metrics.apiErrors++;
|
|
metrics.failed++;
|
|
finish(false);
|
|
}
|
|
});
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
debug(`[Trip ${tripIndex}] Disconnected: ${reason}`);
|
|
});
|
|
|
|
// Timeout fallback
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
console.error(`[Trip ${tripIndex}] ⏰ Timeout: No 'market_new_ride' received within 20s.`);
|
|
metrics.failed++;
|
|
finish(false);
|
|
}
|
|
}, 20000);
|
|
});
|
|
}
|
|
|
|
async function runTest() {
|
|
console.log(`\n======================================================`);
|
|
console.log(`🚀 Siro Stress Test`);
|
|
console.log(`======================================================`);
|
|
console.log(`📡 Backend API: ${argv.baseUrl}`);
|
|
console.log(`🔌 Socket URL: ${argv.socketUrl}`);
|
|
console.log(`🔢 Trips: ${argv.trips}`);
|
|
console.log(`🐛 Debug: ${argv.debug ? 'ON' : 'OFF'}`);
|
|
console.log(`------------------------------------------------------`);
|
|
|
|
console.log(`\n🔑 Pre-flight: Testing JWT generation...`);
|
|
const testJwt = getMockJwt(999999, 'driver');
|
|
if (!testJwt.jwt) {
|
|
console.error(`❌ FATAL: Cannot generate JWT tokens.`);
|
|
process.exit(1);
|
|
}
|
|
console.log(` ✅ JWT generation works.`);
|
|
|
|
console.log(`\n🔌 Pre-flight: Testing socket connection...`);
|
|
const testResult = await new Promise((resolve) => {
|
|
const testSocket = io(argv.socketUrl, {
|
|
query: { driver_id: '999999', platform: 'android', jwt: testJwt.jwt },
|
|
reconnection: false,
|
|
timeout: 5000,
|
|
forceNew: true
|
|
});
|
|
testSocket.on('connect', () => {
|
|
console.log(` ✅ Socket connection successful!`);
|
|
testSocket.disconnect();
|
|
resolve(true);
|
|
});
|
|
testSocket.on('connect_error', (err) => {
|
|
console.error(` ❌ Socket connection failed: ${err.message}`);
|
|
testSocket.disconnect();
|
|
resolve(false);
|
|
});
|
|
testSocket.on('error', (err) => {
|
|
console.error(` ❌ Socket error: ${err}`);
|
|
testSocket.disconnect();
|
|
resolve(false);
|
|
});
|
|
setTimeout(() => {
|
|
testSocket.disconnect();
|
|
resolve(false);
|
|
}, 6000);
|
|
});
|
|
|
|
if (!testResult) {
|
|
console.error(`\n❌ Cannot proceed — socket server unreachable. Aborting.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n------------------------------------------------------`);
|
|
console.log(`🏁 Starting ${argv.trips} concurrent trips...\n`);
|
|
|
|
const startTime = Date.now();
|
|
const promises = [];
|
|
|
|
for (let i = 1; i <= argv.trips; i++) {
|
|
promises.push(simulateTrip(i));
|
|
await new Promise(r => setTimeout(r, 50));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
const durationSec = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
const tps = metrics.successful > 0 ? (metrics.successful / durationSec).toFixed(2) : '0.00';
|
|
|
|
console.log(`\n======================================================`);
|
|
console.log(`📊 STRESS TEST RESULTS`);
|
|
console.log(`======================================================`);
|
|
console.log(`⏱️ Duration: ${durationSec}s`);
|
|
console.log(`⚡ Throughput: ${tps} trips/sec`);
|
|
console.log(`✅ Successful: ${metrics.successful} / ${argv.trips}`);
|
|
console.log(`❌ Failed: ${metrics.failed} / ${argv.trips}`);
|
|
if (metrics.failed > 0) {
|
|
console.log(` ├─ JWT Errors: ${metrics.jwtErrors}`);
|
|
console.log(` ├─ Socket Errors: ${metrics.connectionErrors}`);
|
|
console.log(` └─ API Errors: ${metrics.apiErrors}`);
|
|
}
|
|
console.log(`======================================================\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
runTest();
|