getBool('group_sms', false)) { throw new UnprocessableEntityHttpException('پیامک گروهی غیرفعال است.'); } $numbers = array_values(array_unique(array_filter( array_map('strval', (array) ($request->numbers ?? [])), static fn ($n) => trim($n) !== '' ))); if ($numbers === []) { throw new UnprocessableEntityHttpException('حداقل یک شماره لازم است.'); } // هر درخواست حداکثر ۱۰ شماره — ادمین باید دسته‌ای بفرستد تا پنل پیامک زیر فشار نرود if (count($numbers) > 10) { throw new UnprocessableEntityHttpException('در هر درخواست حداکثر ۱۰ شماره مجاز است.'); } // یافتن مشتریان با شماره‌های داده شده — فقط مشتریان پنل $customers = Customer::whereIn('mobile', $numbers) ->where('type', 2) ->where('is_archive', false) ->get(); $known = $customers->pluck('mobile')->map(static fn ($m) => (string) $m)->all(); $unknown = array_values(array_diff($numbers, $known)); if ($unknown !== []) { throw new UnprocessableEntityHttpException( 'فقط به مشتریان پنل می‌توان پیامک گروهی فرستاد. شماره خارج از لیست مشتری مجاز نیست.' ); } $filter = app(SmsProfanityFilter::class); $filter->assertClean((string) ($request->message ?? '')); $cfg = app(ConfigService::class); $appName = trim((string) ($cfg->get('name_fa', '') ?: '')); $rawMessage = $this->ensureAppNameFooter((string) ($request->message ?? ''), $appName); // پیامک گروهی باید از گلوگاه billing رد شود و از اعتبار کم شود $billingOk = SmsWalletService::isBillingOperational() || app(\App\Services\SmsBilling\SuperAdminSmsClient::class)->enabled(); if (! $billingOk) { throw new UnprocessableEntityHttpException( 'پنل پیامک (billing) فعال نیست؛ ارسال گروهی بدون کسر اعتبار مجاز نیست.' ); } foreach ($customers as $customer) { $message = $this->applyCustomerNamePlaceholders($rawMessage, (string) $customer->name); $this->sendBilledSms((string) $customer->mobile, $message, 'group_sms'); } return response()->json(true); } /** * پیامک تکی به یک مشتری — فقط وقتی customer_direct_sms روشن باشد. */ public function send_customer_message(Request $request) { if (! app(ConfigService::class)->getBool('customer_direct_sms', false)) { throw new UnprocessableEntityHttpException('ارسال پیامک تکی به مشتری غیرفعال است.'); } $customerId = (int) ($request->input('customer_id') ?? 0); $rawMessage = trim((string) ($request->input('message') ?? '')); if ($customerId <= 0) { throw new UnprocessableEntityHttpException('مشتری مشخص نشده است.'); } if ($rawMessage === '') { throw new UnprocessableEntityHttpException('متن پیامک خالی است.'); } if (mb_strlen($rawMessage) > 700) { throw new UnprocessableEntityHttpException('متن پیامک بیش از حد طولانی است.'); } app(SmsProfanityFilter::class)->assertClean($rawMessage); $customer = Customer::find($customerId); if (! $customer || ! $customer->mobile) { throw new UnprocessableEntityHttpException('مشتری یا شماره موبایل یافت نشد.'); } $cfg = app(ConfigService::class); $appName = (string) ($cfg->get('name_fa', '') ?: ''); $appUrl = $cfg->resolvePublicSiteUrl($request); $message = str_replace( ['%app_name%', '%نام_اپ%', '%app_url%', '%لینک_اپ%', '%لینک%'], [ $appName, $appName, $appUrl, $appUrl, $appUrl, ], $rawMessage ); $message = $this->applyCustomerNamePlaceholders($message, (string) $customer->name); $message = $this->ensureAppNameFooter($message, $appName); $this->send($customer->mobile, $message); return response()->json(['ok' => true]); } public function getFunctionDescriptions() { return response()->json([ [ 'function' => 'getAllCustomers', 'name' => 'همه کاربران', ], [ 'function' => 'getNoLoginCustomers', 'name' => 'کاربران وارد نشده', ], [ 'function' => 'getNoValidCustomers', 'name' => 'کاربران احراز نشده', ], [ 'function' => 'getActiveCustomers', 'name' => 'کاربران فعال', ], [ 'function' => 'getInactiveLast1Month', 'name' => 'یک ماه اخیر وارد نشده', ], [ 'function' => 'getInactiveLast3Months', 'name' => 'سه ماه اخیر وارد نشده', ], [ 'function' => 'getInactiveLast6Months', 'name' => 'شش ماه گذشته وارد نشده', ], [ 'function' => 'getInactiveLast1Year', 'name' => 'سال گذشته وارد نشده', ], ]); } public function getCustomerList(Request $request) { $customers = $this->resolveCustomersByFunctions((array) ($request->functions ?? [])); $uniqueCustomers = $this->removeDuplicateNumbers($customers); return response()->json(['customers' => $uniqueCustomers]); } public function getCustomerListTable(Request $request) { $customers = $this->resolveCustomersByFunctions((array) ($request->functions ?? [])); $uniqueCustomers = $this->removeDuplicateNumbers($customers); $uniqueCustomersCollection = collect($uniqueCustomers); $records = Customer::whereIn('id', $uniqueCustomersCollection->pluck('id')); // Initialize TableEngine TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ 'id','mobile', 'name' ]) ->export($records); return response()->json($records); } /** * @param list $actions * @return list */ private function resolveCustomersByFunctions(array $actions): array { $customers = []; foreach ($actions as $action) { $customers = array_merge($customers, match ($action) { 'getAllCustomers' => $this->getAllCustomers(), 'getNoLoginCustomers' => $this->getNoLoginCustomers(), 'getNoValidCustomers' => $this->getNoValidCustomers(), 'getActiveCustomers' => $this->getActiveCustomers(), 'getInactiveLast1Month' => $this->getInactiveSinceMonths(1), 'getInactiveLast3Months' => $this->getInactiveSinceMonths(3), 'getInactiveLast6Months' => $this->getInactiveSinceMonths(6), 'getInactiveLast1Year' => $this->getInactiveSinceMonths(12), default => [], }); } return $customers; } private function getAllCustomers() { return Customer::where('type', 2) ->where('is_archive', false) ->get(['id', 'name', 'mobile']) ->toArray(); } private function getNoLoginCustomers() { return Customer::where('type', 2) ->where('is_archive', false) ->whereNull('last_seen_at') ->get(['id', 'name', 'mobile']) ->toArray(); } private function getNoValidCustomers() { return Customer::where('type', 2) ->where('is_archive', false) ->where('is_legal', 0) ->get(['id', 'name', 'mobile']) ->toArray(); } private function getActiveCustomers() { return Customer::where('type', 2) ->where('is_archive', false) ->whereIn('id', function ($query) { $query->select('owner') ->from('wallets'); }) ->get(['id', 'name', 'mobile']) ->toArray(); } /** * مشتریان خانگی که در N ماه اخیر وارد نشده‌اند (یا هرگز وارد نشده‌اند). * * @return list */ private function getInactiveSinceMonths(int $months): array { $cutoff = now()->subMonths($months); return Customer::where('type', 2) ->where('is_archive', false) ->where(function ($q) use ($cutoff) { $q->whereNull('last_seen_at') ->orWhere('last_seen_at', '<', $cutoff); }) ->get(['id', 'name', 'mobile']) ->toArray(); } /** * ارسال از گلوگاه پنل پیامک با کسر اجباری اعتبار؛ در صورت مسدودی/شکست، خطا برمی‌گردد. */ private function sendBilledSms(string $mobile, string $message, string $source): void { $text = trim($message); if ($text === '') { throw new UnprocessableEntityHttpException('متن پیامک خالی است.'); } $gateway = app(SmsGatewayService::class); try { $result = $gateway->sendOrThrow($mobile, $text, $source); } catch (SmsBlockedException $e) { throw new UnprocessableEntityHttpException( 'اعتبار پنل پیامک کافی نیست یا ارسال مسدود است.' ); } if (! $result->sent) { throw new UnprocessableEntityHttpException( $result->errorMessage ?: 'ارسال پیامک ناموفق بود؛ اعتبار کسر نشد.' ); } Message::create([ 'sms' => $text, 'receiver' => $mobile, 'platforms' => ($result->provider ?: 'sms') . ':' . $mobile, ]); } private function removeDuplicateNumbers(array $customers) { $uniqueCustomers = []; $seenNumbers = []; foreach ($customers as $customer) { if (!in_array($customer['mobile'], $seenNumbers)) { $seenNumbers[] = $customer['mobile']; $uniqueCustomers[] = $customer; } } return $uniqueCustomers; } /** * جایگزینی توکن نام مشتری با واریانت‌های فارسی/انگلیسی و فاصله اختیاری. * مثال: %name ، %name% ، ٪نام ، % نام ، ٪ نام */ private function applyCustomerNamePlaceholders(string $message, string $customerName): string { $patterns = [ '/[%٪]\s*name\s*[%٪]?/iu', '/[%٪]\s*نام\s*[%٪]?/u', ]; foreach ($patterns as $pattern) { $replaced = preg_replace($pattern, $customerName, $message); if (is_string($replaced)) { $message = $replaced; } } return $message; } /** خط آخر پیام باید نام اپ باشد؛ اگر نبود اضافه می‌شود. */ private function ensureAppNameFooter(string $message, string $appName): string { $appName = trim($appName); $trimmed = rtrim($message); if ($appName === '') { return $trimmed; } $lines = preg_split('/\R/u', $trimmed) ?: []; $last = trim((string) end($lines)); if ($last === $appName) { return $trimmed; } return $trimmed === '' ? $appName : ($trimmed . "\n" . $appName); } public function messagesTable(Request $request) { $records = Message::with(['person']); $table = TableEngine::create($records, $request->all()) ->initSearch([]) ->select([ 'id', 'sms', 'receiver','created_at', ]); return response()->json($table->export()); } /** * Display a listing of the messages. */ public function index() { $messages = Message::all(); return response()->json($messages); } /** * Store a newly created message in storage. */ public function store(Request $request) { $request->validate([ 'sms' => 'required|string', 'receiver' => 'required|string', ]); $message = Message::create($request->only(['sms', 'receiver'])); return response()->json($message, 201); } /** * Display the specified message. */ public function show($id) { $message = Message::findOrFail($id); return response()->json($message); } /** * Update the specified message in storage. */ public function update(Request $request, $id) { $request->validate([ 'sms' => 'sometimes|required|string', 'receiver' => 'sometimes|required|string', ]); $message = Message::findOrFail($id); $message->update($request->only(['sms', 'receiver'])); return response()->json($message); } /** * Remove the specified message from storage. */ public function destroy($id) { $message = Message::findOrFail($id); $message->delete(); return response()->json(null, 204); } }