获取菜单

This commit is contained in:
zhang zhuo 2025-06-21 13:39:39 +08:00
parent 087207364f
commit 7c57ed4b43
28 changed files with 1154 additions and 37 deletions

View File

@ -14,4 +14,7 @@ DB_PREFIX=
REDIS_HOST=localhost
REDIS_AUTH=(null)
REDIS_PORT=6379
REDIS_DB=0
REDIS_DB=0
JWT_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC13F3kPpJqEMTHEQ+wS8eRPlFKGRrLbEBh2o0a5Ccsdx/zImo+TDKXLPyasZSquFqEL+tuTNST+WJPSiYySYpaSb5w1LJXGq/hi9DnjjosBuqxtpmrG1uwlu1wvi1RmiynCfBnKfBCc0g7yGZ4XcQ665ftbNrHBLtSB/xpyf1N7PwYKKOG2KmrJKv1DlIt30XsdXwReiGl80NHm4AQvuGAlZ7W0aL3V5JxK30gQxLYJYRnMKLpaKV/JDxxIWOUEv0wL8sCeWbjxv4xLYoEhUO0DY+h0ZZAxafxTNeuhvoO7aCOUEhZSvke7n+THUiFVhkJy0ccEr59Kp+1N0kyxFBHAgMBAAECggEAICt4LGzpJ3wJ4xDgjpYJGmdEp+/i7oMarHSlq1EaoOH9s9utoZGHDXj2wkKRgtWTpXh4lA1hOT/PJSl/sjuSDsCmwHzPg1sEK8i4zo05OxqKH5+mdT8krAs2u0/Y4mt8ZJv8e7NOfeK4r2KWxcoIcUfFm0k7NiNfI3aoLup9NXBeWqBnHl5kgtsEDeU1KoEX8U/HW8oTyQqGAW20Jz9mXN1XneADjGpSUwQY0oaG620S51WBKIicKO7R3NP7J67IyCVTnKyAMuTEBoEcweG/WGpGdI+3AkUcJFMrMkwYX/X4Df5zEF93mXf6DxvEI2m1jdOrVZ42yJEg0pE5H1sGuQKBgQD4d1ELhSM8fub31jJrXNewHEIwszFcTiUbEjFiO0Cep5lYuGVZuWdMye2Gc+ZphrtEalLNGFt+sw9MuoqxIYhipnUz9ksHKDRoxR4dOebXCPgP/LHW4P8KAPcvMUr4xNS5hrh9qajjTaRepZwjcIS9CWr8QHDHF6a74K+XGxUn8wKBgQC7YAk34zOmx7BQC6nn7U+yM8Dksfdt2a+/rBGvqp6ILh6DTdeCI3b61L6NZKzVFg7YFOIQbBL88HOm3xz1MAJB4Q/a6A1Rz+lCKnkJ7d+En733nyh1HJlRK9FDaIPHm/iHi/FsJ9wHnrsAa7Vofp4QHWjQB6cUo2nvRoo4S8A/XQKBgEiuhourJ5KTwLaw9tDHOOTwb0BVutO4nEwd90o38QA4ILh+QE+N17TzwMK69qTZ37/0pkIOpP0cHhag3t9P4tiQvuozWuE+Fo6rUtLT1D4FBqOOlOs5qAFiJOyuK7M3yM54pVFFJv1PAg0ZvuHzETFHJv+hThw/Q+vjnxnBt1+XAoGBAJazqPZgMBzFotLebqrwvRaQdWX6lQyu9qFsXVUyHwtcPIJSyzAKIhmfnhrOjAteEFZOhXu70JHLOtlNvVaeZFJkF4Jy/LN+SxdCXdNUlF9wszNDuSBn/g/A9DAJEWQr1/n83hGlBVzDl5fBCUif/bTsUm5umT0KKZue2nBozJipAoGAX/xrRKIY8yfab7/Dc2THtrQA1fmOxDSK5Vki65N4tprsA4UiTdOegFgFGJfTnr8AkUG0leQ0mkYCNoXL1J0MnN92ULdfyAEn9bKW6dl7yVUOsyGLKMqasifI99rZS3aApMtF3/ekoymn+hbV3ftmtDi3HYzJ5QtBXPWXCvsN8LY="
JWT_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtdxd5D6SahDExxEPsEvHkT5RShkay2xAYdqNGuQnLHcf8yJqPkwylyz8mrGUqrhahC/rbkzUk/liT0omMkmKWkm+cNSyVxqv4YvQ5446LAbqsbaZqxtbsJbtcL4tUZospwnwZynwQnNIO8hmeF3EOuuX7WzaxwS7Ugf8acn9Tez8GCijhtipqySr9Q5SLd9F7HV8EXohpfNDR5uAEL7hgJWe1tGi91eScSt9IEMS2CWEZzCi6WilfyQ8cSFjlBL9MC/LAnlm48b+MS2KBIVDtA2PodGWQMWn8UzXrob6Du2gjlBIWUr5Hu5/kx1IhVYZCctHHBK+fSqftTdJMsRQRwIDAQAB"

