From 0db9995c1952cd3f555228b10621b9d097e27664 Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Sun, 14 Sep 2025 15:58:45 +0800 Subject: [PATCH] feat : common redis cache and common logger --- README.md | 16 +- app/Cache/Redis/BaseScript.php | 173 ++++++++++++++++++++ app/Cache/Redis/Lua/RateLimit.php | 30 ++++ app/Cache/Redis/RedisCache.php | 82 ++++++++++ app/Cache/Redis/RedisKey.php | 15 ++ app/Cache/Redis/RedisProxyWrapper.php | 31 ++++ app/Cache/Redis/Script/rate_limit.lua | 18 ++ app/Lib/Log/Logger.php | 91 ++++++++++ app/Repository/AdminUserRepository.php | 9 + app/Service/Admin/AdminUser/UserService.php | 28 +++- app/Service/Admin/BaseAdminService.php | 7 +- app/Service/Admin/Login/RefreshService.php | 2 - app/Trait/AdminUserTrait.php | 50 ++++++ config/autoload/jwt.php | 4 +- config/autoload/logger.php | 98 ++++++++++- 15 files changed, 640 insertions(+), 14 deletions(-) create mode 100644 app/Cache/Redis/BaseScript.php create mode 100644 app/Cache/Redis/Lua/RateLimit.php create mode 100644 app/Cache/Redis/RedisCache.php create mode 100644 app/Cache/Redis/RedisKey.php create mode 100644 app/Cache/Redis/RedisProxyWrapper.php create mode 100644 app/Cache/Redis/Script/rate_limit.lua create mode 100644 app/Lib/Log/Logger.php create mode 100644 app/Trait/AdminUserTrait.php diff --git a/README.md b/README.md index 9291187..42b277d 100644 --- a/README.md +++ b/README.md @@ -84,4 +84,18 @@ php bin/hyperf.php gen:event TestEvent - `workflow` 工作流改进 - `mod` 不确定分类的修改 - `wip` 开发中 -- `types` 类型 \ No newline at end of file +- `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 | 重要操作记录 | \ No newline at end of file diff --git a/app/Cache/Redis/BaseScript.php b/app/Cache/Redis/BaseScript.php new file mode 100644 index 0000000..4e38370 --- /dev/null +++ b/app/Cache/Redis/BaseScript.php @@ -0,0 +1,173 @@ +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 (Throwable $e) { + $this->logExecution($keys, $args, false, $e); + return false; + } + } + + /** + * @return string + */ + protected function getScriptContent(): string + { +// $path = __DIR__.'/Script/'.$this->getName().'.lua'; + $path = __DIR__.'/Lua/'.$this->getName().'.lua'; + + if (!file_exists($path)) echo 1; + + $content = file_get_contents($path); + if ($content === false) echo 2; + + 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() + ]; + + $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); + } +} \ No newline at end of file diff --git a/app/Cache/Redis/Lua/RateLimit.php b/app/Cache/Redis/Lua/RateLimit.php new file mode 100644 index 0000000..2658254 --- /dev/null +++ b/app/Cache/Redis/Lua/RateLimit.php @@ -0,0 +1,30 @@ +run([$key], [$limit, $window]); + if (!$result) return []; + return [(bool)$result[0], (int)$result[1]]; + } +} \ No newline at end of file diff --git a/app/Cache/Redis/RedisCache.php b/app/Cache/Redis/RedisCache.php new file mode 100644 index 0000000..e767aea --- /dev/null +++ b/app/Cache/Redis/RedisCache.php @@ -0,0 +1,82 @@ +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); + } +} \ No newline at end of file diff --git a/app/Cache/Redis/RedisKey.php b/app/Cache/Redis/RedisKey.php new file mode 100644 index 0000000..45feeff --- /dev/null +++ b/app/Cache/Redis/RedisKey.php @@ -0,0 +1,15 @@ +redisProxy->{$method}(...$arguments); + } + + public function lua(string $scriptClass) + { + // 这里实现你的 Lua 脚本逻辑 + $key = $this->poolName . ':' . $scriptClass; + // 实际实现根据你的需求 + return $this->redisProxy->eval($scriptClass, 0); + } +} \ No newline at end of file diff --git a/app/Cache/Redis/Script/rate_limit.lua b/app/Cache/Redis/Script/rate_limit.lua new file mode 100644 index 0000000..8d945fd --- /dev/null +++ b/app/Cache/Redis/Script/rate_limit.lua @@ -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} \ No newline at end of file diff --git a/app/Lib/Log/Logger.php b/app/Lib/Log/Logger.php new file mode 100644 index 0000000..731c610 --- /dev/null +++ b/app/Lib/Log/Logger.php @@ -0,0 +1,91 @@ +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); + } +} \ No newline at end of file diff --git a/app/Repository/AdminUserRepository.php b/app/Repository/AdminUserRepository.php index 19118a7..7260fcf 100644 --- a/app/Repository/AdminUserRepository.php +++ b/app/Repository/AdminUserRepository.php @@ -31,4 +31,13 @@ final class AdminUserRepository extends BaseRepository ->where('username', $username) ->first(); } + + /** + * @param mixed $id + * @return array|null + */ + public function findById(mixed $id): ?array + { + return $this->getQuery()->whereKey($id)->first()?->toArray() ?? null; + } } \ No newline at end of file diff --git a/app/Service/Admin/AdminUser/UserService.php b/app/Service/Admin/AdminUser/UserService.php index 8a6b63d..0ad7530 100644 --- a/app/Service/Admin/AdminUser/UserService.php +++ b/app/Service/Admin/AdminUser/UserService.php @@ -10,15 +10,20 @@ declare(strict_types=1); namespace App\Service\Admin\AdminUser; +use App\Cache\Redis\Lua\RateLimit; +use App\Cache\Redis\RedisCache; use App\Lib\Jwt\RequestScopedTokenTrait; +use App\Repository\AdminUserRepository; use App\Service\Admin\BaseAdminService; use App\Service\BaseTokenService; +use App\Trait\AdminUserTrait; +use Hyperf\Collection\Arr; use Hyperf\Di\Annotation\Inject; use Lcobucci\JWT\Token\RegisteredClaims; class UserService extends BaseAdminService { - use RequestScopedTokenTrait; + use AdminUserTrait; /** * @var BaseTokenService @@ -26,13 +31,26 @@ class UserService extends BaseAdminService #[Inject] protected BaseTokenService $tokenService; + #[Inject] + protected RedisCache $redisCache; + public function handle(): array { - var_dump($this->getToken()->claims()->all()); - var_dump($this->getToken()->claims()->get(RegisteredClaims::ID)); - var_dump($this->getToken()->claims()->get(RegisteredClaims::AUDIENCE)); + $this->redisCache->with()->set('123',1); + $this->redisCache->with()->lua(RateLimit::class)->check('user:123', 10, 60); + + 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(); } /** diff --git a/app/Service/Admin/BaseAdminService.php b/app/Service/Admin/BaseAdminService.php index 2cb013d..fd9f284 100644 --- a/app/Service/Admin/BaseAdminService.php +++ b/app/Service/Admin/BaseAdminService.php @@ -10,13 +10,16 @@ declare(strict_types=1); namespace App\Service\Admin; +use App\Lib\Jwt\RequestScopedTokenTrait; use App\Lib\Return\AdminReturn; -use Hyperf\Context\Context; use Hyperf\Di\Annotation\Inject; use Hyperf\HttpServer\Contract\RequestInterface; +use Lcobucci\JWT\Token\RegisteredClaims; abstract class BaseAdminService { + use RequestScopedTokenTrait; + /** * 请求对象注入 * @var RequestInterface @@ -42,7 +45,7 @@ abstract class BaseAdminService */ public function __construct() { - $this->adminId = Context::get('admin_id',0); + $this->adminId = (int) $this->getToken()?->claims()?->get(RegisteredClaims::ID) ?? 0; } /** diff --git a/app/Service/Admin/Login/RefreshService.php b/app/Service/Admin/Login/RefreshService.php index b074bc7..e688951 100644 --- a/app/Service/Admin/Login/RefreshService.php +++ b/app/Service/Admin/Login/RefreshService.php @@ -19,8 +19,6 @@ use Lcobucci\JWT\UnencryptedToken; class RefreshService extends BaseAdminService { - use RequestScopedTokenTrait; - /** * @var BaseTokenService */ diff --git a/app/Trait/AdminUserTrait.php b/app/Trait/AdminUserTrait.php new file mode 100644 index 0000000..c1b7ba6 --- /dev/null +++ b/app/Trait/AdminUserTrait.php @@ -0,0 +1,50 @@ +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; + } +} \ No newline at end of file diff --git a/config/autoload/jwt.php b/config/autoload/jwt.php index e0121c3..ffedc49 100644 --- a/config/autoload/jwt.php +++ b/config/autoload/jwt.php @@ -39,9 +39,9 @@ return [ // jwt 签名key 'key' => InMemory::base64Encoded(env('JWT_SECRET')), // token过期时间,单位为秒 - 'ttl' => (int) env('ADMIN_JWT_TTL', 3), + 'ttl' => (int) env('ADMIN_JWT_TTL', 3600), // 刷新token过期时间,单位为秒 - 'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 10), + 'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 7200), 'claims' => [ // 默认的jwt claims RegisteredClaims::ISSUER => (string) env('APP_NAME') .'_admin', diff --git a/config/autoload/logger.php b/config/autoload/logger.php index ee4691f..48e43b1 100644 --- a/config/autoload/logger.php +++ b/config/autoload/logger.php @@ -9,17 +9,22 @@ declare(strict_types=1); * @contact group@hyperf.io * @license https://github.com/hyperf/hyperf/blob/master/LICENSE */ + +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\RotatingFileHandler; +use Monolog\Handler\StreamHandler; + return [ 'default' => [ 'handler' => [ - 'class' => Monolog\Handler\StreamHandler::class, + 'class' => StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ - 'class' => Monolog\Formatter\LineFormatter::class, + 'class' => LineFormatter::class, 'constructor' => [ 'format' => null, '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, + ], + ], + ], ];