fix some handling 3

This commit is contained in:
2026-07-08 12:45:14 +03:30
parent 5781d5bf53
commit a4e98d8047
6 changed files with 311 additions and 61 deletions
+162 -33
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Http\Controllers\Controller;
use App\Models\Product;
use Hekmatinasser\Verta\Verta;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -29,6 +30,22 @@ class PriceChannelBroadcastService
/** وزن هر مثقال به گرم — ضریب تبدیل قیمت هر گرم به هر مثقال (مثقال = گرم × این عدد) */
private const DEFAULT_CHANGE_MESGHAL = 4.3318;
/** قالب پیش‌فرض پیام کانال — وقتی price_channel_message_text خالی باشد */
private const DEFAULT_MESSAGE_TEMPLATE = <<<'TXT'
سلام 👋
%عنوان
📅 %تاریخ 🕐 %ساعت
🔴 خرید گرم: %گرم_خرید
🟢 فروش گرم: %گرم_فروش
🔴 خرید مثقال: %مثقال_خرید
🟢 فروش مثقال: %مثقال_فروش
📞 %شماره
%آیدی
%لینک
TXT;
public function __construct(private readonly ConfigService $config)
{
}
@@ -66,19 +83,19 @@ class PriceChannelBroadcastService
}
$products = $this->eligibleProducts();
if ($products === []) {
Log::debug('PriceChannel: محصول واجد شرایطی نیست (active/قیمت/ساعت کاری)');
$boardProduct = $this->resolveBoardProduct($products);
if ($boardProduct === null || (int) ($boardProduct->price ?? 0) <= 0) {
Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست');
return;
}
// شرط ۲: قیمتِ نمایشی نسبت به آخرین ارسال واقعاً تغییر کرده باشد؛ وگرنه چیزی نفرست
// و تایمر را هم دست نزن (تا سرِ اولین تغییرِ بعدی بلافاصله بفرستد).
$signature = $this->priceSignature($products);
// شرط ۲: قیمت تابلو نسبت به آخرین ارسال واقعاً تغییر کرده باشد
$signature = $this->priceSignature($boardProduct);
if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) {
return;
}
$message = $this->buildMessage($products);
$message = $this->buildMessage($products, $boardProduct);
if ($message === '') {
return;
}
@@ -111,9 +128,16 @@ class PriceChannelBroadcastService
*
* @param array<int, Product> $products
*/
private function priceSignature(array $products): string
private function priceSignature(Product $boardProduct): string
{
return md5($this->buildPriceBody($products));
$prices = $this->computeDisplayPrices($boardProduct);
return md5(implode('|', [
$prices['gram_buy'],
$prices['gram_sell'],
$prices['mesghal_buy'],
$prices['mesghal_sell'],
]));
}
/**
@@ -137,8 +161,9 @@ class PriceChannelBroadcastService
if ($message !== '' && $targets !== []) {
$this->markSent();
$products = $this->eligibleProducts();
if ($products !== []) {
Cache::put(self::SIGNATURE_CACHE_KEY, $this->priceSignature($products), now()->addDay());
$boardProduct = $this->resolveBoardProduct($products);
if ($boardProduct !== null) {
Cache::put(self::SIGNATURE_CACHE_KEY, $this->priceSignature($boardProduct), now()->addDay());
}
}
@@ -239,27 +264,135 @@ class PriceChannelBroadcastService
}
/**
* ساخت متن پیام قیمت — مرتب و بر اساس داده‌ی هر محصول.
* ساخت متن پیام قیمت — قالب از price_channel_message_text (یا پیش‌فرض داخلی).
*/
public function buildMessage(?array $products = null): string
public function buildMessage(?array $products = null, ?Product $boardProduct = null): string
{
$products = $products ?? $this->eligibleProducts();
$body = $this->buildPriceBody($products);
if ($body === '') {
$boardProduct = $boardProduct ?? $this->resolveBoardProduct($products);
if ($boardProduct === null || (int) ($boardProduct->price ?? 0) <= 0) {
return '';
}
return trim($this->header() . "\n\n" . $body);
return $this->renderMessageTemplate($this->messageTemplate(), $products, $boardProduct);
}
private function messageTemplate(): string
{
$fromConfig = trim((string) $this->config->getString('price_channel_message_text', ''));
return $fromConfig !== '' ? $fromConfig : self::DEFAULT_MESSAGE_TEMPLATE;
}
/**
* بدنهٔ قیمت (بدون هدر/ساعت) — همان چیزی که «تغییر قیمت» را می‌سازد.
* امضای تشخیص تغییر روی همین بدنه‌ی نمایشیِ رُندشده گرفته می‌شود تا اختلاف چند ریالی
* که به یک عدد نمایشیِ یکسان می‌رسد، پیام تکراری نفرستد.
* جایگزینی متغیرهای قالب.
* قیمت تابلو: %گرم_خرید %گرم_فروش %مثقال_خرید %مثقال_فروش
* زمان: %تاریخ %ساعت — اپ: %اسم_اپ %نام %عنوان
* تماس: %آیدی %شماره %لینک — اختیاری: %محصولات %فوتر
*
* @param array<int, Product> $products
*/
private function buildPriceBody(array $products): string
private function renderMessageTemplate(string $template, array $products, Product $boardProduct): string
{
$name = trim((string) $this->config->getString('name_fa', ''));
$title = $name !== '' ? "📊 قیمت لحظه‌ای {$name}" : '📊 قیمت لحظه‌ای';
$prices = $this->computeDisplayPrices($boardProduct);
$message = $this->applyTemplatePlaceholders($template, [
'مثقال_فروش' => $prices['mesghal_sell'],
'مثقال_خرید' => $prices['mesghal_buy'],
'گرم_فروش' => $prices['gram_sell'],
'گرم_خرید' => $prices['gram_buy'],
'اسم_اپ' => $name,
'محصولات' => $this->buildProductLines($products),
'عنوان' => $title,
'تاریخ' => Verta::now()->format('Y/n/j'),
'ساعت' => now()->format('H:i'),
'آیدی' => trim((string) $this->config->getString('price_channel_footer_id', '')),
'شماره' => trim((string) $this->config->getString('price_channel_footer_phone', '')),
'لینک' => trim((string) $this->config->getString('price_channel_footer_link', '')),
'فوتر' => $this->buildFooterBlock(),
'نام' => $name,
]);
return preg_replace("/\n{3,}/", "\n\n", trim($message)) ?? trim($message);
}
/** @param array<string, string> $replacements */
private function applyTemplatePlaceholders(string $template, array $replacements): string
{
$keys = array_keys($replacements);
usort($keys, static fn (string $a, string $b): int => mb_strlen($b) <=> mb_strlen($a));
$message = $template;
foreach ($keys as $key) {
$message = preg_replace(
'/[%٪]\s*' . preg_quote($key, '/') . '/u',
$replacements[$key],
$message
) ?? $message;
}
return $message;
}
/**
* محصول مرجع قیمت تابلو — اول آبشده (گرم)، بعد اولین محصول گرم، بعد اولین واجد شرایط.
*
* @param array<int, Product> $products
*/
private function resolveBoardProduct(array $products): ?Product
{
if ($products === []) {
return null;
}
foreach ($products as $product) {
if (trim((string) ($product->group_type ?? '')) === 'آبشده' && $this->isGramUnit($product)) {
return $product;
}
}
foreach ($products as $product) {
if ($this->isGramUnit($product)) {
return $product;
}
}
return $products[0];
}
/**
* قیمت‌های نمایشی تابلو (گرم + مثقال) — همان منطق اپ + price_channel_diff.
*
* @return array{gram_buy: string, gram_sell: string, mesghal_buy: string, mesghal_sell: string}
*/
private function computeDisplayPrices(Product $product): array
{
$channelDiff = (float) $this->config->getInt('price_channel_diff', 0);
$diffBuy = (float) $this->config->displayBuyPriceDiffRial();
$diffSell = (float) $this->config->displaySellPriceDiffRial();
$mesghal = $this->changeMesghal();
$base = (float) ($product->price ?? 0);
// 🔴 خرید (مشتری می‌فروشد، پایین‌تر) — 🟢 فروش (مشتری می‌خرد، بالاتر)
$gramBuy = $base - $diffSell - $channelDiff;
$gramSell = $base + $diffBuy + $channelDiff;
return [
'gram_buy' => $this->formatPrice($gramBuy),
'gram_sell' => $this->formatPrice($gramSell),
'mesghal_buy' => $mesghal > 0 ? $this->formatPrice($gramBuy * $mesghal) : '',
'mesghal_sell' => $mesghal > 0 ? $this->formatPrice($gramSell * $mesghal) : '',
];
}
/**
* بدنهٔ قیمت محصولات (بدون فوتر) — مبنای تشخیص تغییر قیمت.
*
* @param array<int, Product> $products
*/
private function buildProductLines(array $products): string
{
if ($products === []) {
return '';
@@ -283,7 +416,6 @@ class PriceChannelBroadcastService
$lines[] = '🔴 خرید ' . $this->formatPrice($low) . ' 🟢 فروش ' . $this->formatPrice($high);
if ($this->isGramUnit($product) && $mesghal > 0) {
// قیمت پایه «هر گرم» است؛ مثقال = گرم × ضریب (کنوانسیون اپ: مثقال = گرم × 4.3318)
$lines[] = '💰 هر مثقال: ' . $this->formatPrice($low * $mesghal)
. ' / ' . $this->formatPrice($high * $mesghal);
}
@@ -291,25 +423,22 @@ class PriceChannelBroadcastService
$lines[] = '';
}
$footer = $this->footerLines();
if ($footer !== []) {
$lines[] = '➖➖➖➖➖➖';
foreach ($footer as $line) {
$lines[] = $line;
}
}
return trim(implode("\n", $lines));
}
private function header(): string
private function buildFooterBlock(): string
{
$name = trim((string) $this->config->getString('name_fa', ''));
$time = now()->format('H:i');
$footer = $this->footerLines();
if ($footer === []) {
return '';
}
$title = $name !== '' ? "📊 قیمت لحظه‌ای {$name}" : '📊 قیمت لحظه‌ای';
$lines = ['➖➖➖➖➖➖'];
foreach ($footer as $line) {
$lines[] = $line;
}
return $title . "\n🕐 ساعت " . $time;
return implode("\n", $lines);
}
/**