diff --git a/.env.example b/.env.example index 6879583..4751a9e 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,7 @@ DB_PREFIX= REDIS_HOST=localhost REDIS_AUTH=(null) REDIS_PORT=6379 -REDIS_DB=0 \ No newline at end of file +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" diff --git a/app/Annotation/Auth.php b/app/Annotation/Auth.php new file mode 100644 index 0000000..7998c5b --- /dev/null +++ b/app/Annotation/Auth.php @@ -0,0 +1,19 @@ + + */ + +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 + * @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 + * @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; + } +} \ No newline at end of file diff --git a/app/Aspect/UserAspect.php b/app/Aspect/UserAspect.php new file mode 100644 index 0000000..104e8d2 --- /dev/null +++ b/app/Aspect/UserAspect.php @@ -0,0 +1,36 @@ + + */ + +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; + } +} \ No newline at end of file diff --git a/app/Controller/AbstractController.php b/app/Controller/AbstractController.php index 35a122b..ca509f8 100644 --- a/app/Controller/AbstractController.php +++ b/app/Controller/AbstractController.php @@ -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; } diff --git a/app/Controller/Admin/Base.php b/app/Controller/Admin/Base.php new file mode 100644 index 0000000..6d0083b --- /dev/null +++ b/app/Controller/Admin/Base.php @@ -0,0 +1,81 @@ + + */ + +namespace App\Controller\Admin; + +use App\Controller\AbstractController; +use Psr\Http\Message\ResponseInterface; + +abstract class Base extends AbstractController +{ + /** + * 账号ID + * @var int + * Author: cfn + */ + 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 + * @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 + * @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); + } + + +} \ No newline at end of file diff --git a/app/Controller/Admin/Login.php b/app/Controller/Admin/Login.php new file mode 100644 index 0000000..fd1cbd4 --- /dev/null +++ b/app/Controller/Admin/Login.php @@ -0,0 +1,105 @@ + + */ + +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)); + } +} \ No newline at end of file diff --git a/app/Event/LogEvent.php b/app/Event/LogEvent.php new file mode 100644 index 0000000..962f52a --- /dev/null +++ b/app/Event/LogEvent.php @@ -0,0 +1,11 @@ + + */ + +namespace App\Event; + +class LogEvent +{ + public function __construct(public array $data, public array $request, public array $response){} +} \ No newline at end of file diff --git a/app/Listener/DbQueryExecutedListener.php b/app/Listener/DbQueryExecutedListener.php index 5804bb7..4403de1 100644 --- a/app/Listener/DbQueryExecutedListener.php +++ b/app/Listener/DbQueryExecutedListener.php @@ -59,7 +59,7 @@ class DbQueryExecutedListener implements ListenerInterface $position += strlen($value); } } - + echo $sql . "\n"; $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } diff --git a/app/Listener/LogHandleListener.php b/app/Listener/LogHandleListener.php new file mode 100644 index 0000000..1731cc1 --- /dev/null +++ b/app/Listener/LogHandleListener.php @@ -0,0 +1,43 @@ + + */ + +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) + ]); + } +} \ No newline at end of file diff --git a/app/Middleware/CorsMiddleware.php b/app/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..2eccf6d --- /dev/null +++ b/app/Middleware/CorsMiddleware.php @@ -0,0 +1,36 @@ +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); + } +} diff --git a/app/Middleware/JWTMiddleware.php b/app/Middleware/JWTMiddleware.php new file mode 100644 index 0000000..af4cd4a --- /dev/null +++ b/app/Middleware/JWTMiddleware.php @@ -0,0 +1,78 @@ +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); + } +} diff --git a/app/Model/Account.php b/app/Model/Account.php index 115d30b..256b813 100644 --- a/app/Model/Account.php +++ b/app/Model/Account.php @@ -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' + ); + } } diff --git a/app/Model/AccountLog.php b/app/Model/AccountLog.php index 393c8f0..3d64049 100644 --- a/app/Model/AccountLog.php +++ b/app/Model/AccountLog.php @@ -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 + * @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 + ]); + } } diff --git a/app/Model/AccountMenu.php b/app/Model/AccountRole.php similarity index 84% rename from app/Model/AccountMenu.php rename to app/Model/AccountRole.php index 1e9fa5c..6c4b38c 100644 --- a/app/Model/AccountMenu.php +++ b/app/Model/AccountRole.php @@ -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. diff --git a/app/Model/Dept.php b/app/Model/Dept.php new file mode 100644 index 0000000..f5398bc --- /dev/null +++ b/app/Model/Dept.php @@ -0,0 +1,43 @@ + 'integer', 'pid' => 'integer', 'belong_id' => 'integer', 'account_type' => 'integer', 'status' => 'integer', 'rank' => 'integer', 'del_flag' => 'integer']; + +} diff --git a/app/Model/Menu.php b/app/Model/Menu.php index 8df78f7..cf75ace 100644 --- a/app/Model/Menu.php +++ b/app/Model/Menu.php @@ -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 + * @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 + * @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 + * @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 + * @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 + * @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 + * @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 + * @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 + * @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(); + } } diff --git a/app/Model/Post.php b/app/Model/Post.php index dc86212..c924a4f 100644 --- a/app/Model/Post.php +++ b/app/Model/Post.php @@ -20,6 +20,8 @@ class Post extends Model */ protected ?string $table = 'post'; + protected string $primaryKey = 'post_id'; + /** * The attributes that are mass assignable. */ diff --git a/app/Model/Role.php b/app/Model/Role.php index 46a5dba..ac78586 100644 --- a/app/Model/Role.php +++ b/app/Model/Role.php @@ -23,6 +23,8 @@ class Role extends Model */ protected ?string $table = 'role'; + protected string $primaryKey = 'role_id'; + /** * The attributes that are mass assignable. */ diff --git a/app/Request/Account.php b/app/Request/Account.php new file mode 100644 index 0000000..2fba628 --- /dev/null +++ b/app/Request/Account.php @@ -0,0 +1,71 @@ + ['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' => '结算卡正面必传!', + ]; + } +} diff --git a/app/Utils/Ip.php b/app/Utils/Ip.php new file mode 100644 index 0000000..322bd50 --- /dev/null +++ b/app/Utils/Ip.php @@ -0,0 +1,46 @@ + + */ + +namespace App\Utils; + +use Hyperf\Context\ApplicationContext; +use Hyperf\HttpServer\Contract\RequestInterface; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +class Ip +{ + /** + * Author: cfn + * @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 + * @return string + */ + public static function ua(): string + { + $container = ApplicationContext::getContainer(); + $request = $container->get(RequestInterface::class); + return $request->header("user-agent", "unknown"); + } +} \ No newline at end of file diff --git a/app/Utils/Param.php b/app/Utils/Param.php new file mode 100644 index 0000000..1258134 --- /dev/null +++ b/app/Utils/Param.php @@ -0,0 +1,36 @@ + + */ + +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; + } +} \ No newline at end of file diff --git a/app/Utils/Str.php b/app/Utils/Str.php new file mode 100644 index 0000000..14a1bfc --- /dev/null +++ b/app/Utils/Str.php @@ -0,0 +1,26 @@ + + */ + +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; + } + +} \ No newline at end of file diff --git a/app/Utils/Token.php b/app/Utils/Token.php new file mode 100644 index 0000000..e17a730 --- /dev/null +++ b/app/Utils/Token.php @@ -0,0 +1,51 @@ + + */ + +namespace App\Utils; + +use Firebase\JWT\JWT; +use Firebase\JWT\Key; +use function Hyperf\Config\config; + +class Token +{ + /** + * 生成token + * Author: cfn + * @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 + * @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 []; + } +} \ No newline at end of file diff --git a/config/autoload/jwt.php b/config/autoload/jwt.php new file mode 100644 index 0000000..3c4abef --- /dev/null +++ b/config/autoload/jwt.php @@ -0,0 +1,25 @@ + env('JWT_PUBLIC_KEY'), + // 私钥 + 'private' => env('JWT_PRIVATE_KEY'), + // 发行人 + 'iss' => 'leapy.cn', + // 使用者 + 'aud' => 'member', + // 过期时间 + 'ttl' => 30 * 24 * 60 *60 +]; diff --git a/config/autoload/listeners.php b/config/autoload/listeners.php index 8a2c7a2..67f3bac 100644 --- a/config/autoload/listeners.php +++ b/config/autoload/listeners.php @@ -12,4 +12,5 @@ declare(strict_types=1); return [ Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class, Hyperf\Command\Listener\FailToHandleListener::class, + App\Listener\LogHandleListener::class, ]; diff --git a/config/autoload/middlewares.php b/config/autoload/middlewares.php index 49bdec2..307ffb8 100644 --- a/config/autoload/middlewares.php +++ b/config/autoload/middlewares.php @@ -11,5 +11,8 @@ declare(strict_types=1); */ return [ 'http' => [ + Hyperf\Validation\Middleware\ValidationMiddleware::class, + App\Middleware\JWTMiddleware::class, + App\Middleware\CorsMiddleware::class ], ];