定时任务

This commit is contained in:
zhang zhuo 2025-10-23 11:54:42 +08:00
parent 78ada91ff5
commit 0437334bb8
15 changed files with 339 additions and 36 deletions

View File

@ -7,6 +7,7 @@ namespace App\Controller\Admin;
use App\Controller\AbstractController;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Database\Model\Collection;
use Psr\Http\Message\ResponseInterface;
abstract class Base extends AbstractController
@ -29,12 +30,12 @@ abstract class Base extends AbstractController
/**
* Author: cfn <cfn@leapy.cn>
* @param array|string $msg
* @param array|LengthAwarePaginatorInterface|null $data
* @param LengthAwarePaginatorInterface|Collection|array|null $data
* @param null $count
* @param null $summary
* @return ResponseInterface
*/
public function success(array|string $msg = "success", LengthAwarePaginatorInterface|array|null $data = null, $count = null, $summary = null)
public function success(array|string $msg = "success", LengthAwarePaginatorInterface|Collection|array|null $data = null, $count = null, $summary = null)
{
if (!is_string($msg)) {
$data = $msg;
@ -44,6 +45,9 @@ abstract class Base extends AbstractController
$count = $data->total();
$data = $data->items();
}
if ($data instanceof Collection) {
$data = $data->toArray();
}
return $this->response(0, $msg, $data, $count, $summary);
}

View File

@ -6,11 +6,20 @@
namespace App\Controller\Admin;
use App\Annotation\Auth;
use App\Model\Menu as mModel;
use App\Model\Account as aModel;
use App\Model\Crontab as cModel;
use App\Model\CrontabLog as clModel;
use App\Model\Dept as dModel;
use App\Model\Menu as mModel;
use App\Model\Online;
use App\Model\Online as oModel;
use App\Model\Post as pModel;
use App\Model\Role as rModel;
use App\Request\Account as aRequest;
use App\Request\Dept as dRequest;
use App\Request\Menu as mRequest;
use App\Request\Post as pRequest;
use App\Request\Role as rRequest;
use App\Utils\AppInfoHelper;
use App\Utils\CpuHelper;
use App\Utils\DiskInfoHelper;
@ -25,15 +34,6 @@ use Hyperf\HttpServer\Annotation\DeleteMapping;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\PutMapping;
use App\Request\Menu as mRequest;
use App\Request\Role as rRequest;
use App\Request\Dept as dRequest;
use App\Model\Post as pModel;
use App\Request\Post as pRequest;
use App\Model\Account as aModel;
use App\Request\Account as aRequest;
use App\Model\Crontab as cModel;
use App\Model\CrontabLog as clModel;
#[Controller(prefix: "admin")]
class System extends Base
@ -401,6 +401,8 @@ class System extends Base
#[Auth(needAuth: false)]
public function crontabOption()
{
var_dump(cModel::options());
return $this->success("任务列表", cModel::options());
}
@ -408,18 +410,24 @@ class System extends Base
#[Auth(auth: "crontab:add")]
public function crontabAdd()
{
$data = Param::only(['crontab_name' => '', 'rule', 'callback', 'memo', 'singleton', 'params', 'skip_log', 'enable' => 1]);
$data = Param::only(['crontab_name' => '', 'rule', 'callback', 'memo', 'singleton', 'skip_log', 'enable' => 1]);
$res = cModel::add($data);
return $res ? $this->success("操作成功") : $this->error("操作失败");
if ($res) {
return $this->success("操作成功");
}
return $this->error("操作失败");
}
#[PutMapping(path: "crontab/edit")]
#[Auth(auth: "crontab:edit")]
public function crontabEdit()
{
$data = Param::only(['crontab_id' => '', 'crontab_name' => '', 'rule', 'callback', 'memo', 'singleton', 'params', 'skip_log', 'enable' => 1]);
$data = Param::only(['crontab_id' => '', 'crontab_name' => '', 'rule', 'callback', 'memo', 'singleton', 'skip_log', 'enable' => 1]);
$res = cModel::edit($data);
return $res ? $this->success("操作成功") : $this->error("操作失败");
if ($res) {
return $this->success("操作成功");
}
return $this->error("操作失败");
}
#[DeleteMapping(path: "crontab/del")]
@ -429,6 +437,35 @@ class System extends Base
$ids = $this->request->input("ids", "");
if (!$ids) return $this->error("操作失败");
$res = cModel::del($ids);
if ($res) {
return $this->success("操作成功");
}
return $this->error("操作失败");
}
#[GetMapping(path: "crontab_log/list")]
#[Auth(auth: "crontab:log:list")]
public function crontabLogList()
{
$param = Param::only(['crontab_id', 'limit' => 10]);
return $this->success("任务日志列表", clModel::list($param));
}
#[DeleteMapping(path: "crontab_log/del")]
#[Auth(auth: "crontab:log:del")]
public function crontabLogDel()
{
$param = Param::only(['ids']);
$res = clModel::del($param['ids']);
return $res ? $this->success("操作成功") : $this->error("操作失败");
}
#[DeleteMapping(path: "crontab_log/remove_all")]
#[Auth(auth: "crontab:log:empty")]
public function crontabLogEmpty()
{
$param = Param::only(['crontab_id']);
$res = clModel::removeAll($param);
return $res ? $this->success("操作成功") : $this->error("操作失败");
}
}

