fix some issu
This commit is contained in:
@@ -76,12 +76,14 @@ class Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* گزینههای Guzzle/cURL — فقط وقتی افزونه curl و ثابتها موجود باشند.
|
||||
* Direct connect (no local VPN/proxy) + IPv4 + HTTP/1.1:
|
||||
* dead 127.0.0.1 proxies, broken AAAA, and flaky HTTP/2 cause
|
||||
* "Connection refused" / stream errors while browsers still work.
|
||||
*
|
||||
* @param array<int, mixed> $curlOptions
|
||||
* @return array<string, array<int, mixed>>
|
||||
* @param array<int, mixed> $curlOptions
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function httpClientOptions(array $curlOptions = []): array
|
||||
public static function httpClientOptions(array $curlOptions = [], ?float $timeout = null): array
|
||||
{
|
||||
$curl = $curlOptions;
|
||||
|
||||
@@ -92,7 +94,24 @@ class Controller extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
return $curl === [] ? [] : ['curl' => $curl];
|
||||
if (defined('CURLOPT_HTTP_VERSION') && defined('CURL_HTTP_VERSION_1_1')) {
|
||||
$verOpt = (int) constant('CURLOPT_HTTP_VERSION');
|
||||
if (! array_key_exists($verOpt, $curl)) {
|
||||
$curl[$verOpt] = (int) constant('CURL_HTTP_VERSION_1_1');
|
||||
}
|
||||
}
|
||||
|
||||
$options = ['proxy' => false];
|
||||
|
||||
if ($timeout !== null) {
|
||||
$options['connect_timeout'] = min(5.0, max(2.0, (float) $timeout));
|
||||
}
|
||||
|
||||
if ($curl !== []) {
|
||||
$options['curl'] = $curl;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function Send($to , $msg, $is_customer = true)
|
||||
|
||||
@@ -52,7 +52,12 @@ class PriceStreamController extends Controller
|
||||
}
|
||||
|
||||
$cache = app(LivePriceCacheService::class);
|
||||
$last = $cache->get() ?? $cache->buildFromDatabase();
|
||||
$config = app(\App\Services\ConfigService::class);
|
||||
$last = $cache->get();
|
||||
// کش ممکن است no_quote صفرِ کوتاهعمر داشته باشد؛ با build از montzar_live_price جبران کن
|
||||
if (! is_array($last) || ! $config->payloadHasDisplayablePrice($last)) {
|
||||
$last = $cache->buildFromDatabase();
|
||||
}
|
||||
if (is_array($last) && $last !== []) {
|
||||
if ($customerId > 0) {
|
||||
$last = app(\App\Services\CustomerSpecialPricingService::class)
|
||||
|
||||
@@ -327,19 +327,19 @@ class ReportController extends Controller
|
||||
|
||||
$config->UpdateConfig('montzar_closed', 'false');
|
||||
} catch (\Throwable $e) {
|
||||
$montzarLivePrice = 0;
|
||||
|
||||
\Log::error('MONZAR PRICE ERROR', [
|
||||
'message' => $e->getMessage(),
|
||||
'montzar_product_id' => $montzarProductId ?? null,
|
||||
'montzar_response' => $montzar ?? null,
|
||||
]);
|
||||
|
||||
// قطع لحظهای همکار → قیمت ذخیرهشده را نگه دار و دوباره در تیک بعدی تلاش کن
|
||||
$config->applyMontzarSyncFailure();
|
||||
if (!$config->getBool('montzar_closed', true)) {
|
||||
$stale = $config->getInt('montzar_live_price', 0);
|
||||
$montzarLivePrice = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
||||
}
|
||||
$stale = $config->getInt('montzar_live_price', 0);
|
||||
$montzarLivePrice = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
||||
$montzarFlags = $config->montzarOpenFlags();
|
||||
$montzar_open_buy = $montzarFlags['open_buy'];
|
||||
$montzar_open_sell = $montzarFlags['open_sell'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,12 +584,9 @@ class ReportController extends Controller
|
||||
|
||||
$config->UpdateConfig('montzar_closed', 'false');
|
||||
} catch (\Throwable $e) {
|
||||
$montzar = 0;
|
||||
$config->applyMontzarSyncFailure();
|
||||
if (!$config->getBool('montzar_closed', true)) {
|
||||
$stale = $config->getInt('montzar_live_price', 0);
|
||||
$montzar = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
||||
}
|
||||
$stale = $config->getInt('montzar_live_price', 0);
|
||||
$montzar = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
||||
\Log::error('STORE MONZAR PRICE ERROR', ['message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -65,6 +66,7 @@ class ChatWebhookService
|
||||
for ($i = 1; $i <= $attempts; $i++) {
|
||||
try {
|
||||
$response = Http::timeout(8)
|
||||
->withOptions(Controller::httpClientOptions([], 8.0))
|
||||
->retry(0)
|
||||
->post('https://'.$host.'/api/projects/set-webhook', [
|
||||
'api_key' => $chatApiKey,
|
||||
|
||||
@@ -35,6 +35,20 @@ class LivePriceCacheService
|
||||
{
|
||||
$cached = Cache::get(self::FULL_RESPONSE_KEY);
|
||||
$cachedBody = is_array($cached['body'] ?? null) ? $cached['body'] : null;
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
// no_quote صفر را روی مظنهٔ معتبر قبلی ننویس — قطع فید لحظهای نباید UI را خالی کند
|
||||
$incomingEmpty = ! $config->payloadHasDisplayablePrice($body)
|
||||
&& (string) ($body['status'] ?? '') === 'no_quote';
|
||||
$existingGood = is_array($cachedBody) && $config->payloadHasDisplayablePrice($cachedBody);
|
||||
if ($incomingEmpty && $existingGood) {
|
||||
Cache::put(self::FULL_RESPONSE_KEY, [
|
||||
'body' => $cachedBody,
|
||||
'stored_at' => now(),
|
||||
], now()->addSeconds(self::TTL_SECONDS + 5));
|
||||
|
||||
return $cachedBody;
|
||||
}
|
||||
|
||||
if ($cachedBody !== null && PriceBroadcastService::lastPriceIdentity($cachedBody) === PriceBroadcastService::lastPriceIdentity($body)) {
|
||||
Cache::put(self::FULL_RESPONSE_KEY, [
|
||||
@@ -95,7 +109,7 @@ class LivePriceCacheService
|
||||
return now()->diffInSeconds($lastTick) <= self::TICK_MAX_AGE_SECONDS;
|
||||
}
|
||||
|
||||
/** پاسخ سریع بدون API خارجی — فقط وقتی کران اخیراً tick زده (همان فرمول lastPrice) */
|
||||
/** پاسخ سریع بدون API خارجی — از montzar_live_price / آخرین ردیف prices */
|
||||
public function buildFromDatabase(): array
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
@@ -104,6 +118,28 @@ class LivePriceCacheService
|
||||
$base = $last ? (float) $last->price : 0.0;
|
||||
$provider = $config->priceProvider();
|
||||
|
||||
$montzarSource = 0;
|
||||
$zaarboxSource = 0;
|
||||
$mainProductSource = 0;
|
||||
$priceSource = 'database_last';
|
||||
|
||||
if ($provider === 'montzar') {
|
||||
$liveMontzar = (float) $config->get('montzar_live_price', 0);
|
||||
if ($liveMontzar > 0 && $config->isPlausibleQuotePrice($liveMontzar)) {
|
||||
$base = $liveMontzar;
|
||||
$montzarSource = (int) $liveMontzar;
|
||||
$priceSource = 'montzar';
|
||||
}
|
||||
} elseif ($provider === 'zaarbox' && $base > 0) {
|
||||
$zaarboxSource = (int) $base;
|
||||
$priceSource = 'zaarbox';
|
||||
} elseif ($provider === 'main_product' && $base > 0) {
|
||||
$mainProductSource = (int) $base;
|
||||
$priceSource = 'main_product';
|
||||
}
|
||||
|
||||
// قبلاً فقط به جدول prices نگاه میکرد؛ اگر خالی بود SSE همان لحظه no_quote صفر
|
||||
// میفرستاد و UI مظنهٔ معتبر HTTP را پاک میکرد — در حالی که montzar_live_price پر بود.
|
||||
if ($base <= 0) {
|
||||
$payload = [
|
||||
'sell' => 0,
|
||||
@@ -119,27 +155,6 @@ class LivePriceCacheService
|
||||
'time' => now(),
|
||||
];
|
||||
} else {
|
||||
$montzarSource = 0;
|
||||
$zaarboxSource = 0;
|
||||
$mainProductSource = 0;
|
||||
$priceSource = 'database_last';
|
||||
|
||||
if ($provider === 'montzar') {
|
||||
$liveMontzar = (float) $config->get('montzar_live_price', 0);
|
||||
if ($liveMontzar > 0 && $config->isPlausibleQuotePrice($liveMontzar)) {
|
||||
$base = $liveMontzar;
|
||||
$montzarSource = (int) $liveMontzar;
|
||||
$priceSource = 'montzar';
|
||||
} else {
|
||||
$montzarSource = 0;
|
||||
$priceSource = 'database_last';
|
||||
}
|
||||
} elseif ($provider === 'zaarbox') {
|
||||
$zaarboxSource = (int) $base;
|
||||
} elseif ($provider === 'main_product') {
|
||||
$mainProductSource = (int) $base;
|
||||
}
|
||||
|
||||
$trade = $config->buildTradeDisplayPrices($base, $priceSource);
|
||||
|
||||
$payload = [
|
||||
|
||||
@@ -31,11 +31,22 @@ class MontzarService
|
||||
return $this->baseUrl() . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
/** Pending HTTP با همان گزینههای پایدار (بدون پروکسی / IPv4 / HTTP1.1) */
|
||||
private function http(float $timeout = 10)
|
||||
{
|
||||
$extraCurl = defined('CURLOPT_TCP_FASTOPEN')
|
||||
? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1]
|
||||
: [];
|
||||
|
||||
return Http::timeout($timeout)
|
||||
->acceptJson()
|
||||
->withOptions(Controller::httpClientOptions($extraCurl, $timeout));
|
||||
}
|
||||
|
||||
public function montzarLogin(): string
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->acceptJson()
|
||||
$response = $this->http(10)
|
||||
->post($this->endpoint('/server/auth/login'), [
|
||||
'mobile' => $this->config()->Config('montzar_mobile'),
|
||||
'password' => $this->config()->Config('montzar_password'),
|
||||
@@ -89,25 +100,57 @@ class MontzarService
|
||||
}
|
||||
|
||||
public function productList(): array
|
||||
{
|
||||
$attempts = 3;
|
||||
$lastError = null;
|
||||
|
||||
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
|
||||
try {
|
||||
return $this->productListOnce();
|
||||
} catch (\Throwable $e) {
|
||||
$lastError = $e;
|
||||
Log::warning('Montzar productList attempt failed', [
|
||||
'attempt' => $attempt,
|
||||
'max' => $attempts,
|
||||
'base_url' => $this->baseUrl(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
if ($attempt < $attempts) {
|
||||
usleep(250_000 * $attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw $lastError ?? new \RuntimeException('Montzar product list failed');
|
||||
}
|
||||
|
||||
private function productListOnce(): array
|
||||
{
|
||||
$token = $this->getToken();
|
||||
|
||||
$response = Http::timeout(7)
|
||||
->acceptJson()
|
||||
->withOptions(Controller::httpClientOptions(
|
||||
defined('CURLOPT_TCP_FASTOPEN') ? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1] : []
|
||||
))
|
||||
->withToken($token)
|
||||
->get($this->endpoint('/server/customer/product/list'))
|
||||
->json();
|
||||
try {
|
||||
$response = $this->http(7)
|
||||
->withToken($token)
|
||||
->get($this->endpoint('/server/customer/product/list'))
|
||||
->throw()
|
||||
->json();
|
||||
} catch (\Throwable $e) {
|
||||
// توکن مرده / قطع شبکه — یک بار لاگین مجدد
|
||||
$token = $this->montzarLogin();
|
||||
$response = $this->http(7)
|
||||
->withToken($token)
|
||||
->get($this->endpoint('/server/customer/product/list'))
|
||||
->throw()
|
||||
->json();
|
||||
}
|
||||
|
||||
if ((isset($response['message']) && $response['message'] === 'no authorised') || $response === null) {
|
||||
$token = $this->montzarLogin();
|
||||
|
||||
$response = Http::timeout(7)
|
||||
->acceptJson()
|
||||
$response = $this->http(7)
|
||||
->withToken($token)
|
||||
->get($this->endpoint('/server/customer/product/list'))
|
||||
->throw()
|
||||
->json();
|
||||
}
|
||||
|
||||
@@ -263,9 +306,7 @@ class MontzarService
|
||||
|
||||
private function sendColleagueLedgerHttp(string $method, string $url, string $token, ?array $payload)
|
||||
{
|
||||
$pending = Http::timeout(30)
|
||||
->acceptJson()
|
||||
->withToken($token);
|
||||
$pending = $this->http(30)->withToken($token);
|
||||
|
||||
if (strtoupper($method) === 'GET') {
|
||||
return $pending->get($url, $payload ?? []);
|
||||
@@ -368,8 +409,7 @@ class MontzarService
|
||||
|
||||
private function postToMontzar(string $url, string $token, array $payload)
|
||||
{
|
||||
return Http::timeout(15)
|
||||
->acceptJson()
|
||||
return $this->http(15)
|
||||
->withToken($token)
|
||||
->post($url, $payload);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,14 @@ class PriceBroadcastService
|
||||
$lastPayload = app(ConfigService::class)->applyHomePriceDisplayState($lastPayload);
|
||||
$lastPayload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($lastPayload);
|
||||
|
||||
// no_quote صفر را روی SSE پخش نکن — کلاینت مظنهٔ معتبر را پاک میکرد
|
||||
// در حالی که /api/price/last هنوز قیمت داشت.
|
||||
if (! app(ConfigService::class)->payloadHasDisplayablePrice($lastPayload)
|
||||
&& (string) ($lastPayload['status'] ?? '') === 'no_quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->rememberHashIfChanged(self::LAST_HASH_KEY, self::lastPriceIdentity($lastPayload))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -775,7 +775,7 @@ TXT;
|
||||
|
||||
private function sendBale(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response
|
||||
{
|
||||
return Http::withOptions(Controller::httpClientOptions())
|
||||
return Http::withOptions(Controller::httpClientOptions([], 6.0))
|
||||
->withoutVerifying()
|
||||
->timeout(6)
|
||||
->post("https://tapi.bale.ai/bot{$token}/sendMessage", [
|
||||
@@ -787,7 +787,7 @@ TXT;
|
||||
private function sendTelegram(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response
|
||||
{
|
||||
// تلگرام مستقیم از سرور اغلب فیلتر است — از پروکسی wwwapp استفاده میشود (مثل کد قدیمی)
|
||||
return Http::withOptions(Controller::httpClientOptions())
|
||||
return Http::withOptions(Controller::httpClientOptions([], 6.0))
|
||||
->timeout(6)
|
||||
->get('https://api.wwwapp.ir/telegran_send.php', [
|
||||
'chatId' => $chatId,
|
||||
@@ -798,7 +798,7 @@ TXT;
|
||||
|
||||
private function sendRubika(string $token, string $chatId, string $message): \Illuminate\Http\Client\Response
|
||||
{
|
||||
return Http::withOptions(Controller::httpClientOptions())
|
||||
return Http::withOptions(Controller::httpClientOptions([], 6.0))
|
||||
->withoutVerifying()
|
||||
->timeout(6)
|
||||
->post("https://botapi.rubika.ir/v3/{$token}/sendMessage", [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -179,7 +180,10 @@ class ProductPriceSyncService
|
||||
|
||||
$CP = null;
|
||||
try {
|
||||
$cooper = Http::timeout(8)->get('https://www.livedata.ir/main/static/p.json', [])->json();
|
||||
$cooper = Http::timeout(8)
|
||||
->withOptions(Controller::httpClientOptions([], 8.0))
|
||||
->get('https://www.livedata.ir/main/static/p.json', [])
|
||||
->json();
|
||||
if (isset($cooper['s_301006'], $cooper['s_200103'])) {
|
||||
$CP = (($cooper['s_301006'] / 1000000) * $cooper['s_200103']) * 10;
|
||||
}
|
||||
@@ -238,6 +242,7 @@ class ProductPriceSyncService
|
||||
|
||||
try {
|
||||
$RPrices = Http::timeout(8)
|
||||
->withOptions(Controller::httpClientOptions([], 8.0))
|
||||
->get('http://212.23.201.173:8381/price_list', [])
|
||||
->json();
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -26,12 +26,8 @@ class CentralSmsApiProvider implements SmsProviderInterface
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181',
|
||||
]);
|
||||
|
||||
$curlOptions = Controller::httpClientOptions();
|
||||
if ($curlOptions !== []) {
|
||||
$http = $http->withOptions($curlOptions);
|
||||
}
|
||||
])
|
||||
->withOptions(Controller::httpClientOptions([], 15.0));
|
||||
|
||||
$host = trim((string) ($config['name_fa'] ?? ''));
|
||||
if ($host === '') {
|
||||
|
||||
@@ -29,7 +29,7 @@ class MeliPayamakProvider implements SmsProviderInterface
|
||||
];
|
||||
|
||||
$handle = curl_init('https://rest.payamak-panel.com/api/SendSMS/SendSMS');
|
||||
curl_setopt_array($handle, [
|
||||
$opts = [
|
||||
CURLOPT_HTTPHEADER => ['content-type: application/x-www-form-urlencoded'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
@@ -37,7 +37,16 @@ class MeliPayamakProvider implements SmsProviderInterface
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_PROXY => '',
|
||||
];
|
||||
if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) {
|
||||
$opts[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
|
||||
}
|
||||
if (defined('CURLOPT_HTTP_VERSION') && defined('CURL_HTTP_VERSION_1_1')) {
|
||||
$opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
|
||||
}
|
||||
curl_setopt_array($handle, $opts);
|
||||
|
||||
$response = curl_exec($handle);
|
||||
$error = curl_error($handle);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\SmsBilling;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -204,7 +205,8 @@ class SuperAdminSmsClient
|
||||
try {
|
||||
$pending = Http::withHeaders($this->authHeaders($method, $canonicalPath))
|
||||
->acceptJson()
|
||||
->timeout($timeout);
|
||||
->timeout($timeout)
|
||||
->withOptions(Controller::httpClientOptions([], (float) $timeout));
|
||||
|
||||
$url = $this->urlForBase($base, $relativeUrl);
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class WebsocketBroadcastService
|
||||
try {
|
||||
$response = Http::timeout(3)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->withOptions(\App\Http\Controllers\Controller::httpClientOptions())
|
||||
->withOptions(\App\Http\Controllers\Controller::httpClientOptions([], 3.0))
|
||||
->post($url, $body);
|
||||
|
||||
if (! $response->successful()) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
@@ -120,6 +121,7 @@ class ZaarboxPriceService
|
||||
try {
|
||||
$prices = Http::withoutVerifying()
|
||||
->timeout(12)
|
||||
->withOptions(Controller::httpClientOptions([], 12.0))
|
||||
->get($url)
|
||||
->json();
|
||||
|
||||
@@ -543,7 +545,11 @@ class ZaarboxPriceService
|
||||
|
||||
try {
|
||||
if ($apiUrl !== null && trim($apiUrl) !== '') {
|
||||
$prices = Http::withoutVerifying()->timeout(12)->get($apiUrl)->json();
|
||||
$prices = Http::withoutVerifying()
|
||||
->timeout(12)
|
||||
->withOptions(Controller::httpClientOptions([], 12.0))
|
||||
->get($apiUrl)
|
||||
->json();
|
||||
} else {
|
||||
$prices = $this->fetchFeedWithFailover();
|
||||
}
|
||||
@@ -804,7 +810,10 @@ class ZaarboxPriceService
|
||||
$responses = Http::pool(function ($pool) use ($now) {
|
||||
foreach (self::PERIODS as $key => $config) {
|
||||
$start = $now->copy()->subHours($config['hours']);
|
||||
$pool->as($key)->withoutVerifying()->timeout(12)->withHeaders([
|
||||
$pool->as($key)->withoutVerifying()
|
||||
->timeout(12)
|
||||
->withOptions(Controller::httpClientOptions([], 12.0))
|
||||
->withHeaders([
|
||||
'Referer' => 'https://tablo.montest.ir/',
|
||||
'User-Agent' => 'Mozilla/5.0',
|
||||
])->get($this->historyUrl($start, $now));
|
||||
|
||||
Reference in New Issue
Block a user