Browse Source

feat(piadmin): 新增代理端菜单管理功能

feat(proxy):新增代理端用户登录及获取菜单功能
master
zhangf@suq.cn 5 days ago
parent
commit
28a7e9cc42
  1. 93
      plugin/piadmin/app/controller/v1/ProxyMenuController.php
  2. 30
      plugin/piadmin/app/controller/v1/ProxyUserLoginController.php
  3. 17
      plugin/piadmin/app/dao/ProxyMenuDao.php
  4. 17
      plugin/piadmin/app/dao/ProxyUserDao.php
  5. 82
      plugin/piadmin/app/middleware/ProxyUserAuthorizationMiddleware.php
  6. 16
      plugin/piadmin/app/model/ProxyMenu.php
  7. 21
      plugin/piadmin/app/model/ProxyUser.php
  8. 21
      plugin/piadmin/app/route/v1/proxy.php
  9. 13
      plugin/piadmin/app/route/v1/route.php
  10. 222
      plugin/piadmin/app/service/ProxyMenuService.php
  11. 130
      plugin/piadmin/app/service/ProxyUserService.php
  12. 6
      plugin/piadmin/config/piadmin.php
  13. 2
      plugin/piadmin/config/route.php

93
plugin/piadmin/app/controller/v1/ProxyMenuController.php

@ -0,0 +1,93 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\ProxyMenuService;
use plugin\piadmin\app\validate\MenuValidate;
use support\Request;
class ProxyMenuController extends BaseController
{
public function __construct()
{
parent::__construct();
$this->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());
}
}

30
plugin/piadmin/app/controller/v1/ProxyUserLoginController.php

@ -0,0 +1,30 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\ProxyUserService;
use plugin\piadmin\app\validate\LoginValidate;
use support\Request;
class ProxyUserLoginController extends BaseController
{
public function __construct()
{
parent::__construct();
$this->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);
}
}

17
plugin/piadmin/app/dao/ProxyMenuDao.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\ProxyMenu;
class ProxyMenuDao extends BaseDao
{
protected function setModel(): string
{
return ProxyMenu::class;
}
}

17
plugin/piadmin/app/dao/ProxyUserDao.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\ProxyUser;
class ProxyUserDao extends BaseDao
{
protected function setModel(): string
{
return ProxyUser::class;
}
}

82
plugin/piadmin/app/middleware/ProxyUserAuthorizationMiddleware.php

@ -0,0 +1,82 @@
<?php
namespace plugin\piadmin\app\middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use plugin\piadmin\app\dao\ProxyUserDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\CacheUtils;
use support\Context;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class ProxyUserAuthorizationMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$authHeader = config('plugin.piadmin.piadmin.jwt.proxy.auth_header');
$token = $request->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);
}
}
}

16
plugin/piadmin/app/model/ProxyMenu.php

@ -0,0 +1,16 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class ProxyMenu extends BaseModel
{
protected $table = 'pi_proxy_menu';
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
}

21
plugin/piadmin/app/model/ProxyUser.php

@ -0,0 +1,21 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class ProxyUser extends BaseModel
{
protected $table = 'pi_proxy_user';
protected $hidden = ['password'];
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
protected $append = [];
}

21
plugin/piadmin/app/route/v1/proxy.php

@ -0,0 +1,21 @@
<?php
use plugin\piadmin\app\controller\v1\ProxyMenuController;
use plugin\piadmin\app\controller\v1\ProxyUserLoginController;
use plugin\piadmin\app\middleware\ProxyUserAuthorizationMiddleware;
use Webman\Route;
// 不需要登录的接口
Route::group('/piadmin/v1/proxy', function () {
//账号密码登录
Route::post('/login', [ProxyUserLoginController::class, 'login'])->setParams(['perm' => ['userLogin']]);
});
// 需要登录的接口
Route::group('/piadmin/v1/proxy', function () {
// 获取当前用户菜单权限
Route::get('/getMenuPermissions', [ProxyMenuController::class, 'getProxyMenuPermissions'])->setParams(['perm' => ['getProxyMenuPermissions']]);
})->middleware([
ProxyUserAuthorizationMiddleware::class,
]);

13
plugin/piadmin/app/route/v1/route.php

@ -1,9 +1,9 @@
<?php
use plugin\piadmin\app\controller\v1\DataDictionaryController;
use plugin\piadmin\app\controller\v1\IndexController;
use plugin\piadmin\app\controller\v1\LoginController;
use plugin\piadmin\app\controller\v1\MenuController;
use plugin\piadmin\app\controller\v1\ProxyMenuController;
use plugin\piadmin\app\controller\v1\SystemAdminController;
use plugin\piadmin\app\controller\v1\SystemDeptController;
use plugin\piadmin\app\controller\v1\SystemRoleController;
@ -15,8 +15,6 @@ use Webman\Route;
// 不需要登录的接口
Route::group('/piadmin/v1', function () {
// Route::any('/test', [IndexController::class, 'test']);
// Route::any('/testlogin', [IndexController::class, 'index']);
//后台登录
Route::post('/login', [LoginController::class, 'login'])->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']]);

222
plugin/piadmin/app/service/ProxyMenuService.php

@ -0,0 +1,222 @@
<?php
namespace plugin\piadmin\app\service;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\dao\ProxyMenuDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\IdPathUtils;
use plugin\piadmin\app\utils\IdUtils;
use plugin\piadmin\app\utils\TreeUtils;
use support\Log;
use think\facade\Db;
class ProxyMenuService extends BaseService
{
public function __construct(ProxyMenuDao $dao)
{
$this->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)); // 重新索引并去重
}
}

130
plugin/piadmin/app/service/ProxyUserService.php

@ -0,0 +1,130 @@
<?php
namespace plugin\piadmin\app\service;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\dao\ProxyUserDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\CacheUtils;
use plugin\piadmin\app\utils\JwtUtils;
class ProxyUserService extends BaseService
{
public function __construct(ProxyUserDao $dao)
{
$this->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';
}
}

6
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'
]
],

2
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路由

Loading…
Cancel
Save