From 28a7e9cc42fe82306550f86d4999d08bacb38d9e Mon Sep 17 00:00:00 2001 From: "zhangf@suq.cn" Date: Fri, 12 Dec 2025 11:00:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(piadmin):=20=E6=96=B0=E5=A2=9E=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20feat(proxy):=E6=96=B0=E5=A2=9E=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95=E5=8F=8A=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=8F=9C=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controller/v1/ProxyMenuController.php | 93 +++++++++ .../app/controller/v1/ProxyUserLoginController.php | 30 +++ plugin/piadmin/app/dao/ProxyMenuDao.php | 17 ++ plugin/piadmin/app/dao/ProxyUserDao.php | 17 ++ .../ProxyUserAuthorizationMiddleware.php | 82 ++++++++ plugin/piadmin/app/model/ProxyMenu.php | 16 ++ plugin/piadmin/app/model/ProxyUser.php | 21 ++ plugin/piadmin/app/route/v1/proxy.php | 21 ++ plugin/piadmin/app/route/v1/route.php | 13 +- plugin/piadmin/app/service/ProxyMenuService.php | 222 +++++++++++++++++++++ plugin/piadmin/app/service/ProxyUserService.php | 130 ++++++++++++ plugin/piadmin/config/piadmin.php | 6 + plugin/piadmin/config/route.php | 2 + 13 files changed, 667 insertions(+), 3 deletions(-) create mode 100644 plugin/piadmin/app/controller/v1/ProxyMenuController.php create mode 100644 plugin/piadmin/app/controller/v1/ProxyUserLoginController.php create mode 100644 plugin/piadmin/app/dao/ProxyMenuDao.php create mode 100644 plugin/piadmin/app/dao/ProxyUserDao.php create mode 100644 plugin/piadmin/app/middleware/ProxyUserAuthorizationMiddleware.php create mode 100644 plugin/piadmin/app/model/ProxyMenu.php create mode 100644 plugin/piadmin/app/model/ProxyUser.php create mode 100644 plugin/piadmin/app/route/v1/proxy.php create mode 100644 plugin/piadmin/app/service/ProxyMenuService.php create mode 100644 plugin/piadmin/app/service/ProxyUserService.php diff --git a/plugin/piadmin/app/controller/v1/ProxyMenuController.php b/plugin/piadmin/app/controller/v1/ProxyMenuController.php new file mode 100644 index 0000000..1423ce8 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/ProxyMenuController.php @@ -0,0 +1,93 @@ +service = app()->make(ProxyMenuService::class); + } + + public function index(Request $request) + { + + $all = $request->all(); + + $list = $this->service->getMenuList($all); + + return success($list); + } + + public function save(Request $request) + { + $params = requestOnly([ + 'pid' => 0, + 'name' => '', + 'code' => '', + 'icon' => '', + 'route' => '', + 'component' => '', + 'redirect' => '', + 'is_hidden' => 2, + 'is_layout' => 2, + 'type' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '' + ]); + validate(MenuValidate::class)->scene('save')->check($params); + $res = $this->service->saveData($params); + return success($res); + } + + public function update(Request $request) + { + $params = requestOnly([ + 'id' => '', + 'pid' => 0, + 'name' => '', + 'code' => '', + 'icon' => '', + 'route' => '', + 'component' => '', + 'redirect' => '', + 'is_hidden' => 2, + 'is_layout' => 2, + 'type' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '' + ]); + validate(MenuValidate::class)->scene('update')->check($params); + $res = $this->service->updateData($params); + return success($res); + } + + public function delete(Request $request) + { + $ids = $request->input('ids'); + $res = $this->service->deleteData($ids); + return success($res); + } + + public function read(Request $request) + { + $id = $request->input('id'); + $res = $this->service->readData($id); + return success($res); + } + + public function getProxyMenuPermissions() + { + return success($this->service->getUserMenu()); + } + +} diff --git a/plugin/piadmin/app/controller/v1/ProxyUserLoginController.php b/plugin/piadmin/app/controller/v1/ProxyUserLoginController.php new file mode 100644 index 0000000..02b003b --- /dev/null +++ b/plugin/piadmin/app/controller/v1/ProxyUserLoginController.php @@ -0,0 +1,30 @@ +service = app()->make(ProxyUserService::class); + } + + public function login(Request $request){ + $params = $request->only([ + 'username', + 'password' + ]); + validate(LoginValidate::class)->scene('loginByPassword')->check($params); + // 用户登录 + $res = $this->service->login($params); + return success($res); + } + +} diff --git a/plugin/piadmin/app/dao/ProxyMenuDao.php b/plugin/piadmin/app/dao/ProxyMenuDao.php new file mode 100644 index 0000000..1c5bc80 --- /dev/null +++ b/plugin/piadmin/app/dao/ProxyMenuDao.php @@ -0,0 +1,17 @@ +header($authHeader, ''); + if (empty($token)) { + throw new ApiException(403); + } + $token = trim(str_replace('Bearer ', '', $token)); + // 解token payload + $key = config("plugin.piadmin.piadmin.jwt.proxy.key"); + $keyId = config("plugin.piadmin.piadmin.jwt.proxy.key_id"); + $payload = JWT::decode($token, new Key($key, 'HS256')); + if (empty($token)) { + throw new ApiException(403); + } + // 缓存 + $tokenCacheData = CacheUtils::get('proxy.token.'. md5($token)); + + if (empty($tokenCacheData)) { + throw new ApiException(403); + } + $tokenCacheData = json_decode($tokenCacheData, true); + if (empty($tokenCacheData) || !isset($tokenCacheData['uid'])) { + throw new ApiException(403); + } + // 验证账号状态 + $uid = $tokenCacheData['uid']; + $userDao = app()->make(ProxyUserDao::class); + $user = $userDao->getOne(['id' => $uid]); + if (empty($user)) { + throw new ApiException(403); + } + if ($user['status'] != 1 ) { + throw new ApiException(400006); + } + $proxyUserInfo = $user->toArray(); + $proxyUserInfo['is_login'] = true; + $proxyUserInfo['uid'] = $proxyUserInfo['id']; + $proxyUserInfo['token'] = $token; + //数据权限,目前只查自己 + $proxyUserInfo['dataPermission'] = [$proxyUserInfo['id']]; + // 检查版本 + $this->checkVersionKey($uid, $token); + $request->proxy_user = $proxyUserInfo; + // todo 补全session + Context::set('proxy_user', $proxyUserInfo); + // session('user', $userInfo); + return $handler($request); + } + + /** + * 版本检查 + * @param string|int $uid + * @param string $token + * @return void + * @throws ApiException + */ + private function checkVersionKey(string|int $uid, string $token): void + { + $userVersionKey = "user.version.{$uid}"; + $version = CacheUtils::get($userVersionKey); + if ($version != getPiAdminVersion()) { + CacheUtils::delete(md5($token)); + throw new ApiException(403); + } + } +} diff --git a/plugin/piadmin/app/model/ProxyMenu.php b/plugin/piadmin/app/model/ProxyMenu.php new file mode 100644 index 0000000..cead388 --- /dev/null +++ b/plugin/piadmin/app/model/ProxyMenu.php @@ -0,0 +1,16 @@ +setParams(['perm' => ['userLogin']]); +}); + +// 需要登录的接口 +Route::group('/piadmin/v1/proxy', function () { + // 获取当前用户菜单权限 + Route::get('/getMenuPermissions', [ProxyMenuController::class, 'getProxyMenuPermissions'])->setParams(['perm' => ['getProxyMenuPermissions']]); + +})->middleware([ + ProxyUserAuthorizationMiddleware::class, +]); diff --git a/plugin/piadmin/app/route/v1/route.php b/plugin/piadmin/app/route/v1/route.php index 74c903a..49080e9 100644 --- a/plugin/piadmin/app/route/v1/route.php +++ b/plugin/piadmin/app/route/v1/route.php @@ -1,9 +1,9 @@ setParams(['perm' => ['login']]); Route::get('/captcha', [LoginController::class, 'captcha'])->setParams(['perm' => ['captcha']]); @@ -64,6 +62,15 @@ Route::group('/piadmin/v1', function () { Route::post('/delete', [UserMenuController::class, 'delete'])->setParams(['perm' => ['userMenuDelete']]); }); + // 代理端菜单 + Route::group('/proxyMenu', function () { + Route::get('/index', [ProxyMenuController::class, 'index'])->setParams(['perm' => ['proxyMenuIndex']]); + Route::post('/save', [ProxyMenuController::class, 'save'])->setParams(['perm' => ['proxyMenuSave']]); + Route::post('/update', [ProxyMenuController::class, 'update'])->setParams(['perm' => ['proxyMenuUpdate']]); + Route::get('/read', [ProxyMenuController::class, 'read'])->setParams(['perm' => ['proxyMenuRead']]); + Route::post('/delete', [ProxyMenuController::class, 'delete'])->setParams(['perm' => ['proxyMenuDelete']]); + }); + // 角色 Route::group('/role', function () { Route::post('/save', [SystemRoleController::class, 'save'])->setParams(['perm' => ['roleSave']]); diff --git a/plugin/piadmin/app/service/ProxyMenuService.php b/plugin/piadmin/app/service/ProxyMenuService.php new file mode 100644 index 0000000..cd426d3 --- /dev/null +++ b/plugin/piadmin/app/service/ProxyMenuService.php @@ -0,0 +1,222 @@ +dao = $dao; + } + + + public function getMenuList($params = []) + { + $where = []; + $list = $this->dao->getList($where, '*', 0, 0, 'sort asc'); + return TreeUtils::toTree($list); + } + + /** + * 保存数据 + * @param $params + * @return \plugin\piadmin\app\base\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveData($params) + { + // 检查code + $codeExist = $this->dao->be([ + 'code'=>$params['code'] + ]); + if ($codeExist) { + throw new ApiException(trans(401003)); + } + $parentMenu = $this->getMenu($params['pid']); + if (empty($parentMenu)) { + throw new ApiException(trans(401001)); + } + $newUuid = IdUtils::uuid(); + $newIdPath = IdPathUtils::addItem($parentMenu['id_path'], $newUuid); + $params['id_path'] = $newIdPath; + $params['uuid'] = $newUuid; + return $this->dao->save($params); + } + + /** + * 更新数据 + * @param $params + * @return \plugin\piadmin\app\base\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateData($params) + { + $menu = $this->dao->get($params['id']); + if (empty($menu)) { + throw new ApiException(trans(401005)); + } + if($params['pid'] == $menu['id']){ + throw new ApiException(trans(401008)); + } + // code有没有变化 + if ($params['code'] != $menu['code']) { + $codeExist = $this->dao->be([ + 'code'=>$params['code'] + ]); + if ($codeExist) { + throw new ApiException(trans(401003)); + } + } + $newIdPath = ''; + // 上级菜单有没有变化 + if ($params['pid'] != $menu['pid']) { + $parentMenu = $this->getMenu($params['pid']); + if (empty($parentMenu)) { + throw new ApiException(trans(401001)); + } + $newIdPath = IdPathUtils::addItem($parentMenu['id_path'], $menu['id']); + $params['id_path'] = $newIdPath; + } + unset($params['id']); + + // 落库 + Db::startTrans(); + try { + $this->dao->update(['id' => $menu['id']], $params); + if (!empty($newIdPath)) { + $this->updateAllChildrenIdPath($menu['id_path'], $newIdPath); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $params; + } + + + /** + * 删除数据 + * @param mixed $id + * @return array + */ + public function deleteData($ids) + { + //根据ids获取所有子菜单 + $ids = $this->getChildrenMenuIdsBatch($ids); + // 落库 + Db::startTrans(); + try { + foreach ($ids as $id) { + $this->dao->softDel($id); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return []; + } + + /** + * 获取数据 + * @param mixed $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function readData(mixed $id) + { + $menu = $this->dao->get(['id' => $id]); + if (empty($menu)) { + return []; + } + return $menu->toArray(); + } + + public function getUserMenu() + { + $list = $this->dao->getList([], '*', 0, 0, 'sort asc'); + return TreeUtils::toTree($list); + } + + // ============================================================ 私有方法 =============================================== + + + /** + * 获取菜单 + * @param $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + private function getMenu($id): array + { + if ($id == 0) { + return [ + 'id' => 0, + 'uuid' => 0, + 'name' => '顶级菜单', + 'code' => 'top', + 'type' => 'M', + 'status' => 1, + 'sort' => 1000, + 'pid' => 0, + 'id_path' => '0', + ]; + } + $menu = $this->dao->get(['id' => $id]); + if (empty($menu)) { + return []; + } + return $menu->toArray(); + } + + private function updateAllChildrenIdPath($oldIdPath, $newIdPath) + { + // 获取所有子级 + $childrenIds = $this->dao->getColumn([ + ['id_path', 'like', $oldIdPath . ',%'] + ], 'id'); + $children = $this->dao->getList([ + ['id', 'in', $childrenIds] + ], 'id, id_path'); + foreach ($children as $child) { + $newIdPath = str_replace($oldIdPath, $newIdPath, $child['id_path']); + $this->dao->update(['id' => $child['id']], [ + 'id_path' => $newIdPath + ]); + } + } + + public function getChildrenMenuIdsBatch(array $ids): array + { + $allMenus = $this->dao->getList([], 'id, pid'); + $allChildrenIds = []; + + // 批量处理所有ID的子节点查找 + foreach ($ids as $id) { + $childIds = TreeUtils::getChildrenIds($allMenus, $id); + $allChildrenIds = array_merge($allChildrenIds, $childIds); + } + return array_values(array_unique($allChildrenIds)); // 重新索引并去重 + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/ProxyUserService.php b/plugin/piadmin/app/service/ProxyUserService.php new file mode 100644 index 0000000..cd7901e --- /dev/null +++ b/plugin/piadmin/app/service/ProxyUserService.php @@ -0,0 +1,130 @@ +dao = $dao; + } + + // 用户登录 + public function login($params) + { + $proxyUser = $this->dao->getModel()->where(function ($query) use ($params){ + $query->where('email', '=', $params['username']) + ->whereOr('phone', '=', $params['username']); + })->find(); + $this->validPassword($proxyUser, $params); + $tokenInfo = $this->commonLogin($proxyUser, $params); + $tokenInfo['user'] = $proxyUser->toArray(); + $tokenInfo['expire_time'] = $tokenInfo['expire'] + time(); + return $tokenInfo; + } + + // ============================================================ 私有方法 =============================================== + + + /** + * 验证密码 + * @param mixed $adminUser + * @param $params + * @return void + */ + private function validPassword(mixed $proxyUser, $params): void + { + // 验证登录信息 + if (empty($proxyUser)) { + throw new ApiException(trans(400005)); + } + // 密码验证 + if (!password_verify($params['password'], $proxyUser['password'])) { + throw new ApiException(trans(400005)); + } + } + + /** + * 通用登录流程 + * @param mixed $adminUser + * @param array $params + * @return array + * @throws ApiException + */ + private function commonLogin(mixed $proxyUser, array $params) + { + if ($proxyUser['status'] == 2) { + throw new ApiException(trans(400006)); + } + $tokenInfo = JwtUtils::createToken($proxyUser['id'], 'proxy'); + $tokenCacheInfo = [ + 'uid' => $proxyUser['id'], + 'type' => 'user_token', + 'token' => $tokenInfo['token'], + 'expire_time' => time() + $tokenInfo['expire'], + 'user' => $proxyUser->toArray(), + 'login_time' => date('Y-m-d H:i:s'), + 'os_type' => $this->getClientOS(), + 'browser_name' => $this->getClientBrowserName(), + 'login_ip' => request()->getRemoteIp() + ]; + // 保存token缓存 + CacheUtils::set('proxy.token.' . md5($tokenInfo['token']), json_encode($tokenCacheInfo), (int)$tokenInfo['expire']); + // 保存用户对应版本缓存 + $currentVersion = getPiAdminVersion(); + $userVersionKey = "proxy.version.{$proxyUser['id']}"; + CacheUtils::set($userVersionKey, $currentVersion, (int)$tokenInfo['expire']); + return $tokenInfo; + } + + /** + * 获取客户端操作系统 + * @return int + */ + private function getClientOS(): int + { + $userAgent = request()->header('user-agent'); + if (stripos($userAgent, 'Windows') !== false) { + $os = 1; + } elseif (stripos($userAgent, 'Mac') !== false) { + $os = 2; + } elseif (stripos($userAgent, 'Linux') !== false) { + $os = 3; + } elseif (stripos($userAgent, 'iPhone|iPad|iPod') !== false) { + $os = 4; + } elseif (stripos($userAgent, 'Android') !== false) { + $os = 5; + } else { + $os = 6; + } + return $os; + } + + /** + * 获取客户端浏览器名称 + * @return string + */ + private function getClientBrowserName(): string + { + $userAgent = request()->header('user-agent'); + $pattern = '/(MSIE|Trident|Edge|Firefox|Chrome|Safari|Opera|OPR)\/?\s*(\d+\.\d+)?/i'; + preg_match($pattern, $userAgent, $matches); + if (isset($matches[1])) { + // 处理Edge和Opera的特殊标识 + if ($matches[1] === 'Trident') return 'Internet Explorer'; + if ($matches[1] === 'OPR') return 'Opera'; + return $matches[1]; + } + return 'Unknown Browser'; + } + +} \ No newline at end of file diff --git a/plugin/piadmin/config/piadmin.php b/plugin/piadmin/config/piadmin.php index dc5763c..f6eb5b0 100644 --- a/plugin/piadmin/config/piadmin.php +++ b/plugin/piadmin/config/piadmin.php @@ -23,6 +23,12 @@ return [ 'expire' => 600000, 'auth_header' => 'Authorization', 'key_id' => 'admin' + ], + 'proxy' => [ + 'key' => 'v8X9qY2pR7sT4nL6mK3jH5gF1dE8cB0aW3zY6uI9oP2qS5tN7rM4kL6jH8gF', + 'expire' => 600000, + 'auth_header' => 'Authorization', + 'key_id' => 'proxy' ] ], diff --git a/plugin/piadmin/config/route.php b/plugin/piadmin/config/route.php index 2192e96..13195f7 100644 --- a/plugin/piadmin/config/route.php +++ b/plugin/piadmin/config/route.php @@ -6,6 +6,8 @@ use Webman\Route; require_once config('plugin.piadmin.piadmin.path').'/app/route/v1/route.php'; // 引入v1文件模块路由 require_once config('plugin.piadmin.piadmin.path').'/app/route/v1/attachment.php'; +// 引入v1代理端路由 +require_once config('plugin.piadmin.piadmin.path').'/app/route/v1/proxy.php'; // 处理404路由