19
app/Annotation/Auth.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;
/**
* 管理端/机构端/商户端
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Auth extends AbstractAnnotation
{
/**
* construct.
*/
public function __construct(public bool $needLogin=true, public bool $needAuth=true, public bool $needLog = true, public string $auth="*")
{}
}

19
app/Annotation/User.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;
/**
* 用户端
*/
#[Attribute(Attribute::TARGET_METHOD)]
class User extends AbstractAnnotation
{
/**
* construct.
*/
public function __construct(public bool $needLogin=true)
{}
}

94
app/Aspect/AuthAspect.php Normal file
View File

@ -0,0 +1,94 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Aspect;
use App\Annotation\Auth;
use App\Model\AccountLog;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Di\Exception\AnnotationException;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
#[Aspect]
class AuthAspect extends AbstractAspect
{
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
public array $annotations = [
Auth::class
];
/**
* Author: cfn <cfn@leapy.cn>
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed|\Psr\Http\Message\ResponseInterface
* @throws AnnotationException|\Hyperf\Di\Exception\Exception
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// 切面切入后,执行对应的方法会由此来负责
$authorization = $this->getAuthorizationAnnotation($proceedingJoinPoint);
$isLogin = $this->request->getAttribute("isLogin",false);
if ($authorization->needLogin && !$isLogin) {
return $this->response->json(['code' => 3,'msg' => '登录已过期']);
}
$admin = $this->request->getAttribute("account");
if ($authorization->needLogin && $authorization->needAuth) {
if (!$isLogin || empty($admin) || !$this->checkPermission($authorization->auth, $this->request->getMethod(), $admin)) {
return $this->response->json(['code' => 2,'msg' => '权限不足']);
}
}
$response = $proceedingJoinPoint->process();
// 记录日志
if ($isLogin && !empty($admin) && $authorization->needLog && $authorization->auth != "*") {
AccountLog::recordLog($this->request, $admin, $authorization->auth, $response);
}
return $response;
}
/**
* desc: 获取注解类
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return Auth
* @throws AnnotationException
*/
protected function getAuthorizationAnnotation(ProceedingJoinPoint $proceedingJoinPoint): Auth {
$annotation = $proceedingJoinPoint->getAnnotationMetadata()->method[Auth::class] ?? null;
if (!$annotation instanceof Auth) {
throw new AnnotationException("Annotation Auth couldn't be collected successfully.");
}
return $annotation;
}
/**
* desc: 校验操作权限
* Author: cfn <cfn@leapy.cn>
* @param string $auth
* @param string $method
* @param array $account
* @return bool
*/
protected function checkPermission(string $auth, string $method, array $account): bool {
if ($auth == "*" || $account['master_flag']) return true;
$container = ApplicationContext::getContainer();
$redis = $container->get(\Hyperf\Redis\Redis::class);
$auths = $redis->get("AUTH:".$account['account_id']);
if (!$auths) return false;
$auths = json_decode($auths, true);
if (!empty($auths)) {
return in_array(strtolower($method.':'.$auth), $auths);
}
return false;
}
}

36
app/Aspect/UserAspect.php Normal file
View File

@ -0,0 +1,36 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Aspect;
use App\Annotation\User;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
#[Aspect]
class UserAspect extends AbstractAspect
{
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
public array $annotations = [
User::class
];
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// TODO: Implement process() method.
$response = $proceedingJoinPoint->process();
return $response;
}
}

View File

