first commit

This commit is contained in:
2025-09-12 15:23:08 +08:00
commit a80c237bbb
117 changed files with 15628 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class ResponseFormat
{
/**
* @param string $format
*/
public function __construct(public string $format) {}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Aspect;
use App\Annotation\ResponseFormat;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Exception\Exception;
use Hyperf\HttpServer\Request;
use Psr\Container\ContainerInterface;
use Hyperf\Di\Aop\ProceedingJoinPoint;
#[Aspect]
class ResponseFormatAspect extends AbstractAspect
{
/**
* @var array|class-string[]
*/
public array $annotations = [
ResponseFormat::class,
];
/**
* @param ContainerInterface $container
*/
public function __construct(protected ContainerInterface $container) {}
/**
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
* @throws Exception
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint): mixed
{
// 获取注解定义的格式
$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);
}
}
return $proceedingJoinPoint->process();
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Command\GenClass;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Devtool\Generator\GeneratorCommand;
use Psr\Container\ContainerInterface;
#[Command]
class RepositoryGenCommand extends GeneratorCommand
{
/**
* @param ContainerInterface $container
*/
public function __construct(protected ContainerInterface $container)
{
parent::__construct('gen:repository');
}
/**
* @return void
*/
public function configure(): void
{
parent::configure();
$this->setDescription('Create a new repository class');
$this->setHelp('php bin/hyperf.php gen:repository fileRepository');
}
/**
* 获取 stubs
* @return string
*/
protected function getStub(): string
{
return __DIR__ . '/stubs/repository.stub';
}
/**
* 获取默认命名空间
* @return string
*/
protected function getDefaultNamespace(): string
{
return 'App\\Repository';
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Command\GenClass;
use Hyperf\Command\Annotation\Command;
use Hyperf\Devtool\Generator\GeneratorCommand;
use Psr\Container\ContainerInterface;
#[Command]
class ServiceGenCommand extends GeneratorCommand
{
/**
* @param ContainerInterface $container
*/
public function __construct(protected ContainerInterface $container)
{
parent::__construct('gen:service');
}
/**
* @return void
*/
public function configure(): void
{
parent::configure();
$this->setDescription('Create a new service class');
$this->setHelp('php bin/hyperf.php gen:service /folder/fileService');
}
/**
* 获取 stubs
* @return string
*/
protected function getStub(): string
{
return __DIR__ . '/stubs/service.stub';
}
/**
* 获取默认命名空间
* @return string
*/
protected function getDefaultNamespace(): string
{
return 'App\\Service';
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace %NAMESPACE%;
final class %CLASS% extends BaseRepository
{
public function __construct(protected readonly xxx $model) {}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace %NAMESPACE%;
class %CLASS% extends
{
public function handle()
{
//todo Write logic
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Common\Macros;
use Hyperf\Database\Schema\Blueprint;
class BlueprintMacros
{
public static function register(): void
{
Blueprint::macro('authorFields', function () {
/** @var Blueprint $this */
$this->unsignedBigInteger('created_by')->nullable()->comment('创建人ID');
$this->unsignedBigInteger('updated_by')->nullable()->comment('更新人ID');
return $this;
});
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
final class AdminCode extends ResultCode
{
#[Message("登录失败")]
public const int LOGIN_ERROR = 10001;
#[Message("验证已过期")]
public const int LOGIN_TOKEN_ERROR = 10002;
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class ErrorCode extends AbstractConstants
{
/**
* @Message("Server Error")
*/
public const SERVER_ERROR = 500;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Constants\Model\AdminUser;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
enum AdminMenuStatusCode: int
{
use EnumConstantsTrait;
#[Message('正常')]
case Normal = 1;
#[Message('停用')]
case DISABLE = 2;
public function isNormal(): bool
{
return $this === self::Normal;
}
public function isDisable(): bool
{
return $this === self::DISABLE;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Constants\Model\AdminUser;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
enum AdminRoleStatusCode: int
{
use EnumConstantsTrait;
#[Message('正常')]
case Normal = 1;
#[Message('停用')]
case DISABLE = 2;
public function isNormal(): bool
{
return $this === self::Normal;
}
public function isDisable(): bool
{
return $this === self::DISABLE;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Constants\Model\AdminUser;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
enum AdminUserStatusCode: int
{
use EnumConstantsTrait;
#[Message('正常')]
case Normal = 1;
#[Message('停用')]
case DISABLE = 2;
public function isNormal(): bool
{
return $this === self::Normal;
}
public function isDisable(): bool
{
return $this === self::DISABLE;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Constants\Model\AdminUser;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
enum AdminUserTypeCode: int
{
use EnumConstantsTrait;
#[Message('系统用户')]
case SYSTEM = 100;
#[Message('普通用户')]
case USER = 200;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
use Hyperf\Constants\Annotation\Message;
use Hyperf\Constants\EnumConstantsTrait;
#[Constants]
class ResultCode extends AbstractConstants
{
use EnumConstantsTrait;
#[Message("success")]
final public const int SUCCESS = 0;
#[Message("failed")]
final public const int ERROR = 1;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Controller;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
abstract class AbstractController
{
#[Inject]
protected ContainerInterface $container;
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Request\Admin\LoginRequest;
use App\Service\Admin\Login\LoginService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: "admin/login")]
#[ResponseFormat('admin')]
final class LoginController extends AbstractController
{
#[RequestMapping(path: "login", methods: "POST")]
#[Scene(scene: "login")]
public function login(LoginRequest $request)
{
return (new LoginService)->handle();
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Controller;
class IndexController extends AbstractController
{
public function index()
{
$user = $this->request->input('user', 'Hyperf');
$method = $this->request->getMethod();
return [
'method' => $method,
'message' => "Hello {$user}.",
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Exception;
use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class BusinessException extends ServerException
{
public function __construct(int $code = 0, string $message = null, Throwable $previous = null)
{
if (is_null($message)) {
$message = ErrorCode::getMessage($code);
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Exception;
use App\Constants\ResultCode;
use Hyperf\Server\Exception\ServerException;
class ErrException extends ServerException
{
/**
* @var int
*/
protected $code = ResultCode::ERROR;
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Exception\Handler;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class AppExceptionHandler extends ExceptionHandler
{
public function __construct(protected StdoutLoggerInterface $logger)
{
}
public function handle(Throwable $throwable, ResponseInterface $response)
{
$this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
$this->logger->error($throwable->getTraceAsString());
return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.'));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Exception\Handler;
use App\Lib\Return\AdminReturn;
use App\Lib\Return\ApiReturn;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\HttpServer\Request;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
abstract class BaseErrExceptionHandler extends ExceptionHandler
{
public function __construct(
private readonly Request $request,
private readonly ContainerInterface $container,
) {}
/**
* @param Throwable $e
* @param ResponseInterface $response
* @return MessageInterface|ResponseInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
protected function handlerResponse(
Throwable $e,
ResponseInterface $response
): MessageInterface|ResponseInterface
{
// 从注解获取响应格式(优先于路径解析)
$format = $this->request->getAttribute('response_format') ?? $this->repairResponseFormatByPath();
// 动态选择策略
$returnClass = match ($format) {
'admin', 'common' => AdminReturn::class,
'api' => ApiReturn::class,
default => null,
};
if (!$returnClass) return $response;
/**
* @var AdminReturn|ApiReturn $returnObj
*/
$returnObj = $this->container->get($returnClass);
$result = $returnObj->error($e->getMessage(), $e->getCode());
$this->stopPropagation();
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
}
/**
* @return string
*/
private function repairResponseFormatByPath(): string
{
// 兜底逻辑:根据路径前缀推断
return match (explode('/', $this->request->path())[0] ?? '') {
'admin', 'common' => 'admin',
'api' => 'api',
default => 'default',
};
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Exception\Handler;
use App\Exception\ErrException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class ErrExceptionHandler extends BaseErrExceptionHandler
{
/**
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
return $throwable instanceof ErrException
? $this->handlerResponse($throwable, $response)
: $response;
}
/**
* @param Throwable $throwable
* @return bool
*/
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ErrException;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Exception\Handler;
use Hyperf\Validation\ValidationException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class ValidationExceptionHandler extends BaseErrExceptionHandler
{
/**
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
return $throwable instanceof ValidationException
? $this->handlerResponse($throwable, $response)
: $response;
}
/**
* @param Throwable $throwable
* @return bool
*/
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ValidationException;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Interface;
use Lcobucci\JWT\UnencryptedToken;
interface JwtInterface
{
public function builderAccessToken(string $sub, ?\Closure $callable = null): UnencryptedToken;
public function builderRefreshToken(string $sub, ?\Closure $callable = null): UnencryptedToken;
public function parserAccessToken(string $accessToken): UnencryptedToken;
public function parserRefreshToken(string $refreshToken): UnencryptedToken;
public function addBlackList(UnencryptedToken $token): bool;
public function hasBlackList(UnencryptedToken $token): bool;
public function removeBlackList(UnencryptedToken $token): bool;
public function getConfig(string $key, mixed $default = null): mixed;
}

233
app/Lib/Jwt/AbstractJwt.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use Carbon\Carbon;
use Hyperf\Cache\CacheManager;
use Hyperf\Cache\Driver\DriverInterface;
use Hyperf\Collection\Arr;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Psr\SimpleCache\InvalidArgumentException;
abstract class AbstractJwt implements JwtInterface
{
/**
* @param array $config
* @param CacheManager $cacheManager
* @param Clock $clock
* @param AccessTokenConstraint $accessTokenConstraint
* @param RefreshTokenConstraint $refreshTokenConstraint
*/
public function __construct(
private readonly array $config,
private readonly CacheManager $cacheManager,
private readonly Clock $clock,
private readonly AccessTokenConstraint $accessTokenConstraint,
private readonly RefreshTokenConstraint $refreshTokenConstraint
) {}
/**
* @param string $sub
* @param \Closure|null $callable
* @return UnencryptedToken
*/
public function builderAccessToken(string $sub, ?\Closure $callable = null): UnencryptedToken
{
return $this->getJwtFacade()->issue(
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
if ($callable !== null) {
$builder = $callable($builder);
}
return $builder->expiresAt($this->getExpireAt($immutable));
}
);
}
/**
* @param string $sub
* @param \Closure|null $callable
* @return UnencryptedToken
*/
public function builderRefreshToken(string $sub, ?\Closure $callable = null): UnencryptedToken
{
return $this->getJwtFacade()->issue(
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->expiresAt($this->getRefreshExpireAt($immutable));
if ($callable !== null) {
$builder = $callable($builder);
}
return $builder->relatedTo('refresh');
}
);
}
/**
* @param string $accessToken
* @return UnencryptedToken
*/
public function parserAccessToken(string $accessToken): UnencryptedToken
{
return $this->getJwtFacade()
->parse(
$accessToken,
new SignedWith(
$this->getSigner(),
$this->getSigningKey()
),
new StrictValidAt(
$this->clock,
$this->clock->now()->diff($this->getExpireAt($this->clock->now()))
),
$this->getBlackListConstraint(),
$this->refreshTokenConstraint
);
}
/**
* @param string $refreshToken
* @return UnencryptedToken
*/
public function parserRefreshToken(string $refreshToken): UnencryptedToken
{
return $this->getJwtFacade()
->parse(
$refreshToken,
new SignedWith(
$this->getSigner(),
$this->getSigningKey()
),
new StrictValidAt(
$this->clock,
$this->clock->now()->diff($this->getRefreshExpireAt($this->clock->now()))
),
$this->getBlackListConstraint(),
$this->accessTokenConstraint
);
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function addBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->set($token->toString(), 1, $this->getBlackConfig('ttl', 600));
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function hasBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->has($token->toString());
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function removeBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->delete($token->toString());
}
/**
* @param string $key
* @param mixed|null $default
* @return mixed
*/
public function getConfig(string $key, mixed $default = null): mixed
{
return Arr::get($this->config, $key, $default);
}
/**
* @return JwtFacade
*/
private function getJwtFacade(): JwtFacade
{
return new JwtFacade(clock: $this->clock);
}
/**
* @return Signer
*/
private function getSigner(): Signer
{
return Arr::get($this->config, 'alg');
}
/**
* @return Key
*/
private function getSigningKey(): Key
{
return Arr::get($this->config, 'key');
}
/**
* @return DriverInterface
*/
private function getCacheDriver(): DriverInterface
{
return $this->cacheManager->getDriver($this->getBlackConfig('connection'));
}
/**
* @param string $name
* @param mixed|null $default
* @return mixed
*/
private function getBlackConfig(string $name, mixed $default = null): mixed
{
return Arr::get($this->config, 'blacklist.' . $name, $default);
}
/**
* @return Constraint
*/
private function getBlackListConstraint(): Constraint
{
return new BlackListConstraint((bool) $this->getBlackConfig('enable', false), $this->getCacheDriver());
}
/**
* @param \DateTimeImmutable $immutable
* @return \DateTimeImmutable
*/
private function getExpireAt(\DateTimeImmutable $immutable): \DateTimeImmutable
{
return Carbon::create($immutable)
->addSeconds(Arr::get($this->config, 'ttl', 3600))
->toDateTimeImmutable();
}
/**
* @param \DateTimeImmutable $immutable
* @return \DateTimeImmutable
*/
private function getRefreshExpireAt(\DateTimeImmutable $immutable): \DateTimeImmutable
{
return Carbon::create($immutable)
->addSeconds(Arr::get($this->config, 'refresh_ttl', 7200))
->toDateTimeImmutable();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Jwt;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
class AccessTokenConstraint implements Constraint
{
public function assert(Token $token): void
{
if (! $token->isRelatedTo('refresh')) throw ConstraintViolation::error('Token is not a refresh token', $this);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Lib\Jwt;
use Hyperf\Cache\Driver\DriverInterface;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
use Psr\SimpleCache\InvalidArgumentException;
class BlackListConstraint implements Constraint
{
/**
* @param bool $enable
* @param DriverInterface $cache
*/
public function __construct(
private readonly bool $enable,
private readonly DriverInterface $cache
) {}
/**
* @param Token $token
* @return void
* @throws InvalidArgumentException
*/
public function assert(Token $token): void
{
if ($this->enable !== true) return;
if ($this->cache->has($token->toString())) throw ConstraintViolation::error('Token is in blacklist', $this);
}
}

15
app/Lib/Jwt/Clock.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Lib\Jwt;
use Carbon\Carbon;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
class Clock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return Carbon::now()->toDateTimeImmutable();
}
}

7
app/Lib/Jwt/Jwt.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
final class Jwt extends AbstractJwt implements JwtInterface {}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use Hyperf\Collection\Arr;
use Hyperf\Contract\ConfigInterface;
use function Hyperf\Support\make;
final class JwtFactory
{
public function __construct(
private readonly ConfigInterface $config,
) {}
/**
* @param string $name
* @return JwtInterface
*/
public function get(string $name = 'default'): JwtInterface
{
return make(Jwt::class, [
'config' => $this->getConfig($name),
]);
}
/**
* 获取场景配置
* @param string $scene
* @return array
*/
public function getConfig(string $scene): array
{
if ($scene === 'default') {
return $this->config->get($this->getConfigKey());
}
return Arr::merge(
$this->config->get($this->getConfigKey()),
$this->config->get($this->getConfigKey($scene), [])
);
}
/**
* @param string $name
* @return string
*/
private function getConfigKey(string $name = 'default'): string
{
return 'jwt.' . $name;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Lib\Jwt;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
class RefreshTokenConstraint implements Constraint
{
public function assert(Token $token): void
{
if ($token->isRelatedTo('refresh')) throw ConstraintViolation::error('Token is a refresh token', $this);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Return;
class AdminReturn extends CommonReturn
{
/**
* 通用返回
* @param array $res
* @return array
*/
final protected function afterSuccess(array $res): array
{
return $res;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Return;
class ApiReturn extends CommonReturn
{
/**
* 通用返回
* @param array $res
* @return array
*/
final protected function afterSuccess(array $res): array
{
return $res;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Lib\Return;
use App\Constants\ResultCode;
abstract class CommonReturn
{
/**
* 通用 success 返回
* @param string $msg
* @param array $data
* @param int|ResultCode $code
* @param array $debug
* @return array
*/
final public function success(string $msg = 'success', array $data = [], ResultCode|int $code = ResultCode::SUCCESS, array $debug = []): array
{
$res = [
'code' => $code,
'message' => $msg,
'data' => $data
];
return $this->afterSuccess(array_merge($res, $debug));
}
/**
* 通用 fail 返回
* @param string $msg
* @param array $data
* @param int|ResultCode $code
* @param array $debug
* @return array
*/
final public function error(string $msg = 'failed', ResultCode|int $code = ResultCode::ERROR, array $data = [], array $debug = []): array
{
$res = [
'code' => $code,
'message' => $msg,
'data' => $data
];
return $this->afterSuccess(array_merge($res, $debug));
}
/**
* 通用类调子类返回方便切面类识别
* @param array $res
* @return array
*/
abstract protected function afterSuccess(array $res): array;
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Collection\Arr;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event): void
{
if ($event instanceof QueryExecuted) {
$sql = $event->sql;
if (! Arr::isAssoc($event->bindings)) {
$position = 0;
foreach ($event->bindings as $value) {
$position = strpos($sql, '?', $position);
if ($position === false) {
break;
}
$value = "'{$value}'";
$sql = substr_replace($sql, $value, $position, 1);
$position += strlen($value);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\AsyncQueue\AnnotationJob;
use Hyperf\AsyncQueue\Event\AfterHandle;
use Hyperf\AsyncQueue\Event\BeforeHandle;
use Hyperf\AsyncQueue\Event\Event;
use Hyperf\AsyncQueue\Event\FailedHandle;
use Hyperf\AsyncQueue\Event\RetryHandle;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class QueueHandleListener implements ListenerInterface
{
protected LoggerInterface $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('queue');
}
public function listen(): array
{
return [
AfterHandle::class,
BeforeHandle::class,
FailedHandle::class,
RetryHandle::class,
];
}
public function process(object $event): void
{
if ($event instanceof Event && $event->getMessage()->job()) {
$job = $event->getMessage()->job();
$jobClass = get_class($job);
if ($job instanceof AnnotationJob) {
$jobClass = sprintf('Job[%s@%s]', $job->class, $job->method);
}
$date = date('Y-m-d H:i:s');
switch (true) {
case $event instanceof BeforeHandle:
$this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass));
break;
case $event instanceof AfterHandle:
$this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass));
break;
case $event instanceof FailedHandle:
$this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass));
$this->logger->error((string) $event->getThrowable());
break;
case $event instanceof RetryHandle:
$this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass));
break;
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Command\Event\AfterExecute;
use Hyperf\Coordinator\Constants;
use Hyperf\Coordinator\CoordinatorManager;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
#[Listener]
class ResumeExitCoordinatorListener implements ListenerInterface
{
public function listen(): array
{
return [
AfterExecute::class,
];
}
public function process(object $event): void
{
CoordinatorManager::until(Constants::WORKER_EXIT)->resume();
}
}

113
app/Model/AdminMenu.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Model;
use App\Constants\Model\AdminUser\AdminMenuStatusCode;
use App\Model\Meta\AdminUserMeta;
use App\Model\Meta\MetaCast;
use Carbon\Carbon;
use Hyperf\Database\Model\Events\Deleting;
use Hyperf\Database\Model\Relations\BelongsToMany;
use Hyperf\Database\Model\Relations\HasMany;
use Hyperf\Database\Model\Collection;
/**
* @property int $id
* @property int $parent_id
* @property string $name
* @property AdminUserMeta $meta
* @property string $path
* @property string $component
* @property string $redirect
* @property int $status
* @property int $sort
* @property int $created_by
* @property int $updated_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property string $remark
* @property Collection|AdminRole[] $roles
* @property Collection|AdminMenu[] $children
*/
class AdminMenu extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_menu';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'id',
'parent_id',
'name',
'component',
'redirect',
'status',
'sort',
'created_by',
'updated_by',
'created_at',
'updated_at',
'remark',
'meta',
'path',
];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [
'id' => 'integer',
'parent_id' => 'integer',
'status' => AdminMenuStatusCode::class,
'sort' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'meta' => MetaCast::class,
'path' => 'string',
];
/**
* 通过中间表获取角色.
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(
AdminRole::class,
'admin_role_belongs_menu',
'menu_id',
'role_id'
);
}
/**
* @return HasMany
*/
public function children(): HasMany
{
return $this
->hasMany(self::class, 'parent_id', 'id')
->where('status', AdminMenuStatusCode::Normal)
->orderBy('sort')
->with('children');
}
/**
* @param Deleting $event
* @return void
*/
public function deleting(Deleting $event): void
{
$this->roles()->detach();
}
}

96
app/Model/AdminRole.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\Database\Model\Events\Deleting;
use Hyperf\Database\Model\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
* @property string $code
* @property int $status
* @property int $sort
* @property int $created_by
* @property int $updated_by
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $remark
*/
class AdminRole extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_role';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'id',
'name',
'code',
'status',
'sort',
'created_by',
'updated_by',
'created_at',
'updated_at',
'remark'
];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [
'id' => 'integer',
'status' => 'integer',
'sort' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* @return BelongsToMany
*/
public function adminMenus(): BelongsToMany
{
return $this->belongsToMany(
AdminMenu::class,
'role_belongs_menu',
'role_id',
'menu_id'
);
}
/**
* @return BelongsToMany
*/
public function adminUsers(): BelongsToMany
{
return $this->belongsToMany(
AdminUser::class,
'admin_user_belongs_role',
'role_id',
'user_id'
);
}
/**
* @param Deleting $event
* @return void
*/
public function deleting(Deleting $event): void
{
$this->adminUsers()->detach();
$this->adminMenus()->detach();
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property int $admin_role_id
* @property int $admin_menu_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class AdminRoleBelongsMenu extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_role_belongs_menu';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'admin_role_id' => 'integer', 'admin_menu_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
}

189
app/Model/AdminUser.php Normal file
View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace App\Model;
use App\Constants\Model\AdminUser\AdminUserStatusCode;
use App\Constants\Model\AdminUser\AdminUserTypeCode;
use Carbon\Carbon;
use Hyperf\Collection\Enumerable;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\Events\Creating;
use Hyperf\Database\Model\Events\Deleted;
use Hyperf\Database\Model\Relations\BelongsToMany;
/**
* @property int $id
* @property string $username
* @property string $password
* @property string $user_type
* @property string $nickname
* @property string $phone
* @property string $email
* @property string $avatar
* @property string $signed
* @property AdminUserStatusCode $status
* @property string $login_ip
* @property string $login_time
* @property string $backend_setting
* @property int $created_by
* @property int $updated_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property string $remark
*/
class AdminUser extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'id',
'username',
'password',
'user_type',
'nickname',
'phone',
'email',
'avatar',
'signed',
'status',
'login_ip',
'login_time',
'backend_setting',
'created_by',
'updated_by',
'created_at',
'updated_at',
'remark'
];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [
'id' => 'integer',
'status' => AdminUserStatusCode::class,
'user_type' => AdminUserTypeCode::class,
'created_by' => 'integer',
'updated_by' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'backend_setting' => 'json',
];
/**
* 隐藏的字段列表.
* @var string[]
*/
protected array $hidden = ['password'];
/**
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(
AdminRole::class,
'admin_user_belongs_role',
);
}
/**
* @param Creating $event
* @return void
*/
public function creating(Creating $event): void
{
if (!$this->isDirty('password')) $this->resetPassword();
}
/**
* @param Deleted $event
* @return void
*/
public function deleted(Deleted $event): void
{
$this->roles()->detach();
}
/**
* @return void
*/
public function resetPassword(): void
{
$this->password = 'admin';
}
/**
* @param string $password
* @return void
*/
public function setPasswordAttribute(string $password): void
{
$this->attributes['password'] = password_hash($password, PASSWORD_DEFAULT);
}
/**
* @param string $password
* @return bool
*/
public function verifyPassword(string $password): bool
{
return password_verify($password, $this->password);
}
/**
* @return bool
*/
public function isSuperAdmin(): bool
{
return $this->roles()->where('code','SuperAdmin')->exists();
}
/**
* @param array $fields
* @return Collection
*/
public function getRoles(array $fields): Collection
{
return $this->roles()
->where('status',AdminUserStatusCode::Normal)
->select($fields)
->get();
}
/**
* @return \Hyperf\Collection\Collection|Enumerable|Collection<int, AdminMenu>
*/
public function getPermissions(): Collection|Enumerable|\Hyperf\Collection\Collection
{
return $this->roles()
->with('adminMenus')
->orderBy('sort')
->get()
->pluck('adminMenus')
->flatten();
}
/**
* @param string $permission
* @return bool
*/
public function hasPermission(string $permission): bool
{
return $this->roles()
->whereRelation('adminMenus','name',$permission)
->exists();
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
*/
class AdminUserBelongsMenu extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user_belongs_menu';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [];
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property int $admin_user_id
* @property int $admin_role_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class AdminUserBelongsRole extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user_belongs_role';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'admin_user_id' => 'integer', 'admin_role_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property int $admin_user_id
* @property string $username
* @property string $ip
* @property string $os
* @property string $browser
* @property int $status
* @property string $message
* @property string $login_time
* @property string $remark
*/
class AdminUserLoginLog extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user_login_log';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'admin_user_id' => 'integer', 'status' => 'integer'];
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property int $admin_user_id
* @property string $username
* @property string $method
* @property string $router
* @property string $service_name
* @property string $ip
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $remark
*/
class AdminUserOperationLog extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'admin_user_operation_log';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'admin_user_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Model\Meta;
use App\Model\Model;
/**
* @property string $title 标题
* @property string $i18n 国际化
* @property string $badge 徽章
* @property string $icon 图标
* @property bool $affix 是否固定
* @property bool $hidden 是否隐藏
* @property string $type 类型
* @property bool $cache 是否缓存
* @property bool $copyright 是否显示版权
* @property string $link 链接
* @property string $componentPath 视图文件类型
* @property string $componentSuffix 视图前缀路径
* @property string $breadcrumbEnable 是否显示面包屑
* @property string $activeName 激活高亮的菜单标识
* @property string $auth 前端权限判断,允许访问的权限码
* @property string $role 前端权限判断,允许访问的角色码
* @property string $user 前端权限判断,允许访问的用户名
*/
final class AdminUserMeta extends Model
{
/**
* @var bool
*/
public bool $incrementing = false;
/**
* @var array|string[]
*/
protected array $fillable = [
'title',
'i18n',
'badge',
'icon',
'affix',
'hidden',
'type',
'cache',
'copyright',
'breadcrumbEnable',
'componentPath',
'componentSuffix',
'link',
'activeName',
'auth',
'role',
'user',
];
/**
* @var array|string[]
*/
protected array $casts = [
'affix' => 'boolean',
'hidden' => 'boolean',
'cache' => 'boolean',
'copyright' => 'boolean',
'breadcrumbEnable' => 'boolean',
'title' => 'string',
'componentPath' => 'string',
'componentSuffix' => 'string',
'i18n' => 'string',
'badge' => 'string',
'icon' => 'string',
'type' => 'string',
'link' => 'string',
'activeName' => 'string',
'auth' => 'array',
'role' => 'array',
'user' => 'array',
];
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Model\Meta;
use Hyperf\Codec\Json;
use Hyperf\Contract\CastsAttributes;
class MetaCast implements CastsAttributes
{
/**
* @param $model
* @param string $key
* @param $value
* @param array $attributes
* @return AdminUserMeta
*/
public function get($model, string $key, $value, array $attributes): AdminUserMeta
{
return new AdminUserMeta(empty($value) ? [] : Json::decode($value));
}
/**
* @param $model
* @param string $key
* @param $value
* @param array $attributes
* @return array|string
*/
public function set($model, string $key, $value, array $attributes): array|string
{
return Json::encode($value);
}
}

22
app/Model/Model.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Model;
use Hyperf\DbConnection\Model\Model as BaseModel;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
abstract class Model extends BaseModel implements CacheableInterface
{
use Cacheable;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Process;
use Hyperf\AsyncQueue\Process\ConsumerProcess;
use Hyperf\Process\Annotation\Process;
#[Process]
class AsyncQueueConsumer extends ConsumerProcess
{
}

View File

@@ -0,0 +1,34 @@
<?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,151 @@
<?php
namespace App\Repository;
use App\Repository\Traits\BootTrait;
use App\Repository\Traits\RepositoryOrderByTrait;
use Hyperf\Collection\Collection;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Model;
use Hyperf\DbConnection\Traits\HasContainer;
use Hyperf\Paginator\AbstractPaginator;
/**
* @template T of Model
* @property T $model
*/
abstract class BaseRepository
{
use BootTrait;
use HasContainer;
use RepositoryOrderByTrait;
public const string PER_PAGE_PARAM_NAME = 'per_page';
public function handleSearch(Builder $query, array $params): Builder
{
return $query;
}
public function handleItems(Collection $items): Collection
{
return $items;
}
public function handlePage(LengthAwarePaginatorInterface $paginator): array
{
if ($paginator instanceof AbstractPaginator) {
$items = $paginator->getCollection();
} else {
$items = Collection::make($paginator->items());
}
$items = $this->handleItems($items);
return [
'list' => $items->toArray(),
'total' => $paginator->total(),
];
}
public function list(array $params = []): Collection
{
return $this->handleItems($this->perQuery($this->getQuery(), $params)->get());
}
public function count(array $params = []): int
{
return $this->perQuery($this->getQuery(), $params)->count();
}
public function page(array $params = [], ?int $page = null, ?int $pageSize = null): array
{
$result = $this->perQuery($this->getQuery(), $params)->paginate(
perPage: $pageSize,
pageName: static::PER_PAGE_PARAM_NAME,
page: $page,
);
return $this->handlePage($result);
}
/**
* @return T
*/
public function create(array $data): mixed
{
// @phpstan-ignore-next-line
return $this->getQuery()->create($data);
}
public function updateById(mixed $id, array $data): bool
{
return (bool) $this->getQuery()->whereKey($id)->first()?->update($data);
}
/**
* @return null|T
*/
public function saveById(mixed $id, array $data): mixed
{
$model = $this->getQuery()->whereKey($id)->first();
if ($model) {
$model->fill($data)->save();
return $model;
}
return null;
}
public function deleteById(mixed $id): int
{
// @phpstan-ignore-next-line
return $this->model::destroy($id);
}
public function forceDeleteById(mixed $id): bool
{
return (bool) $this->getQuery()->whereKey($id)->forceDelete();
}
/**
* @return null|T
*/
public function findById(mixed $id): mixed
{
return $this->getQuery()->whereKey($id)->first();
}
public function findByField(mixed $id, string $field): mixed
{
return $this->getQuery()->whereKey($id)->value($field);
}
/**
* @return null|T
*/
public function findByFilter(array $params): mixed
{
return $this->perQuery($this->getQuery(), $params)->first();
}
public function perQuery(Builder $query, array $params): Builder
{
$this->startBoot($query, $params);
return $this->handleSearch($query, $params);
}
public function getQuery(): Builder
{
return $this->model->newQuery();
}
public function existsById(mixed $id): bool
{
return (bool) $this->getQuery()->whereKey($id)->exists();
}
/**
* @return T
*/
public function getModel(): Model
{
return $this->model;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Repository\Traits;
use function Hyperf\Support\class_basename;
use function Hyperf\Support\class_uses_recursive;
trait BootTrait
{
protected function startBoot(...$params): void
{
$traits = class_uses_recursive(static::class);
foreach ($traits as $trait) {
$method = 'boot' . class_basename($trait);
if (method_exists($this, $method)) {
$this->{$method}(...$params);
}
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Repository\Traits;
use Hyperf\Database\Model\Builder;
trait RepositoryOrderByTrait
{
public function handleOrderBy(Builder $query, $params): Builder
{
if ($this->enablePageOrderBy()) {
$orderByField = $params[$this->getOrderByParamName()] ?? $query->getModel()->getKeyName();
$orderByDirection = $params[$this->getOrderByDirectionParamName()] ?? 'desc';
$query->orderBy($orderByField, $orderByDirection);
}
return $query;
}
protected function bootRepositoryOrderByTrait(Builder $query, array $params): void
{
$this->handleOrderBy($query, $params);
}
protected function getOrderByParamName(): string
{
return 'order_by';
}
protected function getOrderByDirectionParamName(): string
{
return 'order_by_direction';
}
protected function enablePageOrderBy(): bool
{
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Request\Admin;
use Hyperf\Validation\Request\FormRequest;
class LoginRequest 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 [
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin;
use App\Lib\Return\AdminReturn;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
abstract class BaseAdminService
{
/**
* 请求对象注入
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* 返回对象注入
* @var AdminReturn
*/
#[Inject]
protected AdminReturn $adminReturn;
/**
* 管理员 id
* @var int
*/
protected int $adminId = 0;
/**
* 主构造函数
*/
public function __construct()
{
$this->adminId = Context::get('admin_id',0);
}
/**
* 主函数抽象类
*/
abstract public function handle();
}

View File

@@ -0,0 +1,72 @@
<?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\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 Hyperf\Di\Annotation\Inject;
class LoginService extends BaseAdminService
{
/**
* @var string jwt场景
*/
private string $jwt = 'admin';
/**
* @var AdminUserRepository
*/
#[Inject]
protected AdminUserRepository $userRepository;
/**
* @var JwtFactory
*/
#[Inject]
protected JwtFactory $jwtFactory;
/**
* @return array
*/
public function handle(): array
{
$adminInfo = $this->userRepository->findByUserName((string)$this->request->input('username'));
if (!$adminInfo) throw new ErrException('后台管理员不存在');
if (! $adminInfo->verifyPassword((string) $this->request->input('password'))) {
// $this->dispatcher->dispatch(new UserLoginEvent($user, $ip, $os, $browser, false));
throw new ErrException('密码错误');
}
if ($adminInfo->status == AdminUserStatusCode::DISABLE) throw new ErrException('用户已禁用');
$jwtHandle = $this->getJwt();
return [
'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);
}
}