feat(schedule): add schedule:bootstrap and minute-precise automation runs
Install per-module schedule:run cron from artisan, store sms/in_app send minutes, and match scheduled triggers at exact HH:mm Tehran time. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ScheduleBootstrapCommand extends Command
|
||||
{
|
||||
protected $signature = 'schedule:bootstrap
|
||||
{--install-only : فقط خط cron همین ماژول را نصب کن}
|
||||
{--run-only : فقط یکبار schedule:run بزن، cron را دست نزن}
|
||||
{--dry-run : خط cron را نشان بده بدون نصب}
|
||||
{--php= : مسیر php (پیشفرض PHP_BINARY)}';
|
||||
|
||||
protected $description = 'نصب cron این ماژول (schedule:run) و/یا اجرای یکبار زمانبند';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$runOnly = (bool) $this->option('run-only');
|
||||
$installOnly = (bool) $this->option('install-only');
|
||||
|
||||
if ($runOnly && $installOnly) {
|
||||
$this->error('فقط یکی از --run-only یا --install-only را بزنید.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $runOnly && ! $this->installCronLine()) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($installOnly) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('==> php artisan schedule:run');
|
||||
|
||||
return $this->call('schedule:run');
|
||||
}
|
||||
|
||||
private function installCronLine(): bool
|
||||
{
|
||||
$php = trim((string) ($this->option('php') ?: PHP_BINARY));
|
||||
$artisan = base_path('artisan');
|
||||
$marker = $artisan.' schedule:run';
|
||||
$line = "* * * * * {$php} {$artisan} schedule:run >> /dev/null 2>&1";
|
||||
|
||||
$existing = $this->currentCrontab();
|
||||
if ($this->crontabHasLine($existing, $marker)) {
|
||||
$this->info("cron از قبل نصب است: {$artisan}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->line($line);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$payload = trim($existing);
|
||||
if ($payload !== '') {
|
||||
$payload .= "\n";
|
||||
}
|
||||
$payload .= $line."\n";
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'khaneghi-crontab-');
|
||||
if ($tmp === false) {
|
||||
$this->error('ساخت فایل موقت crontab ناموفق بود.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
file_put_contents($tmp, $payload);
|
||||
exec('crontab '.escapeshellarg($tmp).' 2>&1', $output, $code);
|
||||
@unlink($tmp);
|
||||
|
||||
if ($code !== 0) {
|
||||
$this->error('نصب crontab ناموفق بود.');
|
||||
foreach ($output as $row) {
|
||||
$this->line($row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("cron نصب شد:\n{$line}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function currentCrontab(): string
|
||||
{
|
||||
exec('crontab -l 2>/dev/null', $lines, $code);
|
||||
|
||||
if ($code !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private function crontabHasLine(string $crontab, string $marker): bool
|
||||
{
|
||||
if ($crontab === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (preg_split('/\R/u', $crontab) ?: [] as $row) {
|
||||
$row = trim((string) $row);
|
||||
if ($row === '' || str_starts_with($row, '#')) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($row, $marker)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
// تیک قیمت — ۶ بار در دقیقه با فاصله ۱۰ ثانیه (montzar + zaarbox + محصولات)
|
||||
// نیاز: * * * * * php artisan schedule:run
|
||||
// یا یکبار: php artisan schedule:bootstrap
|
||||
$schedule->command('prices:tick-loop --iterations=6 --interval=10')
|
||||
->everyMinute()
|
||||
->name('prices_tick_10s')
|
||||
|
||||
Reference in New Issue
Block a user