added some keys into setting
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ارسال قیمت لحظهای به کانالهای پیامرسان (بله، تلگرام، روبیکا).
|
||||
*
|
||||
* اصول (طبق rule ساعت کاری):
|
||||
* • هیچ تنظیمی را بازنویسی نمیکند — فقط میخواند.
|
||||
* • فقط پیامرسانهایی که ادمین انتخاب کرده و توکن + مقصد دارند ارسال میشود.
|
||||
* • قیمت هر محصول از قیمت پایه ذخیرهشده + کارمزد نمایش همان اپ ساخته میشود.
|
||||
*/
|
||||
class PriceChannelBroadcastService
|
||||
{
|
||||
private const COOLDOWN_CACHE_KEY = 'price_channel:last_sent_at';
|
||||
|
||||
private const SUPPORTED_MESSENGERS = ['bale', 'telegram', 'rubika'];
|
||||
|
||||
/** مثقال بازار آبشده (گرم) — وقتی تنظیم change_mesghal خالی باشد */
|
||||
private const DEFAULT_CHANGE_MESGHAL = 4.3318;
|
||||
|
||||
public function __construct(private readonly ConfigService $config)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* نقطهی ورود — بعد از تغییر قیمت صدا زده میشود.
|
||||
* فقط وقتی قابلیت روشن است، پیامرسانی انتخاب شده و فاصلهی مجاز گذشته باشد ارسال میکند.
|
||||
*/
|
||||
public function broadcastIfEligible(): void
|
||||
{
|
||||
try {
|
||||
if (! $this->config->getBool('price_channel_enabled', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targets = $this->resolveActiveTargets();
|
||||
if ($targets === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->cooldownElapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->buildMessage();
|
||||
if ($message === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($targets as $messenger => $target) {
|
||||
$this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
||||
}
|
||||
|
||||
$this->markSent();
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('PriceChannelBroadcast failed', ['message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* پیامرسانهای قابل ارسال: انتخابشده + توکن معتبر + حداقل یک مقصد.
|
||||
*
|
||||
* @return array<string, array{token: string, chat_ids: array<int, string>}>
|
||||
*/
|
||||
private function resolveActiveTargets(): array
|
||||
{
|
||||
$selected = $this->selectedMessengers();
|
||||
if ($selected === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$targets = [];
|
||||
foreach ($selected as $messenger) {
|
||||
$token = trim((string) $this->config->getString($this->tokenKey($messenger), ''));
|
||||
$chatIds = $this->parseList($this->config->getString($this->chatIdsKey($messenger), ''));
|
||||
|
||||
if ($token === '' || $chatIds === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets[$messenger] = ['token' => $token, 'chat_ids' => $chatIds];
|
||||
}
|
||||
|
||||
return $targets;
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function selectedMessengers(): array
|
||||
{
|
||||
$raw = $this->parseList($this->config->getString('price_channel_messengers', ''));
|
||||
|
||||
return array_values(array_filter(
|
||||
array_map(static fn ($m) => strtolower(trim($m)), $raw),
|
||||
static fn ($m) => in_array($m, self::SUPPORTED_MESSENGERS, true)
|
||||
));
|
||||
}
|
||||
|
||||
private function tokenKey(string $messenger): string
|
||||
{
|
||||
return match ($messenger) {
|
||||
'bale' => 'bale_bot_token',
|
||||
'telegram' => 'telegram_bot_token',
|
||||
'rubika' => 'rubika_bot_token',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function chatIdsKey(string $messenger): string
|
||||
{
|
||||
return "price_channel_{$messenger}_to";
|
||||
}
|
||||
|
||||
/**
|
||||
* ساخت متن پیام قیمت — مرتب و بر اساس دادهی هر محصول.
|
||||
*/
|
||||
public function buildMessage(): string
|
||||
{
|
||||
$products = $this->eligibleProducts();
|
||||
if ($products === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$channelDiff = (float) $this->config->getInt('price_channel_diff', 0);
|
||||
$diffBuy = (float) $this->config->displayBuyPriceDiffRial();
|
||||
$diffSell = (float) $this->config->displaySellPriceDiffRial();
|
||||
$mesghal = $this->changeMesghal();
|
||||
|
||||
$lines = [];
|
||||
$lines[] = $this->header();
|
||||
$lines[] = '';
|
||||
|
||||
foreach ($products as $product) {
|
||||
$base = (float) ($product->price ?? 0);
|
||||
|
||||
// 🔴 خرید (مشتری میفروشد، پایینتر) — 🟢 فروش (مشتری میخرد، بالاتر)
|
||||
$low = $base - $diffSell - $channelDiff;
|
||||
$high = $base + $diffBuy + $channelDiff;
|
||||
|
||||
$lines[] = '🛒 ' . trim((string) $product->title);
|
||||
$lines[] = '🔴 خرید ' . $this->formatPrice($low) . ' 🟢 فروش ' . $this->formatPrice($high);
|
||||
|
||||
if ($this->isGramUnit($product) && $mesghal > 0) {
|
||||
$lines[] = '💰 هر گرم: ' . $this->formatPrice($low / $mesghal)
|
||||
. ' / ' . $this->formatPrice($high / $mesghal);
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
$footer = $this->footerLines();
|
||||
if ($footer !== []) {
|
||||
$lines[] = '➖➖➖➖➖➖';
|
||||
foreach ($footer as $line) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
return trim(implode("\n", $lines));
|
||||
}
|
||||
|
||||
private function header(): string
|
||||
{
|
||||
$name = trim((string) $this->config->getString('name_fa', ''));
|
||||
$time = now()->format('H:i');
|
||||
|
||||
$title = $name !== '' ? "📊 قیمت لحظهای {$name}" : '📊 قیمت لحظهای';
|
||||
|
||||
return $title . "\n🕐 ساعت " . $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Product>
|
||||
*/
|
||||
private function eligibleProducts(): array
|
||||
{
|
||||
$products = Product::query()
|
||||
->where('active', 1)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$eligible = [];
|
||||
foreach ($products as $product) {
|
||||
if ((int) ($product->price ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (! ($product->open_buy || $product->open_sell)) {
|
||||
continue;
|
||||
}
|
||||
if (! $this->config->isProductWithinTradingHours($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$eligible[] = $product;
|
||||
}
|
||||
|
||||
return $eligible;
|
||||
}
|
||||
|
||||
private function isGramUnit(Product $product): bool
|
||||
{
|
||||
return trim((string) ($product->unit ?? '')) === 'گرم';
|
||||
}
|
||||
|
||||
/** قیمت را به واحد نمایش (تقسیم بر ۱۰٬۰۰۰) با جداکنندهی هزارگان میبرد */
|
||||
private function formatPrice(float $value): string
|
||||
{
|
||||
return number_format(max(0, $value) / 10000, 0);
|
||||
}
|
||||
|
||||
private function changeMesghal(): float
|
||||
{
|
||||
$fromConfig = $this->config->get('change_mesghal');
|
||||
$value = (float) ($fromConfig ?: config('change_mesghal', self::DEFAULT_CHANGE_MESGHAL));
|
||||
|
||||
return $value > 0 ? $value : self::DEFAULT_CHANGE_MESGHAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* خطوط پایانی — فقط مقادیر پرشده نمایش داده میشوند (آیدی چنل، شماره، لینک).
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function footerLines(): array
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach (['price_channel_footer_id', 'price_channel_footer_phone', 'price_channel_footer_link'] as $key) {
|
||||
$value = trim((string) $this->config->getString($key, ''));
|
||||
if ($value !== '') {
|
||||
$lines[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function dispatchToMessenger(string $messenger, string $token, array $chatIds, string $message): void
|
||||
{
|
||||
foreach ($chatIds as $chatId) {
|
||||
try {
|
||||
match ($messenger) {
|
||||
'bale' => $this->sendBale($token, $chatId, $message),
|
||||
'telegram' => $this->sendTelegram($token, $chatId, $message),
|
||||
'rubika' => $this->sendRubika($token, $chatId, $message),
|
||||
default => null,
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("PriceChannel {$messenger} send failed", [
|
||||
'chat_id' => $chatId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sendBale(string $token, string $chatId, string $message): void
|
||||
{
|
||||
Http::withOptions(Controller::httpClientOptions())
|
||||
->withoutVerifying()
|
||||
->timeout(6)
|
||||
->post("https://tapi.bale.ai/bot{$token}/sendMessage", [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
private function sendTelegram(string $token, string $chatId, string $message): void
|
||||
{
|
||||
Http::withOptions(Controller::httpClientOptions())
|
||||
->timeout(6)
|
||||
->post("https://api.telegram.org/bot{$token}/sendMessage", [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
private function sendRubika(string $token, string $chatId, string $message): void
|
||||
{
|
||||
Http::withOptions(Controller::httpClientOptions())
|
||||
->withoutVerifying()
|
||||
->timeout(6)
|
||||
->post("https://botapi.rubika.ir/v3/{$token}/sendMessage", [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/** حداقل فاصلهی مجاز بین دو ارسال — جلوگیری از سیل پیام در هر تیک ۱۰ثانیهای */
|
||||
private function cooldownElapsed(): bool
|
||||
{
|
||||
$minSeconds = max(0, $this->config->getInt('price_channel_min_seconds', 120));
|
||||
if ($minSeconds === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$lastSent = Cache::get(self::COOLDOWN_CACHE_KEY);
|
||||
if ($lastSent === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return now()->timestamp - (int) $lastSent >= $minSeconds;
|
||||
}
|
||||
|
||||
private function markSent(): void
|
||||
{
|
||||
Cache::put(self::COOLDOWN_CACHE_KEY, now()->timestamp, now()->addDay());
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function parseList(string $raw): array
|
||||
{
|
||||
$parts = preg_split('/[,\n]+/', trim($raw)) ?: [];
|
||||
|
||||
return array_values(array_filter(array_map('trim', $parts), static fn ($p) => $p !== ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user