fix some issu

This commit is contained in:
2026-08-26 21:47:09 +03:30
parent 74cc50149f
commit 40b185187c
14 changed files with 179 additions and 72 deletions
+24 -5
View File
@@ -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)
+8 -11
View File
@@ -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
View File
@@ -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,
+37 -22
View File
@@ -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 = [
+57 -17
View File
@@ -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);
}
+8
View File
@@ -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", [
+6 -1
View File
@@ -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);
+1 -1
View File
@@ -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()) {
+11 -2
View File
@@ -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));