定时任务

This commit is contained in:
zhang zhuo 2025-10-22 17:10:42 +08:00
parent bf44e4abb4
commit 78ada91ff5
13 changed files with 369 additions and 16 deletions

View File

@ -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 <cfn@leapy.cn>
* @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 <cfn@leapy.cn>
* @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);
}

View File

@ -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("登出成功");
}

View File

@ -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("操作失败");
}
}

View File

@ -7,5 +7,5 @@ namespace App\Event;
class LogEvent
{
public function __construct(public array $data, public string $uuid){}
public function __construct(public array $data){}
}

76
app/Event/WebSocket.php Normal file
View File

@ -0,0 +1,76 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
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 <cfn@leapy.cn>
*/
class WebSocket implements OnMessageInterface, OnOpenInterface, OnCloseInterface
{
/**
* 连接关闭
* Author: cfn <cfn@leapy.cn>
* @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 <cfn@leapy.cn>
* @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 <cfn@leapy.cn>
* @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);
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Event;
use App\Model\Online;
class WebSocketServer
{
// 访问启动时,关闭之前的连接
public function onStart(): void
{
Online::closeAll();
}
// 服务关闭,关闭之前的连接
public function onShutdown(): void
{
Online::closeAll();
}
}

View File

@ -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'],

View File

@ -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) {
}

62
app/Model/Crontab.php Normal file
View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $crontab_id
* @property string $crontab_name
* @property string $rule
* @property string $callback
* @property string $memo
* @property string $params
* @property int $enable
* @property int $singleton
* @property int $skip_log
* @property string $create_time
* @property string $update_time
* @property string $deleted_at
*/
class Crontab extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'crontab';
protected string $primaryKey = 'crontab_id';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['crontab_id' => '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();
}
}

34
app/Model/CrontabLog.php Normal file
View File

@ -0,0 +1,34 @@
<?php
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
*/
class CrontabLog extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'crontab_log';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['log_id' => 'integer', 'crontab_id' => 'integer', 'status' => 'integer'];
}

View File

@ -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');

View File

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
@ -9,8 +10,9 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\Server\Event;
use Hyperf\Server\Server;
use Hyperf\Server\ServerInterface;
use Swoole\Constant;
use function Hyperf\Support\env;
@ -19,9 +21,9 @@ return [
'servers' => [
[
'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'],

View File

@ -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');
});