Compare commits

..

22 Commits

Author SHA1 Message Date
5890122a37 fix : readme update 2025-09-18 17:26:45 +08:00
b405585bc7 fix : readme update 2025-09-17 17:18:39 +08:00
5d2f32b22b fix : readme update 2025-09-17 17:18:18 +08:00
082c15d697 fix : first finish 2025-09-17 16:17:55 +08:00
21331655f9 fix : common request 2025-09-16 18:07:10 +08:00
be0d0913b6 fix : update path And request 2025-09-16 15:14:47 +08:00
c1d8f02491 fix : update menu 2025-09-16 14:30:37 +08:00
2613b031ae feat : admin log 2025-09-16 14:30:12 +08:00
a6d6738ab2 feat : admin role finish 2025-09-16 10:38:45 +08:00
b1713d8476 feat : admin role unfinish 2025-09-15 18:56:34 +08:00
5ed0e2d4f5 feat : admin menu 2025-09-15 17:09:00 +08:00
1eae4171f6 feat : admin aspect 2025-09-15 14:45:34 +08:00
8046676669 feat : admin aspect 2025-09-15 14:45:29 +08:00
8665b27294 feat : jwt 2025-09-15 09:32:15 +08:00
1d01ecfdcb feat : admin user finish 2025-09-14 22:33:32 +08:00
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
48ad2ebd1b fix : jwt
Some checks failed
Build Docker / build (push) Has been cancelled
2025-09-13 12:16:13 +08:00
a1fdeb9148 fix : jwt
Some checks are pending
Build Docker / build (push) Waiting to run
2025-09-12 23:54:21 +08:00
68ecac2fb9 fix : jwt 2025-09-12 23:54:03 +08:00
ea6abe3043 feat : jwt
Some checks are pending
Build Docker / build (push) Waiting to run
2025-09-12 18:21:19 +08:00
ff3e0105ec feat : jwt 2025-09-12 18:12:30 +08:00
74 changed files with 3966 additions and 149 deletions

View File

@@ -1,22 +1,25 @@
APP_NAME=sfyy
APP_NAME=ctext
APP_ENV=dev
APP_DEBUG=false
APP_URL=http://127.0.0.1:9501
JWT_SECRET=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2A==
JWT_ADMIN_SECRET=azOVxsOWt3r0ozZNz8Ss429ht0T8z6OpeIJAIwNp6X0xqrbEY2epfIWyxtC1qSNM8eD6/LQ/SahcQi2ByXa/2Aaa
# [ide]
DEVTOOL_IDE=phpstorm
DB_DRIVER=mysql
DB_HOST=s2.gnip.vip
DB_PORT=20191
DB_DATABASE=mineadmin
DB_USERNAME=hhl
DB_PASSWORD=hhltest
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=xxx
DB_USERNAME=username
DB_PASSWORD=password
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_PREFIX=
REDIS_HOST=s2.gnip.vip
REDIS_AUTH=hhltest
REDIS_PORT=4379
REDIS_HOST=127.0.0.1
REDIS_AUTH=
REDIS_PORT=6379
REDIS_DB=0

229
README.md
View File

