Files
MrleeAdmin/backend/app/service/admin/crontab/CrontabService.php
T
2026-08-31 11:33:59 +08:00

440 lines
15 KiB
PHP

<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://core.tech
*/
namespace app\service\admin\crontab;
use app\dao\crontab\CrontabDao;
use app\enum\system\OperationResult;
use app\enum\system\TaskScheduleCycle;
use core\base\BaseService;
use core\exception\handler\AdminException;
use core\scheduler\Client;
use core\scheduler\event\EventBootstrap;
use madong\helper\Arr;
use support\Container;
/**
* 定时任务
*
* @author Mr.April
* @since 1.0
*/
class CrontabService extends BaseService
{
public function __construct(CrontabDao $dao)
{
$this->dao = $dao;
}
/**
* 添加定时任务
*
* @param array $data
*
* @return mixed
* @throws \Throwable
*/
public function save(array $data): mixed
{
try {
$model = $this->transaction(function () use ($data) {
$title = $data['title'] ?? '';
$type = $data['type'] ?? '';
$target = $data['target'] ?? '';
$status = $data['enabled'] ?? 1;
$singleton = $data['singleton'] ?? 1;
$task_cycle = TaskScheduleCycle::from($data['task_cycle'] ?? 1);
$month = $data['month'] ?? '';
$week = $data['week'] ?? '';
$day = $data['day'] ?? '';
$hour = $data['hour'] ?? '';
$minute = $data['minute'] ?? '';
$second = $data['second'] ?? '';
$remark = $data['remark'] ?? '';
//验证传入参数
$this->validateTaskData($task_cycle, $minute, $hour, $day, $week, $month, $second);
//生成表达式规则
$rule = $this->generateCronRule($task_cycle, $minute, $hour, $day, $week, $month, $second);
//更新到数据库
$insertData = [
'title' => $title,
'type' => $type,
'rule' => $rule,
'target' => $target,
'enabled' => $status,
'singleton' => $singleton,
'task_cycle' => $task_cycle,
'cycle_rule' => [
'month' => $month,
'week' => $week,
'day' => $day,
'hour' => $hour,
'minute' => $minute,
'second' => $second,
],
'remark' => $remark,
'first_start' => 0, // 新建任务默认为未启动状态
];
return $this->dao->save($insertData);
});
//添加定时任务重启服务
if (!empty($model)) {
$pk = $model->getPk();
$this->requestData($model->getData($pk));
}
return $model;
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
* 更新定时任务
*
* @param $id
* @param array $data
*
* @throws \Throwable
*/
public function update($id, array $data): void
{
try {
$this->transaction(function () use ($id, $data) {
$title = $data['title'] ?? '';
$type = $data['type'] ?? '';
$target = $data['target'] ?? '';
$status = $data['enabled'] ?? 1;
$singleton = $data['singleton'] ?? 1;
$task_cycle = TaskScheduleCycle::from($data['task_cycle'] ?? 1);
$month = $data['month'] ?? '';
$week = $data['week'] ?? '';
$day = $data['day'] ?? '';
$hour = $data['hour'] ?? '';
$minute = $data['minute'] ?? '';
$second = $data['second'] ?? '';
$remark = $data['remark'] ?? '';
//验证传入参数
$this->validateTaskData($task_cycle, $minute, $hour, $day, $week, $month, $second);
//生成表达式规则
$rule = $this->generateCronRule($task_cycle, $minute, $hour, $day, $week, $month, $second);
// 更新到数据库
$this->dao->update($id, [
'title' => $title,
'type' => $type,
'rule' => $rule,
'target' => $target,
'enabled' => $status,
'singleton' => $singleton,
'task_cycle' => $task_cycle->value,
'cycle_rule' => [
'month' => $month,
'week' => $week,
'day' => $day,
'hour' => $hour,
'minute' => $minute,
'second' => $second,
],
'remark' => $remark,
]);
});
// 更新之后重启服务
$this->requestData($id);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
* 定时任务删除
*
* @param array|int|string $id
*
* @return bool
* @throws \Throwable
*/
public function destroy(array|int|string $id): bool
{
try {
//1.0 先关闭再删除,避免删了后直接连不上服务的情况出现
$this->dao->update([['id', 'in', $id]], ['enabled' => 0]);
//2.0 重启任务
$this->requestData($id);
return $this->transaction(function () use ($id) {
//3.0 删除定时任务跟日志数据
$this->dao->destroy($id);
$systemCrontabLogService = Container::make(CrontabLogService::class);
$systemCrontabLogService->delete(['crontab_id' => $id]);
return true;
});
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
* 恢复任务
*
* @param string|int|array $data
*
* @throws \Throwable
*/
public function resumeCrontab(string|int|array $data): void
{
try {
$this->transaction(function () use ($data) {
// 恢复任务时,只更改启用状态
$this->query()->whereIn('id', $data)->update(['enabled' => 1]);
$result = $this->requestData($data);
if (!$result) {
throw new AdminException('恢复失败');
}
});
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
*暂停任务
*
* @param string|int|array $data
*
* @throws \Throwable
*/
public function pauseCrontab(string|int|array $data): void
{
try {
$this->transaction(function () use ($data) {
$this->query()->whereIn('id', Arr::normalize($data))->update(['enabled' => 0]);
$result = $this->requestData($data);
if (!$result) {
throw new AdminException('重启失败');
}
});
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
* 获取所有任务
*
* @param string $field
*
* @return array
* @throws \Exception
*/
public function getTaskAll(string $field = '*'): array
{
return $this->dao->selectList(['enabled' => 1], $field, 0, 0, '', [], true)->toArray();
}
/**
* 传入ID 获取指定的一条任务
*
* @param $id
*
* @return mixed
* @throws \Exception
*/
public function getTask($id): mixed
{
return $this->dao->get($id);
}
/**
* 运行单个任务
*
* @param $id
*
* @return array
* @throws \Exception
*/
public function runOneTask($id): array
{
/** @var $task_handle EventBootstrap[] */
$task_handle = config('core.scheduler.app.task_handle', []);
$crontab = $this->dao->get($id);
$start_time = microtime(true);
try {
if (empty($crontab)) {
throw new \Exception('执行任务失败, 任务不存在');
}
if (!isset($task_handle[$crontab->getAttribute('type')])) {
throw new \Exception('执行任务失败, 任务类型错误: ');
}
$result_data = $task_handle[$crontab['type']]::parse($crontab);
// 记录执行信息
$crontab->last_running_time = $start_time;
$crontab->increment('running_times');
if ($crontab->singleton == 0) {
// 单次任务 直接停用
$crontab->enabled = 0;
}
if (!$crontab->save()) {
throw new \Exception('记录保存失败');
}
$end_time = microtime(true);
$log = $result_data['log'] ?? '';
if (strlen($log) > 300) {
$result_data['log'] = mb_substr($result_data['log'], 0, 300) . '...';
}
// 写入执行日志
$installData = [
'crontab_id' => $crontab['id'] ?? '',
'target' => $crontab['target'] ?? '',
'log' => $result_data['log'] ?? '--',
'return_code' => $result_data['code'] ?? OperationResult::FAILURE->value,
'running_time' => round($end_time - $start_time, 6),
'created_at' => $start_time,
];
$crontabLogService = Container::make(CrontabLogService::class);
$crontabLogModel = $crontabLogService->dao->save($installData);
if (empty($crontabLogModel)) {
throw new \Exception('日志记录保存失败');
}
} catch (\Exception $e) {
return ['code' => 1, 'log' => $e->getMessage() . '---任务id: ' . $id];
}
return [
'code' => $installData['return_code'],
'log' => $installData['log'],
'crontab_id' => $installData['crontab_id'],
];
}
/**
* 重启任务
*
* @param int|string|array $id_str int|string 需要重启的任务id,多个id用,拼接,例:1,2,3,4,5
*
* @return bool
* @throws \Exception
*/
public function requestData(int|string|array $id_str): bool
{
// 如果是数组,使用 implode 连接为字符串;如果是单个值,直接使用
$ids = is_array($id_str) ? implode(',', $id_str) : $id_str;
// 重启任务
$param = ['method' => 'crontabReload', 'args' => ['id' => $ids]];
return Client::request($param);
}
/**
* 验证器
*
* @param \app\enum\system\TaskScheduleCycle $task_cycle
* @param $minute
* @param $hour
* @param $day
* @param $week
* @param $month
* @param $second
*
* @throws \Exception
*/
private function validateTaskData(TaskScheduleCycle $task_cycle, $minute, $hour, $day, $week, $month, $second): void
{
switch ($task_cycle) {
case TaskScheduleCycle::DAILY:
$this->validateInteger($hour, "请输入执行小时", 23);
$this->validateInteger($minute, "请输入执行分钟", 59);
break;
case TaskScheduleCycle::HOURLY:
$this->validateInteger($minute, "请输入执行分钟", 59);
break;
case TaskScheduleCycle::WEEKLY:
$this->validateInteger($week, "请输入星期几执行", 6);
$this->validateInteger($hour, "请输入执行小时", 23);
$this->validateInteger($minute, "请输入执行分钟", 59);
break;
case TaskScheduleCycle::MONTHLY:
$this->validateInteger($day, "请输入执行天数", 31);
$this->validateInteger($hour, "请输入执行小时", 23);
$this->validateInteger($minute, "请输入执行分钟", 59);
break;
case TaskScheduleCycle::YEARLY:
$this->validateInteger($month, "请输入执行月份", 12);
$this->validateInteger($day, "请输入执行天数", 31);
$this->validateInteger($hour, "请输入执行小时", 23);
$this->validateInteger($minute, "请输入执行分钟", 59);
break;
case TaskScheduleCycle::N_HOURS:
$this->validateInteger($hour, "请输入N小时", 23);
break;
case TaskScheduleCycle::N_MINUTES:
$this->validateInteger($minute, "请输入N分钟", 59);
break;
case TaskScheduleCycle::N_SECONDS:
$this->validateInteger($second, "请输入N秒数", 59);
if (60 % (int)$second !== 0) {
throw new \Exception('秒级任务必须是60的因数');
}
break;
default:
throw new \Exception("任务周期不正确");
}
}
/**
* 定义验证规则器
*
* @throws \Exception
*/
private function validateInteger($value, $message, $max): void
{
if (!is_numeric($value) || (int)$value < 0 || (int)$value > $max) {
throw new \Exception($message);
}
}
/**
* 生成任务表达式
*
* @param \app\enum\system\TaskScheduleCycle $task_cycle
* @param $minute
* @param $hour
* @param $day
* @param $week
* @param $month
* @param $second
*
* @return string
*/
private function generateCronRule(TaskScheduleCycle $task_cycle, $minute, $hour, $day, $week, $month, $second): string
{
return match ($task_cycle) {
TaskScheduleCycle::DAILY => "0 {$minute} {$hour} * * *",
TaskScheduleCycle::HOURLY => "0 {$minute} * * * *",
TaskScheduleCycle::WEEKLY => "0 {$minute} {$hour} * * {$week}",
TaskScheduleCycle::MONTHLY => "0 {$minute} {$hour} {$day} * *",
TaskScheduleCycle::YEARLY => "0 {$minute} {$hour} {$day} {$month} *",
TaskScheduleCycle::N_HOURS => "0 {$minute} */{$hour} * * *",
TaskScheduleCycle::N_MINUTES => "0 */{$minute} * * * *", // N分钟
TaskScheduleCycle::N_SECONDS => "*/{$second} * * * * *", // N秒
};
}
}