Compare commits

..

2 Commits

Author SHA1 Message Date
cf5d5059d7 feat : common redis cache 2025-09-14 16:07:34 +08:00
0db9995c19 feat : common redis cache and common logger 2025-09-14 15:58:45 +08:00
15 changed files with 642 additions and 14 deletions

View File

@@ -85,3 +85,17 @@ php bin/hyperf.php gen:event TestEvent
- `mod` 不确定分类的修改 - `mod` 不确定分类的修改
- `wip` 开发中 - `wip` 开发中
- `types` 类型 - `types` 类型
## cache
不允许使用序列化,为跨语言做准备
## 日志(合理安排)
| 分组名称 | 用途 | 日志级别 | 保留天数 | 备注 |
|---------|---------|------------|------|----------|
| app | 应用业务日志 | DEBUG/INFO | 7 | 主要业务逻辑日志 |
| error | 错误日志 | ERROR | 30 | 只记录错误 |
| cache | CACHE日志 | DEBUG | 3 | 开发调试用 |
| request | 请求访问日志 | INFO | 15 | 记录所有请求 |
| cron | 定时任务日志 | INFO | 30 | 定时任务执行记录 |
| payment | 支付相关日志 | INFO | 90 | 重要财务数据 |
| audit | 审计日志 | INFO | 365 | 重要操作记录 |

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Cache\Redis;
use App\Lib\Log\Logger;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Redis\Redis;
use Exception;
use Throwable;
abstract class BaseScript
{
/**
* @var string|null
*/
protected ?string $sha1 = null;
/**
* @var bool
*/
protected bool $debugMode;
/**
* @var Redis
*/
protected Redis $redis;
/**
* @var ConfigInterface
*/
protected ConfigInterface $config;
protected Logger $logger;
/**
* @param Redis $redis
* @param ConfigInterface $config
* @param Logger $logger
*/
public function __construct(
Redis $redis,
ConfigInterface $config,
Logger $logger
)
{
$this->redis = $redis;
$this->debugMode = $config->get('app_debug',false);
$this->logger = $logger;
}
/**
* 获取脚本名称对应lua文件名
* @return string
*/
abstract public function getName(): string;
/**
* @param array $keys
* @param array $args
* @param int|null $numKeys
* @return mixed
*/
protected function run(
array $keys,
array $args,
?int $numKeys = null
): mixed
{
$numKeys = $numKeys ?? count($keys);
try {
$script = $this->getScriptContent();
// $this->logExecution($keys, $args, true);
return $this->execute($script, $keys, $args, $numKeys);
} catch (Exception|Throwable $e) {
$this->logExecution($keys, $args, false, $e);
return false;
}
}
/**
* @return string
* @throws Exception
*/
protected function getScriptContent(): string
{
$path = __DIR__.'/Script/'.$this->getName().'.lua';
// $path = __DIR__.'/Lua/'.$this->getName().'.lua';
if (!file_exists($path)) throw new Exception('lua文件不存在');
$content = file_get_contents($path);
if ($content === false) throw new Exception('lua文件读取失败');
return $content;
}
/**
* @param string $script
* @param array $keys
* @param array $args
* @param int $numKeys
* @return mixed
*/
protected function execute(
string $script,
array $keys,
array $args,
int $numKeys
): mixed
{
if (
!$this->debugMode &&
$this->sha1
)
{
try {
return $this->redis->evalsha(
$this->sha1,
array_merge($keys, $args),
$numKeys
);
} catch (Throwable $e)
{
// SHA1 不存在时回避
$this->sha1 = null;
}
}
$result = $this->redis->eval(
$script,
array_merge($keys, $args),
$numKeys
);
$this->sha1 = sha1($script);
return $result;
}
/**
* @param array $keys
* @param array $args
* @param bool $success
* @param Throwable|null $e
* @return void
*/
protected function logExecution(
array $keys,
array $args,
bool $success,
?Throwable $e = null,
): void
{
$context = [
'script' => $this->getName(),
'keys' => $keys,
'args' => $args,
'success' => $success,
'error' => $e?->getMessage(),
'line' => $e?->getLine(),
'trace' => $e?->getTraceAsString()
];
$logStrategy = match(true) {
!$success => $this->logger->cache()->error(...),
$this->debugMode => $this->logger->cache()->debug(...),
default => fn() => null // 不记录
};
if (!$logStrategy) return;
$logStrategy('Redis Lua execution', $context);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Cache\Redis\Lua;
use App\Cache\Redis\BaseScript;
class RateLimit extends BaseScript
{
/**
* @return string
*/
public function getName(): string
{
return 'rate_limit';
}
/**
* 限流
* @param string $key
* @param int $limit
* @param int $window
* @return array
*/
public function check(string $key, int $limit, int $window): array
{
$result = $this->run([$key], [$limit, $window]);
if (!$result) return [];
return [(bool)$result[0], (int)$result[1]];
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Cache\Redis;
use App\Lib\Log\Logger;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Redis\RedisFactory;
use Hyperf\Redis\RedisProxy;
/**
* @mixin RedisProxy
*/
class RedisCache
{
/**
* @var string
*/
private string $poolName = 'default';
/**
* @var array
*/
private array $luaHandlers = [];
/**
* @param RedisFactory $redisFactory
* @param ConfigInterface $config
* @param Logger $logger
*/
public function __construct(
protected readonly RedisFactory $redisFactory,
protected readonly ConfigInterface $config,
protected readonly Logger $logger
) {}
/**
* @param string $poolName
* @return $this
*/
public function with(string $poolName = 'default'): self
{
$new = clone $this;
$new->poolName = $poolName;
return $new;
}
/**
* @return RedisProxy
*/
public function client(): RedisProxy
{
return $this->redisFactory->get($this->poolName);
}
/**
* @param string $scriptClass
* @return mixed
*/
public function lua(string $scriptClass): mixed
{
$poolName = $this->poolName ?? 'default';
$key = $poolName . ':' . $scriptClass;
if (!isset($this->luaHandlers[$key])) {
$this->luaHandlers[$key] = new $scriptClass(
$this->client(),
$this->config,
$this->logger
);
}
return $this->luaHandlers[$key];
}
/**
* 魔术方法代理原生 Redis 命令 (默认连接池)
*/
public function __call(string $method, array $arguments)
{
return $this->client()->{$method}(...$arguments);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Cache\Redis;
class RedisKey
{
/**
* @param int $id
* @return string
*/
public static function getAdminUserInfoKey(int $id): string
{
return 'admin_user:'.$id;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Cache\Redis;
use Hyperf\Redis\RedisProxy;
class RedisProxyWrapper
{
public function __construct(
protected readonly RedisProxy $redisProxy,
protected string $poolName
) {}
/**
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call(string $method, array $arguments)
{
return $this->redisProxy->{$method}(...$arguments);
}
public function lua(string $scriptClass)
{
// 这里实现你的 Lua 脚本逻辑
$key = $this->poolName . ':' . $scriptClass;
// 实际实现根据你的需求
return $this->redisProxy->eval($scriptClass, 0);
}
}

View File

@@ -0,0 +1,18 @@
-- app/Cache/Redis/Script/rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)
local remaining = 0
if current then
remaining = tonumber(current) - 1
if remaining >= 0 then
redis.call('DECR', key)
else
remaining = -1
end
else
remaining = limit - 1
redis.call('SET', key, remaining, 'EX', window)
end
return {remaining >= 0, remaining}

91
app/Lib/Log/Logger.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
namespace App\Lib\Log;
use Hyperf\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
/**
* Log类分组调用
* config/autoload/logger.php 在前面的配置文件中添加分组 在这个文件中添加方法(也可以直接调用 channel('分组名称'))
*/
class Logger
{
/**
* @var LoggerFactory
*/
protected LoggerFactory $loggerFactory;
/**
* @param LoggerFactory $loggerFactory
*/
public function __construct(LoggerFactory $loggerFactory)
{
$this->loggerFactory = $loggerFactory;
}
/**
* @return LoggerInterface
*/
public function default(): LoggerInterface
{
return $this->loggerFactory->get('default','default');
}
/**
* @return LoggerInterface
*/
public function error(): LoggerInterface
{
return $this->loggerFactory->get('error','error');
}
/**
* @return LoggerInterface
*/
public function request(): LoggerInterface
{
return $this->loggerFactory->get('request','request');
}
/**
* @return LoggerInterface
*/
public function cron(): LoggerInterface
{
return $this->loggerFactory->get('cron','cron');
}
/**
* @return LoggerInterface
*/
public function payment(): LoggerInterface
{
return $this->loggerFactory->get('payment','payment');
}
/**
* @return LoggerInterface
*/
public function audit(): LoggerInterface
{
return $this->loggerFactory->get('audit','audit');
}
/**
* @return LoggerInterface
*/
public function cache(): LoggerInterface
{
return $this->loggerFactory->get('cache','cache');
}
/**
* @param string $channel
* @return LoggerInterface
*/
public function channel(string $channel): LoggerInterface
{
return $this->loggerFactory->get($channel, $channel);
}
}

View File

@@ -31,4 +31,13 @@ final class AdminUserRepository extends BaseRepository
->where('username', $username) ->where('username', $username)
->first(); ->first();
} }
/**
* @param mixed $id
* @return array|null
*/
public function findById(mixed $id): ?array
{
return $this->getQuery()->whereKey($id)->first()?->toArray() ?? null;
}
} }

View File

@@ -10,15 +10,20 @@ declare(strict_types=1);
namespace App\Service\Admin\AdminUser; namespace App\Service\Admin\AdminUser;
use App\Cache\Redis\Lua\RateLimit;
use App\Cache\Redis\RedisCache;
use App\Lib\Jwt\RequestScopedTokenTrait; use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Repository\AdminUserRepository;
use App\Service\Admin\BaseAdminService; use App\Service\Admin\BaseAdminService;
use App\Service\BaseTokenService; use App\Service\BaseTokenService;
use App\Trait\AdminUserTrait;
use Hyperf\Collection\Arr;
use Hyperf\Di\Annotation\Inject; use Hyperf\Di\Annotation\Inject;
use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\Token\RegisteredClaims;
class UserService extends BaseAdminService class UserService extends BaseAdminService
{ {
use RequestScopedTokenTrait; use AdminUserTrait;
/** /**
* @var BaseTokenService * @var BaseTokenService
@@ -26,13 +31,26 @@ class UserService extends BaseAdminService
#[Inject] #[Inject]
protected BaseTokenService $tokenService; protected BaseTokenService $tokenService;
#[Inject]
protected RedisCache $redisCache;
public function handle(): array public function handle(): array
{ {
var_dump($this->getToken()->claims()->all()); $this->redisCache->with()->set('123',1);
var_dump($this->getToken()->claims()->get(RegisteredClaims::ID)); $this->redisCache->with()->lua(RateLimit::class)->check('user:123', 10, 60);
var_dump($this->getToken()->claims()->get(RegisteredClaims::AUDIENCE));
return $this->adminReturn->success(
'success',
Arr::only(
$this->getAdminUserInfo($this->adminId) ?: [],
['username', 'nickname', 'avatar', 'signed', 'backend_setting', 'phone', 'email']
)
);
}
private function user()
{
return $this->adminReturn->success();
} }
/** /**

View File

@@ -10,13 +10,16 @@ declare(strict_types=1);
namespace App\Service\Admin; namespace App\Service\Admin;
use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Lib\Return\AdminReturn; use App\Lib\Return\AdminReturn;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject; use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\HttpServer\Contract\RequestInterface;
use Lcobucci\JWT\Token\RegisteredClaims;
abstract class BaseAdminService abstract class BaseAdminService
{ {
use RequestScopedTokenTrait;
/** /**
* 请求对象注入 * 请求对象注入
* @var RequestInterface * @var RequestInterface
@@ -42,7 +45,7 @@ abstract class BaseAdminService
*/ */
public function __construct() public function __construct()
{ {
$this->adminId = Context::get('admin_id',0); $this->adminId = (int) $this->getToken()?->claims()?->get(RegisteredClaims::ID) ?? 0;
} }
/** /**

View File

@@ -19,8 +19,6 @@ use Lcobucci\JWT\UnencryptedToken;
class RefreshService extends BaseAdminService class RefreshService extends BaseAdminService
{ {
use RequestScopedTokenTrait;
/** /**
* @var BaseTokenService * @var BaseTokenService
*/ */

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Trait;
use App\Cache\Redis\RedisCache;
use App\Cache\Redis\RedisKey;
use App\Repository\AdminUserRepository;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
trait AdminUserTrait
{
/**
* @var RedisCache
*/
#[Inject]
protected RedisCache $redis;
/**
* @var AdminUserRepository
*/
#[Inject]
protected AdminUserRepository $adminUserRepository;
/**
* @param int $adminId
* @return array|null
*/
public function getAdminUserInfo(int $adminId): array|null
{
$key = RedisKey::getAdminUserInfoKey($adminId);
if (Context::has($key)) {
return Context::get($key,false);
}
if ($this->redis->with()->exists($key)) {
$userInfo = $this->redis->with()->get($key);
Context::set($key,$userInfo);
return json_decode($userInfo,true);
}
$userInfo = $this->adminUserRepository->findById($adminId);
if (!$userInfo) return null;
Context::set($key, $userInfo);
$this->redis->with()->set($key, json_encode($userInfo), 3600);
return $userInfo;
}
}

View File

@@ -39,9 +39,9 @@ return [
// jwt 签名key // jwt 签名key
'key' => InMemory::base64Encoded(env('JWT_SECRET')), 'key' => InMemory::base64Encoded(env('JWT_SECRET')),
// token过期时间单位为秒 // token过期时间单位为秒
'ttl' => (int) env('ADMIN_JWT_TTL', 3), 'ttl' => (int) env('ADMIN_JWT_TTL', 3600),
// 刷新token过期时间单位为秒 // 刷新token过期时间单位为秒
'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 10), 'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 7200),
'claims' => [ 'claims' => [
// 默认的jwt claims // 默认的jwt claims
RegisteredClaims::ISSUER => (string) env('APP_NAME') .'_admin', RegisteredClaims::ISSUER => (string) env('APP_NAME') .'_admin',

View File

@@ -9,17 +9,22 @@ declare(strict_types=1);
* @contact group@hyperf.io * @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/ */
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
return [ return [
'default' => [ 'default' => [
'handler' => [ 'handler' => [
'class' => Monolog\Handler\StreamHandler::class, 'class' => StreamHandler::class,
'constructor' => [ 'constructor' => [
'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
'level' => Monolog\Logger::DEBUG, 'level' => Monolog\Logger::DEBUG,
], ],
], ],
'formatter' => [ 'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class, 'class' => LineFormatter::class,
'constructor' => [ 'constructor' => [
'format' => null, 'format' => null,
'dateFormat' => 'Y-m-d H:i:s', 'dateFormat' => 'Y-m-d H:i:s',
@@ -27,4 +32,93 @@ return [
], ],
], ],
], ],
'app' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/app/app.log',
'level' => Monolog\Logger::DEBUG,
'maxFiles' => 7,
],
],
'formatter' => [
'class' => LineFormatter::class,
'constructor' => [
'format' => "[%datetime%] %level_name% [%channel%]: %message% %context%\n",
'dateFormat' => 'Y-m-d H:i:s',
],
],
],
'error' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/error/error.log',
'level' => Monolog\Logger::ERROR,
'maxFiles' => 30,
],
// 可以添加邮件通知handler
// [
// 'class' => \Monolog\Handler\NativeMailerHandler::class,
// 'constructor' => [
// 'to' => 'admin@example.com',
// 'subject' => 'Application Error',
// 'from' => 'error@example.com',
// 'level' => Monolog\Logger::ERROR,
// ],
// ],
],
],
'request' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/request/request.log',
'level' => Monolog\Logger::INFO,
'maxFiles' => 15,
],
],
],
'cron' => [
'handler' => [
[
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/crontab/cron.log',
'level' => Monolog\Logger::INFO,
'maxFiles' => 30,
],
],
],
],
'payment' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/payment/payment.log',
'level' => Monolog\Logger::INFO,
'maxFiles' => 90,
],
],
],
'audit' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/audit.log',
'level' => Monolog\Logger::INFO,
'maxFiles' => 365,
],
],
],
'cache' => [
'handler' => [
'class' => RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/cache/cache.log',
'level' => Monolog\Logger::DEBUG,
'maxFiles' => 3,
],
],
],
]; ];