@@ -1,6 +1,6 @@
## 仓库
- [sfyy_server](https://codeup.aliyun.com/67039465d8d1ada68263f984/hhl/rewrite/hyperf_service.git) - git远程仓库地址
- [hyperf_rbac_framework_server_ctexthuang](https://gitee.com/ctexthuang/hyperf_rbac_framework_server_ctexthuang.git) - git远程仓库地址
## 特性
@@ -27,9 +27,7 @@
- 获取代码
```bash
git clone https://codeup.aliyun.com/67039465d8d1ada68263f984/hhl/rewrite/hyperf_service.git
mkdir uploads
git clone https://gitee.com/ctexthuang/hyperf_rbac_framework_server_ctexthuang.git
```
- vendor
@@ -50,22 +48,32 @@ php bin/hyperf.php start
- command 函数
```bash
#框架自有
php bin/hyperf.php gen:controller LoginController
php bin/hyperf.php gen:model UserModel
php bin/hyperf.php gen:request LoginRequest
php bin/hyperf.php gen:command TestCommand
php bin/hyperf.php gen:job TestJob
php bin/hyperf.php gen:listener TestListener
php bin/hyperf.php gen:middleware AuthMiddleware
php bin/hyperf.php gen:amqp-consumer DemoConsumer
php bin/hyperf.php gen:amqp-producer DemoProducer
php bin/hyperf.php gen:constant ErrorCode --type enum
#框架自有 php bin/hyperf.php + 以下
gen:amqp-consumer Create a new amqp consumer class
gen:amqp-producer Create a new amqp producer class
gen:aspect Create a new aspect class
gen:class Create a new class
gen:command Create a new command class
gen:constant Create a new constant class
gen:controller Create a new controller class
gen:job Create a new job class
gen:kafka-consumer Create a new kafka consumer class
gen:listener Create a new listener class
gen:middleware Create a new middleware class
gen:migration Generate a new migration file
gen:model Create new model classes.
gen:nats-consumer Create a new nats consumer class
gen:nsq-consumer Create a new nsq consumer class
gen:process Create a new process class
gen:repository Create a new repository class
gen:request Create a new form request class
gen:resource create a new resource
gen:seeder Create a new seeder class
gen:service Create a new service class
#新增命令
php bin/hyperf.php gen:service LoginService
php bin/hyperf.php gen:cron OssTask
php bin/hyperf.php gen:event TestEvent
php bin/hyperf.php gen:repository OssRepository
```
## Git 贡献提交规范
@@ -85,3 +93,190 @@ php bin/hyperf.php gen:event TestEvent
- `mod` 不确定分类的修改
- `wip` 开发中
- `types` 类型
## 日志(合理安排)
| 分组名称 | 用途 | 日志级别 | 保留天数 | 备注 |
|---------|---------|------------|------|----------|
| app | 应用业务日志 | DEBUG/INFO | 7 | 主要业务逻辑日志 |
| error | 错误日志 | ERROR | 30 | 只记录错误 |
| cache | CACHE日志 | DEBUG | 3 | 开发调试用 |
| request | 请求访问日志 | INFO | 15 | 记录所有请求 |
| cron | 定时任务日志 | INFO | 30 | 定时任务执行记录 |
| payment | 支付相关日志 | INFO | 90 | 重要财务数据 |
| audit | 审计日志 | INFO | 365 | 重要操作记录 |\
```php
//注入这个类
use App\Lib\Log\Logger;
$this->logger->cache()->error(...),
```
## 缓存
```php
//注入这个类
use App\Cache\Redis\RedisCache
$this->redis->with($poolName)->exists($key)
//$poolName 配置在 config/autoload/redis.php 在 default下面追加即可 看官网
//lua调用涉及多文件请看示例
$this->redis->with($poolName ?? 'default')->lua(RateLimit::class)->check($argument);
//lua()里面是App\Cache\Redis\Lua\*文件 后面跟的方法是 该文件的方法
//我做了返回类型补全 可以直接IDE跳转查看参数
```
## tips
1.AdminService类必须继承BaseAdminService
```php
//例子 app/Service/Admin/AdminUser/UserService
class UserService extends \App\Service\Admin\BaseAdminService {
}
//由于在 app/Controller/Admin/AdminUserController 中 service 是 注解注入的 所以 BaseAdminService只会实例化一次单例
class AdminUserController extends AbstractController
{
/**
* @var UserService
*/
#[Inject]
protected UserService $service;
/**
* @return array
*/
#[RequestMapping(path: "getInfo", methods: "GET")]
public function getInfo(): array
{
return $this->service->handle();
}
}
//所以在service中获取上下文的数据必须重置获取而不能存入BaseService属性中拿取
//假设在 Api目录中写 BaseApiService请务必注意
class UserService extends \App\Service\Admin\BaseAdminService {
/**
* @param string $name
* @return \current_admin_id|mixed
*/
public function __get(string $name)
{
if ($name === 'adminId') return Context::get('current_admin_id',0);
if (!property_exists($this, $name)) throw new ErrException('属性未定义');
return $this->$name;
}
}
class UserService extends BaseAdminService{
public function handle()
{
return $this->adminId;
}
}
// 或者在controller中实例化去调用
class AdminUserController extends AbstractController
{
/**
* @return array
*/
#[RequestMapping(path: "getInfo", methods: "GET")]
public function getInfo(): array
{
return (new UserService)->handle();
}
}
```
2.controller必须增加ResponseFormat注解
```php
//示例 : app/Controller/Admin/AdminUserController
#[Controller(prefix: "admin/adminUser")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserController extends \App\Controller\AbstractController{
}
//因为在涉及到公有抛出 ErrException或者 验证器抛出 的时候 会根据不同的注解 返回不同的 Return
class BaseErrExceptionHandler extends ExceptionHandler{
public function handle()
{
// 从注解获取响应格式(优先于路径解析)
$format = $this->request->getAttribute('response_format') ?? $this->repairResponseFormatByPath();
// 动态选择策略
$returnClass = match ($format) {
'admin', 'common' => AdminReturn::class,
'api' => ApiReturn::class,
default => null,
};
}
}
// 通过切面存入
class ResponseFormatAspect extends AbstractAspect
{
// 获取注解定义的格式
$annotation = $proceedingJoinPoint->getAnnotationMetadata()->class[ResponseFormat::class]
?? $proceedingJoinPoint->getAnnotationMetadata()->method[ResponseFormat::class] ?? null;
if ($annotation) {
// 将注解格式存入请求属性(覆盖中间件的默认值)
$request = $proceedingJoinPoint->arguments['request'] ?? null;
if ($request instanceof Request) {
$request->withAttribute('response_format', $annotation->format);
}
}
}
```
3.token的使用要分模块
```php
//示例 app/Service/Admin/Login/LoginService
use App\Service\BaseTokenService $tokenService
class LoginService extends \App\Service\Admin\BaseAdminService{
public function handle()
{
$jwtHandle = $this->tokenService->setJwt('admin')->getJwt();
return $this->adminReturn->success('success',[
'access_token' => $jwtHandle->builderAccessToken((string) $adminInfo->id)->toString(),
'refresh_token' => $jwtHandle->builderRefreshToken((string) $adminInfo->id)->toString(),
'expire_at' => (int) $jwtHandle->getConfig('ttl', 0),
]);
}
}
//setJwt函数里面类似$poolName 设置在 config/autoload/jwt.php 未设置属性可复用 default 设置
```
4.引入 Repository层架构 解放 model层压力 model 只管理数据模型和关联
```php
//详细请看app/Common/Repository 文件下的文件 示例 app/Service/Admin/Login/LoginService
class LoginService extends \App\Service\Admin\BaseAdminService{
/**
* 需要提前注入
* @var AdminUserRepository
*/
#[Inject]
protected AdminUserRepository $userRepository;
public function handle()
{
$adminInfo = $this->userRepository->findByUserName((string)$this->request->input('username'));
}
}
```
5.公用类都在 app/Common 下面 包括 `Interface``Macros``Repository``Trait`
6.后台权限注解 #[Permission] 和 #[PermissionMiddleware]

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Permission extends AbstractAnnotation
{
public const string OPERATION_AND = 'and';
public const string OPERATION_OR = 'or';
/**
* @param array|string $code
* @param string $operation
*/
public function __construct(
protected array|string $code,
protected string $operation = self::OPERATION_AND
) {}
/**
* @return array
*/
public function getCode(): array
{
return (array) $this->code;
}
/**
* @return string
*/
public function getOperation(): string
{
return $this->operation;
}
}

View File

@@ -3,9 +3,10 @@
namespace App\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class ResponseFormat
class ResponseFormat extends AbstractAnnotation
{
/**
* @param string $format

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Aspect;
use App\Common\Repository\AdminUserRepository;
use App\Common\Trait\ClientIpTrait;
use App\Common\Trait\ClientOsTrait;
use App\Lib\Log\Logger;
use App\Model\AdminUserLoginLog;
use App\Service\Admin\Login\LoginService;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Di\Exception\Exception;
use Hyperf\HttpServer\Contract\RequestInterface;
use Throwable;
#[Aspect]
class AdminLoginLogAspect extends AbstractAspect
{
use ClientIpTrait;
use ClientOsTrait;
/**
* 切入类
* @var array|\class-string[]
*/
public array $classes = [
LoginService::class,
];
/**
* @var bool
*/
private bool $loginSuccess = false;
/**
* @var string
*/
private string $loginMsg = '';
/**
* @param Logger $logger
* @param RequestInterface $request
* @param AdminUserRepository $adminUserRepository
* @param AdminUserLoginLog $adminUserLoginLogModel
*/
public function __construct(
protected readonly Logger $logger,
protected RequestInterface $request,
protected readonly AdminUserRepository $adminUserRepository,
protected readonly AdminUserLoginLog $adminUserLoginLogModel,
) {}
/**
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
* @throws Exception
* @throws Throwable
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint): mixed
{
// 写日志
try {
$res = $proceedingJoinPoint->process();
$this->loginSuccess = true;
$this->loginMsg = 'success';
} catch (Throwable $throwable) {
$this->loginSuccess = false;
$this->loginMsg = $throwable->getMessage();
throw $throwable;
} finally {
$this->writeLoginLog();
}
// 返回
return $res;
}
/**
* @return void
*/
private function writeLoginLog(): void
{
$userInfo = $this->adminUserRepository->findByUserName($this->request->input('username'));
$context = [
'username' => $this->request->input('username',''),
'password' => $this->request->input('password',''),
'user_info' => $userInfo?->toArray() ?? [],
'ip' => $this->getClientIp(),
'os' => $this->getClientOs(),
'browser' => $this->request->header('User-Agent') ?: 'unknown',
];
Coroutine::create(function () use ($userInfo, $context) {
$this->logger->request()->info('admin_login_log', $context);
$this->adminUserLoginLogModel->create([
'admin_user_id' => $userInfo?->id ?? 0,
'username' => $userInfo?->username ?? '',
'ip' => current($context['ip']) ?: '0.0.0.0',
'os' => $context['os'],
'browser' => $context['browser'] ?? '',
'status' => $this->loginSuccess ? 1 : 2,
'message' => $this->loginMsg,
]);
});
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Aspect;
use App\Common\Trait\AdminUserTrait;
use App\Common\Trait\ClientIpTrait;
use App\Lib\Log\Logger;
use App\Lib\Return\AdminReturn;
use App\Model\AdminUser;
use App\Model\AdminUserOperationLog;
use Hyperf\Context\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Di\Exception\Exception;
use Hyperf\HttpServer\Contract\RequestInterface;
#[Aspect]
class AdminReturnLogAspect extends AbstractAspect
{
use ClientIpTrait;
use AdminUserTrait;
/**
* 切入类
* @var array|\class-string[]
*/
public array $classes = [
AdminReturn::class,
];
/**
* @var int
*/
protected int $adminId = 0;
/**
* @var ?AdminUser
*/
protected ?AdminUser $adminUserInfo = null;
/**
* @param Logger $logger
* @param RequestInterface $request
* @param AdminUserOperationLog $adminUserOperationLogModel
*/
public function __construct(
protected readonly Logger $logger,
protected RequestInterface $request,
protected readonly AdminUserOperationLog $adminUserOperationLogModel,
) {
$this->adminId = Context::get('current_admin_id',0);
if ($this->adminId > 0) $this->adminUserInfo = $this->getAdminUserInfo($this->adminId);
}
/**
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
* @throws Exception
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// 直接从方法参数获取请求数据
$requestData = $proceedingJoinPoint->getArguments()[0] ?? [];
// 执行原方法并获取返回值
$responseData = $proceedingJoinPoint->process();
// 没登录不记录日志
if ($this->adminId <= 0) return $responseData;
// 写日志
$this->writeOperationLog($requestData, $responseData);
// 返回
return $responseData;
}
/**
* @param array $requestData
* @param array $responseData
* @return void
*/
private function writeOperationLog(array $requestData = [], array $responseData = []): void
{
$context = [
'user_id' => $this->adminId,
'method' => $this->request->getMethod(),
'router' => $this->request->getUri(),
'ip' => $this->getClientIp(),
'request_data' => $requestData,
'response_data' => $responseData,
];
Coroutine::create(function () use ($requestData, $responseData, $context) {
$this->logger->request()->info('admin_request_log', $context);
$this->adminUserOperationLogModel->create([
'admin_user_id' => $this->adminId,
'username' => $this->adminUserInfo?->username ?? '',
'method' => $context['method'],
'router' => $context['router'],
'service_name' => $context['service_name'] ?? '',
'ip' => current($context['ip']) ?: '0.0.0.0',
]);
});
}
}

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->error()->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,85 @@
<?php
namespace App\Cache\Redis;
use App\Cache\Redis\Lua\RateLimit;
use App\Lib\Log\Logger;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Redis\RedisFactory;
use Hyperf\Redis\RedisProxy;
/**
* @mixin RedisProxy
* @template T
*/
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);
}
/**
* @template TReturn
* @param class-string<TReturn> $scriptClass
* @return TReturn
*/
public function lua(string $scriptClass)
{
$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}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Common\Interface;
use Lcobucci\JWT\UnencryptedToken;
interface CheckTokenInterface
{
public function checkJwt(UnencryptedToken $token): void;
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Interface;
namespace App\Common\Interface;
use Lcobucci\JWT\UnencryptedToken;

View File

@@ -0,0 +1,80 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Common\Repository;
use App\Constants\Model\AdminUser\AdminMenuStatusCode;
use App\Model\AdminMenu;
use Hyperf\Collection\Arr;
use Hyperf\Collection\Collection;
use Hyperf\Database\Model\Builder;
final class AdminMenuRepository extends BaseRepository
{
/**
* @param AdminMenu $model
*/
public function __construct(protected readonly AdminMenu $model) {}
/**
* @return bool
*/
public function enablePageOrderBy(): bool
{
return false;
}
/**
* @param array $params
* @return Collection
*/
public function list(array $params = []): Collection
{
return $this->perQuery($this->getQuery(), $params)->orderBy('sort')->get();
}
/**
* @param Builder $query
* @param array $params
* @return Builder
*/
public function handleSearch(Builder $query, array $params): Builder
{
$whereInName = static function (Builder $query, array|string $code) {
$query->whereIn('name', Arr::wrap($code));
};
return $query
->when(Arr::get($params, 'sortable'), static function (Builder $query, array $sortable) {
$query->orderBy(key($sortable), current($sortable));
})
->when(Arr::get($params, 'code'), $whereInName)
->when(Arr::get($params, 'name'), $whereInName)
->when(Arr::get($params, 'children'), static function (Builder $query) {
$query->with('children');
})->when(Arr::get($params, 'status'), static function (Builder $query, AdminMenuStatusCode $status) {
$query->where('status', $status);
})
->when(Arr::has($params, 'parent_id'), static function (Builder $query) use ($params) {
$query->where('parent_id', Arr::get($params, 'parent_id'));
});
}
/**
* @return Builder[]|\Hyperf\Database\Model\Collection
*/
public function allTree(): \Hyperf\Database\Model\Collection|array
{
return $this->model
->newQuery()
->where('parent_id', 0)
->with('children')
->get();
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Common\Repository;
use App\Model\AdminRole;
use Hyperf\Collection\Arr;
use Hyperf\Database\Model\Builder;
final class AdminRoleRepository extends BaseRepository
{
public function __construct(protected readonly AdminRole $model) {}
/**
* @param Builder $query
* @param array $params
* @return Builder
*/
public function handleSearch(Builder $query, array $params): Builder
{
return $query->when(Arr::get($params, 'name'), static function (Builder $query, $name) {
$query->where('name', 'like', '%' . $name . '%');
})->when(Arr::get($params, 'code'), static function (Builder $query, $code) {
$query->whereIn('code', Arr::wrap($code));
})->when(Arr::has($params, 'status'), static function (Builder $query) use ($params) {
$query->where('status', $params['status']);
})->when(Arr::get($params, 'created_at'), static function (Builder $query, $createdAt) {
$query->whereBetween('created_at', $createdAt);
});
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Common\Repository;
use App\Model\AdminUserLoginLog;
use Hyperf\Collection\Arr;
use Hyperf\Database\Model\Builder;
final class AdminUserLoginLogRepository extends BaseRepository
{
/**
* @param AdminUserLoginLog $model
*/
public function __construct(protected readonly AdminUserLoginLog $model) {}
/**
* @param Builder $query
* @param array $params
* @return Builder
*/
public function handleSearch(Builder $query, array $params): Builder
{
return $query
->when(Arr::get($params, 'username'), static function (Builder $query, $username) {
$query->where('username', $username);
})
->when(Arr::get($params, 'ip'), static function (Builder $query, $ip) {
$query->where('ip', $ip);
})
->when(Arr::get($params, 'os'), static function (Builder $query, $os) {
$query->where('os', $os);
})
->when(Arr::get($params, 'browser'), static function (Builder $query, $browser) {
$query->where('browser', $browser);
})
->when(Arr::get($params, 'status'), static function (Builder $query, $status) {
$query->where('status', $status);
})
->when(Arr::get($params, 'message'), static function (Builder $query, $message) {
$query->where('message', $message);
})
->when(Arr::get($params, 'login_time'), static function (Builder $query, $login_time) {
$query->whereBetween('login_time', $login_time);
})
->when(Arr::get($params, 'remark'), static function (Builder $query, $remark) {
$query->where('remark', $remark);
});
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Common\Repository;
use App\Model\AdminUserOperationLog;
final class AdminUserOperationLogRepository extends BaseRepository
{
public function __construct(protected readonly AdminUserOperationLog $model) {}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Common\Repository;
use App\Model\AdminUser;
use Hyperf\Collection\Arr;
use Hyperf\Database\Model\Builder;
/**
* Class AdminUserRepository
* @extends BaseRepository<AdminUser>
*/
final class AdminUserRepository extends BaseRepository
{
public function __construct(protected readonly AdminUser $model) {}
/**
* @param string $username
* @return AdminUser|null
*/
public function findByUserName(string $username): AdminUser|null
{
// @phpstan-ignore-next-line
return $this->model->newQuery()
->where('username', $username)
->first();
}
/**
* @param Builder $query
* @param array $params
* @return Builder
*/
public function handleSearch(Builder $query, array $params): Builder
{
return $query
->when(Arr::get($params, 'unique_username'), static function (Builder $query, $uniqueUsername) {
$query->where('username', $uniqueUsername);
})
->when(Arr::get($params, 'username'), static function (Builder $query, $username) {
$query->where('username', 'like', '%' . $username . '%');
})
->when(Arr::get($params, 'phone'), static function (Builder $query, $phone) {
$query->where('phone', $phone);
})
->when(Arr::get($params, 'email'), static function (Builder $query, $email) {
$query->where('email', $email);
})
->when(Arr::exists($params, 'status'), static function (Builder $query) use ($params) {
$query->where('status', Arr::get($params, 'status'));
})
->when(Arr::exists($params, 'user_type'), static function (Builder $query) use ($params) {
$query->where('user_type', Arr::get($params, 'user_type'));
})
->when(Arr::exists($params, 'nickname'), static function (Builder $query) use ($params) {
$query->where('nickname', 'like', '%' . Arr::get($params, 'nickname') . '%');
})
->when(Arr::exists($params, 'created_at'), static function (Builder $query) use ($params) {
$query->whereBetween('created_at', [
Arr::get($params, 'created_at')[0] . ' 00:00:00',
Arr::get($params, 'created_at')[1] . ' 23:59:59',
]);
})
->when(Arr::get($params, 'user_ids'), static function (Builder $query, $userIds) {
$query->whereIn('id', $userIds);
})
->when(Arr::get($params, 'role_id'), static function (Builder $query, $roleId) {
$query->whereHas('roles', static function (Builder $query) use ($roleId) {
$query->where('role_id', $roleId);
});
});
}
}

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Repository;
namespace App\Common\Repository;
use App\Repository\Traits\BootTrait;
use App\Repository\Traits\RepositoryOrderByTrait;
use App\Common\Repository\Traits\BootTrait;
use App\Common\Repository\Traits\RepositoryOrderByTrait;
use Hyperf\Collection\Collection;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Database\Model\Builder;
@@ -13,7 +13,7 @@ use Hyperf\Paginator\AbstractPaginator;
/**
* @template T of Model
* @property T $model
* @property Model $model
*/
abstract class BaseRepository
{
@@ -67,7 +67,7 @@ abstract class BaseRepository
}
/**
* @return T
* @return Model
*/
public function create(array $data): mixed
{
@@ -81,7 +81,7 @@ abstract class BaseRepository
}
/**
* @return null|T
* @return null|Model
*/
public function saveById(mixed $id, array $data): mixed
{
@@ -105,7 +105,7 @@ abstract class BaseRepository
}
/**
* @return null|T
* @return null|Model
*/
public function findById(mixed $id): mixed
{
@@ -118,7 +118,7 @@ abstract class BaseRepository
}
/**
* @return null|T
* @return null|Model
*/
public function findByFilter(array $params): mixed
{
@@ -142,7 +142,7 @@ abstract class BaseRepository
}
/**
* @return T
* @return Model
*/
public function getModel(): Model
{

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Repository\Traits;
namespace App\Common\Repository\Traits;
use function Hyperf\Support\class_basename;
use function Hyperf\Support\class_uses_recursive;

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Repository\Traits;
namespace App\Common\Repository\Traits;
use Hyperf\Database\Model\Builder;

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Common\Trait;
use App\Cache\Redis\Lua\RateLimit;
use App\Cache\Redis\RedisCache;
use App\Cache\Redis\RedisKey;
use App\Common\Repository\AdminUserRepository;
use App\Model\AdminUser;
use Hyperf\Context\Context;
use Hyperf\Database\Model\Model;
use Hyperf\Di\Annotation\Inject;
trait AdminUserTrait
{
/**
* @var RedisCache
*/
#[Inject]
protected RedisCache $redis;
/**
* @var AdminUserRepository
*/
#[Inject]
protected AdminUserRepository $adminUserRepository;
/**
* 单例加缓存模型(仅仅用户信息) tips: 只可读取显示的数据(不影响大致问题),不可操作类敏感数据 比如 inc 账户余额 之类的 会导致脏读
* @param int $adminId
* @return AdminUser|Model|mixed|string|null
*/
public function getAdminUserInfo(int $adminId): mixed
{
$key = RedisKey::getAdminUserInfoKey($adminId);
if (Context::has($key)) {
return Context::get($key,false);
}
if ($this->redis->with()->exists($key)) {
$userInfo = unserialize($this->redis->with()->get($key));
Context::set($key,$userInfo);
return $userInfo;
}
$userInfo = $this->adminUserRepository->findById($adminId) ?? null;
if (!$userInfo) return null;
Context::set($key, $userInfo);
$this->redis->with()->set($key, serialize($userInfo), 3600);
return $userInfo;
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace App\Common\Trait;
use App\Constants\Common\ClientIpRequestConstant;
use App\Exception\ErrException;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\IpUtils;
trait ClientIpTrait
{
/**
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* @var string[]
*/
protected static array $trustedProxies = [];
/**
* @var int
*/
private static int $trustedHeaderSet = -1;
/**
* @var array
*/
private array $trustedValuesCache = [];
/**
* @var bool
*/
private bool $isForwardedValid = true;
/**
* @var bool
*/
private static bool $isTrustedRemoteAddr = false;
/**
* 设置可信代理
* @return bool
*/
public static function isTrustedRemoteAddr(): bool
{
return self::$isTrustedRemoteAddr;
}
/**
* 返回客户端IP地址。
* 在返回的数组中最可信的IP地址排在第一位最不可信的IP地址排在最后。“真正的”客户端IP地址是最后一个但这也是最不可信的一个。可信代理被剥离。
* @return array
*/
public function getClientIp(): array
{
$ip = $this->request->server('remote_addr');
if (! $this->isFromTrustedProxy()) return [$ip];
return $this->getTrustedValues(ClientIpRequestConstant::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}
/**
* 判断此请求是否来自受信任的代理。这可以用于确定是否信任
* @return bool
*/
private function isFromTrustedProxy(): bool
{
return (
self::$trustedProxies &&
IpUtils::checkIp($this->request->server('remote_addr',''),self::$trustedProxies) ||
self::isTrustedRemoteAddr()
);
}
/**
* @param int $type
* @param string|null $ip
* @return array|mixed|null[]|string[]
*/
private function getTrustedValues(int $type, ?string $ip = null): mixed
{
$cacheKey = $type . "\0" . ((self::$trustedHeaderSet & $type) ? $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type]) : '');
$cacheKey .= "\0" . $ip . "\0" . $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]);
if (isset($this->trustedValuesCache[$cacheKey])) return $this->trustedValuesCache[$cacheKey];
$clientValues = [];
$forwardedValues = [];
if (
(self::$trustedHeaderSet & $type) &&
$this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[$type])
) {
foreach (explode(',', $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type])) as $value) {
$clientValues[] = ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT ? '0.0.0.0' : trim($value));
}
}
if (
(self::$trustedHeaderSet & ClientIpRequestConstant::HEADER_FORWARDED) &&
(isset(ClientIpRequestConstant::FORWARDED_PARAMS[$type])) &&
$this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED])
) {
$forward = $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]);
$parts = HeaderUtils::split($forward, ',;=');
$param = ClientIpRequestConstant::FORWARDED_PARAMS[$type];
foreach ($parts as $subParts) {
if (($value = HeaderUtils::combine($subParts)[$param] ?? null) === null) continue;
if ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT) {
if (
str_ends_with($value, ']') ||
($value = mb_strrchr($value, ':')) === false
) $value = $this->isSecure() ? ':443' : ':80';
$value = '0.0.0.0' . $value;
}
$forwardedValues[] = $value;
}
}
if (null !== $ip) {
$clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
$forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
}
if ($forwardedValues === $clientValues || !$clientValues) return $this->trustedValuesCache[$cacheKey] = $forwardedValues;
if (!$forwardedValues) return $this->trustedValuesCache[$cacheKey] = $clientValues;
if (!$this->isForwardedValid) return (($ip = $this->trustedValuesCache[$cacheKey]) !== null) ? ['0.0.0.0',$ip] : [];
$this->isForwardedValid = false;
throw new ErrException('转发报头无效,请检查服务器配置。');
}
/**
* @return bool
*/
private function isSecure(): bool
{
return $this->request->getHeaderLine(ClientIpRequestConstant::HEADER_X_FORWARDED_PROTO) === 'https'
|| ($this->request->getServerParams()['https'] ?? '') === 'on';
}
/**
* @param array $clientIps
* @param string $ip
* @return array|null[]|string[]
*/
private function normalizeAndFilterClientIps(array $clientIps, string $ip): array
{
if (!$clientIps) return [];
$clientIps[] = $ip;
$firstTrustedIp = null;
foreach ($clientIps as $key => $clientIp) {
if (mb_strpos($clientIp, '.')) {
// ipv4
$i = mb_strpos($clientIp, '.');
if ($i) $clientIps[$key] = $clientIp = mb_substr($clientIp, 0, $i);
} elseif (str_starts_with($clientIp, '[')) {
// ipv6
$i = mb_strpos($clientIp, ']',1);
$clientIps[$key] = $clientIp = mb_substr($clientIp, 1, $i - 1);
}
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
unset($clientIps[$key]);
continue;
}
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
unset($clientIps[$key]);
$firstTrustedIp ??= $clientIp;
}
}
return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Common\Trait;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
trait ClientOsTrait
{
/**
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* @return string
*/
public function getClientOs(): string
{
$userAgent = $this->request->header('user-agent');
if (empty($userAgent)) return 'Unknown';
return match (true) {
preg_match('/win/i', $userAgent) => 'Windows',
preg_match('/mac/i', $userAgent) => 'MAC',
preg_match('/linux/i', $userAgent) => 'Linux',
preg_match('/unix/i', $userAgent) => 'Unix',
preg_match('/bsd/i', $userAgent) => 'BSD',
default => 'Other',
};
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Common\Trait;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
trait HttpMethodTrait
{
/**
* @var RequestInterface
*/
#[Inject]
protected readonly RequestInterface $request;
/**
* @return bool
*/
public function isCreate(): bool
{
return $this->request->isMethod('POST');
}
/**
* @return bool
*/
public function isUpdate(): bool
{
return $this->request->isMethod('PATCH') || $this->request->isMethod('PUT');
}
/**
* @return bool
*/
public function isDelete(): bool
{
return $this->request->isMethod('DELETE');
}
/**
* @return bool
*/
public function isSearch(): bool
{
return $this->request->isMethod('GET');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Common\Trait;
trait ParserRouterTrait
{
/**
* @param $callback
* @return array|string[]|null
*/
final protected function parse($callback): ?array
{
if (is_array($callback) && count($callback) == 2) return $callback;
if (is_string($callback)) {
if (str_contains($callback, '@')) $explode = explode('@', $callback);
if (str_contains($callback, '::')) $explode = explode('::', $callback);
if (isset($explode) && count($explode) === 2) return $explode;
}
return null;
}
}

View File

@@ -11,9 +11,10 @@ use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
final class AdminCode extends ResultCode
{
#[Message("登录失败")]
public const int LOGIN_ERROR = 10001;
#[Message("账号已禁用")]
public const int DISABLED = 423;
#[Message("暂无权限")]
public const int FORBIDDEN = 403;
#[Message("验证已过期")]
public const int LOGIN_TOKEN_ERROR = 10002;
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Constants\Common;
final class ClientIpRequestConstant
{
/**
* RFC 7239 标准的 Forwarded 头
*/
public const int HEADER_FORWARDED = 0b000001;
/**
* 客户端原始 IP
*/
public const int HEADER_X_FORWARDED_FOR = 0b000010;
/**
* 原始主机名
*/
public const int HEADER_X_FORWARDED_HOST = 0b000100;
/**
* 原始协议 (http/https)
*/
public const int HEADER_X_FORWARDED_PROTO = 0b001000;
/**
* 原始端口
*/
public const int HEADER_X_FORWARDED_PORT = 0b010000;
/**
* 原始路径前缀
*/
public const int HEADER_X_FORWARDED_PREFIX = 0b100000;
/**
* 将部分标志映射到 RFC 7239 Forwarded 头中的参数名
*/
public const array FORWARDED_PARAMS = [
self::HEADER_X_FORWARDED_FOR => 'for',
self::HEADER_X_FORWARDED_HOST => 'host',
self::HEADER_X_FORWARDED_PROTO => 'proto',
self::HEADER_X_FORWARDED_PORT => 'host',
];
/**
* 将标志映射到实际的 HTTP 头名称
*/
public const array TRUSTED_HEADERS = [
self::HEADER_FORWARDED => 'forwarded',
self::HEADER_X_FORWARDED_FOR => 'x-forwarded-for',
self::HEADER_X_FORWARDED_HOST => 'x-forwarded-host',
self::HEADER_X_FORWARDED_PROTO => 'x-forwarded-proto',
self::HEADER_X_FORWARDED_PORT => 'x-forwarded-port',
self::HEADER_X_FORWARDED_PREFIX => 'x-forwarded-prefix',
];
}

View File

@@ -19,4 +19,13 @@ class ResultCode extends AbstractConstants
#[Message("failed")]
final public const int ERROR = 1;
#[Message("token已过期")]
final public const int JWT_EXPIRED = 10001;
#[Message("token错误")]
final public const int JWT_ERROR = 10002;
#[Message("旧密码错误")]
final public const int OLD_PASSWORD_ERROR = 10003;
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Middleware\Admin\PermissionMiddleware;
use App\Request\Admin\AdminMenuRequest;
use App\Service\Admin\AdminUser\MenuService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/menu")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminMenuController extends AbstractController
{
/**
* @var MenuService
*/
#[Inject]
protected MenuService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'permission:menu:index')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @param AdminMenuRequest $request
* @return array
*/
#[RequestMapping(path: "", methods: "POST")]
#[Permission(code: 'permission:menu:create')]
#[Scene(scene: "create")]
public function createMenu(AdminMenuRequest $request): array
{
return $this->service->create();
}
/**
* @param int $id
* @param AdminMenuRequest $request
* @return array
*/
#[RequestMapping(path: "{id}", methods: "PUT")]
#[Permission(code: 'permission:menu:save')]
#[Scene(scene: "update")]
public function updateMenu(int $id, AdminMenuRequest $request): array
{
return $this->service->update($id);
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'permission:menu:delete')]
public function deleteMenu(): array
{
return $this->service->delete();
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Middleware\Admin\PermissionMiddleware;
use App\Request\Admin\AdminRoleRequest;
use App\Service\Admin\AdminUser\RoleService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/role")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminRoleController extends AbstractController
{
/**
* @var RoleService
*/
#[Inject]
protected RoleService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'permission:role:index')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @param AdminRoleRequest $request
* @return array
*/
#[RequestMapping(path: "", methods: "POST")]
#[Permission(code: 'permission:role:save')]
#[Scene(scene: "create")]
public function createRole(AdminRoleRequest $request): array
{
return $this->service->create();
}
/**
* @param int $id
* @param AdminRoleRequest $request
* @return array
*/
#[RequestMapping(path: "{id}", methods: "PUT")]
#[Permission(code: 'permission:role:update')]
#[Scene(scene: "create")]
public function updateRole(int $id, AdminRoleRequest $request): array
{
return $this->service->update($id);
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'permission:role:delete')]
public function deleteRole(): array
{
return $this->service->delete();
}
/**
* @param int $id
* @return array
*/
#[RequestMapping(path: "{id}/permission", methods: "GET")]
#[Permission(code: 'permission:role:getMenu')]
public function getRolePermission(int $id): array
{
return $this->service->getRole($id);
}
/**
* @param int $id
* @param AdminRoleRequest $request
* @return array
*/
#[RequestMapping(path: "{id}/permission", methods: "PUT")]
#[Permission(code: 'permission:role:setMenu')]
#[Scene(scene: "batch_grant_permission")]
public function batchGrantPermissionByRole(int $id,AdminRoleRequest $request): array
{
return $this->service->setRole($id);
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Middleware\Admin\PermissionMiddleware;
use App\Request\Admin\AdminUserRequest;
use App\Service\Admin\AdminUser\UserService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/adminUser")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserController extends AbstractController
{
/**
* @var UserService
*/
#[Inject]
protected UserService $service;
/**
* @return array
*/
#[RequestMapping(path: "getInfo", methods: "GET")]
public function getInfo(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "logout", methods: "POST")]
public function logout(): array
{
return $this->service->logout();
}
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'permission:user:index')]
public function pageList(): array
{
return $this->service->list();
}
/**
* @param AdminUserRequest $request
* @return array
*/
#[RequestMapping(path: "", methods: "PUT")]
#[Permission(code: 'permission:user:update')]
#[Scene(scene: "update")]
public function updateInfo(AdminUserRequest $request): array
{
return $this->service->updateInfo();
}
/**
* @return array
*/
#[RequestMapping(path: "password", methods: "PUT")]
#[Permission(code: 'permission:user:password')]
public function resetPassword(): array
{
return $this->service->resetPassword();
}
/**
* @param AdminUserRequest $request
* @return array
*/
#[RequestMapping(path: "", methods: "POST")]
#[Permission(code: 'permission:user:save')]
#[Scene(scene: "create")]
public function createAdminUser(AdminUserRequest $request): array
{
return $this->service->createUser();
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'permission:user:delete')]
public function deleteAdminUser(): array
{
return $this->service->deleteUser();
}
#[RequestMapping(path: "{userId}", methods: "PUT")]
#[Permission(code: 'permission:user:update')]
#[Scene(scene: "update")]
public function saveInfo(int $userId,AdminUserRequest $request): array
{
return $this->service->saveUser($userId);
}
/**
* @param int $userId
* @return array
*/
#[RequestMapping(path: "{userId}/roles", methods: "GET")]
#[Permission(code: 'permission:user:getRole')]
public function getAdminUserRole(int $userId): array
{
return $this->service->getUserRole($userId);
}
/**
* @param int $userId
* @param AdminUserRequest $request
* @return array
*/
#[RequestMapping(path: "{userId}/roles", methods: "PUT")]
#[Permission(code: 'permission:user:setRole')]
#[Scene(scene: "batch_grant_role")]
public function batchGrantRolesForAdminUser(int $userId,AdminUserRequest $request): array
{
return $this->service->batchGrantRoleForUser($userId);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin\Log;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Middleware\Admin\PermissionMiddleware;
use App\Service\Admin\Log\AdminUserLoginLogService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "admin/user-login-log")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserLoginLogController extends AbstractController
{
/**
* @var AdminUserLoginLogService
*/
#[Inject]
protected AdminUserLoginLogService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'log:userLogin:list')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'log:userLogin:delete')]
public function delete(): array
{
return $this->service->deleteLog();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin\Log;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Middleware\Admin\PermissionMiddleware;
use App\Service\Admin\Log\AdminUserOperationLogService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "admin/user-operation-log")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserOperationLogController extends AbstractController
{
/**
* @var AdminUserOperationLogService
*/
#[Inject]
protected AdminUserOperationLogService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'log:userOperation:list')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'log:userOperation:delete')]
public function delete(): array
{
return $this->service->deleteLog();
}
}

