From 78ada91ff5bda02f4d75221f474f0a3411e03263 Mon Sep 17 00:00:00 2001 From: zhang zhuo Date: Wed, 22 Oct 2025 17:10:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Controller/Admin/Base.php | 30 ++++++++---- app/Controller/Admin/Login.php | 13 ++++- app/Controller/Admin/System.php | 66 ++++++++++++++++++++++++++ app/Event/LogEvent.php | 2 +- app/Event/WebSocket.php | 76 ++++++++++++++++++++++++++++++ app/Event/WebSocketServer.php | 24 ++++++++++ app/Listener/LogHandleListener.php | 2 +- app/Middleware/JWTMiddleware.php | 11 ++++- app/Model/Crontab.php | 62 ++++++++++++++++++++++++ app/Model/CrontabLog.php | 34 +++++++++++++ app/Model/Online.php | 38 +++++++++++++++ config/autoload/server.php | 23 +++++++-- config/routes.php | 4 ++ 13 files changed, 369 insertions(+), 16 deletions(-) create mode 100644 app/Event/WebSocket.php create mode 100644 app/Event/WebSocketServer.php create mode 100644 app/Model/Crontab.php create mode 100644 app/Model/CrontabLog.php diff --git a/app/Controller/Admin/Base.php b/app/Controller/Admin/Base.php index 15d6c69..0a46a0a 100644 --- a/app/Controller/Admin/Base.php +++ b/app/Controller/Admin/Base.php @@ -6,6 +6,7 @@ namespace App\Controller\Admin; use App\Controller\AbstractController; +use Hyperf\Contract\LengthAwarePaginatorInterface; use Psr\Http\Message\ResponseInterface; abstract class Base extends AbstractController @@ -15,6 +16,11 @@ abstract class Base extends AbstractController return $this->request->getAttribute("account_id"); } + public function uuid(): string + { + return $this->account()['uuid']; + } + public function account() { return $this->request->getAttribute("account"); @@ -23,34 +29,42 @@ abstract class Base extends AbstractController /** * Author: cfn * @param array|string $msg - * @param array|null $data - * @param $count - * @param $summary + * @param array|LengthAwarePaginatorInterface|null $data + * @param null $count + * @param null $summary * @return ResponseInterface */ - public function success(array|string $msg = "success", array|null $data = null, $count = null, $summary = null) + public function success(array|string $msg = "success", LengthAwarePaginatorInterface|array|null $data = null, $count = null, $summary = null) { if (!is_string($msg)) { $data = $msg; $msg = "success"; } + if ($data instanceof LengthAwarePaginatorInterface) { + $count = $data->total(); + $data = $data->items(); + } return $this->response(0, $msg, $data, $count, $summary); } /** * Author: cfn * @param array|string $msg - * @param array|null $data - * @param $count - * @param $summary + * @param LengthAwarePaginatorInterface|array|null $data + * @param null $count + * @param null $summary * @return ResponseInterface */ - public function error(array|string $msg = "error", array|null $data = null, $count = null, $summary = null) + public function error(array|string $msg = "error", LengthAwarePaginatorInterface|array|null $data = null, $count = null, $summary = null) { if (!is_string($msg)) { $data = $msg; $msg = "error"; } + if ($data instanceof LengthAwarePaginatorInterface) { + $count = $data->total(); + $data = $data->items(); + } return $this->response(1, $msg, $data, $count, $summary); } diff --git a/app/Controller/Admin/Login.php b/app/Controller/Admin/Login.php index 698fc61..c52c3ec 100644 --- a/app/Controller/Admin/Login.php +++ b/app/Controller/Admin/Login.php @@ -9,6 +9,7 @@ use App\Annotation\Auth; use App\Event\LogEvent; use App\Model\Account; use App\Model\AccountLog; +use App\Model\Online; use App\Utils\Param; use App\Utils\Str; use App\Utils\Token; @@ -77,16 +78,17 @@ class Login extends Base return $this->error("账号或者密码错误!"); } // 商户ID + $uuid = Str::uuid(); $tData['account_id'] = $account['account_id']; $tData['account_type'] = $account['account_type']; $tData['belong_id'] = $account['belong_id']; $tData['username'] = $account['username']; $tData['master_flag'] = $account['master_flag']; + $tData['uuid'] = $uuid; - $uuid = Str::uuid(); $token = Token::buildToken(['uuid' => $uuid, 'time' => time() + config("jwt.ttl")], config("jwt.ttl")); // 记录登录日志 - $this->eventDispatcher->dispatch(new LogEvent($tData, $uuid)); + $this->eventDispatcher->dispatch(new LogEvent($tData)); // 根据账号所属角色缓存相应的权限数据 $auths = Account::getAuths($account['account_id'], $account['account_type'], $account['master_flag']); $redis->set("AUTH:" . $account['account_id'], json_encode($auths)); @@ -113,6 +115,13 @@ class Login extends Base #[Auth(needAuth: false)] public function logout() { + // 退出登录状态 + $container = ApplicationContext::getContainer(); + $redis = $container->get(\Hyperf\Redis\Redis::class); + $redis->del("USER:" . $this->uuid()); + $redis->del("AUTH:" . $this->account()['account_id']); + // 设置离线 + Online::leave($this->uuid()); return $this->success("登出成功"); } diff --git a/app/Controller/Admin/System.php b/app/Controller/Admin/System.php index eda03c3..6ad169c 100644 --- a/app/Controller/Admin/System.php +++ b/app/Controller/Admin/System.php @@ -8,6 +8,7 @@ namespace App\Controller\Admin; use App\Annotation\Auth; use App\Model\Menu as mModel; use App\Model\Dept as dModel; +use App\Model\Online; use App\Model\Online as oModel; use App\Model\Role as rModel; use App\Utils\AppInfoHelper; @@ -18,6 +19,7 @@ use App\Utils\MemoryHelper; use App\Utils\Param; use App\Utils\RedisInfoHelper; use App\Utils\SystemHelper; +use Hyperf\Context\ApplicationContext; use Hyperf\HttpServer\Annotation\Controller; use Hyperf\HttpServer\Annotation\DeleteMapping; use Hyperf\HttpServer\Annotation\GetMapping; @@ -30,6 +32,8 @@ 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 @@ -365,4 +369,66 @@ class System extends Base } return $this->success('在线用户', $paginate->items(), $paginate->total()); } + + #[GetMapping(path: "online/quit")] + #[Auth(auth: "online:quit")] + public function onlineQuit() + { + $param = Param::only(['session_id' => '']); + // 退出登录状态 + $online = Online::getByUuid($param['session_id']); + if (empty($online) || $online['status'] == 0) { + return $this->success("用户已不在线"); + } + $container = ApplicationContext::getContainer(); + $redis = $container->get(\Hyperf\Redis\Redis::class); + $redis->del("USER:" . $param['session_id']); + $redis->del("AUTH:" . $online['account_id']); + // 设置离线 + Online::leave($param['session_id']); + return $this->success("强退成功"); + } + + #[GetMapping(path: "crontab/list")] + #[Auth(auth: "crontab:list")] + public function crontabList() + { + $param = Param::only(['crontab_name', 'enable', 'limit' => 10]); + return $this->success("任务列表", cModel::list($param)); + } + + #[GetMapping(path: "crontab/option")] + #[Auth(needAuth: false)] + public function crontabOption() + { + return $this->success("任务列表", cModel::options()); + } + + #[PostMapping(path: "crontab/add")] + #[Auth(auth: "crontab:add")] + public function crontabAdd() + { + $data = Param::only(['crontab_name' => '', 'rule', 'callback', 'memo', 'singleton', 'params', 'skip_log', 'enable' => 1]); + $res = cModel::add($data); + return $res ? $this->success("操作成功") : $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]); + $res = cModel::edit($data); + return $res ? $this->success("操作成功") : $this->error("操作失败"); + } + + #[DeleteMapping(path: "crontab/del")] + #[Auth(auth: "crontab:del")] + public function crontabDel() + { + $ids = $this->request->input("ids", ""); + if (!$ids) return $this->error("操作失败"); + $res = cModel::del($ids); + return $res ? $this->success("操作成功") : $this->error("操作失败"); + } } \ No newline at end of file diff --git a/app/Event/LogEvent.php b/app/Event/LogEvent.php index 3065114..3c7c2ed 100644 --- a/app/Event/LogEvent.php +++ b/app/Event/LogEvent.php @@ -7,5 +7,5 @@ namespace App\Event; class LogEvent { - public function __construct(public array $data, public string $uuid){} + public function __construct(public array $data){} } \ No newline at end of file diff --git a/app/Event/WebSocket.php b/app/Event/WebSocket.php new file mode 100644 index 0000000..0a75d74 --- /dev/null +++ b/app/Event/WebSocket.php @@ -0,0 +1,76 @@ + + */ + +namespace App\Event; + +use App\Model\Online; +use App\Utils\Token; +use Hyperf\Context\ApplicationContext; +use Hyperf\Contract\OnCloseInterface; +use Hyperf\Contract\OnMessageInterface; +use Hyperf\Contract\OnOpenInterface; +use Hyperf\WebSocketServer\Context; + +/** + * 商户端socket + * Author: cfn + */ +class WebSocket implements OnMessageInterface, OnOpenInterface, OnCloseInterface +{ + /** + * 连接关闭 + * Author: cfn + * @param $server + * @param int $fd + * @param int $reactorId + * @return void + */ + public function onClose($server, int $fd, int $reactorId): void + { + $uuid = Context::get('uuid', ""); + Online::leave($uuid); + } + + /** + * 连接时 + * Author: cfn + * @param $server + * @param $request + * @return void + */ + public function onOpen($server, $request): void + { + $token = $request->get['Authorization']; + $result = Token::parseToken(str_replace("Bearer ", "", $token)); + + $container = ApplicationContext::getContainer(); + $redis = $container->get(\Hyperf\Redis\Redis::class); + $account = !empty($result) ? json_decode($redis->get("USER:" . $result['uuid']), true) : null; + if (!empty($account)) { + Online::live($result['uuid'], $request->fd); + Context::set('account', $account); + Context::set('uuid', $result['uuid']); + $server->push($request->fd, 'connection successful'); + return; + } + $server->push($request->fd, 'connection refused'); + } + + /** + * 当接收到信息 + * Author: cfn + * @param $server + * @param $frame + * @return void + */ + public function onMessage($server, $frame): void + { + if (strtolower($frame->data) == "ping") { + $server->push($frame->fd, 'pong'); + return; + } + $server->push($frame->fd, 'Recv: ' . $frame->data); + } +} \ No newline at end of file diff --git a/app/Event/WebSocketServer.php b/app/Event/WebSocketServer.php new file mode 100644 index 0000000..3728fa6 --- /dev/null +++ b/app/Event/WebSocketServer.php @@ -0,0 +1,24 @@ + + */ + +namespace App\Event; + + +use App\Model\Online; + +class WebSocketServer +{ + // 访问启动时,关闭之前的连接 + public function onStart(): void + { + Online::closeAll(); + } + + // 服务关闭,关闭之前的连接 + public function onShutdown(): void + { + Online::closeAll(); + } +} \ No newline at end of file diff --git a/app/Listener/LogHandleListener.php b/app/Listener/LogHandleListener.php index 5cc9950..08108c5 100644 --- a/app/Listener/LogHandleListener.php +++ b/app/Listener/LogHandleListener.php @@ -22,7 +22,7 @@ class LogHandleListener implements ListenerInterface public function process(object $event): void { Online::insert([ - 'session_id' => $event->uuid, + 'session_id' => $event->data['uuid'], 'account_type' => $event->data['account_type'], 'belong_id' => $event->data['belong_id'], 'account_id' => $event->data['account_id'], diff --git a/app/Middleware/JWTMiddleware.php b/app/Middleware/JWTMiddleware.php index 8d25eb5..cb3d5e0 100644 --- a/app/Middleware/JWTMiddleware.php +++ b/app/Middleware/JWTMiddleware.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Middleware; +use App\Model\Online; use App\Utils\Token; use Hyperf\Context\ApplicationContext; use Hyperf\Context\Context; @@ -43,7 +44,13 @@ class JWTMiddleware implements MiddlewareInterface try { $token = $request->getHeaderLine("Authorization", ""); $result = Token::parseToken(str_replace("Bearer ", "", $token)); - $account = !empty($result) ? json_decode($redis->get("USER:" . $result['uuid']), true) : null; + if (!empty($result)) { + $user = $redis->get("USER:" . $result['uuid']); + if (!empty($user)) { + $account = json_decode($user, true); + } + } + // 判断登录状态是否强退 if (!empty($account)) { // 是否登录 $request = $request->withAttribute("isLogin", true); @@ -52,6 +59,8 @@ class JWTMiddleware implements MiddlewareInterface $request = $request->withAttribute("account_id", $account['account_id']); // 基础信息 $request = $request->withAttribute("account", $account); + // 如果账户离线恢复在线 + Online::reLive($result['uuid']); } } catch (\Exception $exception) { } diff --git a/app/Model/Crontab.php b/app/Model/Crontab.php new file mode 100644 index 0000000..014c34b --- /dev/null +++ b/app/Model/Crontab.php @@ -0,0 +1,62 @@ + 'integer', 'enable' => 'integer', 'singleton' => 'integer', 'skip_log' => 'integer']; + + public static function list(array $param) + { + return self::query() + ->when(isset($param['enable']), function ($query) use ($param) { + $query->where('enable', $param['enable']); + }) + ->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']) + ->orderByDesc('crontab_id') + ->paginate((int)$param['limit']); + } + + public static function options() + { + return self::query() + ->orderByDesc('crontab_id') + ->orderByDesc('crontab_id') + ->select(['crontab_id', 'crontab_name']) + ->get(); + } +} diff --git a/app/Model/CrontabLog.php b/app/Model/CrontabLog.php new file mode 100644 index 0000000..147d339 --- /dev/null +++ b/app/Model/CrontabLog.php @@ -0,0 +1,34 @@ + 'integer', 'crontab_id' => 'integer', 'status' => 'integer']; +} diff --git a/app/Model/Online.php b/app/Model/Online.php index b55bcb5..771c026 100644 --- a/app/Model/Online.php +++ b/app/Model/Online.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace App\Model; +use App\Utils\Ip; +use Swoole\Http\Request; + /** * @property int $online_id * @property string $session_id @@ -50,6 +53,41 @@ class Online extends Model ->paginate((int)$param['limit']); } + public static function closeAll(): int + { + return self::where("status", 1) + ->where("online_time", ">", date("Y-m-d H:i:s", time() - 24 * 60 * 60)) + ->update(['status' => 0, 'offline_time' => date("Y-m-d H:i:s")]); + } + + public static function leave(string $uuid): int + { + return self::where('session_id', $uuid) + ->update(['status' => 0, 'offline_time' => date("Y-m-d H:i:s")]); + } + + public static function live(string $uuid, int $fd): int + { + return self::where("session_id", $uuid) + ->where("status", 1) + ->update(['fd' => $fd, 'update_time' => date("Y-m-d H:i:s")]); + } + + public static function reLive(string $uuid): bool + { + $online = self::getByUuid($uuid); + if ($online['status'] == 0) { + return (bool)self::where("session_id", $uuid) + ->update(['update_time' => date("Y-m-d H:i:s"), 'status' => 1]); + } + return true; + } + + public static function getByUuid(string $uuid) + { + return self::where("session_id", $uuid)->first(['status', 'session_id', 'account_id']); + } + public function account() { return $this->hasOne(Account::class, 'account_id', 'account_id'); diff --git a/config/autoload/server.php b/config/autoload/server.php index 4b348da..d4e0978 100644 --- a/config/autoload/server.php +++ b/config/autoload/server.php @@ -1,6 +1,7 @@ [ [ 'name' => 'http', - 'type' => Server::SERVER_HTTP, + 'type' => ServerInterface::SERVER_HTTP, 'host' => '0.0.0.0', - 'port' => (int) env('APP_PORT'), + 'port' => (int)env('APP_PORT', 9501), 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], @@ -30,6 +32,20 @@ return [ 'enable_request_lifecycle' => false, ], ], + [ + 'name' => 'ws', + 'type' => ServerInterface::SERVER_WEBSOCKET, + 'host' => '0.0.0.0', + 'port' => (int)env('WS_PORT', 9502), + 'sock_type' => SWOOLE_SOCK_TCP, + 'callbacks' => [ + 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'], + ], + ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, @@ -47,6 +63,7 @@ return [ 'enable_static_handler' => true, ], 'callbacks' => [ + Event::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], diff --git a/config/routes.php b/config/routes.php index 46d04e5..d7104f1 100644 --- a/config/routes.php +++ b/config/routes.php @@ -16,3 +16,7 @@ Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@i Router::get('/favicon.ico', function () { return ''; }); + +Router::addServer('ws', function () { + Router::get('/ws', 'App\Event\WebSocket'); +}); \ No newline at end of file