diff --git a/app/Controller/Admin/Base.php b/app/Controller/Admin/Base.php index 0a46a0a..1df7f2d 100644 --- a/app/Controller/Admin/Base.php +++ b/app/Controller/Admin/Base.php @@ -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 * @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); } diff --git a/app/Controller/Admin/System.php b/app/Controller/Admin/System.php index 6ad169c..c742119 100644 --- a/app/Controller/Admin/System.php +++ b/app/Controller/Admin/System.php @@ -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("操作失败"); } } \ No newline at end of file diff --git a/app/Listener/AppBootListener.php b/app/Listener/AppBootListener.php index 45ecb44..5130ed4 100644 --- a/app/Listener/AppBootListener.php +++ b/app/Listener/AppBootListener.php @@ -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 diff --git a/app/Listener/CrontabLogListener.php b/app/Listener/CrontabLogListener.php new file mode 100644 index 0000000..6c45f54 --- /dev/null +++ b/app/Listener/CrontabLogListener.php @@ -0,0 +1,93 @@ +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); + } +} \ No newline at end of file diff --git a/app/Listener/LogHandleListener.php b/app/Listener/LogHandleListener.php index 08108c5..ee16ab3 100644 --- a/app/Listener/LogHandleListener.php +++ b/app/Listener/LogHandleListener.php @@ -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 diff --git a/app/Model/Crontab.php b/app/Model/Crontab.php index 014c34b..71b117f 100644 --- a/app/Model/Crontab.php +++ b/app/Model/Crontab.php @@ -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(); } } diff --git a/app/Model/CrontabLog.php b/app/Model/CrontabLog.php index 147d339..73b46ab 100644 --- a/app/Model/CrontabLog.php +++ b/app/Model/CrontabLog.php @@ -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(); + } } diff --git a/app/Process/CrontabDispatcherProcess.php b/app/Process/CrontabDispatcherProcess.php new file mode 100644 index 0000000..2827f1e --- /dev/null +++ b/app/Process/CrontabDispatcherProcess.php @@ -0,0 +1,42 @@ +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'); + } + } +} \ No newline at end of file diff --git a/app/Service/DemoService.php b/app/Service/DemoService.php new file mode 100644 index 0000000..a681283 --- /dev/null +++ b/app/Service/DemoService.php @@ -0,0 +1,12 @@ + */ -namespace App\Event; +namespace App\Service; use App\Model\Online; -class WebSocketServer +class WebSocketService { // 访问启动时,关闭之前的连接 public function onStart(): void diff --git a/app/Utils/Log.php b/app/Utils/Log.php new file mode 100644 index 0000000..96740cc --- /dev/null +++ b/app/Utils/Log.php @@ -0,0 +1,77 @@ + + */ + +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); + } +} \ No newline at end of file diff --git a/config/autoload/crontab.php b/config/autoload/crontab.php new file mode 100644 index 0000000..77d4d93 --- /dev/null +++ b/config/autoload/crontab.php @@ -0,0 +1,6 @@ + true, +]; \ No newline at end of file diff --git a/config/autoload/listeners.php b/config/autoload/listeners.php index 6f3cb55..bda5453 100644 --- a/config/autoload/listeners.php +++ b/config/autoload/listeners.php @@ -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 ]; diff --git a/config/autoload/processes.php b/config/autoload/processes.php index f46bd96..87e3b4c 100644 --- a/config/autoload/processes.php +++ b/config/autoload/processes.php @@ -10,4 +10,5 @@ declare(strict_types=1); * @license https://github.com/hyperf/hyperf/blob/master/LICENSE */ return [ + App\Process\CrontabDispatcherProcess::class, ]; diff --git a/config/autoload/server.php b/config/autoload/server.php index d4e0978..743d758 100644 --- a/config/autoload/server.php +++ b/config/autoload/server.php @@ -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'], ], ], ],