View File

@@ -6,9 +6,12 @@ namespace App\Controller\Admin;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\RefreshAdminTokenMiddleware;
use App\Request\Admin\LoginRequest;
use App\Service\Admin\Login\LoginService;
use App\Service\Admin\Login\RefreshService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
@@ -18,8 +21,15 @@ final class LoginController extends AbstractController
{
#[RequestMapping(path: "login", methods: "POST")]
#[Scene(scene: "login")]
public function login(LoginRequest $request)
public function login(LoginRequest $request): array
{
return (new LoginService)->handle();
}
#[RequestMapping(path: "refresh", methods: "POST")]
#[Middleware(RefreshAdminTokenMiddleware::class)]
public function refresh(): array
{
return (new RefreshService)->handle();
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Request\Admin\PermissionRequest;
use App\Service\Admin\AdminUser\PermissionService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/permission")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
class PermissionController extends AbstractController
{
/**
* @var PermissionService
*/
#[Inject]
protected PermissionService $service;
/**
* @return array
*/
#[RequestMapping(path: "menus", methods: "GET")]
public function menus(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "roles", methods: "GET")]
public function roles(): array
{
return $this->service->getRoleByAdminUser();
}
/**
* @param PermissionRequest $request
* @return array
*/
#[RequestMapping(path: "update", methods: "POST")]
#[Scene(scene: "update")]
public function update(PermissionRequest $request): array
{
return $this->service->update();
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Exception;
use App\Constants\ResultCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class ErrException extends ServerException
{
@@ -11,4 +12,23 @@ class ErrException extends ServerException
* @var int
*/
protected $code = ResultCode::ERROR;
/**
* @var array
*/
protected array $data = [];
public function __construct(string $message = 'failed', int $code = 0, array $data = [], ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->data = $data;
}
/**
* @return array
*/
public function getData(): array
{
return $this->data;
}
}

View File

@@ -49,7 +49,7 @@ abstract class BaseErrExceptionHandler extends ExceptionHandler
* @var AdminReturn|ApiReturn $returnObj
*/
$returnObj = $this->container->get($returnClass);
$result = $returnObj->error($e->getMessage(), $e->getCode());
$result = $returnObj->error($e->getMessage(), $e->getCode(), $e?->getData() ?? []);
$this->stopPropagation();
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Exception\Handler;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Lcobucci\JWT\Exception as JWTException;
class JwtExceptionHandler extends BaseErrExceptionHandler
{
/**
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
if ($throwable instanceof JWTException) {
$throwable = $this->modifyException($throwable);
// 传递给基类处理
return $this->handlerResponse($throwable, $response);
}
return $response;
}
/**
* @param JWTException $e
* @return ErrException
*/
protected function modifyException(JWTException $e): ErrException
{
// 根据不同的异常类型设置不同的code和message
switch ($e->getMessage()) {
case 'The token is expired':
$code = ResultCode::JWT_EXPIRED;
$message = 'token已过期';
break;
default:
$code = ResultCode::JWT_ERROR;
$message = 'token错误';
}
if (method_exists($e, 'setCustomCode')) {
$e->setCustomCode($code);
}
if (method_exists($e, 'setCustomMessage')) {
$e->setCustomMessage($message);
}
return New ErrException($message,$code);
}
/**
* @param Throwable $throwable
* @return bool
*/
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof JWTException;
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use App\Common\Interface\JwtInterface;
use Carbon\Carbon;
use Hyperf\Cache\CacheManager;
use Hyperf\Cache\Driver\DriverInterface;
@@ -11,6 +11,7 @@ use Lcobucci\JWT\Builder;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
@@ -45,7 +46,8 @@ abstract class AbstractJwt implements JwtInterface
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->identifiedBy($sub)
->issuedBy($this->getConfig('claims.'.RegisteredClaims::ISSUER,''));
if ($callable !== null) {
$builder = $callable($builder);
}
@@ -65,8 +67,10 @@ abstract class AbstractJwt implements JwtInterface
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->expiresAt($this->getRefreshExpireAt($immutable));
$builder = $builder->identifiedBy($sub)
->issuedBy($this->getConfig('claims.'.RegisteredClaims::ISSUER,''))
->expiresAt($this->getRefreshExpireAt($immutable));
if ($callable !== null) {
$builder = $callable($builder);
}

View File

@@ -2,6 +2,6 @@
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use App\Common\Interface\JwtInterface;
final class Jwt extends AbstractJwt implements JwtInterface {}

View File

@@ -2,7 +2,7 @@
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use App\Common\Interface\JwtInterface;
use Hyperf\Collection\Arr;
use Hyperf\Contract\ConfigInterface;
use function Hyperf\Support\make;

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Lib\Jwt;
use Hyperf\Context\RequestContext;
use Lcobucci\JWT\UnencryptedToken;
trait RequestScopedTokenTrait
{
public function getToken(): ?UnencryptedToken
{
return RequestContext::get()->getAttribute('token');
}
}

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

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Admin;
use App\Common\Interface\JwtInterface;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use App\Middleware\Token\AbstractTokenMiddleware;
use Hyperf\Context\Context;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use function Hyperf\Support\env;
final class AdminTokenMiddleware extends AbstractTokenMiddleware
{
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get('admin');
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function checkIssuer(UnencryptedToken $token): void
{
$audience = $token->claims()->get(RegisteredClaims::ISSUER);
if ($audience !== env('APP_NAME') .'_admin') throw new ErrException('token错误',ResultCode::JWT_ERROR);
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function setContext(UnencryptedToken $token): void
{
Context::set('current_admin_id',(int)$token->claims()?->get(RegisteredClaims::ID) ?? 0);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Admin;
use App\Annotation\Permission;
use App\Common\Trait\AdminUserTrait;
use App\Common\Trait\ParserRouterTrait;
use App\Constants\AdminCode;
use App\Constants\Model\AdminUser\AdminUserStatusCode;
use App\Exception\ErrException;
use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Model\AdminUser;
use Hyperf\Collection\Arr;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\HttpServer\Router\Dispatched;
use Lcobucci\JWT\Token\RegisteredClaims;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class PermissionMiddleware implements MiddlewareInterface
{
use RequestScopedTokenTrait;
use AdminUserTrait;
use ParserRouterTrait;
/**
* @var AdminUser
*/
protected AdminUser $adminUserInfo;
/**
* @param ContainerInterface $container
*/
public function __construct(protected ContainerInterface $container) {}
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$adminId = (int) $this->getToken()?->claims()?->get(RegisteredClaims::ID) ?? 0;
if ($adminId <= 0) throw new ErrException('账户不存在');
$this->adminUserInfo = $this->getAdminUserInfo($adminId);
if ($this->adminUserInfo->status == AdminUserStatusCode::DISABLE) throw new ErrException('账号已禁用',AdminCode::DISABLED);
// 超级管理员提前下场 不用判断权限
if ($this->adminUserInfo->isSuperAdmin()) return $handler->handle($request);
$this->check($request->getAttribute(Dispatched::class));
return $handler->handle($request);
}
/**
* @param Dispatched $dispatched
* @return bool
*/
private function check(Dispatched $dispatched): bool
{
$parseResult = $this->parse($dispatched->handler->callback);
if (! $parseResult) return true;
[$controller, $method] = $parseResult;
$annotations = AnnotationCollector::getClassMethodAnnotation($controller, $method);
$classAnnotation = AnnotationCollector::getClassAnnotation($controller, Permission::class);
/**
* @var Permission[] $permissions
*/
$permissions = [];
$classAnnotation && $permissions[] = $classAnnotation;
$methodPermission = Arr::get($annotations, Permission::class);
$methodPermission && $permissions[] = $methodPermission;
foreach ($permissions as $permission) {
$this->handlePermission($permission);
}
return true;
}
/**
* @param Permission $permission
* @return void
*/
private function handlePermission(Permission $permission): void
{
$operation = $permission->getOperation();
$codes = $permission->getCode();
foreach ($codes as $code) {
$isMenu = $this->adminUserInfo->hasPermission($code);
if ($operation === Permission::OPERATION_AND && !$isMenu) throw new ErrException('暂无权限',AdminCode::FORBIDDEN);
if ($operation === Permission::OPERATION_OR && $isMenu) return;
}
if ($operation === Permission::OPERATION_OR) throw new ErrException('暂无权限',AdminCode::FORBIDDEN);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Admin;
use App\Common\Interface\JwtInterface;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use App\Middleware\Token\AbstractTokenMiddleware;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swow\Psr7\Message\ServerRequestPlusInterface;
use function Hyperf\Support\env;
use function Hyperf\Support\value;
class RefreshAdminTokenMiddleware extends AbstractTokenMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->checkToken->checkJwt($this->parserToken($request));
$this->checkIssuer($this->parserToken($request));
return $handler->handle(
value(
static function (ServerRequestPlusInterface $request, UnencryptedToken $token) {
return $request->setAttribute('token', $token);
},
$request,
$this->getJwt()->parserRefreshToken(
$this->getToken($request)
)
)
);
}
/**
* @return JwtInterface
*/
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get('admin');
}
/**
* @param ServerRequestInterface $request
* @return UnencryptedToken
*/
protected function parserToken(ServerRequestInterface $request): UnencryptedToken
{
try {
return $this->getJwt()->parserRefreshToken($this->getToken($request));
} catch (RequiredConstraintsViolated $e) {
throw new ErrException('token过期',ResultCode::JWT_EXPIRED,['err_msg' => $e->getMessage()]);
}
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function checkIssuer(UnencryptedToken $token): void
{
$audience = $token->claims()->get(RegisteredClaims::ISSUER);
if ($audience !== env('APP_NAME') .'_admin') throw new ErrException('token错误',ResultCode::JWT_ERROR);
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function setContext(UnencryptedToken $token): void
{
return;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Token;
use App\Common\Interface\CheckTokenInterface;
use App\Common\Interface\JwtInterface;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use App\Lib\Jwt\JwtFactory;
use Hyperf\Collection\Arr;
use Hyperf\Stringable\Str;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swow\Psr7\Message\ServerRequestPlusInterface;
use function Hyperf\Support\value;
abstract class AbstractTokenMiddleware
{
public function __construct(
protected ContainerInterface $container,
protected readonly JwtFactory $jwtFactory,
protected readonly CheckTokenInterface $checkToken,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $this->parserToken($request);
$this->checkToken->checkJwt($token);
$this->checkIssuer($token);
$this->setContext($token);
return $handler->handle(
value(
static function (ServerRequestPlusInterface $request, UnencryptedToken $token) {
return $request->setAttribute('token', $token);
},
$request,
$token
)
);
}
/**
* @param UnencryptedToken $token
* @return void
*/
abstract public function checkIssuer(UnencryptedToken $token): void;
abstract public function getJwt(): JwtInterface;
/**
* @param UnencryptedToken $token
* @return void
*/
abstract public function setContext(UnencryptedToken $token): void;
/**
* @param ServerRequestInterface $request
* @return Token
*/
protected function parserToken(ServerRequestInterface $request): Token
{
try {
return $this->getJwt()->parserAccessToken($this->getToken($request));
} catch (RequiredConstraintsViolated $e) {
throw new ErrException('token过期',ResultCode::JWT_EXPIRED,['err_msg' => $e->getMessage()]);
}
}
/**
* @param ServerRequestInterface $request
* @return string
*/
protected function getToken(ServerRequestInterface $request): string
{
if ($request->hasHeader('Authorization')) {
return Str::replace('Bearer ', '', $request->getHeaderLine('Authorization'));
}
if ($request->hasHeader('token')) {
return $request->getHeaderLine('token');
}
if (Arr::has($request->getQueryParams(), 'token')) {
return $request->getQueryParams()['token'];
}
return '';
}
}

View File

@@ -85,8 +85,8 @@ class AdminMenu extends Model
return $this->belongsToMany(
AdminRole::class,
'admin_role_belongs_menu',
'menu_id',
'role_id'
'admin_menu_id',
'admin_role_id'
);
}

View File

@@ -65,9 +65,9 @@ class AdminRole extends Model
{
return $this->belongsToMany(
AdminMenu::class,
'role_belongs_menu',
'role_id',
'menu_id'
'admin_role_belongs_menu',
'admin_role_id',
'admin_menu_id'
);
}
@@ -79,8 +79,8 @@ class AdminRole extends Model
return $this->belongsToMany(
AdminUser::class,
'admin_user_belongs_role',
'role_id',
'user_id'
'admin_role_id',
'admin_user_id'
);
}

View File

@@ -6,6 +6,9 @@ namespace App\Model;
use Carbon\Carbon;
use Hyperf\Database\Model\Events\Creating;
/**
* @property int $id
* @property int $admin_user_id
@@ -20,6 +23,9 @@ namespace App\Model;
*/
class AdminUserLoginLog extends Model
{
public bool $timestamps = false;
/**
* The table associated with the model.
*/
@@ -28,10 +34,21 @@ class AdminUserLoginLog extends Model
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
protected array $fillable = ['id','admin_user_id', 'username', 'ip', 'os', 'browser', 'status', 'message', 'login_time', 'remark'];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'admin_user_id' => 'integer', 'status' => 'integer'];
/**
* @param Creating $event
* @return void
*/
public function creating(Creating $event): void
{
if ($event->getModel()->login_time === null) {
$event->getModel()->login_time = Carbon::now();
}
}
}

View File

@@ -28,7 +28,7 @@ class AdminUserOperationLog extends Model
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
protected array $fillable = ['id', 'admin_user_id','username', 'method', 'router', 'service_name', 'ip', 'ip_location', 'created_at', 'updated_at', 'remark'];
/**
* The attributes that should be cast to native types.

View File

@@ -1,34 +0,0 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Repository;
use App\Model\AdminUser;
/**
* Class AdminUserRepository
* @extends BaseRepository<AdminUser>
*/
final class AdminUserRepository extends BaseRepository
{
public function __construct(protected readonly AdminUser $model) {}
/**
* @param string $username
* @return AdminUser|null
*/
public function findByUserName(string $username): AdminUser|null
{
// @phpstan-ignore-next-line
return $this->model->newQuery()
->where('username', $username)
->first();
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Request\Admin;
use Hyperf\Validation\Request\FormRequest;
class AdminMenuRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'parent_id' => 'sometimes|integer',
'name' => 'required|string|max:255',
'path' => 'sometimes|string|max:255',
'component' => 'sometimes|string|max:255',
'redirect' => 'sometimes|string|max:255',
'status' => 'sometimes|integer',
'sort' => 'sometimes|integer',
'remark' => 'sometimes|string|max:255',
'meta.title' => 'required|string|max:255',
'meta.i18n' => 'sometimes|string|max:255',
'meta.badge' => 'sometimes|string|max:255',
'meta.link' => 'sometimes|string|max:255',
'meta.icon' => 'sometimes|string|max:255',
'meta.affix' => 'sometimes|boolean',
'meta.hidden' => 'sometimes|boolean',
'meta.type' => 'sometimes|string|max:255',
'meta.cache' => 'sometimes|boolean',
'meta.breadcrumbEnable' => 'sometimes|boolean',
'meta.copyright' => 'sometimes|boolean',
'meta.componentPath' => 'sometimes|string|max:64',
'meta.componentSuffix' => 'sometimes|string|max:4',
'meta.activeName' => 'sometimes|string|max:255',
'btnPermission' => 'sometimes|array',
];
}
/**
* @return array
*/
public function messages(): array
{
return parent::messages();
}
/**
* @var array|array[]
*/
protected array $scenes = [
'update' => [
'parent_id',
'name',
'path',
'component',
'redirect',
'status',
'sort',
'remark',
'meta.title',
'meta.i18n',
'meta.badge',
'meta.link',
'meta.icon',
'meta.affix',
'meta.hidden',
'meta.type',
'meta.cache',
'meta.breadcrumbEnable',
'meta.copyright',
'meta.componentPath',
'meta.componentSuffix',
'meta.activeName',
'btnPermission',
],
'create' => [
'parent_id',
'name',
'path',
'component',
'redirect',
'status',
'sort',
'remark',
'meta.title',
'meta.i18n',
'meta.badge',
'meta.link',
'meta.icon',
'meta.affix',
'meta.hidden',
'meta.type',
'meta.cache',
'meta.breadcrumbEnable',
'meta.copyright',
'meta.componentPath',
'meta.componentSuffix',
'meta.activeName',
'btnPermission',
],
];
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Request\Admin;
use App\Common\Trait\HttpMethodTrait;
use Hyperf\Validation\Request\FormRequest;
class AdminRoleRequest extends FormRequest
{
use HttpMethodTrait;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => 'required|string|max:60',
'code' => [
'required',
'string',
'max:60',
'regex:/^[a-zA-Z0-9_]+$/',
],
'status' => 'sometimes|integer|in:1,2',
'sort' => 'required|integer',
'remark' => 'nullable|string|max:255',
'permissions' => 'sometimes|array',
'permissions.*' => 'string|exists:admin_menu,name',
];
if ($this->isCreate()) {
$rules['code'][] = 'unique:admin_role,code';
}
if ($this->isUpdate()) {
$rules['code'][] = 'unique:admin_role,code,' . $this->route('id');
}
return $rules;
}
/**
* @return array
*/
public function messages(): array
{
return parent::messages();
}
protected array $scenes = [
'update' => [
'code',
'name',
'status',
'sort',
'remark',
],
'create' => [
'code',
'name',
'status',
'sort',
'remark',
],
'batch_grant_permission' => [
'permissions',
'permissions.*',
]
];
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Request\Admin;
use Hyperf\Validation\Request\FormRequest;
class AdminUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'username' => 'required|string|max:20',
'user_type' => 'required|integer',
'nickname' => ['required', 'string', 'max:60', 'regex:/^[^\s]+$/'],
'phone' => 'sometimes|string|max:12',
'email' => 'sometimes|string|max:60|email:rfc,dns',
'avatar' => 'sometimes|string|max:255|url',
'signed' => 'sometimes|string|max:255',
'status' => 'sometimes|integer',
'backend_setting' => 'sometimes|array|max:255',
'remark' => 'sometimes|string|max:255',
'password' => 'sometimes|string|min:6|max:20',
'role_codes' => 'required|array',
'role_codes.*' => 'string|exists:admin_role,code',
];
}
/**
* @return array
*/
public function messages(): array
{
return parent::messages();
}
protected array $scenes = [
'update' => [
'username',
'user_type',
'nickname',
'phone',
'email',
'avatar',
'signed',
'status',
'backend_setting',
'remark',
'password',
],
'create' => [
'username',
'user_type',
'nickname',
'phone',
'email',
'avatar',
'signed',
'status',
'backend_setting',
'remark',
'password',
],
'batch_grant_role' => [
'role_codes',
'role_codes.*',
]
];
}

View File

@@ -22,7 +22,20 @@ class LoginRequest extends FormRequest
public function rules(): array
{
return [
'username' => 'required|string|exists:admin_user,username',
'password' => 'required|string'
];
}
/**
* @return array
*/
public function messages(): array
{
return parent::messages();
}
protected array $scenes = [
'login' => ['username', 'password'],
];
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Request\Admin;
use Hyperf\Validation\Request\FormRequest;
class PermissionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'nickname' => 'sometimes|string|max:255',
'new_password' => 'sometimes|confirmed|string|min:8',
'new_password_confirmation' => 'sometimes|string|min:8',
'old_password' => ['sometimes', 'string'],
'avatar' => 'sometimes|string|max:255',
'signed' => 'sometimes|string|max:255',
'backend_setting' => 'sometimes|array',
];
}
/**
* @return array
*/
public function messages(): array
{
return parent::messages();
}
/**
* @var array|array[]
*/
protected array $scenes = [
'update' => [
'nickname',
'new_password',
'new_password_confirmation',
'old_password',
'avatar',
'signed',
'backend_setting',
],
];
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Common\Repository\AdminMenuRepository;
use App\Exception\ErrException;
use App\Model\AdminMenu;
use App\Service\Admin\BaseAdminService;
use Hyperf\Di\Annotation\Inject;
class MenuService extends BaseAdminService
{
/**
* @var AdminMenuRepository
*/
#[Inject]
protected AdminMenuRepository $adminMenuRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success('success',$this->adminMenuRepository->list([
'children' => true,
'parent_id' => 0
])?->toArray() ?? []);
}
/**
* @return array
*/
public function create(): array
{
$data = array_merge($this->getRequestData(),[
'created_by' => $this->adminId
]);
if (empty($data['parent_id'])) $data['parent_id'] = 0;
/**
* @var AdminMenu $model
*/
$model = $this->adminMenuRepository->create($data);
if (!$model) throw new ErrException('添加失败');
if ($data['meta']['type'] !== 'M' || empty($data['btnPermission'])) return $this->adminReturn->success('success',$model->toArray());
foreach ($data['btnPermission'] as $item) {
$this->adminMenuRepository->create([
'parent_id' => $model->id,
'name' => $item['code'],
'sort' => 0,
'status' => 1,
'meta' => [
'title' => $item['title'],
'i18n' => $item['i18n'],
'type' => 'B'
],
]);
}
return $this->adminReturn->success('success',$model->toArray());
}
/**
* @param int $id
* @return array
*/
public function update(int $id): array
{
$data = array_merge($this->getRequestData(),[
'updated_by' => $this->adminId
]);
$model = $this->adminMenuRepository->updateById($id, $data);
if (!$model) throw new ErrException('修改失败');
if ($data['meta']['type'] !== 'M' || !isset($data['btnPermission'])) return $this->adminReturn->success();
$existsBtnPermissions = array_flip(
$this->adminMenuRepository
->getQuery()
->where('parent_id', $id)
->whereJsonContains('meta->type', 'B')
->pluck('id')
->toArray()
);
if (!empty($data['btnPermission'])) {
foreach ($data['btnPermission'] as $item) {
if (empty($item['type']) || $item['type'] !== 'B') continue;
$data = [
'name' => $item['code'],
'meta' => [
'title' => $item['title'],
'i18n' => $item['i18n'],
'type' => 'B'
],
];
if (!empty($item['id'])) {
$this->adminMenuRepository->updateById($item['id'], $data);
unset($existsBtnPermissions[$item['id']]);
} else {
$data['parent_id'] = $id;
$this->adminMenuRepository->create($data);
}
}
}
if (!empty($existsBtnPermissions)) $this->adminMenuRepository->deleteById(array_keys($existsBtnPermissions));
return $this->adminReturn->success();
}
/**
* @return array
*/
public function delete(): array
{
$res = $this->adminMenuRepository->deleteById($this->getRequestData());
if (!$res) throw new ErrException('删除失败');
return $this->adminReturn->success();
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Common\Repository\AdminMenuRepository;
use App\Common\Repository\AdminRoleRepository;
use App\Common\Trait\AdminUserTrait;
use App\Constants\Model\AdminUser\AdminMenuStatusCode;
use App\Constants\Model\AdminUser\AdminRoleStatusCode;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use App\Service\Admin\BaseAdminService;
use Hyperf\Collection\Arr;
use Hyperf\Di\Annotation\Inject;
class PermissionService extends BaseAdminService
{
use AdminUserTrait;
/**
* @var AdminRoleRepository
*/
#[Inject]
protected AdminRoleRepository $adminRoleRepository;
/**
* @var AdminMenuRepository
*/
#[Inject]
protected AdminMenuRepository $adminMenuRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success(
'success',
$this->getAdminUserInfo($this->adminId)->isSuperAdmin() ?
$this->getAdminMenuBySuperAdmin() :
$this->getAdminMenuByAdminId()
);
}
/**
* @return array
*/
private function getAdminMenuByAdminId(): array
{
$permissions = $this->getAdminUserInfo($this->adminId)->getPermissions()->pluck('name')->unique();
$menuList = $permissions->isEmpty() ?
[] :
$this->adminMenuRepository->list([
'status' => AdminMenuStatusCode::Normal,
'name' => $permissions->toArray()
])->toArray();
$tree = [];
$map = [];
foreach ($menuList as &$menu) {
$menu['children'] = [];
$map[$menu['id']] = &$menu;
}
unset($menu);
foreach ($menuList as &$menu) {
$pid = $menu['parent_id'];
if ($pid === 0 || !isset($map[$pid])) {
$tree[] = &$menu;
} else {
$map[$pid]['children'][] = &$menu;
}
}
unset($menu);
return $tree;
}
/**
* @return array
*/
private function getAdminMenuBySuperAdmin(): array
{
return $this->adminMenuRepository->list([
'status' => AdminMenuStatusCode::Normal,
'children' => true,
'parent_id' => 0,
])?->toArray() ?? [];
}
/**
* @return array
*/
public function getRoleByAdminUser(): array
{
return $this->adminReturn->success(
'success',
$this->getAdminUserInfo($this->adminId)->isSuperAdmin() ?
$this->adminRoleRepository->list(['status' => AdminRoleStatusCode::Normal])->toArray() :
$this->getAdminUserInfo($this->adminId)->getRoles(['name', 'code', 'remark'])->toArray()
);
}
/**
* @return array
*/
public function update(): array
{
$userInfo = $this->getAdminUserInfo($this->adminId);
if (!$userInfo) throw new ErrException('用户不存在');
$data = $this->getRequestData();
if (Arr::exists($data, 'new_password')) {
if (!$userInfo->verifyPassword(Arr::get($data,'old_password')))
throw new ErrException('旧密码错误',ResultCode::OLD_PASSWORD_ERROR);
$data['password'] = $data['new_password'];
}
if (!$this->adminUserRepository->updateById($userInfo->id, $data)) throw new ErrException('更新失败');
return $this->adminReturn->success();
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Common\Repository\AdminMenuRepository;
use App\Common\Repository\AdminRoleRepository;
use App\Exception\ErrException;
use App\Model\AdminMenu;
use App\Service\Admin\BaseAdminService;
use Hyperf\Collection\Arr;
use Hyperf\Di\Annotation\Inject;
class RoleService extends BaseAdminService
{
/**
* @var AdminRoleRepository
*/
#[Inject]
protected AdminRoleRepository $adminRoleRepository;
/**
* @var AdminMenuRepository
*/
#[Inject]
protected AdminMenuRepository $adminMenuRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success(
'success',
$this->adminRoleRepository->page(
$this->getRequestData(),
$this->getCurrentPage(),
$this->getPageSize()
)
);
}
/**
* @return array
*/
public function create(): array
{
if (!$this->adminRoleRepository->create(
array_merge(
$this->getRequestData(),
['created_by' => $this->adminId]
)
)) throw new ErrException('添加失败');
return $this->adminReturn->success();
}
/**
* @param int $id
* @return array
*/
public function update(int $id): array
{
if (!$this->adminRoleRepository->updateById(
$id,
array_merge(
$this->getRequestData(),
['updated_by' => $this->adminId]
)
)) throw new ErrException('更新失败');
return $this->adminReturn->success();
}
/**
* @return array
*/
public function delete(): array
{
if (!$this->adminRoleRepository->deleteById($this->getRequestData())) throw new ErrException('删除失败');
return $this->adminReturn->success();
}
/**
* @param int $id
* @return array
*/
public function getRole(int $id): array
{
return $this->adminReturn->success(
'success',
$this->adminRoleRepository
->findById($id)
->adminMenus()
->get()
->map(static fn (AdminMenu $adminMenu) => $adminMenu->only([
'id' , 'name'
]))->toArray()
);
}
/**
* @param int $id
* @return array
*/
public function setRole(int $id): array
{
if (!$this->adminRoleRepository->existsById($id)) throw new ErrException('角色不存在');
$permissionsCode = Arr::get($this->getRequestData(), 'permissions', []);
if (count($permissionsCode) === 0) {
$this->adminRoleRepository->findById($id)->adminMenus()->detach();
return $this->adminReturn->success();
}
if (!$this->adminRoleRepository
->findById($id)
->adminMenus()
->sync(
$this->adminMenuRepository
->list([
'code' => $permissionsCode
])
->map(static fn ($item) => $item->id)
->toArray()
)) throw new ErrException('更新失败');
return $this->adminReturn->success();
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Cache\Redis\Lua\RateLimit;
use App\Cache\Redis\RedisCache;
use App\Common\Repository\AdminRoleRepository;
use App\Common\Repository\AdminUserRepository;
use App\Common\Trait\AdminUserTrait;
use App\Exception\ErrException;
use App\Model\AdminRole;
use App\Service\Admin\BaseAdminService;
use App\Service\BaseTokenService;
use Hyperf\Collection\Arr;
use Hyperf\Di\Annotation\Inject;
class UserService extends BaseAdminService
{
use AdminUserTrait;
/**
* @var BaseTokenService
*/
#[Inject]
protected BaseTokenService $tokenService;
/**
* @var AdminUserRepository
*/
#[Inject]
protected AdminUserRepository $adminUserRepository;
/**
* @var AdminRoleRepository
*/
#[Inject]
protected AdminRoleRepository $adminRoleRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success(
'success',
Arr::only(
$this->getAdminUserInfo($this->adminId)?->toArray() ?: [],
['username', 'nickname', 'avatar', 'signed', 'backend_setting', 'phone', 'email']
)
);
}
/**
* @return array
*/
public function logout(): array
{
$this->tokenService->setJwt('admin')->getJwt()->addBlackList($this->getToken());
return $this->adminReturn->success();
}
/**
* @return array
*/
public function list(): array
{
return $this->adminReturn->success('success',$this->adminUserRepository->page(
$this->getRequestData(),
$this->getCurrentPage(),
$this->getPageSize()
));
}
/**
* @return array
*/
public function updateInfo(): array
{
$res = $this->adminUserRepository->updateById($this->adminId,Arr::except($this->getRequestData(),['password']));
if (!$res) throw new ErrException('修改失败');
return $this->adminReturn->success();
}
/**
* @return array
*/
public function resetPassword(): array
{
$adminUserInfo = $this->adminUserRepository->findById($this->adminId);
if (!$adminUserInfo) throw new ErrException('用户异常');
$adminUserInfo->resetPassword();
if (!$adminUserInfo->save()) throw new ErrException('保存密码失败');
return $this->adminReturn->success();
}
/**
* @return array
*/
public function createUser(): array
{
if (! $this->adminUserRepository->create(array_merge(
$this->getRequestData(),
['created_by' => $this->adminId]
))) throw new ErrException();
return $this->adminReturn->success();
}
/**
* @return array
*/
public function deleteUser(): array
{
if (! $this->adminUserRepository->deleteById($this->getRequestData())) throw new ErrException();
return $this->adminReturn->success();
}
/**
* @param int $userId
* @return array
*/
public function saveUser(int $userId): array
{
if (! $this->adminUserRepository->updateById(
$userId,
array_merge(
$this->getRequestData(),
['updated_by' => $this->adminId]
)
)) throw new ErrException();
return $this->adminReturn->success();
}
/**
* @param int $userId
* @return array
*/
public function getUserRole(int $userId): array
{
$userInfo = $this->adminUserRepository->findById($userId);
if (!$userInfo) throw new ErrException('获取用户信息失败');
return $this->adminReturn->success(
'success',
$userInfo->roles()->get()->map(
static fn (AdminRole $adminRole) => $adminRole->only([
'id','code','name'
])
)->toArray()
);
}
/**
* @param int $userId
* @return array
*/
public function batchGrantRoleForUser(int $userId): array
{
$userInfo = $this->adminUserRepository->findById($userId);
if (!$userInfo) throw new ErrException('获取用户信息失败');
try {
$userInfo->roles()->sync(
$this->adminRoleRepository->list([
'code' => $this->request->input('role_codes')
])->map(static function(AdminRole $adminRole) {
return $adminRole->id;
})
);
return $this->adminReturn->success();
} catch (\Throwable $e) {
throw new ErrException($e->getMessage());
}
}
}

View File

@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace App\Service\Admin;
use App\Exception\ErrException;
use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Lib\Return\AdminReturn;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
@@ -17,6 +19,8 @@ use Hyperf\HttpServer\Contract\RequestInterface;
abstract class BaseAdminService
{
use RequestScopedTokenTrait;
/**
* 请求对象注入
* @var RequestInterface
@@ -32,21 +36,53 @@ abstract class BaseAdminService
protected AdminReturn $adminReturn;
/**
* 管理员 id
* @var int
* @param string $name
* @return \current_admin_id|mixed
*/
protected int $adminId = 0;
/**
* 主构造函数
*/
public function __construct()
public function __get(string $name)
{
$this->adminId = Context::get('admin_id',0);
if ($name === 'adminId') return Context::get('current_admin_id',0);
if (!property_exists($this, $name)) throw new ErrException('属性未定义');
return $this->$name;
}
// /**
// * 主构造函数
// */
// public function __construct()
// {
// $this->adminId = Context::get('current_admin_id',0);
// var_dump('BaseAdminService获取到的'.$this->adminId);
// }
/**
* 主函数抽象类
*/
abstract public function handle();
/**
* @return int
*/
protected function getCurrentPage(): int
{
return (int) $this->request->input('page', 1);
}
/**
* @return int
*/
protected function getPageSize(): int
{
return (int) $this->request->input('page_size', 20);
}
/**
* @return array
*/
protected function getRequestData(): array
{
return $this->request->all();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Log;
use App\Common\Repository\AdminUserLoginLogRepository;
use App\Exception\ErrException;
use App\Service\Admin\BaseAdminService;
use Hyperf\Di\Annotation\Inject;
class AdminUserLoginLogService extends BaseAdminService
{
/**
* @var AdminUserLoginLogRepository
*/
#[Inject]
protected AdminUserLoginLogRepository $adminUserLoginLogRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success(
'success',
$this->adminUserLoginLogRepository->page(
$this->getRequestData(),
$this->getCurrentPage(),
$this->getPageSize()
)
);
}
/**
* @return array
*/
public function deleteLog(): array
{
if (!$this->adminUserLoginLogRepository->deleteById($this->request->input('ids')))
throw new ErrException('删除失败');
return $this->adminReturn->success();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Log;
use App\Common\Repository\AdminUserOperationLogRepository;
use App\Exception\ErrException;
use App\Service\Admin\BaseAdminService;
use Hyperf\Di\Annotation\Inject;
class AdminUserOperationLogService extends BaseAdminService
{
/**
* @var AdminUserOperationLogRepository
*/
#[Inject]
protected AdminUserOperationLogRepository $adminUserOperationLogRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success(
'success',
$this->adminUserOperationLogRepository->page(
$this->getRequestData(),
$this->getCurrentPage(),
$this->getPageSize()
)
);
}
/**
* @return array
*/
public function deleteLog(): array
{
if (!$this->adminUserOperationLogRepository->deleteById($this->request->input('ids')))
throw new ErrException('删除失败');
return $this->adminReturn->success();
}
}

View File

@@ -10,21 +10,15 @@ declare(strict_types=1);
namespace App\Service\Admin\Login;
use App\Common\Repository\AdminUserRepository;
use App\Constants\Model\AdminUser\AdminUserStatusCode;
use App\Exception\ErrException;
use App\Interface\JwtInterface;
use App\Lib\Jwt\JwtFactory;
use App\Repository\AdminUserRepository;
use App\Service\Admin\BaseAdminService;
use App\Service\BaseTokenService;
use Hyperf\Di\Annotation\Inject;
class LoginService extends BaseAdminService
{
/**
* @var string jwt场景
*/
private string $jwt = 'admin';
/**
* @var AdminUserRepository
*/
@@ -32,10 +26,10 @@ class LoginService extends BaseAdminService
protected AdminUserRepository $userRepository;
/**
* @var JwtFactory
* @var BaseTokenService
*/
#[Inject]
protected JwtFactory $jwtFactory;
protected BaseTokenService $tokenService;
/**
* @return array
@@ -52,21 +46,12 @@ class LoginService extends BaseAdminService
}
if ($adminInfo->status == AdminUserStatusCode::DISABLE) throw new ErrException('用户已禁用');
$jwtHandle = $this->tokenService->setJwt('admin')->getJwt();
$jwtHandle = $this->getJwt();
return [
return $this->adminReturn->success('success',[
'access_token' => $jwtHandle->builderAccessToken((string) $adminInfo->id)->toString(),
'refresh_token' => $jwtHandle->builderRefreshToken((string) $adminInfo->id)->toString(),
'expire_at' => (int) $jwtHandle->getConfig('ttl', 0),
];
}
/**
* @return JwtInterface
*/
private function getJwt(): JwtInterface
{
return $this->jwtFactory->get($this->jwt);
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Login;
use App\Lib\Jwt\RequestScopedTokenTrait;
use App\Service\Admin\BaseAdminService;
use App\Service\BaseTokenService;
use Hyperf\Di\Annotation\Inject;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
class RefreshService extends BaseAdminService
{
/**
* @var BaseTokenService
*/
#[Inject]
protected BaseTokenService $tokenService;
/**
* @return array
*/
public function handle(): array
{
return $this->adminReturn->success('success',$this->refreshToken($this->getToken()));
}
/**
* @param UnencryptedToken $token
* @return array<string,int|string>
*/
public function refreshToken(UnencryptedToken $token): array
{
$jwt = $this->tokenService->setJwt('admin')->getJwt();
$jwt->addBlackList($token);
return [
'access_token' => $jwt->builderAccessToken($token->claims()->get(RegisteredClaims::ID))->toString(),
'refresh_token' => $jwt->builderRefreshToken($token->claims()->get(RegisteredClaims::ID))->toString(),
'expire_at' => (int) $jwt->getConfig('ttl', 0),
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service;
use App\Common\Interface\CheckTokenInterface;
use App\Common\Interface\JwtInterface;
use App\Exception\ErrException;
use App\Lib\Jwt\JwtFactory;
use Hyperf\Di\Annotation\Inject;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use function Hyperf\Support\value;
final class BaseTokenService implements CheckTokenInterface
{
private string $jwt = 'admin';
/**
* @var JwtFactory
*/
#[Inject]
protected JwtFactory $jwtFactory;
/**
* @return JwtInterface
*/
public function getJwt(): JwtInterface
{
return $this->jwtFactory->get($this->jwt);
}
/**
* @param UnencryptedToken $token
* @return void
*/
public function checkJwt(UnencryptedToken $token): void
{
$this->getJwt()->hasBlackList($token) && throw new ErrException('token已过期');
}
/**
* @param string $jwt
* @return $this
*/
public function setJwt(string $jwt): self
{
$this->jwt = $jwt;
return $this;
}
}

View File

@@ -11,4 +11,5 @@ declare(strict_types=1);
*/
return [
Hyperf\Database\Schema\Blueprint::class => App\Common\Macros\BlueprintMacros::class,
App\Common\Interface\CheckTokenInterface::class => App\Service\BaseTokenService::class,
];

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
return [
'handler' => [
'http' => [
@@ -16,6 +17,7 @@ return [
App\Exception\Handler\ErrExceptionHandler::class,
App\Exception\Handler\ValidationExceptionHandler::class,
App\Exception\Handler\AppExceptionHandler::class,
App\Exception\Handler\JwtExceptionHandler::class,
],
],
];

View File

@@ -31,34 +31,20 @@ return [
],
'claims' => [
// 默认的jwt claims
RegisteredClaims::ISSUER => (string) env('APP_NAME'),
RegisteredClaims::ISSUER => (string) env('APP_NAME') . '_default',
RegisteredClaims::AUDIENCE => 'default', // 明确标识受众
],
],
'admin' => [
// jwt 配置 https://lcobucci-jwt.readthedocs.io/en/latest/
'driver' => Jwt::class,
// jwt 签名key
'key' => InMemory::base64Encoded(env('JWT_SECRET')),
// jwt 签名算法 可选 https://lcobucci-jwt.readthedocs.io/en/latest/supported-algorithms/
'alg' => new Sha256(),
// token过期时间单位为秒
'ttl' => (int) env('ADMIN_JWT_TTL', 3600),
// 刷新token过期时间单位为秒
'refresh_ttl' => (int) env('ADMIN_JWT_REFRESH_TTL', 7200),
// 黑名单模式
'blacklist' => [
// 是否开启黑名单
'enable' => true,
// 黑名单缓存前缀
'prefix' => 'jwt_blacklist',
// 黑名单缓存驱动
'connection' => 'default',
// 黑名单缓存时间 该时间一定要设置比token过期时间要大一点最好设置跟过期时间一样
'ttl' => (int) env('ADMIN_JWT_BLACKLIST_TTL', 7201),
],
'claims' => [
// 默认的jwt claims
RegisteredClaims::ISSUER => 'ADMIN'. env('APP_NAME'),
RegisteredClaims::ISSUER => (string) env('APP_NAME') .'_admin',
],
],
// 在你想要使用不同的场景时,可以在这里添加配置.可以填一个。其他会使用默认配置

View File

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

View File

@@ -4,7 +4,51 @@ Content-Type: application/x-www-form-urlencoded
username=admin&password=admin
#> {%
# client.global.set("admin_token", response.body.data.token);
#%}
> {%
client.global.set("admin_token", response.body.data.access_token);
client.global.set("refresh_token", response.body.data.refresh_token);
%}
### 登录
GET {{host}}/admin/adminUser/getInfo
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}
### 列表
GET {{host}}/admin/adminUser/list
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}
### 修改用户资料
PUT {{host}}/admin/adminUser
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}
nickname=超超级管理员
### 获取角色
GET {{host}}/admin/adminUser/1/roles
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}
### 登录
POST {{host}}/admin/login/refresh
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{refresh_token}}
> {%
client.global.set("admin_token", response.body.data.access_token);
client.global.set("refresh_token", response.body.data.refresh_token);
%}
### 登录
GET {{host}}/admin/passport/getInfo
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{admin_token}}