Update: 2026-06-25 03:28:59

This commit is contained in:
Hamza-Ayed
2026-06-25 03:28:59 +03:00
parent 85e85fe4d3
commit 89c1348f08
3 changed files with 188 additions and 73 deletions

View File

@@ -0,0 +1,75 @@
<?php
// ============================================================
// auth/syria/uploadImage.php
// رفع صور وثائق السائق (هوية، رخصة، صورة شخصية، ...)
// يخزّنها في مجلدات حسب الدولة: auth/uploads/{country}/
// ============================================================
require_once __DIR__ . '/../../connect.php';
uploadLog("🚀 [uploadImage.php] Document upload started.");
// --------- قراءة الحقول ---------
$driverID = filterRequest('driverID');
$imageType = filterRequest('imageType');
$country = filterRequest('country');
if (empty($driverID)) {
jsonError('driverID is required.');
}
if (empty($imageType)) {
jsonError('imageType is required.');
}
// --------- تحديد الدولة ---------
$country = strtolower(trim($country ?: ''));
if (empty($country)) {
try {
$stmt = $con->prepare("SELECT country FROM drivers WHERE id = ?");
$stmt->execute([$driverID]);
$country = strtolower(trim((string)$stmt->fetchColumn()));
} catch (Exception $e) {
$country = '';
}
}
if (!in_array($country, ['syria', 'jordan', 'egypt'])) {
$country = 'syria';
}
uploadLog("📥 Params: driverID=$driverID, imageType=$imageType, country=$country");
// --------- رفع الملف ---------
$targetDir = __DIR__ . "/../../auth/uploads/{$country}/";
$result = uploadImageSecure('image', $targetDir, $driverID . '_' . $imageType);
if (!$result['success']) {
uploadLog("❌ Upload failed: {$result['error']}", 'ERROR', [
'driverID' => $driverID,
'imageType' => $imageType,
'country' => $country,
]);
jsonError($result['error']);
}
// --------- بناء الرابط العام ---------
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = getenv('APP_DOMAIN') ?: ($_SERVER['HTTP_HOST'] ?? 'api.siromove.com');
$basePath = rtrim(dirname(dirname(dirname($_SERVER['SCRIPT_NAME']))), '/');
$url = "$protocol://$host{$basePath}/auth/uploads/{$country}/{$result['filename']}";
uploadLog("✅ Uploaded: {$result['path']} -> $url", 'INFO', [
'driverID' => $driverID,
'imageType' => $imageType,
'country' => $country,
]);
jsonSuccess([
'url' => $url,
'file_link' => $url,
'filename' => $result['filename'],
'driverID' => $driverID,
'imageType' => $imageType,
'country' => $country,
]);

View File