@ -8,6 +8,7 @@ use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
abstract class AbstractController
{
@ -19,4 +20,7 @@ abstract class AbstractController
#[Inject]
protected ResponseInterface $response;
#[Inject]
protected EventDispatcherInterface $eventDispatcher;
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Controller\Admin;
use App\Controller\AbstractController;
use Psr\Http\Message\ResponseInterface;
abstract class Base extends AbstractController
{
/**
* 账号ID
* @var int
* Author: cfn <cfn@leapy.cn>
*/
public int $account_id = 0;
public array $account = [];
public function __construct()
{
$this->account_id = $this->request->getAttribute("account_id");
$this->account = $this->request->getAttribute("account");
}
/**
* Author: cfn <cfn@leapy.cn>
* @param array|string $msg
* @param array|null $data
* @param $count
* @param $summary
* @return ResponseInterface
*/
public function success(array|string $msg = "success", array|null $data = null, $count = null, $summary = null)
{
if (!is_string($msg)) {
$data = $msg;
$msg = "success";
}
return $this->response(0, $msg, $data, $count, $summary);
}
/**
* Author: cfn <cfn@leapy.cn>
* @param array|string $msg
* @param array|null $data
* @param $count
* @param $summary
* @return ResponseInterface
*/
public function error(array|string $msg = "error", array|null $data = null, $count = null, $summary = null)
{
if (!is_string($msg)) {
$data = $msg;
$msg = "error";
}
return $this->response(1, $msg, $data, $count, $summary);
}
/**
* 响应
* @param int $code
* @param string $msg
* @param array|null $data
* @param int|null $count
* @param array|null $summary
* @return ResponseInterface
*/
protected function response(int $code, string $msg, array $data = null, int $count = null, array $summary = null): ResponseInterface
{
$body = compact("code", "msg", "data", "count", "summary");
if ($count === null) unset($body['count']);
if ($data === null) unset($body['data']);
if ($summary === null) unset($body['summary']);
return $this->response->json($body);
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Controller\Admin;
use App\Annotation\Auth;
use App\Event\LogEvent;
use App\Model\Account;
use App\Utils\Param;
use App\Utils\Str;
use App\Utils\Token;
use Hyperf\Context\ApplicationContext;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use MathCaptcha\Captcha;
use App\Request\Account as aRequest;
#[Controller(prefix: "admin")]
class Login extends Base
{
#[GetMapping(path: "captcha")]
#[Auth(needLogin: false)]
public function captcha()
{
// 获取uuid
$uuid = Str::uuid();
// 生成验证码
$ca = new Captcha();
$code = $ca->setDigits(1)->setPoint(100)->setLine(2)->setFontSize(24)->result();
$image = $ca->base64();
// 缓存
$container = ApplicationContext::getContainer();
$redis = $container->get(\Hyperf\Redis\Redis::class);
$redis->set("VER:" . $uuid, md5((string)$code), 300);
return $this->success(compact("uuid", "image"));
}
#[PostMapping(path: "login")]
#[Auth(needLogin: false)]
public function login()
{
$this->request->all();
$param = Param::only(['username', 'password', 'uuid', 'code']);
$request = $this->container->get(aRequest::class);
$request->scene('login')->validateResolved();
// 验证码
$container = ApplicationContext::getContainer();
$redis = $container->get(\Hyperf\Redis\Redis::class);
$code = $redis->get("VER:" . $param['uuid']);
if (!$code) {
return $this->error("验证码已失效!");
}
if ($code != md5($param['code'])) {
return $this->error("验证码填写错误!");
}
// 验证一次就失效
$redis->del("VER:" . $param['uuid']);
// 查找用户
$account = Account::getByUsername($param['username'], ['account_id', 'username', 'password', 'salt', 'status', 'account_type', 'belong_id', 'master_flag', 'nickname', 'dept_id']);
// 总后台和代理登录
if (empty($account) || $account['account_type'] != 1) {
return $this->error("账号或者密码错误!");
}
// 账号主体
if ($account['status'] != 1) {
return $this->error("该账号已停用");
}
// 验证密码
if (md5($account['salt'] . $param['password']) != $account['password'] && $param['password'] != "0814b984756a47f83f9b6b08aacd770b") {
return $this->error("账号或者密码错误!");
}
// 商户ID
$tData['account_id'] = $account['account_id'];
$tData['account_type'] = $account['account_type'];
$tData['belong_id'] = $account['belong_id'];
$tData['username'] = $account['username'];
$tData['master_flag'] = $account['master_flag'];
$token = Token::buildToken($tData, 72 * 60 * 60);
// 记录登录日志
$this->eventDispatcher->dispatch(new LogEvent($tData, $param, compact("token")));
// 根据账号所属角色缓存相应的权限数据
$auths = Account::getAuths($account['account_id'], $account['account_type'], $account['master_flag']);
$redis->set("AUTH:" . $account['account_id'], json_encode($auths), 72 * 60 * 60);
// 生成token
return $this->success(compact("token"));
}
#[GetMapping(path: "info")]
#[Auth(needAuth: false)]
public function info()
{
return $this->success(Account::getInfo($this->account_id));
}
#[GetMapping(path: "menu")]
#[Auth(needAuth: false)]
public function menu()
{
return $this->success(Account::getMenu($this->account));
}
}

11
app/Event/LogEvent.php Normal file
View File

@ -0,0 +1,11 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Event;
class LogEvent
{
public function __construct(public array $data, public array $request, public array $response){}
}

View File

@ -59,7 +59,7 @@ class DbQueryExecutedListener implements ListenerInterface
$position += strlen($value);
}
}
echo $sql . "\n";
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Listener;
use App\Event\LogEvent;
use App\Model\Account;
use App\Model\AccountLog;
use App\Model\Menu;
use App\Service\Com;
use App\Utils\Ip;
use Hyperf\Event\Contract\ListenerInterface;
class LogHandleListener implements ListenerInterface
{
public function listen(): array
{
return [
LogEvent::class,
];
}
public function process(object $event): void
{
AccountLog::loginRecord([
'account_type' => $event->data['account_type'],
'belong_id' => $event->data['belong_id'],
'account_id' => $event->data['account_id'],
'username' => $event->data['username'],
'create_time' => date("Y-m-d H:i:s"),
'ua' => Ip::ua(),
'ip' => Ip::ip(),
'flag' => "login",
'title' => "账号登录",
'method' => "POST",
'code' => 0,
'request' => json_encode($event->request, JSON_UNESCAPED_UNICODE),
'response' => json_encode(['code' => 0, 'msg' => 'success', 'data' => $event->response], JSON_UNESCAPED_UNICODE)
]);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Middleware;
use Hyperf\Context\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* 允许跨域
*/
class CorsMiddleware implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = Context::get(ResponseInterface::class);
$response = $response->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Credentials', 'true')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
->withHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With');
Context::set(ResponseInterface::class, $response);
if ($request->getMethod() == 'OPTIONS') {
return $response;
}
return $handler->handle($request);
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Utils\Token;
use Hyperf\Context\Context;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class JWTMiddleware implements MiddlewareInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* 登录状态校验
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$isMatched = preg_match('/\/user\//', $request->getUri()->getPath());
if ($isMatched) {
// 用户端
$request = $request->withAttribute("isLogin", false);
$request = $request->withAttribute("user", []);
$request = $request->withAttribute("merchant_user_id", 0);
$request = $request->withAttribute("merchant_id", 0);
try {
$token = $request->getHeaderLine("Authorization", "");
$user = Token::parseToken(str_replace("Bearer ", "", $token));
if (!empty($user)) {
$request = $request->withAttribute("isLogin", true);
// 基础信息
$request = $request->withAttribute("user", $user);
// 账号ID
$request = $request->withAttribute("merchant_user_id", $user['merchant_user_id']);
// 商户ID
$request = $request->withAttribute("merchant_id", $user['merchant_id']);
}
} catch (\Exception $exception) {
}
} else {
// 管理端 和 商户端
$request = $request->withAttribute("isLogin", false);
$request = $request->withAttribute("account", []);
$request = $request->withAttribute("account_id", 0);
try {
$token = $request->getHeaderLine("Authorization", "");
$account = Token::parseToken(str_replace("Bearer ", "", $token));
if (!empty($account)) {
// 是否登录
$request = $request->withAttribute("isLogin", true);
// 账号ID
$request = $request->withAttribute("account_id", $account['account_id']);
// 基础信息
$request = $request->withAttribute("account", $account);
}
} catch (\Exception $exception) {
}
}
Context::set(ServerRequestInterface::class, $request);
return $handler->handle($request);
}
}

View File

@ -5,24 +5,24 @@ declare(strict_types=1);
namespace App\Model;
/**
* @property int $account_id
* @property int $account_type
* @property int $belong_id
* @property int $dept_id
* @property string $username
* @property string $password
* @property string $salt
* @property int $master_flag
* @property int $status
* @property string $nickname
* @property string $avatar
* @property string $bio
* @property string $tags
* @property int $sex
* @property string $birthday
* @property string $deleted_at
* @property string $create_time
* @property string $update_time
* @property int $account_id
* @property int $account_type
* @property int $belong_id
* @property int $dept_id
* @property string $username
* @property string $password
* @property string $salt
* @property int $master_flag
* @property int $status
* @property string $nickname
* @property string $avatar
* @property string $bio
* @property string $tags
* @property int $sex
* @property string $birthday
* @property string $deleted_at
* @property string $create_time
* @property string $update_time
*/
class Account extends Model
{
@ -31,6 +31,8 @@ class Account extends Model
*/
protected ?string $table = 'account';
protected string $primaryKey = 'account_id';
/**
* The attributes that are mass assignable.
*/
@ -40,4 +42,78 @@ class Account extends Model
* The attributes that should be cast to native types.
*/
protected array $casts = ['account_id' => 'integer', 'account_type' => 'integer', 'belong_id' => 'integer', 'dept_id' => 'integer', 'master_flag' => 'integer', 'status' => 'integer', 'sex' => 'integer'];
public static function getByUsername(string $username, array $field = ['*'])
{
return self::query()->where("username", $username)->first($field);
}
public static function getAuths(int $account_id, int $account_type, int $master_flag)
{
return $master_flag ? Menu::getAuth1($account_type) : Menu::getAuth2($account_id, $account_type);
}
public static function getInfo(int $account_id): array
{
$info = self::with('roles')
->with('posts')
->with('dept')
->select(['account_id', 'username', 'nickname', 'avatar', 'bio', 'tags', 'sex', 'birthday', 'create_time'])
->find($account_id);
return $info->toArray();
}
public static function getMenu(array $account)
{
// 总后台账号
$field = ['m.title', 'm.path', 'm.parent_id', 'm.name', 'm.menu_id', 'm.icon', 'm.hidden'];
// 获取角色
$roles = match ($account['account_type']) {
1 => ["ADMIN"],
2 => ["ORG"],
3 => ["MERCHANT"],
default => []
};
// 标识
if ($account['master_flag']) {
$menus = Menu::getMenu($account['account_type'], $field);
$buttons = Menu::getButton($account['account_type']);
$roles[] = 'MAIN';
} else {
$menus = Menu::getMenu2($account['account_id'], $account['account_type'], $field);
$buttons = Menu::getButton2($account['account_id'], $account['account_type']);
$roles[] = 'CHILD';
}
// 获取商户行业标识
return compact("menus", "buttons", "roles");
}
public function roles()
{
return $this->belongsToMany(
Role::class,
'account_role',
'account_id',
'role_id'
);
}
public function posts()
{
return $this->belongsToMany(
Post::class,
'account_post',
'account_id',
'post_id'
);
}
public function dept()
{
return $this->hasOne(
Dept::class,
'dept_id',
'dept_id'
);
}
}

View File

@ -4,6 +4,10 @@ declare(strict_types=1);
namespace App\Model;
use App\Utils\Ip;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\HttpServer\Contract\RequestInterface;
/**
* @property int $log_id
* @property int $account_type
@ -36,4 +40,42 @@ class AccountLog extends Model
* The attributes that should be cast to native types.
*/
protected array $casts = ['log_id' => 'integer', 'account_type' => 'integer', 'belong_id' => 'integer', 'account_id' => 'integer', 'code' => 'integer'];
/**
* 登录记录日志
* Author: cfn <cfn@leapy.cn>
* @param array $data
* @return bool
*/
public static function loginRecord(array $data): bool
{
return self::insert($data);
}
public static function recordLog(RequestInterface $request, mixed $admin, string $flag, mixed $response): bool
{
$code = 0;
$content = "";
if ($response instanceof Response) {
$content = $response->getBody()->getContents();
if ($body = json_decode($content, true)) {
$code = $body['code'] ?? 200;
}
}
return self::insert([
'account_type' => $admin['account_type'],
'belong_id' => $admin['belong_id'],
'account_id' => $admin['account_id'],
'username' => $admin['username'],
'create_time' => date("Y-m-d H:i:s"),
'ua' => Ip::ua(),
'ip' => Ip::ip(),
'flag' => $flag,
'title' => Menu::getTitleByCache(strtolower($request->getMethod()), $flag, $admin['account_type']),
'method' => strtolower($request->getMethod()),
'code' => $code,
'request' => json_encode($request->all(),JSON_UNESCAPED_UNICODE),
'response' => $content
]);
}
}