View File

@ -3,9 +3,11 @@
namespace App\Listener;
use App\Utils\AppInfoHelper;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
#[Listener]
class AppBootListener implements ListenerInterface
{
public function listen(): array

View File

@ -0,0 +1,93 @@
<?php
namespace App\Listener;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Crontab\Event\AfterExecute;
use Hyperf\Crontab\Event\FailToExecute;
use Hyperf\Crontab\Event\BeforeExecute;
use Hyperf\Event\Contract\ListenerInterface;
use App\Model\CrontabLog as clModel;
#[Listener]
class CrontabLogListener implements ListenerInterface
{
private array $starts = [];
public function listen(): array
{
return [
BeforeExecute::class,
AfterExecute::class,
FailToExecute::class,
];
}
public function process(object $event): void
{
if ($event instanceof BeforeExecute) {
$key = $this->getKey($event);
$this->starts[$key] = microtime(true);
}
if ($event instanceof AfterExecute) {
$options = $event->crontab->getOptions();
$key = $this->getKey($event);
if (isset($this->starts[$key])) {
$start = $this->starts[$key];
// 清理开始时间 防止内存泄露
unset($this->starts[$key]);
}
if ($options['skip_log']) {
clModel::insert([
'crontab_id' => $event->crontab->getName(),
'crontab_name' => $event->crontab->getMemo(),
'callback' => $options['callback'],
'duration' => microtime(true) - ($start ?? microtime(true)),
'status' => 1,
'result' => '执行成功',
'create_time' => date("Y-m-d H:i:s")
]);
}
}
if ($event instanceof FailToExecute) {
$options = $event->crontab->getOptions();
$key = $this->getKey($event);
if (isset($this->starts[$key])) {
$start = $this->starts[$key];
// 清理开始时间 防止内存泄露
unset($this->starts[$key]);
}
if ($options['skip_log']) {
clModel::insert([
'crontab_id' => $event->crontab->getName(),
'crontab_name' => $event->crontab->getMemo(),
'callback' => $options['callback'],
'duration' => microtime(true) - ($start ?? microtime(true)),
'status' => $event->throwable ? 0 : 1,
'result' => $event->throwable?->getMessage() ?? '执行失败',
'create_time' => date("Y-m-d H:i:s")
]);
}
}
}
protected function getKey(object $event): string
{
if (property_exists($event, 'crontab') && $event->crontab) {
try {
$name = method_exists($event->crontab, 'getName')
? $event->crontab->getName()
: ($event->crontab->name ?? null);
if ($name) {
return (string) $name;
}
return spl_object_hash($event->crontab);
} catch (\Throwable $e) {
return spl_object_hash($event->crontab);
}
}
return spl_object_hash($event);
}
}

View File

@ -8,8 +8,10 @@ namespace App\Listener;
use App\Event\LogEvent;
use App\Model\Online;
use App\Utils\Ip;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
#[Listener]
class LogHandleListener implements ListenerInterface
{
public function listen(): array

View File

@ -10,7 +10,6 @@ namespace App\Model;
* @property string $rule
* @property string $callback
* @property string $memo
* @property string $params
* @property int $enable
* @property int $singleton
* @property int $skip_log
@ -46,7 +45,7 @@ class Crontab extends Model
->when(isset($param['crontab_name']), function ($query) use ($param) {
$query->where('crontab_name', 'like', '%' . $param['crontab_name'] . '%');
})
->select(['crontab_id', 'crontab_name', 'enable', 'singleton', 'skip_log', 'rule', 'callback', 'params', 'memo', 'create_time'])
->select(['crontab_id', 'crontab_name', 'enable', 'singleton', 'skip_log', 'rule', 'callback', 'memo', 'create_time'])
->orderByDesc('crontab_id')
->paginate((int)$param['limit']);
}
@ -54,9 +53,17 @@ class Crontab extends Model
public static function options()
{
return self::query()
->orderByDesc('crontab_id')
->orderByDesc('crontab_id')
->select(['crontab_id', 'crontab_name'])
->orderByDesc('crontab_id')
->get();
}
public static function queryEnable()
{
return self::query()
->where('enable', 1)
->orderByDesc('crontab_id')
->select(['crontab_id', 'crontab_name', 'enable', 'singleton', 'skip_log', 'rule', 'callback', 'memo', 'create_time'])
->get();
}
}

