433 lines
15 KiB
PHP
433 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\DB\Te\TableEngine;
|
|
use App\Exceptions\SmsBlockedException;
|
|
use App\Models\Customer;
|
|
use App\Models\Message;
|
|
use App\Models\Order;
|
|
use Illuminate\Http\Request;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Services\ConfigService;
|
|
use App\Services\SmsBilling\SmsGatewayService;
|
|
use App\Services\SmsBilling\SmsWalletService;
|
|
use App\Services\SmsProfanityFilter;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
|
|
|
|
|
|
class MassageController extends Controller
|
|
{
|
|
public function send_message(Request $request)
|
|
{
|
|
if (! app(ConfigService::class)->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<string> $actions
|
|
* @return list<array{id:mixed,name:mixed,mobile:mixed}>
|
|
*/
|
|
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<array{id:mixed,name:mixed,mobile:mixed}>
|
|
*/
|
|
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
|
|
{
|
|
// فقط فاصلهٔ بین % و کلمهٔ name/نام مجاز است؛ فاصلهٔ بعد از توکن حفظ میشود
|
|
$pattern = '/[%٪]\s*(?:name|نام)[%٪]?/iu';
|
|
|
|
$replaced = preg_replace($pattern, $customerName, $message);
|
|
|
|
return is_string($replaced) ? $replaced : $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);
|
|
}
|
|
|
|
}
|