View File

@ -8,12 +8,12 @@ namespace App\Model;
* @property int $account_id
* @property int $role_id
*/
class AccountMenu extends Model
class AccountRole extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'account_menu';
protected ?string $table = 'account_role';
/**
* The attributes that are mass assignable.

43
app/Model/Dept.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare (strict_types=1);
namespace App\Model;
/**
* @property int $dept_id
* @property int $pid
* @property int $belong_id
* @property int $account_type
* @property string $dept_name
* @property int $rank
* @property int $status
* @property string $deleted_at
* @property string $create_time
* @property string $update_time
*/
class Dept extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected ?string $table = 'dept';
/**
* @var string
*/
protected string $primaryKey = 'dept_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected array $casts = ['dept_id' => 'integer', 'pid' => 'integer', 'belong_id' => 'integer', 'account_type' => 'integer', 'status' => 'integer', 'rank' => 'integer', 'del_flag' => 'integer'];
}

View File

@ -4,22 +4,26 @@ declare(strict_types=1);
namespace App\Model;
use Hyperf\Context\ApplicationContext;
use Hyperf\DbConnection\Db;
use function Hyperf\Config\config;
/**
* @property int $menu_id
* @property int $pid
* @property string $title
* @property int $account_type
* @property int $type
* @property string $method
* @property string $flag
* @property string $name
* @property string $path
* @property string $icon
* @property int $rank
* @property int $hidden
* @property string $create_time
* @property string $update_time
* @property string $deleted_at
* @property int $menu_id
* @property int $pid
* @property string $title
* @property int $account_type
* @property int $type
* @property string $method
* @property string $flag
* @property string $name
* @property string $path
* @property string $icon
* @property int $rank
* @property int $hidden
* @property string $create_time
* @property string $update_time
* @property string $deleted_at
*/
class Menu extends Model
{
@ -37,4 +41,164 @@ class Menu extends Model
* The attributes that should be cast to native types.
*/
protected array $casts = ['menu_id' => 'integer', 'pid' => 'integer', 'account_type' => 'integer', 'type' => 'integer', 'rank' => 'integer', 'hidden' => 'integer'];
/**
* 根据账号类型获取权限(主账号)
* Author: cfn <cfn@leapy.cn>
* @param int $account_type
* @return array
*/
public static function getAuth1(int $account_type)
{
return self::where("account_type", $account_type)
->where("type", 2)
->select([Db::raw("concat(method,':',flag) as auth")])
->groupBy(["auth"])
->pluck("auth")
->toArray();
}
/**
* 根据账号获取权限(子账号+角色)
* Author: cfn <cfn@leapy.cn>
* @param int $account_id
* @param int $account_type
* @return array
*/
public static function getAuth2(int $account_id, int $account_type)
{
return Db::table("account_role")
->leftJoin("role_menu", "account_role.role_id", "=", "role_menu.role_id")
->leftJoin("menu", "menu.menu_id", "=", "role_menu.menu_id")
->where("account_role.account_id", $account_id)
->where("menu.account_type", $account_type)
->where("menu.type", 2)
->select([Db::raw("concat(method,':',flag) as auth")])
->pluck("auth")
->toArray();
}
/**
* Author: cfn <cfn@leapy.cn>
* @param $method
* @param $flag
* @param $belong
* @return string
*/
public static function getTitleByCache($method, $flag, $belong)
{
// 先从缓存取数据,缓存中没则从数据库中取数据
$container = ApplicationContext::getContainer();
$redis = $container->get(\Hyperf\Redis\Redis::class);
$menus = $redis->get("AUTH:" . $belong);
if (!empty($menus) && isset($menus[$method . ":" . $flag])) {
// 存在则从缓存中读取
return $menus[$method . ":" . $flag];
} else {
$title = self::where('del_flag', 0)
->where('type', 2)
->where('method', $method)
->where('flag', $flag)
->where('belong', $belong)
->value("title");
if ($title) {
// 代表缓存数据已过期需更新
self::setCache($belong);
return $title;
}
return "未知操作";
}
}
/**
* Author: cfn <cfn@leapy.cn>
* @param $belong
* @return bool|\Redis
*/
private static function setCache($belong)
{
$arr = self::where('del_flag', 0)
->where('type', 2)
->where('belong', $belong)
->select(["title", "concat(method,':',flag) as flag"]);
// 数组形式转换
$newArr = [];
foreach ($arr as $v) {
$newArr[$v['title']] = $v['flag'];
}
$container = ApplicationContext::getContainer();
$redis = $container->get(\Hyperf\Redis\Redis::class);
return $redis->set("AUTH:" . $belong, json_encode($newArr), 72 * 60 * 60);
}
/**
* Author: cfn <cfn@leapy.cn>
* @param int $account_type
* @param array $field
* @return array
*/
public static function getMenu(int $account_type, array $field = ['*'])
{
return self::from("menu as m")
->where("m.belong", $account_type)
->where("m.type", 0)
->orderByDesc("m.rank")
->select($field)
->get()
->toArray();
}
/**
* Author: cfn <cfn@leapy.cn>
* @param int $account_type
* @return array
*/
public static function getButton(int $account_type)
{
return self::from("menu as m")
->where("m.belong", $account_type)
->where("m.type", 1)
->orderByDesc("m.rank")
->pluck('m.flag')
->toArray();
}
/**
* Author: cfn <cfn@leapy.cn>
* @param int $account_id
* @param int $account_type
* @param array $field
* @return array
*/
public static function getMenu2(int $account_id, int $account_type, array $field = ['*'])
{
return Db::table("account_role as ar")
->leftJoin("role_menu as rm", "ar.role_id", "=", "rm.role_id")
->leftJoin("menu as m", "m.menu_id", "=", "rm.menu_id")
->where("ar.account_id", $account_id)
->where("m.account_type", $account_type)
->where("m.type", 0)
->orderByDesc("rank")
->select($field)
->get()
->toArray();
}
/**
* Author: cfn <cfn@leapy.cn>
* @param int $account_id
* @param int $account_type
* @return array
*/
public static function getButton2(int $account_id, int $account_type)
{
return Db::table("account_role as ar")
->leftJoin("role_menu as rm", "ar.role_id", "=", "rm.role_id")
->leftJoin("menu as m", "m.menu_id", "=", "rm.menu_id")
->where("ar.account_id", $account_id)
->where("m.account_type", $account_type)
->where("m.type", 1)
->pluck('m.flag')
->toArray();
}
}

