Files
MrleeAdmin/backend/app/service/admin/system/MenuService.php
T
2026-08-31 11:33:59 +08:00

374 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace app\service\admin\system;
use app\dao\system\MenuDao;
use app\model\system\Menu;
use core\base\BaseService;
use core\exception\handler\AdminException;
use madong\helper\Arr;
use madong\helper\Tree;
use support\Container;
/**
* @method save(array $data)
* @method selectModel(array $where, array|string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false, ?array $withoutScopes = null)
*/
class MenuService extends BaseService
{
/**
* 缓存key
*/
const CACHE_KEY = 'menu_list';
/**
* 所有权限
*/
const CACHE_ALL_AUTHS_DATA = 'all_auths_data';
/**
* 所有菜单
*/
const CACHE_ALL_MENUS_DATA = 'all_menus_data';
public function __construct(MenuDao $dao)
{
$this->dao = $dao;
}
/**
* 输出所有权限
*
* @param array $menuIds
*
* @return array
* @throws \Exception
*/
public function getAllAuth(array $menuIds = []): array
{
// 1. 优先从缓存获取所有权限数据(自动处理缓存未命中时的数据库查询)
$allAuthItems = $this->cacheDriver()->remember(
self::CACHE_ALL_AUTHS_DATA,
function () {
// 缓存未命中时,从数据库获取数据(仅查询需要的字段)
return $this->dao->getModel()
->where('path', '<>', '')
->where('type', '=', 4)
->get(['id', 'path', 'methods'])
->toArray();
}
);
// 2. 如果传入了 $menuIds,则筛选出对应的权限项
if (!empty($menuIds)) {
$allAuthItems = array_filter($allAuthItems, function ($item) use ($menuIds) {
return in_array($item['id'], $menuIds);
});
}
// 3. 格式化输出(复用现有方法)
return $this->formatAuthDataFromItems($allAuthItems);
}
/**
* 根据授权项数组格式化输出为 ['METHOD' => ['/path1', '/path2'], ...] 的形式。
*
* @param array $allAuthItems 授权项数组,每个元素包含 'id', 'path', 'methods'
*
* @return array 格式化后的授权数据
*/
private function formatAuthDataFromItems(array $allAuthItems): array
{
$allAuth = [];
foreach ($allAuthItems as $item) {
// 假设 'methods' 是逗号分隔的字符串,如 "GET,POST"
$methodArray = explode(',', $item['methods']); // 转换为数组
$pathKey = strtolower(trim(str_replace(' ', '', $item['path'])));
foreach ($methodArray as $method) {
$methodKey = strtolower(trim($method));
$allAuth[$methodKey][] = $pathKey;
}
}
// 如果需要,可以对每个方法的路径数组进行去重
foreach ($allAuth as &$paths) {
$paths = array_unique($paths);
}
unset($paths); // 解除引用
return $allAuth;
}
/**
* 获取所有菜单,优先从缓存读取,并根据 $where 条件筛选。
*
* @param array $where 筛选条件数组,键为字段名,值为条件值或数组(用于 IN 条件)或操作符数组(如 ['>', 5])
*
* @return array 处理后的菜单树形结构
*/
// public function getAllMenus(array $where = [], $mode = 'menu', $isTree = true): array
// {
// // 1. 优先从缓存获取所有菜单数据
// $allMenus = $this->cacheDriver()->remember(
// self::CACHE_ALL_MENUS_DATA, // 缓存键名
// function () {
// $list = $this->dao->getModel()
// ->where('enabled', 1)
//// ->whereIn('type', [1, 2])// 目录 & 菜单类型 全量保存缓存条件放到缓存输出
// ->orderBy('sort', 'asc')
// ->get();
// foreach ($list as $item) {
// $item->set('name', $item->code);
// $item->set('meta', SysMenu::getMetaAttribute($item));
// }
// $list->makeVisible(['id', 'pid', 'type', 'sort', 'redirect', 'path', 'name', 'meta', 'component']);
// return $list->toArray();
// }
// );
//
// // 2. 根据 $where 条件筛选菜单数据
// $menusToProcess = Arr::filterByWhere($allMenus, $where);
//
// if ($mode == 'code') {
// return array_column($menusToProcess, 'code');
// }
// if (!$isTree) {
// return $menusToProcess;
// }
//
// // 构建树形结构
// return $this->buildMenuTree($menusToProcess);
//
// }
public function getAllMenus(array $where = [], $mode = 'menu', $isTree = true): array
{
// 1. 优先从缓存获取所有菜单数据
$allMenus = $this->cacheDriver()->remember(
self::CACHE_ALL_MENUS_DATA, // 缓存键名
function () {
return $this->dao->getModel()
->where('enabled', 1)
// ->whereIn('type', [1, 2])// 目录 & 菜单类型 全量保存缓存条件放到缓存输出
->orderBy('sort', 'asc')
->get();
// foreach ($list as $item) {
// $item->set('name', $item->code);
// $item->set('meta', SysMenu::getMetaAttribute($item));
// }
// $list->makeVisible(['id', 'pid', 'type', 'sort', 'redirect', 'path', 'name', 'meta', 'component']);
// return $list->toArray();
}
);
// 2. 根据 $where 条件筛选菜单数据
$menusToProcess = Arr::filterByWhere($allMenus, $where);
if ($mode == 'code') {
return array_column($menusToProcess, 'code');
}
if (!$isTree) {
return $menusToProcess;
}
// 构建树形结构
return $this->buildMenuTree($menusToProcess);
}
/**
* 构建菜单的树形结构。
*
* @param array $formattedMenus 已处理的菜单数据
*
* @return array 树形结构的菜单数据
*/
protected function buildMenuTree(array $formattedMenus): array
{
$tree = new Tree($formattedMenus);
return $tree->getTree();
}
/*** 重写方法***/
/**
* 重写更新方法:统一事务管理菜单更新、缓存清理、Casbin同步
*
* @param int|string $id 菜单ID
* @param array $data 更新数据
*
* @return \app\model\system\Menu 更新后的模型
* @throws \Throwable
* @throws \core\exception\handler\AdminException
*/
public function update(int|string $id, array $data): \app\model\system\Menu
{
return $this->transaction(function () use ($id, $data) {
// 1. 获取菜单模型
$model = $this->get($id);
if (!$model) {
throw new AdminException("菜单ID:{$id}不存在");
}
// 2. 更新菜单数据(使用属性复制或直接赋值)
$model->fill($data);
$model->save();
// 3. 清理缓存(原控制器中的缓存删除逻辑迁移至此)
$this->cacheDriver()->delete(self::CACHE_ALL_AUTHS_DATA);
$this->cacheDriver()->delete(self::CACHE_ALL_MENUS_DATA);
// 4. 同步Casbin策略(复用已实现的同步方法)
$this->syncPermissionCacheAfterUpdate($id);
return $model;
});
}
/**
* 批量删除菜单(含子菜单)并同步删除Casbin策略
*
* @param array $data 菜单ID数组或逗号分隔字符串
*
* @return array 被删除的所有菜单ID
* @throws \Throwable
* @throws \core\exception\handler\AdminException
*/
public function batchDelete(array $data = []): array
{
return $this->transaction(function () use ($data) {
$deletedIds = [];
foreach ($data as $id) {
/** @var Menu $item */
$item = $this->get($id);
if (!$item) {
continue;
}
// 删除菜单及子菜单,获取所有被删除的ID
$ids = $item->deleteWithAllChildren();
$deletedIds = array_merge($deletedIds, $ids);
}
// 清理角色菜单中间表关联数据
if (!empty($deletedIds)) {
/** @var RoleMenuService $roleMenuService */
$roleMenuService = Container::make(RoleMenuService::class);
/** @var RoleMenuModel $roleMenuModel */
$roleMenuModel = $roleMenuService->getModel();
$roleMenuModel->whereIn('menu_id', $deletedIds)->delete();
// 清理相关用户的权限缓存
$this->clearUserPermissionCacheByMenus($deletedIds);
}
return array_unique($deletedIds);
});
}
/**
* 菜单更新后清理相关用户权限缓存
*
* @param int|string $menuId 被更新的菜单ID
*
* @throws \core\exception\handler\AdminException
*/
private function syncPermissionCacheAfterUpdate(int|string $menuId): void
{
try {
// 1. 查询所有关联此菜单的角色ID(通过角色菜单服务)
/** @var RoleMenuService $roleMenuService */
$roleMenuService = Container::make(RoleMenuService::class);
$roleIds = $roleMenuService->getColumn(
['menu_id' => $menuId],
'role_id'
);
if (empty($roleIds)) {
return; // 无关联角色,无需同步
}
// 2. 清理拥有这些角色的用户的权限缓存
foreach ($roleIds as $roleId) {
/** @var \app\service\admin\system\AdminRoleService $adminRoleService */
$adminRoleService = Container::make(\app\service\admin\system\AdminRoleService::class);
// 获取拥有此角色的用户ID列表
$userIds = \app\model\system\AdminRole::where('role_id', $roleId)
->pluck('admin_id')
->toArray();
if (!empty($userIds)) {
// 清理这些用户的权限缓存
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
foreach ($userIds as $userId) {
$currentUser->clearCache($userId);
}
}
}
} catch (\Throwable $e) {
throw new AdminException($e->getMessage());
}
}
/**
* 清理与指定菜单相关的用户权限缓存
*
* @param array $menuIds 菜单ID数组
*/
private function clearUserPermissionCacheByMenus(array $menuIds): void
{
try {
if (empty($menuIds)) {
return;
}
// 获取拥有这些菜单的角色ID
/** @var RoleMenuService $roleMenuService */
$roleMenuService = Container::make(RoleMenuService::class);
/** @var RoleMenuModel $roleMenuModel */
$roleMenuModel = $roleMenuService->getModel();
$roleIds = $roleMenuModel->whereIn('menu_id', $menuIds)
->pluck('role_id')
->unique()
->toArray();
if (!empty($roleIds)) {
// 获取拥有这些角色的用户ID
$userIds = \app\model\system\AdminRole::whereIn('role_id', $roleIds)
->pluck('admin_id')
->unique()
->toArray();
if (!empty($userIds)) {
// 清理这些用户的权限缓存
$currentUser = Container::make(\app\adminapi\CurrentUser::class);
foreach ($userIds as $userId) {
$currentUser->clearCache($userId);
}
}
}
} catch (\Throwable $e) {
// 缓存清理失败不影响主要业务,记录日志即可
\core\logger\Logger::error("清理菜单相关用户权限缓存失败: " . $e->getMessage());
}
}
}