View File

@ -5,15 +5,15 @@ declare(strict_types=1);
namespace App\Model;
/**
* @property int $log_id
* @property int $crontab_id
* @property string $crontab_name
* @property string $callback
* @property string $duration
* @property int $status
* @property string $result
* @property string $create_time
* @property string $deleted_at
* @property int $log_id
* @property int $crontab_id
* @property string $crontab_name
* @property string $callback
* @property string $duration
* @property int $status
* @property string $result
* @property string $create_time
* @property string $deleted_at
*/
class CrontabLog extends Model
{
@ -22,6 +22,8 @@ class CrontabLog extends Model
*/
protected ?string $table = 'crontab_log';
protected string $primaryKey = 'log_id';
/**
* The attributes that are mass assignable.
*/
@ -31,4 +33,24 @@ class CrontabLog extends Model
* The attributes that should be cast to native types.
*/
protected array $casts = ['log_id' => 'integer', 'crontab_id' => 'integer', 'status' => 'integer'];
public static function list(array $param)
{
return self::query()
->when(isset($param['crontab_id']), function ($query) use ($param) {
$query->where('crontab_id', $param['crontab_id']);
})
->select(['crontab_id', 'crontab_name', 'log_id', 'callback', 'duration', 'status', 'result', 'create_time'])
->orderByDesc('log_id')
->paginate((int)$param['limit']);
}
public static function removeAll($param)
{
return self::query()
->when(isset($param['crontab_id']), function ($query) use ($param) {
$query->where('crontab_id', $param['crontab_id']);
})
->delete();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Process;
use App\Model\Crontab as cModel;
use App\Utils\Log;
use Hyperf\Crontab\Crontab;
use Hyperf\Crontab\Process\CrontabDispatcherProcess as BaseDispatcher;
use Hyperf\Crontab\CrontabManager;
use Psr\Container\ContainerInterface;
class CrontabDispatcherProcess extends BaseDispatcher
{
public function __construct(ContainerInterface $container, CrontabManager $manager)
{
$this->registerDatabaseTasks($manager);
parent::__construct($container);
}
protected function registerDatabaseTasks(CrontabManager $manager): void
{
try {
// 获取所有启用的定时任务
$tasks = cModel::queryEnable();
foreach ($tasks as $task) {
$crontab = (new Crontab())
->setName($task->crontab_id)
->setMemo($task->crontab_name)
->setEnable((bool)$task->enable)
->setRule($task->rule)
->setSingleton((bool)$task->singleton)
->setOnOneServer(true)
->setOptions(['skip_log' => $task->skip_log, 'callback' => $task->callback])
->setCallback(explode('@', $task->callback));
$manager->register($crontab);
}
} catch (\Throwable $e) {
// 记录日志
Log::record('动态加载crontab任务失败: ' . $e->getMessage(), 'error');
}
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Service;
class DemoService
{
public function test()
{
sleep(rand(1,10));
var_dump(date('Y-m-d H:i:s'));
}
}

View File

@ -3,12 +3,12 @@
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Event;
namespace App\Service;
use App\Model\Online;
class WebSocketServer
class WebSocketService
{
// 访问启动时,关闭之前的连接
public function onStart(): void

77
app/Utils/Log.php Normal file
View File

@ -0,0 +1,77 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Utils;
use Hyperf\Context\ApplicationContext;
use Hyperf\Logger\Logger;
use Hyperf\Logger\LoggerFactory;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
class Log
{
public static function getInstance(string $name = 'app')
{
return ApplicationContext::getContainer()->get(LoggerFactory::class)->get($name);
}
public static function __callStatic($name, $arguments)
{
self::getInstance()->$name(...$arguments);
}
public static function record($message, string $filename = "info")
{
self::_save($message, $filename);
}
private static function _save($message, string $filename = 'log')
{
$log = new Logger('app');
$filename = $filename . '.log';
$path = 'runtime/logs/' . date('Y') . "/" . date("m");
// 有时候运维没给号权限,容易导致写入日志失败
self::mkDirs($path);
$path = $path . '/' . date("d") . "_". $filename;
if (gettype($message) == 'array') {
$message = json_encode($message, true);
}
$microtime = microtime();
$message = '[' . substr($microtime, 0, 8) . '] ' . $message;// 记录毫秒时间
// finally, create a formatter
$formatter = new LineFormatter("[%datetime%] %message% %context%\n", "Y-m-d H:i:s");
$stream = new StreamHandler($path, Level::Info);
$stream->setFormatter($formatter);
$log->pushHandler($stream);
$log->info($message);
}
/**
* 给日志文件夹权限
* @param string $dir
* @param int $mode
* @return bool
*/
private static function mkDirs(string $dir, int $mode = 0777): bool
{
if (is_dir($dir) || @mkdir($dir, $mode)) {
return TRUE;
}
if (!self::mkdirs(dirname($dir), $mode)) {
return FALSE;
}
return @mkdir($dir, $mode);
}
}

View File

@ -0,0 +1,6 @@
<?php
return [
// 是否开启定时任务
'enable' => true,
];

View File

@ -11,7 +11,5 @@ declare(strict_types=1);
*/
return [
Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class,
Hyperf\Command\Listener\FailToHandleListener::class,
App\Listener\LogHandleListener::class,
App\Listener\AppBootListener::class,
Hyperf\Command\Listener\FailToHandleListener::class
];

View File

@ -10,4 +10,5 @@ declare(strict_types=1);
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
return [
App\Process\CrontabDispatcherProcess::class,
];

View File

@ -42,8 +42,8 @@ return [
Event::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
Event::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
Event::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
Event::ON_START => [App\Event\WebSocketServer::class, 'onStart'],
Event::ON_SHUTDOWN => [App\Event\WebSocketServer::class, 'onShutdown'],
Event::ON_START => [App\Service\WebSocketService::class, 'onStart'],
Event::ON_SHUTDOWN => [App\Service\WebSocketService::class, 'onShutdown'],
],
],
],