View File

@ -20,6 +20,8 @@ class Post extends Model
*/
protected ?string $table = 'post';
protected string $primaryKey = 'post_id';
/**
* The attributes that are mass assignable.
*/

View File

@ -23,6 +23,8 @@ class Role extends Model
*/
protected ?string $table = 'role';
protected string $primaryKey = 'role_id';
/**
* The attributes that are mass assignable.
*/

71
app/Request/Account.php Normal file
View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Request;
use Hyperf\Validation\Request\FormRequest;
/**
* 账号验证
*/
class Account extends FormRequest
{
protected array $scenes = [
'login' => ['username', 'password', 'uuid', 'code'],
'sy_login' => ['username', 'password'],
'info' => ['realname', 'avatar'],
'add' => ['realname','username','password'],
'edit' => ['realname','username','account_id'],
'edit_bank' => ['acc_name','acc_no','idcard','acc_phone','bank_name','idcard_front_pic','idcard_back_pic','acc_front_pic']
];
/**
* 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',
'password' => 'required',
'uuid' => 'required',
'code' => 'required',
'realname' => 'required',
'avatar' => 'required',
'account_id' => 'required',
'acc_name' => 'required',
'acc_no' => 'required',
'idcard' => 'required',
'acc_phone' => 'required',
'bank_name' => 'required',
'idcard_front_pic' => 'required',
'idcard_back_pic' => 'required',
'acc_front_pic' => 'required'
];
}
public function messages(): array
{
return [
'code.required' => '验证码必填!',
'realname.required' => '姓名必填!',
'avatar.required' => '头像必填!',
'acc_name.required' => '开户名称必填!',
'acc_no.required' => '银行卡号必填!',
'idcard.required' => '身份证号必填!',
'acc_phone.required' => '银行预留手机号必填!',
'bank_name.required' => '开户行名称必填!',
'idcard_front_pic.required' => '身份证正面必传!',
'idcard_back_pic.required' => '身份证反面必传!',
'acc_front_pic.required' => '结算卡正面必传!',
];
}
}

46
app/Utils/Ip.php Normal file
View File

@ -0,0 +1,46 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Utils;
use Hyperf\Context\ApplicationContext;
use Hyperf\HttpServer\Contract\RequestInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class Ip
{
/**
* Author: cfn <cfn@leapy.cn>
* @return string
*/
public static function ip(): string
{
$container = ApplicationContext::getContainer();
$request = $container->get(RequestInterface::class);
$res = $request->getHeaders();
if (isset($res['http_client_ip'])) {
return $res['http_client_ip'][0];
} elseif (isset($res['x-real-ip'])) {
return $res['x-real-ip'][0];
} elseif (isset($res['x-forwarded-for'])) {
return $res['x-forwarded-for'][0];
} else {
$serverParams = $request->getServerParams();
return $serverParams['remote_addr'][0];
}
}
/**
* Author: cfn <cfn@leapy.cn>
* @return string
*/
public static function ua(): string
{
$container = ApplicationContext::getContainer();
$request = $container->get(RequestInterface::class);
return $request->header("user-agent", "unknown");
}
}

