diff --git a/app/Http/Admin/Controller/Permission/DepartmentController.php b/app/Http/Admin/Controller/Permission/DepartmentController.php index 1bb98a61..47fd80ab 100644 --- a/app/Http/Admin/Controller/Permission/DepartmentController.php +++ b/app/Http/Admin/Controller/Permission/DepartmentController.php @@ -56,7 +56,7 @@ public function __construct( public function pageList(): Result { return $this->success([ - 'list' => $this->service->getList($this->getRequestData()) + 'list' => $this->service->getList($this->getRequestData()), ]); } diff --git a/app/Http/Admin/Controller/Permission/PositionController.php b/app/Http/Admin/Controller/Permission/PositionController.php index 002835e4..613d5676 100644 --- a/app/Http/Admin/Controller/Permission/PositionController.php +++ b/app/Http/Admin/Controller/Permission/PositionController.php @@ -118,4 +118,4 @@ public function delete(): Result $this->service->deleteById($this->getRequestData()); return $this->success(); } -} \ No newline at end of file +} diff --git a/app/Http/Admin/Request/Permission/PositionRequest.php b/app/Http/Admin/Request/Permission/PositionRequest.php index 6a059eb5..e40b6b69 100644 --- a/app/Http/Admin/Request/Permission/PositionRequest.php +++ b/app/Http/Admin/Request/Permission/PositionRequest.php @@ -55,4 +55,4 @@ public function attributes(): array 'dept_id' => '部门ID', ]; } -} \ No newline at end of file +} diff --git a/app/Library/DataPermission/Aspects/DataScopeAspect.php b/app/Library/DataPermission/Aspects/DataScopeAspect.php index 45730542..998b01ed 100644 --- a/app/Library/DataPermission/Aspects/DataScopeAspect.php +++ b/app/Library/DataPermission/Aspects/DataScopeAspect.php @@ -1,8 +1,19 @@ getAnnotationMetadata()->class[DataScope::class]) || - isset($proceedingJoinPoint->getAnnotationMetadata()->method[DataScope::class]) - ){ + isset($proceedingJoinPoint->getAnnotationMetadata()->class[DataScope::class]) + || isset($proceedingJoinPoint->getAnnotationMetadata()->method[DataScope::class]) + ) { return $this->handleDataScope($proceedingJoinPoint); } - if ($proceedingJoinPoint->className === Builder::class){ - if ($proceedingJoinPoint->methodName==='runSelect'){ + if ($proceedingJoinPoint->className === Builder::class) { + if ($proceedingJoinPoint->methodName === 'runSelect') { return $this->handleSelect($proceedingJoinPoint); } - if ($proceedingJoinPoint->methodName==='delete'){ + if ($proceedingJoinPoint->methodName === 'delete') { return $this->handleDelete($proceedingJoinPoint); } - if ($proceedingJoinPoint->methodName==='update'){ - return $this->handleUpdate($proceedingJoinPoint); + if ($proceedingJoinPoint->methodName === 'update') { + return $this->handleUpdate($proceedingJoinPoint); } } return $proceedingJoinPoint->process(); @@ -64,18 +74,27 @@ protected function handleSelect(ProceedingJoinPoint $proceedingJoinPoint) * @var Builder $builder */ $builder = $proceedingJoinPoint->getInstance(); - if (Context::has(self::CONTEXT_KEY)){ + if (Context::has(self::CONTEXT_KEY)) { // todo 做数据权限处理 } return $proceedingJoinPoint->process(); } - protected function handleDataScope(ProceedingJoinPoint $proceedingJoinPoint) { Context::set(self::CONTEXT_KEY, 1); - $result = $proceedingJoinPoint->process(); + /** + * @var DataScope $attribute + */ + $attribute = $proceedingJoinPoint->getAnnotationMetadata()->class[DataScope::class]; + if ($attribute === null) { + $attribute = $proceedingJoinPoint->getAnnotationMetadata()->method[DataScope::class]; + } + DataPermissionContext::setDeptColumn($attribute->getDeptColumn()); + DataPermissionContext::setCreatedByColumn($attribute->getCreatedByColumn()); + DataPermissionContext::setScopeType($attribute->getScopeType()); + $result = $proceedingJoinPoint->process(); Context::destroy(self::CONTEXT_KEY); return $result; } -} \ No newline at end of file +} diff --git a/app/Library/DataPermission/Attribute/DataScope.php b/app/Library/DataPermission/Attribute/DataScope.php index 48f52751..9d239677 100644 --- a/app/Library/DataPermission/Attribute/DataScope.php +++ b/app/Library/DataPermission/Attribute/DataScope.php @@ -1,12 +1,41 @@ deptColumn; + } + + public function getCreatedByColumn(): string + { + return $this->createdByColumn; + } -} \ No newline at end of file + public function getScopeType(): ScopeType + { + return $this->scopeType; + } +} diff --git a/app/Library/DataPermission/Context.php b/app/Library/DataPermission/Context.php new file mode 100644 index 00000000..955bcf5e --- /dev/null +++ b/app/Library/DataPermission/Context.php @@ -0,0 +1,54 @@ +isSuperAdmin()) { + return; + } + if (($policy = $user->getPolicy()) === null) { + return; + } + $scopeType = Context::getScopeType(); + switch ($scopeType) { + case ScopeType::CREATED_BY: + self::handleCreatedBy($user, $policy, $builder); + break; + case ScopeType::DEPT: + self::handleDept($user, $policy, $builder); + break; + case ScopeType::DEPT_CREATED_BY: + self::handleDeptCreatedBy($user, $policy, $builder); + break; + case ScopeType::DEPT_OR_CREATED_BY: + self::handleDeptOrCreatedBy($user, $policy, $builder); + break; + } + } + + private function handleCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $createdByList = $this->rule->getCreatedByList($user, $policy); + $builder->whereIn(Context::getCreatedByColumn(), $createdByList); + } + + private function handleDept(User $user, Policy $policy, Builder $builder): void + { + $deptList = $this->rule->getDeptIds($user, $policy); + $builder->whereIn(Context::getDeptColumn(), $deptList); + } + + private function handleDeptCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $createdByList = $this->rule->getCreatedByList($user, $policy); + $deptList = $this->rule->getDeptIds($user, $policy); + $builder->whereIn(Context::getCreatedByColumn(), $createdByList) + ->whereIn(Context::getDeptColumn(), $deptList); + } + + private function handleDeptOrCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $createdByList = $this->rule->getCreatedByList($user, $policy); + $deptList = $this->rule->getDeptIds($user, $policy); + $builder->where(static function (Builder $query) use ($createdByList, $deptList) { + $query->whereIn(Context::getCreatedByColumn(), $createdByList) + ->orWhereIn(Context::getDeptColumn(), $deptList); + }); + } +} diff --git a/app/Library/DataPermission/Manager.php b/app/Library/DataPermission/Manager.php index 8ba96c6a..a04ab05d 100644 --- a/app/Library/DataPermission/Manager.php +++ b/app/Library/DataPermission/Manager.php @@ -1,8 +1,15 @@ + */ + private static array $customFunc = []; + + public static function registerCustomFunc(string $name, \Closure $func): void + { + self::$customFunc[$name] = $func; + } + + public static function getCustomFunc(string $name): \Closure + { + if (isset(self::$customFunc[$name])) { + return self::$customFunc[$name]; + } + throw new \RuntimeException('Custom func not found'); + } +} diff --git a/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php b/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php new file mode 100644 index 00000000..7e9648b4 --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php @@ -0,0 +1,66 @@ + $className + * @return T + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function getRepository(string $className): mixed + { + return ApplicationContext::getContainer()->get($className); + } + + protected function getUser(): User + { + return $this->user; + } + + protected function getPolicy(): Policy + { + return $this->policy; + } + + /** + * @return Collection + */ + protected function loadCustomFunc(Policy $policy): Collection + { + $result = CustomFuncFactory::getCustomFunc($policy->value[0])->call($this, $this->getUser(), $this->getPolicy()); + if ($result instanceof Collection) { + return $result; + } + throw new \RuntimeException('Custom func must return Collection'); + } +} diff --git a/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php b/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php new file mode 100644 index 00000000..aca5b779 --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php @@ -0,0 +1,125 @@ +getPolicy(); + $policyType = $policy->policy_type; + if ($policyType->isAll()) { + return null; + } + /* + * @var Department[]|Collection $departmentList + */ + if ($policyType->isCustomDept()) { + $departmentList = $this->getRepository(DepartmentRepository::class)->getByIds($policy->value); + } + if ($policyType->isDeptSelf()) { + $departmentList = $this->getUser()->department()->get(); + } + if ($policyType->isDeptTree()) { + $departmentList = collect(); + /** + * @var Collection|Department[] $currentList + */ + $currentList = $this->getUser()->department()->get(); + foreach ($currentList as $item) { + $departmentList->merge($item->getFlatChildren()); + } + } + if ($policyType->isCustomFunc()) { + $departmentList = $this->loadCustomFunc($policy); + } + $ids = [ + $this->getUser()->id, + ]; + foreach ($departmentList as $department) { + if ($policyType->isSelf()) { + break; + } + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->whereIn('id', $department->id); + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + } + if ($policyType->isNotCustomFunc() && $policyType->isNotCustomDept()) { + /** + * @var Collection|Department[] $leaderDepartmentList + */ + $leaderDepartmentList = $this->getUser()->department()->get(); + foreach ($leaderDepartmentList as $department) { + if ($policyType->isSelf() || $policyType->isDeptSelf()) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->whereIn('id', $department->id); + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + } + if ($policyType->isDeptTree()) { + $department->getFlatChildren()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->whereIn('id', $department->id); + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + } + } + /** + * @var Collection|Position[] $positionList + */ + $positionList = $this->getUser()->position()->get(); + foreach ($positionList as $position) { + if ($policyType->isSelf()) { + break; + } + if ($policyType->isDeptSelf()) { + $position->department()->get()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->whereIn('id', $department->id); + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + } + if ($policyType->isDeptTree()) { + $position->department()->get()->each(function (Department $department) use (&$ids) { + $department->getFlatChildren()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->whereIn('id', $department->id); + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + }); + } + } + } + return $ids; + } +} diff --git a/app/Library/DataPermission/Rule/Executable/DeptExecute.php b/app/Library/DataPermission/Rule/Executable/DeptExecute.php new file mode 100644 index 00000000..fa1e7b9a --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/DeptExecute.php @@ -0,0 +1,61 @@ +getPolicy(); + $policyType = $policy->policy_type; + if ($policyType->isAll()) { + return null; + } + /* + * @var Department[]|Collection $departmentList + */ + if ($policyType->isCustomDept()) { + $departmentList = $this->getRepository(DepartmentRepository::class)->getByIds($policy->value); + } + if ($policyType->isCustomFunc()) { + $departmentList = $this->loadCustomFunc($policy); + } + if ($policyType->isDeptSelf() || $policyType->isSelf()) { + $departmentList = $this->getUser()->department()->get(); + } + if ($policyType->isDeptTree()) { + $departmentList = collect(); + /** + * @var Collection|Department[] $currentList + */ + $currentList = $this->getUser()->department()->get(); + foreach ($currentList as $item) { + $departmentList->merge($item->getFlatChildren()); + } + } + $departmentList->merge($this->getUser()->dept_leader()->get()); + /** + * @var Collection|Position[] $positionList + */ + $positionList = $this->getUser()->position()->get(); + foreach ($positionList as $position) { + $departmentList->merge($position->department()->get()); + } + return $departmentList->pluck('id')->toArray(); + } +} diff --git a/app/Library/DataPermission/Rule/Rule.php b/app/Library/DataPermission/Rule/Rule.php new file mode 100644 index 00000000..e4f7540d --- /dev/null +++ b/app/Library/DataPermission/Rule/Rule.php @@ -0,0 +1,72 @@ +cache = $this->cacheManager->getDriver($this->config->get('department.cache.driver')); + } + + public function isCache(): bool + { + return (bool) $this->config->get('department.cache.enable'); + } + + public function getTtl(): mixed + { + return $this->config->get('department.cache.ttl'); + } + + public function getPrefix(): string + { + return $this->config->get('department.cache.prefix'); + } + + public function getDeptIds(User $user, Policy $policy): array + { + $cacheKey = $this->getPrefix() . ':deptIds:' . $user->id; + if ($this->isCache() && $this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + $execute = new DeptExecute($user, $policy); + $result = $execute->execute(); + $this->cache->set($cacheKey, $result, $this->getTtl()); + return $result; + } + + public function getCreatedByList(User $user, Policy $policy): array + { + $cacheKey = $this->getPrefix() . ':createdBy:' . $user->id; + if ($this->isCache() && $this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + $execute = new CreatedByIdsExecute($user, $policy); + $result = $execute->execute(); + $this->cache->set($cacheKey, $result, $this->getTtl()); + return $result; + } +} diff --git a/app/Library/DataPermission/Scope/DataScope.php b/app/Library/DataPermission/Scope/DataScope.php deleted file mode 100644 index 5d2ecd8a..00000000 --- a/app/Library/DataPermission/Scope/DataScope.php +++ /dev/null @@ -1,26 +0,0 @@ -currentUser->user() === null){ - return; - } - $user = $this->currentUser->user(); - if ($user->isSuperAdmin()){ - return; - } - } -} \ No newline at end of file diff --git a/app/Library/DataPermission/ScopeType.php b/app/Library/DataPermission/ScopeType.php new file mode 100644 index 00000000..61b87398 --- /dev/null +++ b/app/Library/DataPermission/ScopeType.php @@ -0,0 +1,28 @@ +|Position[] $positions 岗位 * @property Collection|User[] $department_users 部门用户 * @property Collection|User[] $leader 部门领导 + * @property Collection|Department[] $children 子部门 */ class Department extends Model { @@ -74,6 +76,22 @@ public function leader(): BelongsToMany public function children(): HasMany { - return $this->hasMany(self::class, 'parent_id', 'id'); + return $this->hasMany(self::class, 'parent_id', 'id')->with('children'); + } + + public function getFlatChildren(): BaseCollection + { + $flat = collect(); + $this->load('children'); // 预加载子部门 + $traverse = static function ($departments) use (&$traverse, $flat) { + foreach ($departments as $department) { + $flat->push($department); + if ($department->children->isNotEmpty()) { + $traverse($department->children); + } + } + }; + $traverse($this->children); + return $flat->prepend($this); // 包含自身 } } diff --git a/app/Model/Permission/User.php b/app/Model/Permission/User.php index fa5f2ebc..4cb9106b 100644 --- a/app/Model/Permission/User.php +++ b/app/Model/Permission/User.php @@ -164,26 +164,24 @@ public function position(): BelongsToMany return $this->belongsToMany(Position::class, 'user_position', 'user_id', 'position_id'); } - public function getPolicy(): Policy|null + public function getPolicy(): ?Policy { /** - * @var Policy|null $policy + * @var null|Policy $policy */ $policy = $this->policy()->first(); - if (!empty($policy)){ + if (! empty($policy)) { return $policy; } $this->load('roles'); $roleList = $this->roles; - foreach ($roleList as $role){ + foreach ($roleList as $role) { $current = $role->policy()->first(); - if (!empty($current)){ + if (! empty($current)) { return $current; } } - return null; - } } diff --git a/app/Repository/Permission/DepartmentRepository.php b/app/Repository/Permission/DepartmentRepository.php index bdf30302..83368ce3 100644 --- a/app/Repository/Permission/DepartmentRepository.php +++ b/app/Repository/Permission/DepartmentRepository.php @@ -14,6 +14,8 @@ use App\Model\Permission\Department; use App\Repository\IRepository; +use Hyperf\Collection\Arr; +use Hyperf\Collection\Collection; use Hyperf\Database\Model\Builder; /** @@ -25,9 +27,17 @@ public function __construct( protected readonly Department $model ) {} + public function getByIds(array $ids): Collection + { + return $this->model->whereIn('id', $ids)->get(); + } + public function handleSearch(Builder $query, array $params): Builder { return $query + ->when(isset($params['id']), static function (Builder $query) use ($params) { + $query->whereIn('id', Arr::wrap($params['id'])); + }) ->when(isset($params['name']), static function (Builder $query) use ($params) { $query->where('name', 'like', '%' . $params['name'] . '%'); }) diff --git a/app/Repository/Permission/PositionRepository.php b/app/Repository/Permission/PositionRepository.php index 2b0cd4ae..71a6b87d 100644 --- a/app/Repository/Permission/PositionRepository.php +++ b/app/Repository/Permission/PositionRepository.php @@ -42,4 +42,4 @@ public function handleSearch(Builder $query, array $params): Builder }) ->with(['department']); } -} \ No newline at end of file +} diff --git a/app/Schema/PositionSchema.php b/app/Schema/PositionSchema.php index 668a2699..de178c63 100644 --- a/app/Schema/PositionSchema.php +++ b/app/Schema/PositionSchema.php @@ -12,7 +12,6 @@ namespace App\Schema; -use Hyperf\Swagger\Annotation\Items; use Hyperf\Swagger\Annotation\Property; use Hyperf\Swagger\Annotation\Schema; @@ -47,4 +46,4 @@ public function jsonSerialize(): mixed { return []; } -} \ No newline at end of file +} diff --git a/app/Service/Permission/PositionService.php b/app/Service/Permission/PositionService.php index f975cecc..1f6d46b9 100644 --- a/app/Service/Permission/PositionService.php +++ b/app/Service/Permission/PositionService.php @@ -24,4 +24,4 @@ class PositionService extends IService public function __construct( protected readonly PositionRepository $repository ) {} -} \ No newline at end of file +} diff --git a/config/autoload/casbin/rbac-model.conf b/config/autoload/casbin/rbac-model.conf deleted file mode 100644 index 7854845e..00000000 --- a/config/autoload/casbin/rbac-model.conf +++ /dev/null @@ -1,14 +0,0 @@ -[request_definition] -r = sub, obj, act - -[policy_definition] -p = sub, obj, act - -[role_definition] -g = _, _ - -[policy_effect] -e = some(where (p.eft == allow)) - -[matchers] -m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "SuperAdmin" \ No newline at end of file diff --git a/config/autoload/department/cache.php b/config/autoload/department/cache.php new file mode 100644 index 00000000..16abc2b3 --- /dev/null +++ b/config/autoload/department/cache.php @@ -0,0 +1,21 @@ + env('DATA_PERMISSION_CACHE_ENABLE',false), + /** + * 缓存时间,单位秒 + */ + 'ttl' => env('DATA_PERMISSION_CACHE_TTL',60 * 5), + /** + * 缓存前缀 + */ + 'prefix' => env('DATA_PERMISSION_CACHE_PREFIX','data_permission'), + /** + * 缓存驱动 + */ + 'driver' => env('DATA_PERMISSION_CACHE_DRIVER','default'), +]; \ No newline at end of file diff --git a/web/src/modules/base/locales/en[English].yaml b/web/src/modules/base/locales/en[English].yaml index 550dd048..eb1c3a53 100644 --- a/web/src/modules/base/locales/en[English].yaml +++ b/web/src/modules/base/locales/en[English].yaml @@ -8,7 +8,7 @@ baseUserManage: userType: User type role: Role signed: Signed - mainTitle: User Manager + mainTitle: User Rule subTitle: Provide users with the functions of adding, editing, and deleting setRole: Set role setRoleSuccess: The role was set successfully @@ -16,7 +16,7 @@ baseUserManage: setPassword: Whether to reset the password to 123456? setPasswordSuccess: The password was reset successfully baseRoleManage: - mainTitle: Role Manager + mainTitle: Role Rule subTitle: Provide user roles and permission settings name: Role name code: Role code