@@ -282,13 +282,21 @@ class RegistrationController extends GetxController {
} }
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type /// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
Future<void> choosImage(String link, String imageType) async { Future<void> choosImage(String link, String imageType, {String? backupLink}) async {
if (isloading) return;
try { try {
isloading = true;
update();
final pickedImage = await picker.pickImage( final pickedImage = await picker.pickImage(
source: ImageSource.camera, source: ImageSource.camera,
preferredCameraDevice: CameraDevice.rear, preferredCameraDevice: CameraDevice.rear,
); );
if (pickedImage == null) return; if (pickedImage == null) {
isloading = false;
update();
return;
}
image = File(pickedImage.path); image = File(pickedImage.path);
@@ -305,14 +313,15 @@ class RegistrationController extends GetxController {
IOSUiSettings(title: 'Cropper'.tr), IOSUiSettings(title: 'Cropper'.tr),
], ],
); );
if (croppedFile == null) return; if (croppedFile == null) {
isloading = false;
update();
return;
}
// صورة للمعاينة داخل التطبيق // صورة للمعاينة داخل التطبيق
myImage = File(croppedFile.path); myImage = File(croppedFile.path);
isloading = true;
update();
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت) // ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
final File compressedImage = await compressImage(File(croppedFile.path)); final File compressedImage = await compressImage(File(croppedFile.path));
@@ -320,12 +329,14 @@ class RegistrationController extends GetxController {
final driverId = box.read(BoxName.driverID); final driverId = box.read(BoxName.driverID);
final payload = <String, String>{ final payload = <String, String>{
'driverID': driverId, 'driverID': driverId?.toString() ?? '',
'imageType': imageType, // مثال: driver_license_front 'imageType': imageType,
'country': box.read(BoxName.countryCode) ?? '',
}; };
// الرفع وإرجاع الرابط // الرفع وإرجاع الرابط (يحاول الأساسي ثم الاحتياطي)
final String imageUrl = await uploadImage(compressedImage, payload, link); final String imageUrl =
await uploadImage(compressedImage, payload, link, backupLink: backupLink);
// حفظ الرابط محلياً حسب النوع // حفظ الرابط محلياً حسب النوع
docUrls[imageType] = imageUrl; docUrls[imageType] = imageUrl;
@@ -340,15 +351,22 @@ class RegistrationController extends GetxController {
} }
} }
/// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة في حال فشل الاتصال /// ترفع الملف وترجع رابط الصورة النهائي كـ String مع إعادة المحاولة.
/// يحاول الرابط الأساسي first، فإن فشل جرب الاحتياطي (إن وُجد).
Future<String> uploadImage( Future<String> uploadImage(
File file, Map<String, String> data, String link) async { File file,
int maxRetries = 3; Map<String, String> data,
int attempt = 0; String link, {
while (attempt < maxRetries) { String? backupLink,
attempt++; }) async {
const int maxRetries = 3;
final List<String> urlsToTry = [link, if (backupLink != null && backupLink.trim().isNotEmpty) backupLink];
for (final currentUrl in urlsToTry) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try { try {
final uri = Uri.parse(link); final uri = Uri.parse(currentUrl);
final request = http.MultipartRequest('POST', uri); final request = http.MultipartRequest('POST', uri);
final _jwt = box.read(BoxName.jwt); final _jwt = box.read(BoxName.jwt);
@@ -371,7 +389,6 @@ class RegistrationController extends GetxController {
data.forEach((k, v) => request.fields[k] = v); data.forEach((k, v) => request.fields[k] = v);
// المهلة الزمنية 120 ثانية لتناسب الاتصالات الضعيفة
final streamed = final streamed =
await request.send().timeout(const Duration(seconds: 120)); await request.send().timeout(const Duration(seconds: 120));
final res = await http.Response.fromStream(streamed); final res = await http.Response.fromStream(streamed);
@@ -394,14 +411,19 @@ class RegistrationController extends GetxController {
return url; return url;
} catch (e) { } catch (e) {
Log.print("⚠️ [Image Upload Attempt $attempt Failed] Error: $e"); Log.print("⚠️ [Upload Attempt $attempt/$maxRetries @ $currentUrl] Error: $e");
if (attempt >= maxRetries) { if (attempt >= maxRetries) {
if (urlsToTry.last == currentUrl) {
rethrow; rethrow;
} }
Log.print("➡️ Switching to backup URL...");
} else {
await Future.delayed(Duration(seconds: attempt * 2)); await Future.delayed(Duration(seconds: attempt * 2));
} }
} }
throw Exception('Upload failed after $maxRetries attempts'); }
}
throw Exception('Upload failed after all attempts');
} }
Future<File> compressImage(File file) async { Future<File> compressImage(File file) async {

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/auth/syria/registration_controller.dart'; import '../../../controller/auth/syria/registration_controller.dart';
import '../../../main.dart'; import '../../../main.dart';
@@ -116,8 +117,17 @@ class RegistrationView extends StatelessWidget {
if (v == null || v.isEmpty) { if (v == null || v.isEmpty) {
return 'Required field'.tr; return 'Required field'.tr;
} }
if (v.length != 11) { final country = box.read(BoxName.countryCode) ?? 'Jordan';
return 'National ID must be 11 digits'.tr; int expected;
if (country == 'Syria') {
expected = 11;
} else if (country == 'Egypt') {
expected = 12;
} else {
expected = 10; // Jordan
}
if (v.length != expected) {
return 'National ID must be $expected digits'.tr;
} }
return null; return null;
}, },
@@ -355,7 +365,9 @@ class RegistrationView extends StatelessWidget {
// STEP 3 // STEP 3
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) { Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
final String linkUpload = final String primaryLink =
'${AppLink.server}/auth/syria/uploadImage.php';
const String backupLink =
'https://syria.intaleq.xyz/siro/auth/syria/uploadImage.php'; 'https://syria.intaleq.xyz/siro/auth/syria/uploadImage.php';
return GetBuilder<RegistrationController>( return GetBuilder<RegistrationController>(
@@ -371,45 +383,51 @@ class RegistrationView extends StatelessWidget {
_buildImagePickerBox( _buildImagePickerBox(
'National ID (Front)'.tr, 'National ID (Front)'.tr,
ctrl.docUrls['id_front'], ctrl.docUrls['id_front'],
() async => await ctrl.choosImage(linkUpload, 'id_front'), () async =>
await ctrl.choosImage(primaryLink, 'id_front', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
'National ID (Back)'.tr, 'National ID (Back)'.tr,
ctrl.docUrls['id_back'], ctrl.docUrls['id_back'],
() async => await ctrl.choosImage(linkUpload, 'id_back'), () async =>
await ctrl.choosImage(primaryLink, 'id_back', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
'Driver License (Front)'.tr, 'Driver License (Front)'.tr,
ctrl.docUrls['driver_license'], ctrl.docUrls['driver_license'],
() async => await ctrl.choosImage(linkUpload, 'driver_license'), () async =>
await ctrl.choosImage(primaryLink, 'driver_license', backupLink: backupLink),
), ),
if (box.read(BoxName.countryCode) == 'Syria') if (box.read(BoxName.countryCode) == 'Syria')
_buildImagePickerBox( _buildImagePickerBox(
'Driver License (Back)'.tr, 'Driver License (Back)'.tr,
ctrl.docUrls['driver_license_back'], ctrl.docUrls['driver_license_back'],
() async => () async =>
await ctrl.choosImage(linkUpload, 'driver_license_back'), await ctrl.choosImage(primaryLink, 'driver_license_back', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
'Profile Picture'.tr, 'Profile Picture'.tr,
ctrl.docUrls['profile_picture'], ctrl.docUrls['profile_picture'],
() async => await ctrl.choosImage(linkUpload, 'profile_picture'), () async =>
await ctrl.choosImage(primaryLink, 'profile_picture', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
ctrl.getCriminalRecordTitle(), ctrl.getCriminalRecordTitle(),
ctrl.docUrls['criminal_record'], ctrl.docUrls['criminal_record'],
() async => await ctrl.choosImage(linkUpload, 'criminal_record'), () async =>
await ctrl.choosImage(primaryLink, 'criminal_record', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
'Car Registration (Front)'.tr, 'Car Registration (Front)'.tr,
ctrl.docUrls['car_license_front'], ctrl.docUrls['car_license_front'],
() async => () async =>
await ctrl.choosImage(linkUpload, 'car_license_front'), await ctrl.choosImage(primaryLink, 'car_license_front', backupLink: backupLink),
), ),
_buildImagePickerBox( _buildImagePickerBox(
'Car Registration (Back)'.tr, 'Car Registration (Back)'.tr,
ctrl.docUrls['car_license_back'], ctrl.docUrls['car_license_back'],
() async => await ctrl.choosImage(linkUpload, 'car_license_back'), () async =>
await ctrl.choosImage(primaryLink, 'car_license_back', backupLink: backupLink),
), ),
], ],
), ),