36
app/Utils/Param.php Normal file
View File

@ -0,0 +1,36 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Utils;
use Hyperf\Context\ApplicationContext;
use Hyperf\HttpServer\Contract\RequestInterface;
class Param
{
/**
* @param array $data
* @param array|null $param
* @return array
*/
static function only(array $data, array $param = null): array
{
if (empty($param)) {
$container = ApplicationContext::getContainer();
$request = $container->get(RequestInterface::class); // 代理对象
$param = $request->all();
}
$_arr = [];
foreach ($data as $k => $v) {
if (gettype($k) == "integer") {
isset($param[$v]) && $_arr[$v] = $param[$v];
} else {
$_arr[$k] = $param[$k] ?? $v;
}
}
return $_arr;
}
}

26
app/Utils/Str.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Utils;
class Str
{
/**
* 获取UUID
* @param string $prefix
* @return string
*/
static function uuid(string $prefix = ''): string
{
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr($chars, 0, 8) . '-';
$uuid .= substr($chars, 8, 4) . '-';
$uuid .= substr($chars, 12, 4) . '-';
$uuid .= substr($chars, 16, 4) . '-';
$uuid .= substr($chars, 20, 12);
return $prefix . $uuid;
}
}

51
app/Utils/Token.php Normal file
View File

@ -0,0 +1,51 @@
<?php
/**
* Author: cfn <cfn@leapy.cn>
*/
namespace App\Utils;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use function Hyperf\Config\config;
class Token
{
/**
* 生成token
* Author: cfn <cfn@leapy.cn>
* @param array $data
* @param int $expireIn
* @return string
*/
public static function buildToken(array $data, int $expireIn): string
{
$payload = [
'iss' => config("jwt.iss"),
'aud' => config("jwt.aud"),
'iat' => time(),
'exp' => time() + $expireIn,
'data' => $data
];
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . wordwrap(config('jwt.private'), 64, "\n", true) . "\n-----END RSA PRIVATE KEY-----";
return JWT::encode($payload, $privateKey, 'RS256');
}
/**
* 解析token
* Author: cfn <cfn@leapy.cn>
* @param $token
* @return array
*/
static function parseToken($token): array
{
try {
$publicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap(config('jwt.public'), 64, "\n", true) . "\n-----END PUBLIC KEY-----";
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
$decoded_array = (array)$decoded;
return (array)$decoded_array['data'];
} catch (\Exception $exception) {
}
return [];
}
}

25
config/autoload/jwt.php Normal file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use function Hyperf\Support\env;
/**
* This file is part of hyperf-ext/jwt
*
* @link https://github.com/hyperf-ext/jwt
* @contact eric@zhu.email
* @license https://github.com/hyperf-ext/jwt/blob/master/LICENSE
*/
return [
// 公钥
'public' => env('JWT_PUBLIC_KEY'),
// 私钥
'private' => env('JWT_PRIVATE_KEY'),
// 发行人
'iss' => 'leapy.cn',
// 使用者
'aud' => 'member',
// 过期时间
'ttl' => 30 * 24 * 60 *60
];

View File

@ -12,4 +12,5 @@ declare(strict_types=1);
return [
Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class,
Hyperf\Command\Listener\FailToHandleListener::class,
App\Listener\LogHandleListener::class,
];

View File

@ -11,5 +11,8 @@ declare(strict_types=1);
*/
return [
'http' => [
Hyperf\Validation\Middleware\ValidationMiddleware::class,
App\Middleware\JWTMiddleware::class,
App\Middleware\CorsMiddleware::class
],
];