diff --git a/README-en.md b/README-en.md
index 59a1cba7f..f88f6576f 100644
--- a/README-en.md
+++ b/README-en.md
@@ -1,5 +1,5 @@
-[Chinese](./README.md) | English
-# Project Introduction
+[中文](./README.md) | English
+# Projects
diff --git a/README.md b/README.md
index 3f327d0ce..5effcb547 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,9 @@ PHP有很多优秀的后台管理系统,但基于Swoole的后台管理系统
如果觉着还不错的话,就请点个 ⭐star 支持一下吧,这将是对我最大的支持和鼓励!
在使用 MineAdmin 前请认真阅读[《免责声明》](https://doc.mineadmin.com/guide/start/declaration.html)并同意该声明。
-## 部门岗位分支(包含数据权限功能)
-当前默认分支不带部门、岗位、数据权限等功能,如需使用这些功能请移步 [`【包含部门岗位的MineAdmin----master-department】`](https://github.com/mineadmin/MineAdmin/tree/master-department) 分支代码下载
+## 默认主分支(不包含部门、岗位、数据权限功能)
+当前分支带有部门、岗位、数据权限等功能,如果不需要使用这些功能请移步 [`【主分支】`](https://github.com/mineadmin/MineAdmin) 下载代码
+
## 官方交流群
> QQ群用于交流学习,请勿水群
@@ -41,7 +42,10 @@ PHP有很多优秀的后台管理系统,但基于Swoole的后台管理系统
4. 操作日志,用户对系统的一些正常操作的查询
5. 登录日志,用户登录系统的记录查询
6. 附件管理,管理当前系统上传的文件及图片等信息
-7. 应用市场,可下载各种基础应用、插件、前端组件等等
+7. 部门管理,可以管理组织架构
+8. 岗位管理,在部门内管理,可以为部门设置岗位,再为用户分配岗位
+9. 数据权限,数据权限功能跟随岗位而设置,同时,也可以对用户单独设置数据权限,使岗位的数据权限失效。
+10. 应用市场,可下载各种基础应用、插件、前端组件等等
## 环境需求
diff --git a/app/Http/Admin/Controller/Permission/DepartmentController.php b/app/Http/Admin/Controller/Permission/DepartmentController.php
new file mode 100644
index 000000000..47fd80ab1
--- /dev/null
+++ b/app/Http/Admin/Controller/Permission/DepartmentController.php
@@ -0,0 +1,117 @@
+ [], 'ApiKey' => []]],
+ tags: ['部门管理'],
+ )]
+ #[PageResponse(instance: DepartmentSchema::class)]
+ #[Permission(code: 'permission:department:index')]
+ public function pageList(): Result
+ {
+ return $this->success([
+ 'list' => $this->service->getList($this->getRequestData()),
+ ]);
+ }
+
+ #[Post(
+ path: '/admin/department',
+ operationId: 'departmentCreate',
+ summary: '创建部门',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['部门管理'],
+ )]
+ #[RequestBody(
+ content: new JsonContent(ref: DepartmentRequest::class)
+ )]
+ #[Permission(code: 'permission:department:save')]
+ #[ResultResponse(instance: new Result())]
+ public function create(DepartmentRequest $request): Result
+ {
+ $this->service->create(array_merge($request->validated(), [
+ 'created_by' => $this->currentUser->id(),
+ ]));
+ return $this->success();
+ }
+
+ #[Put(
+ path: '/admin/department/{id}',
+ operationId: 'departmentSave',
+ summary: '保存部门',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['部门管理'],
+ )]
+ #[RequestBody(
+ content: new JsonContent(ref: DepartmentRequest::class)
+ )]
+ #[Permission(code: 'permission:department:update')]
+ #[ResultResponse(instance: new Result())]
+ public function save(int $id, DepartmentRequest $request): Result
+ {
+ $this->service->updateById($id, array_merge($request->validated(), [
+ 'updated_by' => $this->currentUser->id(),
+ ]));
+ return $this->success();
+ }
+
+ #[Delete(
+ path: '/admin/department',
+ operationId: 'departmentDelete',
+ summary: '删除部门',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['部门管理'],
+ )]
+ #[ResultResponse(instance: new Result())]
+ #[Permission(code: 'permission:department:delete')]
+ public function delete(): Result
+ {
+ $this->service->deleteById($this->getRequestData());
+ return $this->success();
+ }
+}
diff --git a/app/Http/Admin/Controller/Permission/LeaderController.php b/app/Http/Admin/Controller/Permission/LeaderController.php
new file mode 100644
index 000000000..e5e39be5a
--- /dev/null
+++ b/app/Http/Admin/Controller/Permission/LeaderController.php
@@ -0,0 +1,100 @@
+ [], 'ApiKey' => []]],
+ tags: ['部门领导管理'],
+ )]
+ #[PageResponse(instance: LeaderSchema::class)]
+ #[Permission(code: 'permission:leader:index')]
+ public function pageList(): Result
+ {
+ return $this->success(
+ $this->service->page(
+ $this->getRequestData(),
+ $this->getCurrentPage(),
+ $this->getPageSize()
+ )
+ );
+ }
+
+ #[Post(
+ path: '/admin/leader',
+ operationId: 'leaderCreate',
+ summary: '部门领导岗位',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['部门领导管理'],
+ )]
+ #[RequestBody(
+ content: new JsonContent(ref: LeaderRequest::class)
+ )]
+ #[Permission(code: 'permission:leader:save')]
+ #[ResultResponse(instance: new Result())]
+ public function create(LeaderRequest $request): Result
+ {
+ $this->service->create(array_merge($request->validated(), [
+ 'created_by' => $this->currentUser->id(),
+ ]));
+ return $this->success();
+ }
+
+ #[Delete(
+ path: '/admin/leader',
+ operationId: 'leaderDelete',
+ summary: '删除部门领导',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['部门领导管理'],
+ )]
+ #[ResultResponse(instance: new Result())]
+ #[Permission(code: 'permission:leader:delete')]
+ public function delete(): Result
+ {
+ $this->service->deleteByDoubleKey($this->getRequestData());
+ return $this->success();
+ }
+}
diff --git a/app/Http/Admin/Controller/Permission/PositionController.php b/app/Http/Admin/Controller/Permission/PositionController.php
new file mode 100644
index 000000000..5682d64d4
--- /dev/null
+++ b/app/Http/Admin/Controller/Permission/PositionController.php
@@ -0,0 +1,137 @@
+ [], 'ApiKey' => []]],
+ tags: ['岗位管理'],
+ )]
+ #[PageResponse(instance: PositionSchema::class)]
+ #[Permission(code: 'permission:position:index')]
+ public function pageList(): Result
+ {
+ return $this->success(
+ $this->service->page(
+ $this->getRequestData(),
+ $this->getCurrentPage(),
+ $this->getPageSize()
+ )
+ );
+ }
+
+ #[Put(
+ path: '/admin/position/{id}/data_permission',
+ operationId: 'positionDataPermission',
+ summary: '设置岗位数据权限',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['岗位管理'],
+ )]
+ #[Permission(code: 'permission:position:data_permission')]
+ #[ResultResponse(instance: new Result())]
+ public function batchDataPermission(int $id, BatchGrantDataPermissionForPositionRequest $request): Result
+ {
+ $this->service->batchDataPermission($id, $request->validated());
+ return $this->success();
+ }
+
+ #[Post(
+ path: '/admin/position',
+ operationId: 'positionCreate',
+ summary: '创建岗位',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['岗位管理'],
+ )]
+ #[RequestBody(
+ content: new JsonContent(ref: PositionRequest::class)
+ )]
+ #[Permission(code: 'permission:position:save')]
+ #[ResultResponse(instance: new Result())]
+ public function create(PositionRequest $request): Result
+ {
+ $this->service->create(array_merge($request->validated(), [
+ 'created_by' => $this->currentUser->id(),
+ ]));
+ return $this->success();
+ }
+
+ #[Put(
+ path: '/admin/position/{id}',
+ operationId: 'positionSave',
+ summary: '保存岗位',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['岗位管理'],
+ )]
+ #[RequestBody(
+ content: new JsonContent(ref: PositionRequest::class)
+ )]
+ #[Permission(code: 'permission:position:update')]
+ #[ResultResponse(instance: new Result())]
+ public function save(int $id, PositionRequest $request): Result
+ {
+ $this->service->updateById($id, array_merge($request->validated(), [
+ 'updated_by' => $this->currentUser->id(),
+ ]));
+ return $this->success();
+ }
+
+ #[Delete(
+ path: '/admin/position',
+ operationId: 'positionDelete',
+ summary: '删除岗位',
+ security: [['Bearer' => [], 'ApiKey' => []]],
+ tags: ['岗位管理'],
+ )]
+ #[ResultResponse(instance: new Result())]
+ #[Permission(code: 'permission:position:delete')]
+ public function delete(): Result
+ {
+ $this->service->deleteById($this->getRequestData());
+ return $this->success();
+ }
+}
diff --git a/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php b/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php
new file mode 100644
index 000000000..118136d5d
--- /dev/null
+++ b/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php
@@ -0,0 +1,47 @@
+ [
+ 'required',
+ 'string',
+ Rule::enum(PolicyType::class),
+ ],
+ 'value' => [
+ 'sometimes',
+ 'array',
+ 'min:1',
+ ],
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'policy_type' => '策略类型',
+ 'value' => '策略值',
+ ];
+ }
+}
diff --git a/app/Http/Admin/Request/Permission/DepartmentRequest.php b/app/Http/Admin/Request/Permission/DepartmentRequest.php
new file mode 100644
index 000000000..715e2e57a
--- /dev/null
+++ b/app/Http/Admin/Request/Permission/DepartmentRequest.php
@@ -0,0 +1,62 @@
+ 'required|string|max:60',
+ 'parent_id' => 'sometimes|integer',
+ ];
+ if ($this->isCreate()) {
+ $rules['name'] = 'required|string|max:60|unique:department,name';
+ }
+ if ($this->isUpdate()) {
+ $rules['name'] = 'required|string|max:60|unique:department,name,' . $this->route('id');
+ }
+ $rules['department_users'] = 'sometimes|array';
+ $rules['department_users.*'] = 'sometimes|integer';
+ $rules['leader'] = 'sometimes|array';
+ $rules['leader.*'] = 'sometimes|integer';
+ return $rules;
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'name' => '部门名称',
+ 'parent_id' => '上级部门',
+ ];
+ }
+}
diff --git a/app/Http/Admin/Request/Permission/LeaderRequest.php b/app/Http/Admin/Request/Permission/LeaderRequest.php
new file mode 100644
index 000000000..d59dcc3ac
--- /dev/null
+++ b/app/Http/Admin/Request/Permission/LeaderRequest.php
@@ -0,0 +1,51 @@
+ 'required|array',
+ 'dept_id' => 'required|integer',
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'user_id' => '用户ID',
+ 'dept_id' => '部门ID',
+ ];
+ }
+}
diff --git a/app/Http/Admin/Request/Permission/PositionRequest.php b/app/Http/Admin/Request/Permission/PositionRequest.php
new file mode 100644
index 000000000..e40b6b69e
--- /dev/null
+++ b/app/Http/Admin/Request/Permission/PositionRequest.php
@@ -0,0 +1,58 @@
+ 'required|string|max:50',
+ 'dept_id' => 'required|integer|exists:department,id',
+ ];
+ if ($this->isCreate()) {
+ $rules['name'] = 'required|string|max:50|unique:position,name';
+ }
+ if ($this->isUpdate()) {
+ $rules['name'] = 'required|string|max:50|unique:position,name,' . $this->route('id');
+ }
+ return $rules;
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'name' => '岗位名称',
+ 'dept_id' => '部门ID',
+ ];
+ }
+}
diff --git a/app/Http/Admin/Request/Permission/RoleRequest.php b/app/Http/Admin/Request/Permission/RoleRequest.php
index ab9d96fa8..4f51226f3 100644
--- a/app/Http/Admin/Request/Permission/RoleRequest.php
+++ b/app/Http/Admin/Request/Permission/RoleRequest.php
@@ -20,7 +20,7 @@
#[\Mine\Swagger\Attributes\FormRequest(
schema: RoleSchema::class,
only: [
- 'name', 'code', 'status', 'sort', 'remark',
+ 'name', 'code', 'status', 'sort', 'remark', 'policy',
]
)]
class RoleRequest extends FormRequest
diff --git a/app/Http/Admin/Request/Permission/UserRequest.php b/app/Http/Admin/Request/Permission/UserRequest.php
index 7f308fc20..eb9d53be1 100644
--- a/app/Http/Admin/Request/Permission/UserRequest.php
+++ b/app/Http/Admin/Request/Permission/UserRequest.php
@@ -13,8 +13,10 @@
namespace App\Http\Admin\Request\Permission;
use App\Http\Common\Request\Traits\NoAuthorizeTrait;
+use App\Model\Enums\DataPermission\PolicyType;
use App\Schema\UserSchema;
use Hyperf\Validation\Request\FormRequest;
+use Hyperf\Validation\Rule;
use Mine\Swagger\Attributes\FormRequest as FormRequestAnnotation;
#[FormRequestAnnotation(
@@ -43,6 +45,7 @@
'status',
'backend_setting',
'remark',
+ 'policy',
]
)]
class UserRequest extends FormRequest
@@ -62,7 +65,34 @@ public function rules(): array
'status' => 'sometimes|integer',
'backend_setting' => 'sometimes|array|max:255',
'remark' => 'sometimes|string|max:255',
- 'password' => 'sometimes|string|min:6|max:20',
+ 'policy' => 'sometimes|array',
+ 'policy.policy_type' => [
+ 'required_with:policy',
+ 'string',
+ 'max:20',
+ Rule::enum(PolicyType::class),
+ ],
+ 'policy.value' => [
+ 'sometimes',
+ ],
+ 'department' => [
+ 'sometimes',
+ 'array',
+ ],
+ 'department.*' => [
+ 'required_with:department',
+ 'integer',
+ 'exists:department,id',
+ ],
+ 'position' => [
+ 'sometimes',
+ 'array',
+ ],
+ 'position.*' => [
+ 'sometimes',
+ 'integer',
+ 'exists:position,id',
+ ],
];
}
@@ -80,7 +110,7 @@ public function attributes(): array
'backend_setting' => trans('user.backend_setting'),
'created_by' => trans('user.created_by'),
'remark' => trans('user.remark'),
- 'password' => trans('user.password'),
+ 'department' => trans('user.department'),
];
}
}
diff --git a/app/Http/CurrentUser.php b/app/Http/CurrentUser.php
index d5a37aebf..a09647af9 100644
--- a/app/Http/CurrentUser.php
+++ b/app/Http/CurrentUser.php
@@ -18,6 +18,7 @@
use App\Service\Permission\UserService;
use Hyperf\Collection\Arr;
use Hyperf\Collection\Collection;
+use Hyperf\Context\Context;
use Lcobucci\JWT\Token\RegisteredClaims;
use Mine\Jwt\Traits\RequestScopedTokenTrait;
@@ -30,9 +31,19 @@ public function __construct(
private readonly UserService $userService
) {}
+ public static function ctxUser(): ?User
+ {
+ return Context::get('current_user');
+ }
+
public function user(): ?User
{
- return $this->userService->getInfo($this->id());
+ if (Context::has('current_user')) {
+ return Context::get('current_user');
+ }
+ $user = $this->userService->getInfo($this->id());
+ Context::set('current_user', $user);
+ return $user;
}
public function refresh(): array
diff --git a/app/Library/DataPermission/Aspects/DataScopeAspect.php b/app/Library/DataPermission/Aspects/DataScopeAspect.php
new file mode 100644
index 000000000..60c904105
--- /dev/null
+++ b/app/Library/DataPermission/Aspects/DataScopeAspect.php
@@ -0,0 +1,119 @@
+getAnnotationMetadata()->class[DataScope::class])
+ || isset($proceedingJoinPoint->getAnnotationMetadata()->method[DataScope::class])
+ ) {
+ return $this->handleDataScope($proceedingJoinPoint);
+ }
+
+ if ($proceedingJoinPoint->className === Builder::class) {
+ if ($proceedingJoinPoint->methodName === 'runSelect') {
+ return $this->handleSelect($proceedingJoinPoint);
+ }
+ if ($proceedingJoinPoint->methodName === 'delete') {
+ return $this->handleDelete($proceedingJoinPoint);
+ }
+ if ($proceedingJoinPoint->methodName === 'update') {
+ return $this->handleUpdate($proceedingJoinPoint);
+ }
+ }
+ return $proceedingJoinPoint->process();
+ }
+
+ protected function handleDelete(ProceedingJoinPoint $proceedingJoinPoint)
+ {
+ return $proceedingJoinPoint->process();
+ }
+
+ protected function handleUpdate(ProceedingJoinPoint $proceedingJoinPoint)
+ {
+ return $proceedingJoinPoint->process();
+ }
+
+ protected function handleSelect(ProceedingJoinPoint $proceedingJoinPoint)
+ {
+ /**
+ * @var Builder $builder
+ */
+ $builder = $proceedingJoinPoint->getInstance();
+ if (! \in_array($builder->from, DataPermissionContext::getOnlyTables() ?: [], true)) {
+ return $proceedingJoinPoint->process();
+ }
+ if (Context::has(self::CONTEXT_KEY) && $user = CurrentUser::ctxUser()) {
+ Context::destroy(self::CONTEXT_KEY);
+ $this->factory->build(
+ $builder,
+ $user
+ );
+ Context::set(self::CONTEXT_KEY, 1);
+ }
+ return $proceedingJoinPoint->process();
+ }
+
+ protected function handleDataScope(ProceedingJoinPoint $proceedingJoinPoint)
+ {
+ Context::set(self::CONTEXT_KEY, 1);
+ /**
+ * @var null|DataScope $attribute
+ */
+ $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->class, DataScope::class);
+ if ($attribute === null) {
+ $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->method, DataScope::class);
+ }
+ if ($attribute === null) {
+ return $proceedingJoinPoint->process();
+ }
+ DataPermissionContext::setDeptColumn($attribute->getDeptColumn());
+ DataPermissionContext::setCreatedByColumn($attribute->getCreatedByColumn());
+ DataPermissionContext::setScopeType($attribute->getScopeType());
+ DataPermissionContext::setOnlyTables($attribute->getOnlyTables());
+ $result = $proceedingJoinPoint->process();
+ Context::destroy(self::CONTEXT_KEY);
+ return $result;
+ }
+}
diff --git a/app/Library/DataPermission/Attribute/DataScope.php b/app/Library/DataPermission/Attribute/DataScope.php
new file mode 100644
index 000000000..398ddfa3a
--- /dev/null
+++ b/app/Library/DataPermission/Attribute/DataScope.php
@@ -0,0 +1,47 @@
+onlyTables;
+ }
+
+ public function getDeptColumn(): string
+ {
+ return $this->deptColumn;
+ }
+
+ public function getCreatedByColumn(): string
+ {
+ return $this->createdByColumn;
+ }
+
+ 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 000000000..e51308684
--- /dev/null
+++ b/app/Library/DataPermission/Context.php
@@ -0,0 +1,66 @@
+get(self::class);
+ }
+
+ public function build(
+ Builder $builder,
+ User $user,
+ ): void {
+ if ($user->isSuperAdmin()) {
+ return;
+ }
+ if (($policy = $user->getPolicy()) === null) {
+ return;
+ }
+ $scopeType = Context::getScopeType();
+ if ($policy->policy_type === PolicyType::CustomFunc) {
+ $customFunc = $policy->value[0] ?? null;
+ if (! \is_string($customFunc)) {
+ throw new \RuntimeException(\sprintf('Invalid custom function: %s', $customFunc));
+ }
+ $this->rule->loadCustomFunc($customFunc, $builder, $user, $policy, $scopeType);
+ }
+ 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
+ {
+ $builder->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) {
+ $query->whereIn(Context::getCreatedByColumn(), $createdByList);
+ });
+ }
+
+ private function handleDept(User $user, Policy $policy, Builder $builder): void
+ {
+ $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) {
+ $query->whereIn(Context::getDeptColumn(), $deptList);
+ });
+ }
+
+ private function handleDeptCreatedBy(User $user, Policy $policy, Builder $builder): void
+ {
+ $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) {
+ $query->whereIn(Context::getDeptColumn(), $deptList);
+ })->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) {
+ $query->whereIn(Context::getCreatedByColumn(), $createdByList);
+ });
+ }
+
+ 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) {
+ if ($createdByList) {
+ $query->whereIn(Context::getCreatedByColumn(), $createdByList);
+ }
+ if ($deptList) {
+ $query->orWhereIn(Context::getDeptColumn(), $deptList);
+ }
+ });
+ }
+}
diff --git a/app/Library/DataPermission/Manager.php b/app/Library/DataPermission/Manager.php
new file mode 100644
index 000000000..a04ab05d6
--- /dev/null
+++ b/app/Library/DataPermission/Manager.php
@@ -0,0 +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 000000000..7e9648b42
--- /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 000000000..9d00fac92
--- /dev/null
+++ b/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php
@@ -0,0 +1,150 @@
+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,
+ ];
+ if ($policyType->isSelf()) {
+ return $ids;
+ }
+
+ if ($policyType->isNotCustomFunc() && $policyType->isNotCustomDept()) {
+ /**
+ * @var Collection|Department[] $leaderDepartmentList
+ */
+ $leaderDepartmentList = $this->getUser()->department()->get();
+ foreach ($leaderDepartmentList as $department) {
+ if ($policyType->isDeptSelf()) {
+ // @phpstan-ignore-next-line
+ $this->getUser()->newQuery()
+ ->whereHas('department', static function ($query) use ($department) {
+ $query->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->get()->each(static function (User $user) use (&$ids) {
+ $ids[] = $user->id;
+ });
+ }
+ if ($policyType->isDeptTree()) {
+ // @phpstan-ignore-next-line
+ $department->getFlatChildren()->each(function (Department $department) use (&$ids) {
+ $this->getUser()->newQuery()
+ ->whereHas('department', static function ($query) use ($department) {
+ $query->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->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->isDeptSelf()) {
+ // @phpstan-ignore-next-line
+ $position->department()->get()->each(function (Department $department) use (&$ids) {
+ $this->getUser()->newQuery()
+ ->whereHas('department', static function ($query) use ($department) {
+ $query->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->get()->each(static function (User $user) use (&$ids) {
+ $ids[] = $user->id;
+ });
+ });
+ }
+ if ($policyType->isDeptTree()) {
+ // @phpstan-ignore-next-line
+ $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->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->get()->each(static function (User $user) use (&$ids) {
+ $ids[] = $user->id;
+ });
+ });
+ });
+ }
+ }
+
+ if (! empty($departmentList)) {
+ foreach ($departmentList as $department) {
+ if ($policyType->isDeptSelf()) {
+ // @phpstan-ignore-next-line
+ $this->getUser()->newQuery()
+ ->whereHas('department', static function ($query) use ($department) {
+ $query->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->get()->each(static function (User $user) use (&$ids) {
+ $ids[] = $user->id;
+ });
+ }
+ if ($policyType->isDeptTree()) {
+ // @phpstan-ignore-next-line
+ $department->getFlatChildren()->each(function (Department $department) use (&$ids) {
+ $this->getUser()->newQuery()
+ ->whereHas('department', static function ($query) use ($department) {
+ $query->where('id', $department->id);
+ // @phpstan-ignore-next-line
+ })->get()->each(static function (User $user) use (&$ids) {
+ $ids[] = $user->id;
+ });
+ });
+ }
+ }
+ }
+ }
+ return array_unique($ids);
+ }
+}
diff --git a/app/Library/DataPermission/Rule/Executable/DeptExecute.php b/app/Library/DataPermission/Rule/Executable/DeptExecute.php
new file mode 100644
index 000000000..487f899ba
--- /dev/null
+++ b/app/Library/DataPermission/Rule/Executable/DeptExecute.php
@@ -0,0 +1,64 @@
+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 = $departmentList->merge($item->getFlatChildren());
+ }
+ }
+ if (empty($departmentList)) {
+ return null;
+ }
+ $departmentList = $departmentList->merge($this->getUser()->dept_leader()->get());
+ /**
+ * @var Collection|Position[] $positionList
+ */
+ $positionList = $this->getUser()->position()->get();
+ foreach ($positionList as $position) {
+ $departmentList = $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 000000000..1fbf37cc8
--- /dev/null
+++ b/app/Library/DataPermission/Rule/Rule.php
@@ -0,0 +1,83 @@
+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;
+ }
+
+ public function loadCustomFunc(string $customFunc, Builder $builder, User $user, ?Policy $policy, ScopeType $scopeType): void
+ {
+ $func = $this->config->get('department.custom.' . $customFunc);
+ if ($func === null) {
+ throw new \RuntimeException(\sprintf('Custom function %s not found', $customFunc));
+ }
+ $func($builder, $scopeType, $policy, $user);
+ }
+}
diff --git a/app/Library/DataPermission/Scope/DataScope.php b/app/Library/DataPermission/Scope/DataScope.php
new file mode 100644
index 000000000..e27a7cb45
--- /dev/null
+++ b/app/Library/DataPermission/Scope/DataScope.php
@@ -0,0 +1,32 @@
+build($builder->getQuery(), $user);
+ }
+}
diff --git a/app/Library/DataPermission/Scope/DataScopes.php b/app/Library/DataPermission/Scope/DataScopes.php
new file mode 100644
index 000000000..3d7f8a26b
--- /dev/null
+++ b/app/Library/DataPermission/Scope/DataScopes.php
@@ -0,0 +1,27 @@
+checkRedisConnection();
+ }
+ }
+
+ private function checkRedisConnection(): void
+ {
+ try {
+ $redis = $this->container->get(RedisFactory::class)->get('default');
+ $result = $redis->ping();
+ if (! $result) {
+ $this->logger->error('Redis connection failed: Invalid ping response');
+ $this->container->get(StdoutLoggerInterface::class)->error('Redis connection failed: Invalid ping response');
+ }
+ } catch (\Throwable $e) {
+ $this->logger->error('Redis connection failed: ' . $e->getMessage());
+ $this->container->get(StdoutLoggerInterface::class)->error('Redis connection failed, please check redis config in .env file');
+ }
+ }
+}
diff --git a/app/Model/DataPermission/Policy.php b/app/Model/DataPermission/Policy.php
new file mode 100644
index 000000000..040ea005b
--- /dev/null
+++ b/app/Model/DataPermission/Policy.php
@@ -0,0 +1,70 @@
+|Position[] $positions
+ * @property Collection|User[] $users
+ */
+class Policy extends Model
+{
+ use SoftDeletes;
+
+ /**
+ * The table associated with the model.
+ */
+ protected ?string $table = 'data_permission_policy';
+
+ /**
+ * The attributes that are mass assignable.
+ */
+ protected array $fillable = ['id', 'user_id', 'position_id', 'policy_type', 'is_default', 'created_at', 'updated_at', 'deleted_at', 'value'];
+
+ /**
+ * The attributes that should be cast to native types.
+ */
+ protected array $casts = [
+ 'id' => 'integer', 'user_id' => 'integer', 'position_id' => 'integer',
+ 'is_default' => 'bool', 'created_at' => 'datetime',
+ 'updated_at' => 'datetime', 'deleted_at' => 'datetime',
+ 'policy_type' => PolicyType::class, 'value' => 'array',
+ ];
+
+ public function positions(): BelongsToMany
+ {
+ return $this->belongsToMany(Position::class, 'data_permission_policy_position', 'policy_id', 'position_id');
+ }
+
+ public function users(): BelongsToMany
+ {
+ return $this->belongsToMany(User::class, 'data_permission_policy_user', 'policy_id', 'user_id');
+ }
+}
diff --git a/app/Model/Enums/DataPermission/PolicyType.php b/app/Model/Enums/DataPermission/PolicyType.php
new file mode 100644
index 000000000..167d3f93f
--- /dev/null
+++ b/app/Model/Enums/DataPermission/PolicyType.php
@@ -0,0 +1,86 @@
+isDeptSelf();
+ }
+
+ public function isNotDeptTree(): bool
+ {
+ return ! $this->isDeptTree();
+ }
+
+ public function isNotAll(): bool
+ {
+ return ! $this->isAll();
+ }
+
+ public function isNotSelf(): bool
+ {
+ return ! $this->isSelf();
+ }
+
+ public function isNotCustomDept(): bool
+ {
+ return ! $this->isCustomDept();
+ }
+
+ public function isNotCustomFunc(): bool
+ {
+ return ! $this->isCustomFunc();
+ }
+}
diff --git a/app/Model/Permission/Department.php b/app/Model/Permission/Department.php
new file mode 100644
index 000000000..80635130b
--- /dev/null
+++ b/app/Model/Permission/Department.php
@@ -0,0 +1,98 @@
+|Position[] $positions 岗位
+ * @property Collection|User[] $department_users 部门用户
+ * @property Collection|User[] $leader 部门领导
+ * @property Collection|Department[] $children 子部门
+ */
+class Department extends Model
+{
+ use SoftDeletes;
+
+ /**
+ * The table associated with the model.
+ */
+ protected ?string $table = 'department';
+
+ /**
+ * The attributes that are mass assignable.
+ */
+ protected array $fillable = ['id', 'name', 'parent_id', 'created_at', 'updated_at', 'deleted_at'];
+
+ /**
+ * The attributes that should be cast to native types.
+ */
+ protected array $casts = ['id' => 'integer', 'parent_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime'];
+
+ public function deleted(Deleted $event): void
+ {
+ $this->positions()->delete();
+ $this->department_users()->detach();
+ $this->leader()->detach();
+ }
+
+ public function positions(): HasMany
+ {
+ return $this->hasMany(Position::class, 'dept_id', 'id');
+ }
+
+ public function department_users(): BelongsToMany
+ {
+ return $this->belongsToMany(User::class, 'user_dept', 'dept_id', 'user_id');
+ }
+
+ public function leader(): BelongsToMany
+ {
+ return $this->belongsToMany(User::class, 'dept_leader', 'dept_id', 'user_id');
+ }
+
+ public function children(): HasMany
+ {
+ // @phpstan-ignore-next-line
+ return $this->hasMany(self::class, 'parent_id', 'id')->with(['children', 'positions']);
+ }
+
+ 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/Leader.php b/app/Model/Permission/Leader.php
new file mode 100644
index 000000000..57e566722
--- /dev/null
+++ b/app/Model/Permission/Leader.php
@@ -0,0 +1,59 @@
+ 'integer', 'dept_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime'];
+
+ public function department(): BelongsTo
+ {
+ return $this->belongsTo(Department::class, 'dept_id', 'id');
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'user_id', 'id');
+ }
+}
diff --git a/app/Model/Permission/Position.php b/app/Model/Permission/Position.php
new file mode 100644
index 000000000..3fcf3f415
--- /dev/null
+++ b/app/Model/Permission/Position.php
@@ -0,0 +1,68 @@
+|User[] $users
+ * @property Policy $policy
+ */
+class Position extends Model
+{
+ use SoftDeletes;
+
+ /**
+ * The table associated with the model.
+ */
+ protected ?string $table = 'position';
+
+ /**
+ * The attributes that are mass assignable.
+ */
+ protected array $fillable = ['id', 'name', 'dept_id', 'created_at', 'updated_at', 'deleted_at'];
+
+ /**
+ * The attributes that should be cast to native types.
+ */
+ protected array $casts = ['id' => 'integer', 'dept_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime'];
+
+ public function department(): BelongsTo
+ {
+ return $this->belongsTo(Department::class, 'dept_id', 'id');
+ }
+
+ public function users(): BelongsToMany
+ {
+ return $this->belongsToMany(User::class, 'user_position', 'position_id', 'user_id');
+ }
+
+ public function policy(): HasOne
+ {
+ return $this->hasOne(Policy::class, 'position_id', 'id');
+ }
+}
diff --git a/app/Model/Permission/User.php b/app/Model/Permission/User.php
index 313b47282..e850a4f6e 100644
--- a/app/Model/Permission/User.php
+++ b/app/Model/Permission/User.php
@@ -12,6 +12,7 @@
namespace App\Model\Permission;
+use App\Model\DataPermission\Policy;
use App\Model\Enums\User\Status;
use App\Model\Enums\User\Type;
use Carbon\Carbon;
@@ -19,6 +20,7 @@
use Hyperf\Database\Model\Events\Creating;
use Hyperf\Database\Model\Events\Deleted;
use Hyperf\Database\Model\Relations\BelongsToMany;
+use Hyperf\Database\Model\Relations\HasOne;
use Hyperf\DbConnection\Model\Model;
/**
@@ -41,6 +43,10 @@
* @property string $remark 备注
* @property null|Collection|Role[] $roles
* @property mixed $password 密码
+ * @property null|Policy $policy 数据权限策略
+ * @property Collection|Department[] $department 部门
+ * @property Collection|Department[] $dept_leader 部门领导
+ * @property Collection|Position[] $position 岗位
*/
final class User extends Model
{
@@ -87,6 +93,7 @@ public function roles(): BelongsToMany
public function deleted(Deleted $event)
{
$this->roles()->detach();
+ $this->policy()->delete();
}
public function setPasswordAttribute($value): void
@@ -136,4 +143,45 @@ public function hasPermission(string $permission): bool
{
return $this->roles()->whereRelation('menus', 'name', $permission)->exists();
}
+
+ public function policy(): HasOne
+ {
+ return $this->hasOne(Policy::class, 'user_id', 'id');
+ }
+
+ public function department(): BelongsToMany
+ {
+ return $this->belongsToMany(Department::class, 'user_dept', 'user_id', 'dept_id');
+ }
+
+ public function dept_leader(): BelongsToMany
+ {
+ return $this->belongsToMany(Department::class, 'dept_leader', 'user_id', 'dept_id');
+ }
+
+ public function position(): BelongsToMany
+ {
+ return $this->belongsToMany(Position::class, 'user_position', 'user_id', 'position_id');
+ }
+
+ public function getPolicy(): ?Policy
+ {
+ /**
+ * @var null|Policy $policy
+ */
+ $policy = $this->policy()->first();
+ if (! empty($policy)) {
+ return $policy;
+ }
+
+ $this->load('position');
+ $positionList = $this->position;
+ foreach ($positionList as $position) {
+ $current = $position->policy()->first();
+ if (! empty($current)) {
+ return $current;
+ }
+ }
+ return null;
+ }
}
diff --git a/app/Repository/Permission/DepartmentRepository.php b/app/Repository/Permission/DepartmentRepository.php
new file mode 100644
index 000000000..972f1938a
--- /dev/null
+++ b/app/Repository/Permission/DepartmentRepository.php
@@ -0,0 +1,66 @@
+
+ */
+final class DepartmentRepository extends IRepository
+{
+ public function __construct(
+ protected readonly Department $model
+ ) {}
+
+ public function getByIds(array $ids): Collection
+ {
+ return $this->model->newQuery()->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'] . '%');
+ })
+ ->when(isset($params['parent_id']), static function (Builder $query) use ($params) {
+ $query->where('parent_id', $params['parent_id']);
+ })
+ ->when(isset($params['created_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('created_at', $params['created_at']);
+ })
+ ->when(isset($params['updated_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('updated_at', $params['updated_at']);
+ })
+ ->when(! empty($params['append_position']), static function (Builder $query) {
+ $query->with('positions:id,name');
+ })
+ ->when(isset($params['level']), static function (Builder $query) use ($params) {
+ if ((int) $params['level'] === 1) {
+ $query->where('parent_id', 0);
+ $query->with('children');
+ }
+
+ // todo 指定层级查询
+ })
+ ->with(['positions', 'department_users:id,nickname,username,avatar', 'leader:id,nickname,username,avatar']);
+ }
+}
diff --git a/app/Repository/Permission/LeaderRepository.php b/app/Repository/Permission/LeaderRepository.php
new file mode 100644
index 000000000..8e78012db
--- /dev/null
+++ b/app/Repository/Permission/LeaderRepository.php
@@ -0,0 +1,65 @@
+
+ */
+final class LeaderRepository extends IRepository
+{
+ public function __construct(
+ protected readonly Leader $model
+ ) {}
+
+ public function create(array $data): mixed
+ {
+ foreach ($data['user_id'] as $id) {
+ Leader::query()->where('dept_id', $data['dept_id'])->where('user_id', $id)->forceDelete();
+ Leader::create(['dept_id' => $data['dept_id'], 'user_id' => $id, 'created_at' => date('Y-m-d H:i:s')]);
+ }
+ // @phpstan-ignore-next-line
+ return null;
+ }
+
+ public function deleteByDoubleKey(int $dept_id, array $user_ids): void
+ {
+ Leader::query()->where('dept_id', $dept_id)->whereIn('user_id', $user_ids)->forceDelete();
+ }
+
+ public function handleSearch(Builder $query, array $params): Builder
+ {
+ return $query
+ ->when(isset($params['user_id']), static function (Builder $query) use ($params) {
+ $query->where('user_id', $params['user_id']);
+ })
+ ->when(isset($params['dept_id']), static function (Builder $query) use ($params) {
+ $query->where('dept_id', $params['dept_id']);
+ })
+ ->when(isset($params['created_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('created_at', $params['created_at']);
+ })
+ ->when(isset($params['updated_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('updated_at', $params['updated_at']);
+ })
+ ->with(['department', 'user']);
+ }
+
+ protected function enablePageOrderBy(): bool
+ {
+ return false;
+ }
+}
diff --git a/app/Repository/Permission/PositionRepository.php b/app/Repository/Permission/PositionRepository.php
new file mode 100644
index 000000000..bee3ad160
--- /dev/null
+++ b/app/Repository/Permission/PositionRepository.php
@@ -0,0 +1,45 @@
+
+ */
+final class PositionRepository extends IRepository
+{
+ public function __construct(
+ protected readonly Position $model
+ ) {}
+
+ public function handleSearch(Builder $query, array $params): Builder
+ {
+ return $query
+ ->when(isset($params['name']), static function (Builder $query) use ($params) {
+ $query->where('name', 'like', '%' . $params['name'] . '%');
+ })
+ ->when(isset($params['dept_id']), static function (Builder $query) use ($params) {
+ $query->where('dept_id', $params['dept_id']);
+ })
+ ->when(isset($params['created_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('created_at', $params['created_at']);
+ })
+ ->when(isset($params['updated_at']), static function (Builder $query) use ($params) {
+ $query->whereBetween('updated_at', $params['updated_at']);
+ })
+ ->with(['department', 'policy']);
+ }
+}
diff --git a/app/Repository/Permission/RoleRepository.php b/app/Repository/Permission/RoleRepository.php
index da1a9be20..4a894edeb 100644
--- a/app/Repository/Permission/RoleRepository.php
+++ b/app/Repository/Permission/RoleRepository.php
@@ -25,14 +25,18 @@ public function __construct(
public function handleSearch(Builder $query, array $params): Builder
{
- return $query->when(Arr::get($params, 'name'), static function (Builder $query, $name) {
- $query->where('name', 'like', '%' . $name . '%');
- })->when(Arr::get($params, 'code'), static function (Builder $query, $code) {
- $query->whereIn('code', Arr::wrap($code));
- })->when(Arr::has($params, 'status'), static function (Builder $query) use ($params) {
- $query->where('status', $params['status']);
- })->when(Arr::get($params, 'created_at'), static function (Builder $query, $createdAt) {
- $query->whereBetween('created_at', $createdAt);
- });
+ return $query
+ ->when(Arr::get($params, 'name'), static function (Builder $query, $name) {
+ $query->where('name', 'like', '%' . $name . '%');
+ })
+ ->when(Arr::get($params, 'code'), static function (Builder $query, $code) {
+ $query->whereIn('code', Arr::wrap($code));
+ })
+ ->when(Arr::has($params, 'status'), static function (Builder $query) use ($params) {
+ $query->where('status', $params['status']);
+ })
+ ->when(Arr::get($params, 'created_at'), static function (Builder $query, $createdAt) {
+ $query->whereBetween('created_at', $createdAt);
+ });
}
}
diff --git a/app/Repository/Permission/UserRepository.php b/app/Repository/Permission/UserRepository.php
index 000858318..f112ec791 100644
--- a/app/Repository/Permission/UserRepository.php
+++ b/app/Repository/Permission/UserRepository.php
@@ -72,6 +72,20 @@ public function handleSearch(Builder $query, array $params): Builder
$query->whereHas('roles', static function (Builder $query) use ($roleId) {
$query->where('role_id', $roleId);
});
- });
+ })
+ ->when(Arr::get($params, 'department_id'), static function (Builder $query, $departmentId) {
+ $query->where(static function (Builder $query) use ($departmentId) {
+ $query->whereHas('department', static function (Builder $query) use ($departmentId) {
+ $query->where('id', $departmentId);
+ });
+ $query->orWhereHas('dept_leader', static function (Builder $query) use ($departmentId) {
+ $query->where('id', $departmentId);
+ });
+ $query->orWhereHas('position.department', static function (Builder $query) use ($departmentId) {
+ $query->where('id', $departmentId);
+ });
+ });
+ })
+ ->with(['policy', 'department', 'dept_leader', 'position']);
}
}
diff --git a/app/Schema/DepartmentSchema.php b/app/Schema/DepartmentSchema.php
new file mode 100644
index 000000000..d8dffd557
--- /dev/null
+++ b/app/Schema/DepartmentSchema.php
@@ -0,0 +1,59 @@
+ $this->policyType->value,
+ 'is_default' => $this->isDefault,
+ 'value' => $this->value,
+ ];
+ }
+}
diff --git a/app/Schema/PositionSchema.php b/app/Schema/PositionSchema.php
new file mode 100644
index 000000000..de178c639
--- /dev/null
+++ b/app/Schema/PositionSchema.php
@@ -0,0 +1,49 @@
+id = $model->id;
@@ -66,10 +69,11 @@ public function __construct(Role $model)
$this->createdAt = $model->created_at;
$this->updatedAt = $model->updated_at->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT);
$this->remark = $model->remark;
+ $this->policy = isset($model->policy) ? new PolicySchema($model->policy) : null;
}
public function jsonSerialize(): mixed
{
- return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark];
+ return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy->jsonSerialize()];
}
}
diff --git a/app/Schema/UserSchema.php b/app/Schema/UserSchema.php
index c51e59385..1e5e00aad 100644
--- a/app/Schema/UserSchema.php
+++ b/app/Schema/UserSchema.php
@@ -72,6 +72,9 @@ final class UserSchema implements \JsonSerializable
#[Property(property: 'remark', title: '备注', type: 'string')]
public ?string $remark;
+ #[Property(property: 'policy', ref: '#/components/schemas/PolicySchema', title: '权限')]
+ public ?PolicySchema $policy;
+
public function __construct(User $model)
{
$this->id = $model->id;
@@ -91,6 +94,7 @@ public function __construct(User $model)
$this->createdAt = $model->created_at;
$this->updatedAt = $model->updated_at;
$this->remark = $model->remark;
+ $this->policy = isset($model->policy) ? new PolicySchema($model->policy) : null;
}
public function jsonSerialize(): mixed
diff --git a/app/Service/Permission/DepartmentService.php b/app/Service/Permission/DepartmentService.php
new file mode 100644
index 000000000..18510e3e2
--- /dev/null
+++ b/app/Service/Permission/DepartmentService.php
@@ -0,0 +1,69 @@
+
+ */
+class DepartmentService extends IService
+{
+ public function __construct(
+ protected readonly DepartmentRepository $repository
+ ) {}
+
+ public function create(array $data): mixed
+ {
+ return Db::transaction(function () use ($data) {
+ $entity = $this->repository->create($data);
+ $this->handleEntity($entity, $data);
+ return $entity;
+ });
+ }
+
+ public function updateById(mixed $id, array $data): mixed
+ {
+ return Db::transaction(function () use ($id, $data) {
+ $entity = $this->repository->findById($id);
+ if (empty($entity)) {
+ throw new BusinessException(ResultCode::NOT_FOUND);
+ }
+ $this->handleEntity($entity, $data);
+ });
+ }
+
+ public function getPositionsByDepartmentId(int $id): array
+ {
+ $entity = $this->repository->findById($id);
+ if (empty($entity)) {
+ throw new BusinessException(ResultCode::NOT_FOUND);
+ }
+ return $entity->positions()->get(['id', 'name'])->toArray();
+ }
+
+ protected function handleEntity(Department $entity, array $data): void
+ {
+ if (isset($data['department_users'])) {
+ $entity->department_users()->sync($data['department_users']);
+ }
+ if (isset($data['leader'])) {
+ $entity->leader()->sync($data['leader']);
+ }
+ }
+}
diff --git a/app/Service/Permission/LeaderService.php b/app/Service/Permission/LeaderService.php
new file mode 100644
index 000000000..c9f6881ec
--- /dev/null
+++ b/app/Service/Permission/LeaderService.php
@@ -0,0 +1,38 @@
+
+ */
+class LeaderService extends IService
+{
+ public function __construct(
+ protected readonly LeaderRepository $repository
+ ) {}
+
+ public function deleteByDoubleKey(array $data): bool
+ {
+ try {
+ $this->repository->deleteByDoubleKey($data['dept_id'], $data['user_ids']);
+ return true;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+}
diff --git a/app/Service/Permission/PositionService.php b/app/Service/Permission/PositionService.php
new file mode 100644
index 000000000..c53c3f419
--- /dev/null
+++ b/app/Service/Permission/PositionService.php
@@ -0,0 +1,43 @@
+
+ */
+class PositionService extends IService
+{
+ public function __construct(
+ protected readonly PositionRepository $repository
+ ) {}
+
+ public function batchDataPermission(int $id, array $policy): void
+ {
+ $entity = $this->repository->findById($id);
+ if ($entity === null) {
+ throw new BusinessException(ResultCode::NOT_FOUND);
+ }
+ $policyEntity = $entity->policy()->first();
+ if (empty($policyEntity)) {
+ $entity->policy()->create($policy);
+ } else {
+ $policyEntity->update($policy);
+ }
+ }
+}
diff --git a/app/Service/Permission/UserService.php b/app/Service/Permission/UserService.php
index 87b39d2d1..b1fe4339e 100644
--- a/app/Service/Permission/UserService.php
+++ b/app/Service/Permission/UserService.php
@@ -12,12 +12,17 @@
namespace App\Service\Permission;
+use App\Exception\BusinessException;
+use App\Http\Common\ResultCode;
+use App\Library\DataPermission\Attribute\DataScope;
+use App\Library\DataPermission\ScopeType;
use App\Model\Permission\Role;
use App\Model\Permission\User;
use App\Repository\Permission\RoleRepository;
use App\Repository\Permission\UserRepository;
use App\Service\IService;
use Hyperf\Collection\Collection;
+use Hyperf\DbConnection\Db;
/**
* @extends IService
@@ -62,4 +67,54 @@ public function batchGrantRoleForUser(int $id, array $roleCodes): void
})
);
}
+
+ public function create(array $data): mixed
+ {
+ return Db::transaction(function () use ($data) {
+ /** @var User $entity */
+ $entity = parent::create($data);
+ $this->handleWith($entity, $data);
+ return $entity;
+ });
+ }
+
+ public function updateById(mixed $id, array $data): mixed
+ {
+ return Db::transaction(function () use ($id, $data) {
+ /** @var null|User $entity */
+ $entity = $this->repository->findById($id);
+ if (empty($entity)) {
+ throw new BusinessException(ResultCode::NOT_FOUND);
+ }
+ $entity->fill($data)->save();
+ $this->handleWith($entity, $data);
+ });
+ }
+
+ #[DataScope(
+ scopeType: ScopeType::CREATED_BY,
+ onlyTables: ['user']
+ )]
+ public function page(array $params, int $page = 1, int $pageSize = 10): array
+ {
+ return parent::page($params, $page, $pageSize); // TODO: Change the autogenerated stub
+ }
+
+ protected function handleWith(User $entity, array $data): void
+ {
+ if (isset($data['department'])) {
+ $entity->department()->sync($data['department']);
+ }
+ if (isset($data['position'])) {
+ $entity->position()->sync($data['position']);
+ }
+ if (! empty($data['policy'])) {
+ $policy = $entity->policy()->first();
+ if ($policy) {
+ $policy->fill($data['policy'])->save();
+ } else {
+ $entity->policy()->create($data['policy']);
+ }
+ }
+ }
}
diff --git a/config/autoload/casbin/rbac-model.conf b/config/autoload/casbin/rbac-model.conf
deleted file mode 100644
index 7854845e4..000000000
--- 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 000000000..f7b718194
--- /dev/null
+++ b/config/autoload/department/cache.php
@@ -0,0 +1,30 @@
+ 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'),
+];
diff --git a/config/autoload/department/custom.php b/config/autoload/department/custom.php
new file mode 100644
index 000000000..ca6db8a1c
--- /dev/null
+++ b/config/autoload/department/custom.php
@@ -0,0 +1,54 @@
+ function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) {
+ // 只针对 id 为 2 的用户生效
+ if ($user->id !== 2) {
+ return;
+ }
+ // 获取当前上下文中的创建人字段名称
+ $createdByColumn = Context::getCreatedByColumn();
+ // 获取当前上下文中的部门字段名称
+ $deptColumn = Context::getDeptColumn();
+ switch ($scopeType){
+ // 隔离类型为根据创建人
+ case ScopeType::CREATED_BY:
+ // 创建人字段为当前用户
+ $builder->where($createdByColumn, $user->id);
+ break;
+ case ScopeType::DEPT:
+ // 部门字段为当前用户部门
+ $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+ break;
+ case ScopeType::DEPT_CREATED_BY:
+ // 部门字段为当前用户部门
+ $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+ // 创建人为当前用户
+ $builder->where($createdByColumn, $user->id);
+ break;
+ case ScopeType::DEPT_OR_CREATED_BY:
+ // 部门字段为当前用户部门
+ $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+ // 创建人为当前用户
+ $builder->orWhere($createdByColumn, $user->id);
+ break;
+ }
+ return;
+ }
+];
diff --git a/config/config.php b/config/config.php
index c9ecaadc5..56e410d1c 100644
--- a/config/config.php
+++ b/config/config.php
@@ -15,7 +15,7 @@
return [
'app_name' => env('APP_NAME', 'MineAdmin'),
'scan_cacheable' => ! env('APP_DEBUG', false),
- 'debug' => env('APP_DEBUG', false),
+ 'debug' => env('APP_DEBUG', true),
StdoutLoggerInterface::class => [
'log_level' => [
LogLevel::ALERT,
diff --git a/databases/migrations/2025_02_24_195620_create_department_tables.php b/databases/migrations/2025_02_24_195620_create_department_tables.php
new file mode 100644
index 000000000..b40bc1715
--- /dev/null
+++ b/databases/migrations/2025_02_24_195620_create_department_tables.php
@@ -0,0 +1,82 @@
+comment('部门表');
+ $table->bigIncrements('id');
+ $table->string('name', 50)->comment('部门名称');
+ $table->bigInteger('parent_id')->default(0)->comment('父级部门ID');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ Schema::create('position', function (Blueprint $table) {
+ $table->comment('岗位表');
+ $table->bigIncrements('id');
+ $table->string('name', 50)->comment('岗位名称');
+ $table->bigInteger('dept_id')->comment('部门ID');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ Schema::create('user_dept', function (Blueprint $table) {
+ $table->comment('用户-部门关联表');
+ $table->bigInteger('user_id');
+ $table->bigInteger('dept_id');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ Schema::create('user_position', function (Blueprint $table) {
+ $table->comment('用户-岗位关联表');
+ $table->bigInteger('user_id');
+ $table->bigInteger('position_id');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ Schema::create('dept_leader', function (Blueprint $table) {
+ $table->comment('部门领导表');
+ $table->bigInteger('dept_id');
+ $table->bigInteger('user_id');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ Schema::create('data_permission_policy', function (Blueprint $table) {
+ $table->comment('数据权限策略');
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id')->default(0)->comment('用户ID(与角色二选一)');
+ $table->bigInteger('position_id')->default(0)->comment('岗位ID(与用户二选一)');
+ $table->string('policy_type', 20)->comment('策略类型(DEPT_SELF, DEPT_TREE, ALL, SELF, CUSTOM_DEPT, CUSTOM_FUNC)');
+ $table->boolean('is_default')->default(true)->comment('是否默认策略(默认值:true)');
+ $table->json('value')->nullable()->comment('策略值');
+ $table->datetimes();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('department');
+ Schema::dropIfExists('position');
+ Schema::dropIfExists('user_dept');
+ Schema::dropIfExists('user_position');
+ Schema::dropIfExists('dept_leader');
+ Schema::dropIfExists('data_permission_policy');
+ }
+};
diff --git a/databases/seeders/menu_seeder_20240926.php b/databases/seeders/menu_seeder_20240926.php
index 95b2de4f7..fa3dc20a0 100644
--- a/databases/seeders/menu_seeder_20240926.php
+++ b/databases/seeders/menu_seeder_20240926.php
@@ -354,107 +354,6 @@ public function data(): array
],
],
],
- /* [
- 'name' => '工具',
- 'code' => 'devTools',
- 'icon' => 'ma-icon-tool',
- 'path' => 'devTools',
- 'hidden' => '2',
- 'type' => 'M',
- 'children' => [
- [
- 'name' => '代码生成器',
- 'code' => 'setting:code',
- 'icon' => 'ma-icon-code',
- 'path' => 'code',
- 'component' => 'setting/code/index',
- 'hidden' => '2',
- 'type' => 'M',
- 'children' => [
- [
- 'name' => '预览代码',
- 'code' => 'setting:code:preview',
- 'type' => 'B',
- ],
- [
- 'name' => '装载数据表',
- 'code' => 'setting:code:loadTable',
- 'type' => 'B',
- ],
- [
- 'name' => '删除业务表',
- 'code' => 'setting:code:delete',
- 'type' => 'B',
- ],
- [
- 'name' => '同步业务表',
- 'code' => 'setting:code:sync',
- 'type' => 'B',
- ],
- [
- 'name' => '生成代码',
- 'code' => 'setting:code:generate',
- 'type' => 'B',
- ],
- ],
- ],
- [
- 'name' => '数据源管理',
- 'code' => 'setting:datasource',
- 'icon' => 'icon-storage',
- 'path' => 'setting/datasource',
- 'component' => 'setting/datasource/index',
- 'hidden' => '2',
- 'type' => 'M',
- 'children' => [
- [
- 'name' => '数据源管理列表',
- 'code' => 'setting:datasource:index',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理保存',
- 'code' => 'setting:datasource:save',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理更新',
- 'code' => 'setting:datasource:update',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理读取',
- 'code' => 'setting:datasource:read',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理删除',
- 'code' => 'setting:datasource:delete',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理导入',
- 'code' => 'setting:datasource:import',
- 'type' => 'B',
- ],
- [
- 'name' => '数据源管理导出',
- 'code' => 'setting:datasource:export',
- 'type' => 'B',
- ],
- ],
- ],
- [
- 'name' => '系统接口',
- 'code' => 'systemInterface',
- 'icon' => 'icon-compass',
- 'path' => 'systemInterface',
- 'component' => 'setting/systemInterface/index',
- 'hidden' => '2',
- 'type' => 'M',
- ],
- ],
- ],*/
];
}
diff --git a/databases/seeders/user_dept_20250310.php b/databases/seeders/user_dept_20250310.php
new file mode 100644
index 000000000..1a24a848e
--- /dev/null
+++ b/databases/seeders/user_dept_20250310.php
@@ -0,0 +1,89 @@
+firstOrFail();
+ $now = Menu::create([
+ 'name' => 'permission:department',
+ 'path' => '/permission/departments',
+ 'parent_id' => $parent->id,
+ 'component' => 'base/views/permission/department/index',
+ 'meta' => [
+ 'title' => '部门管理',
+ 'icon' => 'mingcute:department-line',
+ 'i18n' => 'baseMenu.permission.department',
+ 'type' => 'M',
+ 'hidden' => 0,
+ 'componentPath' => 'modules/',
+ 'componentSuffix' => '.vue',
+ 'breadcrumbEnable' => 1,
+ 'copyright' => 1,
+ 'cache' => 1,
+ 'affix' => 0,
+ ]
+ ]);
+ $children = [
+ 'permission:department:index' => '部门列表',
+ 'permission:department:save' => '部门新增',
+ 'permission:department:update' => '部门编辑',
+ 'permission:department:delete' => '部门删除',
+ 'permission:position:index' => '岗位列表',
+ 'permission:position:save' => '岗位新增',
+ 'permission:position:update' => '岗位编辑',
+ 'permission:position:delete' => '岗位删除',
+ 'permission:position:data_permission' => '设置岗位数据权限',
+ 'permission:leader:index' => '部门领导列表',
+ 'permission:leader:save' => '新增部门领导',
+ 'permission:leader:delete' => '部门领导移除',
+ ];
+ $i18n = [
+ 'baseMenu.permission.departmentList',
+ 'baseMenu.permission.departmentCreate',
+ 'baseMenu.permission.departmentSave',
+ 'baseMenu.permission.departmentDelete',
+ 'baseMenu.permission.positionList',
+ 'baseMenu.permission.positionCreate',
+ 'baseMenu.permission.positionSave',
+ 'baseMenu.permission.positionDelete',
+ 'baseMenu.permission.positionDataScope',
+ 'baseMenu.permission.leaderList',
+ 'baseMenu.permission.leaderCreate',
+ 'baseMenu.permission.leaderDelete',
+ ];
+ $i = 0;
+ foreach ($children as $child => $title) {
+ Menu::create([
+ 'name' => $child,
+ 'path' => '/permission/departments',
+ 'meta' => [
+ 'title' => $title,
+ 'type' => 'B',
+ 'i18n' => $i18n[$i],
+ 'hidden' => 1,
+ 'cache' => 1,
+ 'affix' => 0,
+ ],
+ 'parent_id' => $now->id
+ ]);
+ $i++;
+ }
+ }
+}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index ea2e2ebc1..39b582027 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -7,6 +7,7 @@ parameters:
reportUnmatchedIgnoredErrors: false
excludePaths:
- tests/
+ - app/Library/DataPermission/Scope/DataScopes.php
ignoreErrors:
- '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#'
- '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#'
diff --git a/storage/languages/zh_CN/user.php b/storage/languages/zh_CN/user.php
index 714f237bd..23eab4ac2 100644
--- a/storage/languages/zh_CN/user.php
+++ b/storage/languages/zh_CN/user.php
@@ -44,4 +44,5 @@
'old_password_error' => '旧密码错误',
'old_password' => '旧密码',
'password_confirmation' => '确认密码',
+ 'department' => '部门',
];
diff --git a/tests/Feature/Library/DataPermission/DataScopeAspectTest.php b/tests/Feature/Library/DataPermission/DataScopeAspectTest.php
new file mode 100644
index 000000000..ddd7f2e8b
--- /dev/null
+++ b/tests/Feature/Library/DataPermission/DataScopeAspectTest.php
@@ -0,0 +1,160 @@
+factory = $this->createMock(Factory::class);
+ $this->aspect = new DataScopeAspect($this->factory);
+ Context::set(DataScopeAspect::CONTEXT_KEY, null);
+ DataPermissionContext::setOnlyTables([]);
+ }
+
+ protected function tearDown(): void
+ {
+ Context::destroy(DataScopeAspect::CONTEXT_KEY);
+ parent::tearDown();
+ }
+
+ public function testProcessWithAnnotationMetadata(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $annotation = new DataScope();
+ $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[
+ 'class' => [DataScope::class => $annotation],
+ 'method' => [],
+ ]));
+ $pjp->expects(self::once())->method('process')->willReturn('result');
+
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('result', $result);
+ }
+
+ public function testProcessWithBuilderRunSelect(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $pjp->className = Builder::class;
+ $pjp->methodName = 'runSelect';
+
+ $builder = $this->createMock(Builder::class);
+ $builder->from = 'test_table';
+
+ DataPermissionContext::setOnlyTables(['test_table']);
+
+ $pjp->method('getInstance')->willReturn($builder);
+ $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[
+ 'class' => [],
+ 'method' => [],
+ ]));
+ $pjp->expects(self::exactly(1))->method('process')
+ ->willReturn('select_result');
+
+ // 模拟 CurrentUser::ctxUser() 返回用户对象
+ $user = new User();
+ Context::set('current_user', $user);
+
+ $this->factory->expects(self::once())->method('build')->with($builder, $user);
+ Context::set(DataScopeAspect::CONTEXT_KEY, 1);
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('select_result', $result);
+ }
+
+ public function testProcessWithBuilderDelete(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $pjp->className = Builder::class;
+ $pjp->methodName = 'delete';
+ $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[
+ 'class' => [],
+ 'method' => [],
+ ]));
+ $pjp->expects(self::once())->method('process')->willReturn('delete_result');
+
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('delete_result', $result);
+ }
+
+ public function testProcessWithBuilderUpdate(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $pjp->className = Builder::class;
+ $pjp->methodName = 'update';
+ $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[
+ 'class' => [],
+ 'method' => [],
+ ]));
+ $pjp->expects(self::once())->method('process')->willReturn('update_result');
+
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('update_result', $result);
+ }
+
+ public function testProcessWithoutAnnotationOrBuilder(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $pjp->className = 'OtherClass';
+ $pjp->methodName = 'otherMethod';
+ $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[
+ 'class' => [],
+ 'method' => [],
+ ]));
+ $pjp->expects(self::once())->method('process')->willReturn('other_result');
+
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('other_result', $result);
+ }
+
+ public function testHandleDataScopeSetsContextAndCallsProcess(): void
+ {
+ $pjp = $this->createMock(ProceedingJoinPoint::class);
+ $attribute = $this->createMock(DataScope::class);
+ $attribute->method('getDeptColumn')->willReturn('dept_id');
+ $attribute->method('getCreatedByColumn')->willReturn('created_by');
+ $attribute->method('getScopeType')->willReturn(ScopeType::DEPT_CREATED_BY);
+ $attribute->method('getOnlyTables')->willReturn(['table1']);
+ $metaData = new AnnotationMetadata([DataScope::class => $attribute], []);
+ $pjp->method('getAnnotationMetadata')->willReturn($metaData);
+ $pjp->expects(self::once())->method('process')->willReturn('scoped_result');
+
+ $result = $this->aspect->process($pjp);
+ self::assertEquals('scoped_result', $result);
+ self::assertNull(Context::get(DataScopeAspect::CONTEXT_KEY));
+ }
+}
diff --git a/web/package.json b/web/package.json
index ce4c68031..7e815a42a 100644
--- a/web/package.json
+++ b/web/package.json
@@ -47,7 +47,7 @@
"sortablejs": "1.15.6",
"vaul-vue": "^0.3.0",
"vue": "^3.5.13",
- "vue-i18n": "^11.1.2",
+ "vue-i18n": "11.1.2",
"vue-m-message": "^4.0.2",
"vue-router": "^4.5.0",
"vue3-sfc-loader": "^0.9.5",
diff --git a/web/src/components/ma-tree/index.vue b/web/src/components/ma-tree/index.vue
index 03c25661f..1cfc78f90 100644
--- a/web/src/components/ma-tree/index.vue
+++ b/web/src/components/ma-tree/index.vue
@@ -42,7 +42,7 @@ import { useLocalTrans } from '@/hooks/useLocalTrans.ts'
defineOptions({ name: 'MaTree' })
const { treeKey = 'label' } = defineProps<{
- treeKey: string
+ treeKey?: string
}>()
const t = useLocalTrans()
diff --git a/web/src/locales/en[English].yaml b/web/src/locales/en[English].yaml
index e9f95c03d..c111918b4 100644
--- a/web/src/locales/en[English].yaml
+++ b/web/src/locales/en[English].yaml
@@ -175,3 +175,10 @@ dictionary:
base:
systemUser: System user
normalUser: Normal user
+ dataScope:
+ all: Full data permissions
+ deptSelf: Current department data permissions
+ deptTree: Current department and all sub-departments data permissions
+ self: Personal data permissions
+ customDept: Custom department selection data permissions
+ customFunc: Custom function data permissions
diff --git "a/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" "b/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
index 91fba092f..ff6f3ec3c 100644
--- "a/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
+++ "b/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
@@ -175,3 +175,10 @@ dictionary:
base:
systemUser: 系统用户
normalUser: 普通用户
+ dataScope:
+ all: 全部数据权限
+ deptSelf: 本部门数据权限
+ deptTree: 本部门及所有子部门数据权限
+ self: 本人数据权限
+ customDept: 自选部门数据权限
+ customFunc: 自定义函数数据权限
diff --git "a/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" "b/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
index f105f18c8..81264ad1b 100644
--- "a/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
+++ "b/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
@@ -175,3 +175,10 @@ dictionary:
base:
systemUser: 系統用戶
normalUser: 普通用戶
+ dataScope:
+ all: 全部數據權限
+ deptSelf: 本部門數據權限
+ deptTree: 本部門及所有子部門數據權限
+ self: 本人數據權限
+ customDept: 自選部門數據權限
+ customFunc: 自定義數據權限
diff --git a/web/src/modules/base/api/department.ts b/web/src/modules/base/api/department.ts
new file mode 100644
index 000000000..45ca17e52
--- /dev/null
+++ b/web/src/modules/base/api/department.ts
@@ -0,0 +1,36 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+import type { PageList, ResponseStruct } from '#/global'
+
+export interface DepartmentVo {
+ id?: number
+ name?: string
+}
+
+export interface DepartmentSearchVo {
+ name?: string
+ [key: string]: any
+}
+
+export function page(data: DepartmentSearchVo | null = null): Promise>> {
+ return useHttp().get('/admin/department/list?level=1', { params: data })
+}
+
+export function create(data: DepartmentVo): Promise> {
+ return useHttp().post('/admin/department', data)
+}
+
+export function save(id: number, data: DepartmentVo): Promise> {
+ return useHttp().put(`/admin/department/${id}`, data)
+}
+
+export function deleteByIds(ids: number[]): Promise> {
+ return useHttp().delete('/admin/department', { data: ids })
+}
diff --git a/web/src/modules/base/api/leader.ts b/web/src/modules/base/api/leader.ts
new file mode 100644
index 000000000..18ea8d569
--- /dev/null
+++ b/web/src/modules/base/api/leader.ts
@@ -0,0 +1,37 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+import type { PageList, ResponseStruct } from '#/global'
+
+export interface LeaderVo {
+ user_id?: number | null
+ dept_id?: number
+ dept_name?: string
+}
+
+export interface LeaderSearchVo {
+ user_id?: string
+ [key: string]: any
+}
+
+export function page(data: LeaderSearchVo | null = null): Promise>> {
+ return useHttp().get('/admin/leader/list', { params: data })
+}
+
+export function create(data: LeaderVo): Promise> {
+ return useHttp().post('/admin/leader', data)
+}
+
+export function save(id: number, data: LeaderVo): Promise> {
+ return useHttp().put(`/admin/leader/${id}`, data)
+}
+
+export function deleteByDoubleKey(dept_id: number, user_ids: number[]): Promise> {
+ return useHttp().delete('/admin/leader', { data: { dept_id, user_ids } })
+}
diff --git a/web/src/modules/base/api/position.ts b/web/src/modules/base/api/position.ts
new file mode 100644
index 000000000..0069e11b8
--- /dev/null
+++ b/web/src/modules/base/api/position.ts
@@ -0,0 +1,43 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+import type { PageList, ResponseStruct } from '#/global'
+
+export interface PositionVo {
+ id?: number
+ dept_id?: number
+ dept_name?: string
+ name?: string
+ [key: string]: any
+}
+
+export interface PositionSearchVo {
+ name?: string
+ [key: string]: any
+}
+
+export function page(data: PositionSearchVo | null = null): Promise>> {
+ return useHttp().get('/admin/position/list', { params: data })
+}
+
+export function create(data: PositionVo): Promise> {
+ return useHttp().post('/admin/position', data)
+}
+
+export function save(id: number, data: PositionVo): Promise> {
+ return useHttp().put(`/admin/position/${id}`, data)
+}
+
+export function setDataScope(id: number, data: PositionVo): Promise> {
+ return useHttp().put(`/admin/position/${id}/data_permission`, data)
+}
+
+export function deleteByIds(ids: number[]): Promise> {
+ return useHttp().delete('/admin/position', { data: ids })
+}
diff --git a/web/src/modules/base/api/user.ts b/web/src/modules/base/api/user.ts
index 4121a2f1f..2b609412c 100644
--- a/web/src/modules/base/api/user.ts
+++ b/web/src/modules/base/api/user.ts
@@ -25,6 +25,9 @@ export interface UserVo {
backend_setting?: Record
remark?: string
password?: string
+ policy?: any
+ department?: any[]
+ position?: any[]
}
export interface UserSearchVo {
diff --git a/web/src/modules/base/locales/en[English].yaml b/web/src/modules/base/locales/en[English].yaml
index 550dd048a..c164c33e3 100644
--- a/web/src/modules/base/locales/en[English].yaml
+++ b/web/src/modules/base/locales/en[English].yaml
@@ -8,15 +8,17 @@ 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
+ dataScope: Data Scope
setRole: Set role
+ setDataScope: Set data scope
setRoleSuccess: The role was set successfully
initPassword: Init password
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
@@ -112,6 +114,27 @@ baseMenu:
menuSave: Menu save
menuUpdate: Menu updated
menuDelete: Menu delete
+ department: Department
+ departmentList: Department list
+ departmentCreate: Add department
+ departmentSave: Edit department
+ departmentDelete: Delete department
+ positionList: Post list
+ positionCreate: Add post
+ positionSave: Edit post
+ positionDelete: Delete post
+ positionDataScope: Set post data scope
+basePost:
+ name: Post Name
+ belongPost: Belongs to Position
+ belongDept: Belongs to Department
+ created_at: Creation Time
+ updated_at: Update Time
+ dataScope: Data Permissions
+ selectDept: Selected Departments
+ callFunc: Function Call Name
+ placeholder:
+ name: Please enter position name
log:
index: Log Management
userLoginLog: Login Log
diff --git "a/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" "b/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
index b5698cf03..873f5029d 100644
--- "a/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
+++ "b/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml"
@@ -3,6 +3,8 @@ baseUserManage:
username: 用户名
nickname: 昵称
phone: 手机
+ dept: 所属部门
+ post: 部门岗位
email: 邮箱
password: 密码
userType: 用户类型
@@ -10,7 +12,9 @@ baseUserManage:
signed: 个人签名
mainTitle: 用户管理
subTitle: 提供用户添加、编辑、删除功能,超管不可修改。
+ dataScope: 数据权限
setRole: 赋予角色
+ setDataScope: 设置数据权限
setRoleSuccess: 用户角色设置成功
initPassword: 初始化密码
setPassword: 是否将用户密码重置为[123456]?
@@ -124,6 +128,43 @@ baseOperationLog:
ip: 请求IP
created_at: 创建时间
updated_at: 更新时间
+baseDepartment:
+ name: 部门名称
+ created_at: 创建时间
+ updated_at: 更新时间
+ leaderCount: 负责人数
+ positionsCount: 岗位数
+ usersCount: 人员数
+ parentDepartment: 上级部门
+ page:
+ mainTitle: 部门管理
+ subTitle: 提供公司、机构的人员部门管理、以及部门负责人。
+ setLeader: 设置负责人
+ previewUser: 查看用户
+ managePost: 管理岗位
+ placeholder:
+ name: 请输入部门名称
+ parentDepartment: 请选择上级部门
+ error:
+ selfNotParent: 不可选择自己作为上级部门,已还原默认数据
+basePost:
+ name: 岗位名称
+ belongPost: 所属岗位
+ belongDept: 所属部门
+ created_at: 创建时间
+ updated_at: 更新时间
+ dataScope: 数据权限
+ selectDept: 自选部门
+ callFunc: 调用函数名
+ placeholder:
+ name: 请输入岗位名称
+baseDeptLeader:
+ belongDept: 所属部门
+ username: 用户名
+ nickname: 昵称
+ user_id: 选择用户
+ placeholder:
+ user_id: 请选择要设置为负责人的用户
baseMenu:
permission:
index: 权限管理
@@ -147,6 +188,20 @@ baseMenu:
menuSave: 菜单保存
menuUpdate: 菜单更新
menuDelete: 菜单删除
+ department: 部门管理
+ departmentList: 部门列表
+ departmentCreate: 新增部门
+ departmentSave: 编辑部门
+ departmentDelete: 删除部门
+ positionList: 岗位列表
+ positionCreate: 新增岗位
+ positionSave: 编辑岗位
+ positionDelete: 删除岗位
+ positionDataScope: 设置岗位数据权限
+ leaderList: 部门领导列表
+ leaderCreate: 新增部门领导
+ leaderSave: 编辑部门领导
+ leaderDelete: 删除部门领导
log:
index: 日志管理
userLoginLog: 登录日志
diff --git "a/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" "b/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
index 46b315a38..8bf2c041a 100644
--- "a/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
+++ "b/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml"
@@ -10,7 +10,9 @@ baseUserManage:
role: 角色
mainTitle: 用戶管理
subTitle: 提供使用者添加、編輯、刪除功能,超管不可修改。
+ dataScope: 數據權限
setRole: 賦予角色
+ setDataScope: 設置數據權限
setRoleSuccess: 用戶角色設置成功
initPassword: 重設密碼
setPassword: 是否將密碼重設為[123456]?
@@ -112,6 +114,27 @@ baseMenu:
menuSave: 菜單管保存
menuUpdate: 菜單管更新
menuDelete: 菜單管刪除
+ department: 部門管理
+ departmentList: 部門列表
+ departmentCreate: 新增部門
+ departmentSave: 編輯部門
+ departmentDelete: 刪除部門
+ positionList: 職位列表
+ positionCreate: 新增職位
+ positionSave: 編輯職位
+ positionDelete: 刪除職位
+ positionDataScope: 設置職位數據權限
+basePost:
+ name: 職位名稱
+ belongPost: 所屬職位
+ belongDept: 所屬部門
+ created_at: 創建時間
+ updated_at: 更新時間
+ dataScope: 數據權限
+ selectDept: 自選部門
+ callFunc: 調用函數名
+ placeholder:
+ name: 請輸入職位名稱
log:
index: 日志管理
userLoginLog: 登錄日志
diff --git a/web/src/modules/base/views/permission/component/dataScope.vue b/web/src/modules/base/views/permission/component/dataScope.vue
new file mode 100644
index 000000000..99d242f24
--- /dev/null
+++ b/web/src/modules/base/views/permission/component/dataScope.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/department/data/getFormItems.tsx b/web/src/modules/base/views/permission/department/data/getFormItems.tsx
new file mode 100644
index 000000000..28a853880
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/data/getFormItems.tsx
@@ -0,0 +1,64 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+import type { MaFormItem } from '@mineadmin/form'
+import { cloneDeep } from 'lodash-es'
+
+export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, model: any, msg: any): MaFormItem[] {
+ const treeSelectRef = ref()
+ const deptList = ref([])
+ const sourceModel = cloneDeep(model)
+
+ if (formType === 'add') {
+ model.parent_id = 0
+ }
+
+ useHttp().get('/admin/department/list?level=1').then((res: any) => {
+ deptList.value = res.data.list
+ deptList.value.unshift({ id: 0, name: '顶级部门', value: 0 } as any)
+ })
+
+ return [
+ {
+ label: () => t('baseDepartment.parentDepartment'), prop: 'parent_id',
+ render: () => (
+ {
+ if (val === model.id) {
+ msg.error(t('baseDepartment.error.selfNotParent'))
+ model.parent_id = sourceModel.parent_id
+ }
+ }}
+ >
+
+ ),
+ renderProps: {
+ class: 'w-full',
+ placeholder: t('baseDepartment.placeholder.parentDepartment'),
+ },
+ },
+ {
+ label: () => t('baseDepartment.name'),
+ prop: 'name',
+ render: 'input',
+ renderProps: {
+ placeholder: t('form.pleaseInput', { msg: t('baseDepartment.name') }),
+ },
+ itemProps: {
+ rules: [{ required: true, message: t('form.requiredInput', { msg: t('baseDepartment.placeholder.name') }) }],
+ },
+ },
+ ]
+}
diff --git a/web/src/modules/base/views/permission/department/data/getSearchItems.tsx b/web/src/modules/base/views/permission/department/data/getSearchItems.tsx
new file mode 100644
index 000000000..2e5151200
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/data/getSearchItems.tsx
@@ -0,0 +1,21 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+
+import type { MaSearchItem } from '@mineadmin/search'
+
+export default function getSearchItems(t: any): MaSearchItem[] {
+ return [
+ {
+ label: () => t('baseDepartment.name'),
+ prop: 'name',
+ render: 'input',
+ },
+ ]
+}
diff --git a/web/src/modules/base/views/permission/department/data/getTableColumns.tsx b/web/src/modules/base/views/permission/department/data/getTableColumns.tsx
new file mode 100644
index 000000000..ea16bc823
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/data/getTableColumns.tsx
@@ -0,0 +1,106 @@
+/**
+ * MineAdmin is committed to providing solutions for quickly building web applications
+ * Please view the LICENSE file that was distributed with this source code,
+ * For the full copyright and license information.
+ * Thank you very much for using MineAdmin.
+ *
+ * @Author X.Mo
+ * @Link https://github.com/mineadmin
+ */
+import type { MaProTableColumns, MaProTableExpose } from '@mineadmin/pro-table'
+import type { UseDialogExpose } from '@/hooks/useDialog.ts'
+
+import { useMessage } from '@/hooks/useMessage.ts'
+import { deleteByIds } from '~/base/api/department.ts'
+import { ResultCode } from '@/utils/ResultCode.ts'
+import hasAuth from '@/utils/permission/hasAuth.ts'
+
+export default function getTableColumns(dialog: UseDialogExpose, formRef: any, t: any): MaProTableColumns[] {
+ const msg = useMessage()
+
+ const showBtn = (auth: string | string[]) => {
+ return hasAuth(auth)
+ }
+
+ return [
+ // 多选列
+ { type: 'selection', showOverflowTooltip: false, label: () => t('crud.selection') },
+ // 普通列
+ { label: () => t('baseDepartment.name'), prop: 'name', align: 'left' },
+ { label: () => t('baseDepartment.leaderCount'), prop: 'leader', cellRender: ({ row }) => row.leader?.length ?? 0 },
+ { label: () => t('baseDepartment.positionsCount'), prop: 'positions', cellRender: ({ row }) => row.positions?.length ?? 0 },
+ { label: () => t('baseDepartment.usersCount'), prop: 'users', cellRender: ({ row }) => row.department_users?.length ?? 0 },
+ { label: () => t('baseDepartment.created_at'), prop: 'created_at', width: 200 },
+ { label: () => t('baseDepartment.updated_at'), prop: 'updated_at', width: 200 },
+
+ // 操作列
+ {
+ type: 'operation',
+ label: () => t('crud.operation'),
+ width: '180px',
+ operationConfigure: {
+ actions: [
+ {
+ name: 'setLeader',
+ show: () => showBtn('permission:leader:index'),
+ icon: 'material-symbols:checklist-rounded',
+ text: () => t('baseDepartment.page.setLeader'),
+ onClick: ({ row }) => {
+ dialog.setTitle(t('baseDepartment.page.setLeader'))
+ dialog.setAttr({ width: '55%' })
+ dialog.open({ formType: 'setLeader', data: row })
+ },
+ },
+ {
+ name: 'managePost',
+ icon: 'material-symbols:position-bottom-right-outline-rounded',
+ show: () => showBtn('permission:position:index'),
+ text: () => t('baseDepartment.page.managePost'),
+ onClick: ({ row }) => {
+ dialog.setTitle(t('baseDepartment.page.managePost'))
+ dialog.setAttr({ width: '55%' })
+ dialog.open({ formType: 'position', data: row })
+ },
+ },
+ {
+ name: 'viewUser',
+ icon: 'uil:users-alt',
+ show: () => showBtn('permission:department:update'),
+ text: () => t('baseDepartment.page.previewUser'),
+ onClick: ({ row }) => {
+ dialog.setTitle(t('baseDepartment.page.previewUser'))
+ dialog.setAttr({ width: '55%' })
+ dialog.open({ formType: 'viewUser', data: row })
+ },
+ },
+ {
+ name: 'edit',
+ icon: 'material-symbols:person-edit',
+ show: () => showBtn('permission:department:update'),
+ text: () => t('crud.edit'),
+ onClick: ({ row }) => {
+ dialog.setTitle(t('crud.edit'))
+ dialog.setAttr({ width: '550px' })
+ dialog.open({ formType: 'edit', data: row })
+ },
+ },
+ {
+ name: 'del',
+ show: () => showBtn('permission:department:delete'),
+ icon: 'mdi:delete',
+ text: () => t('crud.delete'),
+ onClick: ({ row }, proxy: MaProTableExpose) => {
+ msg.delConfirm(t('crud.delDataMessage')).then(async () => {
+ const response = await deleteByIds([row.id])
+ if (response.code === ResultCode.SUCCESS) {
+ msg.success(t('crud.delSuccess'))
+ await proxy.refresh()
+ }
+ })
+ },
+ },
+ ],
+ },
+ },
+ ]
+}
diff --git a/web/src/modules/base/views/permission/department/form.vue b/web/src/modules/base/views/permission/department/form.vue
new file mode 100644
index 000000000..8b1e48af7
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/department/index.vue b/web/src/modules/base/views/permission/department/index.vue
new file mode 100644
index 000000000..c8d513445
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+ {
+ maDialog.setTitle(t('crud.add'))
+ maDialog.setAttr({ width: '550px' })
+ maDialog.open({ formType: 'add' })
+ }"
+ >
+ {{ t('crud.add') }}
+
+
+
+
+
+ {{ t('crud.delete') }}
+
+
+
+ {{ t('crud.searchUnFold') }}
+
+
+
+
+
+
+ {
+ maDialog.setTitle(t('crud.add'))
+ maDialog.setAttr({ width: '550px' })
+ maDialog.open({ formType: 'add' })
+ }"
+ >
+ {{ t('crud.add') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/department/position.vue b/web/src/modules/base/views/permission/department/position.vue
new file mode 100644
index 000000000..9fdd9754f
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/position.vue
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+ {
+ postModel.name = ''
+ maDialog.setTitle(t('crud.add'))
+ maDialog.open({ formType: 'add' })
+ }"
+ >
+ {{ t('crud.add') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/department/setLeader.vue b/web/src/modules/base/views/permission/department/setLeader.vue
new file mode 100644
index 000000000..3b6eaed14
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/setLeader.vue
@@ -0,0 +1,291 @@
+
+
+
+
+
+ {
+ form.user_id = form.users.id
+ }"
+ @search-reset="(form: any) => {
+ form.user_id = undefined
+ }"
+ >
+
+
+ {
+ maDialog.setTitle(t('crud.add'))
+ maDialog.open({ formType: 'add' })
+ }"
+ >
+ {{ t('crud.add') }}
+
+
+ {{ t('crud.delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/department/viewUser.vue b/web/src/modules/base/views/permission/department/viewUser.vue
new file mode 100644
index 000000000..83fa07620
--- /dev/null
+++ b/web/src/modules/base/views/permission/department/viewUser.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/modules/base/views/permission/menu/index.vue b/web/src/modules/base/views/permission/menu/index.vue
index 2a8da12c6..4733b35e8 100644
--- a/web/src/modules/base/views/permission/menu/index.vue
+++ b/web/src/modules/base/views/permission/menu/index.vue
@@ -11,7 +11,8 @@
import type { ElForm } from 'element-plus'
import { useMessage } from '@/hooks/useMessage.ts'
import getOnlyWorkAreaHeight from '@/utils/getOnlyWorkAreaHeight.ts'
-import { create, type MenuVo, page, save } from '~/base/api/menu.ts'
+import { create, page, save } from '~/base/api/menu.ts'
+import type { MenuVo } from '~/base/api/menu.ts'
import MenuTree from './menu-tree.vue'
import MenuForm from './menu-form.vue'
diff --git a/web/src/modules/base/views/permission/user/data/getFormItems.tsx b/web/src/modules/base/views/permission/user/data/getFormItems.tsx
index 559ce27ed..d1149c05d 100644
--- a/web/src/modules/base/views/permission/user/data/getFormItems.tsx
+++ b/web/src/modules/base/views/permission/user/data/getFormItems.tsx
@@ -11,16 +11,59 @@ import type { MaFormItem } from '@mineadmin/form'
import type { UserVo } from '~/base/api/user.ts'
import MaUploadImage from '@/components/ma-upload-image/index.vue'
import MaDictRadio from '@/components/ma-dict-picker/ma-dict-radio.vue'
+import type { UseDialogExpose } from '@/hooks/useDialog.ts'
-export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, model: UserVo): MaFormItem[] {
+export default function getFormItems(
+ formType: 'add' | 'edit' = 'add',
+ t: any,
+ model: UserVo,
+ deptData: any,
+ dialog: UseDialogExpose,
+ scopeRef: any,
+): MaFormItem[] {
if (formType === 'add') {
model.password = '123456'
model.status = 1
model.user_type = 100
+ model.department = []
+ model.position = []
}
+ const departmentList = deptData.value.filter((_, index) => index > 0)
+ const deptIds = ref([])
+ const postList = ref([])
+
model.backend_setting = []
+ if (formType === 'edit') {
+ const findNode = (nodes: any[], id: number) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].id === id) {
+ return nodes[i]
+ }
+ if (nodes[i].children) {
+ const node = findNode(nodes[i].children, id)
+ if (node) {
+ return node
+ }
+ }
+ }
+ return null
+ }
+
+ model.department = model.department?.map((item: any) => {
+ // 添加
+ deptIds.value.push(item.id)
+ const post = JSON.parse(JSON.stringify(findNode(departmentList, item.id) ?? null))
+ if (post) {
+ post.disabled = true
+ postList.value.push(post)
+ }
+ return item.id
+ })
+ model.position = model.position?.map((item: any) => item.id)
+ }
+
return [
{
label: () => t('baseUserManage.avatar'),
@@ -73,6 +116,115 @@ export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, m
placeholder: t('form.pleaseInput', { msg: t('baseUserManage.phone') }),
},
},
+ {
+ label: () => t('baseUserManage.dept'),
+ prop: 'department',
+ render: () => ,
+ renderProps: {
+ data: departmentList,
+ multiple: true,
+ filterable: true,
+ clearable: true,
+ props: { label: 'name' },
+ checkStrictly: true,
+ nodeKey: 'id',
+ placeholder: t('form.pleaseInput', { msg: t('baseUserManage.dept') }),
+ onNodeClick: (row: any) => {
+ if (deptIds.value.includes(row.id)) {
+ // 移除
+ deptIds.value = deptIds.value.filter((id: number) => id !== row.id)
+ postList.value = postList.value.filter((item: any) => item.id !== row.id)
+ }
+ else {
+ // 添加
+ deptIds.value.push(row.id)
+ const post = JSON.parse(JSON.stringify(row))
+ post.disabled = true
+ postList.value.push(post)
+ }
+ },
+ onRemoveTag: (value: number) => {
+ const current = postList.value.find((item: any) => item.id === value)?.positions ?? []
+ if (current.length > 0) {
+ current?.map((item: any) => {
+ if (model.position?.includes(item.id)) {
+ model.position?.splice(model.position?.indexOf(item.id), 1)
+ }
+ })
+ }
+ postList.value = postList.value.filter((item: any) => item.id !== value)
+ },
+ onClear: () => {
+ postList.value = []
+ model.position = []
+ },
+ },
+ },
+ {
+ label: () => t('baseUserManage.post'),
+ prop: 'position',
+ render: () => ,
+ cols: { md: 12, xs: 24 },
+ renderProps: {
+ data: postList,
+ defaultExpandAll: true,
+ multiple: true,
+ filterable: true,
+ clearable: true,
+ props: { label: 'name', children: 'positions' },
+ checkStrictly: true,
+ nodeKey: 'id',
+ placeholder: t('form.pleaseInput', { msg: t('baseUserManage.post') }),
+ },
+ },
+ {
+ render: () => ,
+ cols: { md: 12, xs: 24 },
+ renderProps: {
+ type: 'primary',
+ plain: true,
+ onClick: async () => {
+ dialog.setTitle(t('baseUserManage.setDataScope'))
+ dialog.open()
+ if (formType === 'add') {
+ model.policy = {
+ value: [],
+ name: model.username,
+ policy_type: '',
+ }
+ }
+ if (formType === 'edit') {
+ if (model.policy) {
+ model.policy.name = model.username
+ if (model.policy.policy_type === 'CUSTOM_FUNC') {
+ model.policy.func_name = model.policy.value
+ }
+ if (model.policy.policy_type === 'CUSTOM_DEPT') {
+ await nextTick(() => {
+ scopeRef.value.deptRef.elTree?.setCheckedKeys(model.policy.value, true)
+ })
+ }
+ }
+ else {
+ model.policy = { name: model.username }
+ }
+ }
+ },
+ },
+ itemSlots: {
+ label: () => (
+
+ {t('baseUserManage.dataScope')}
+
+
+
+
+ ),
+ },
+ renderSlots: {
+ default: () => t('baseUserManage.setDataScope'),
+ },
+ },
{
label: () => t('baseUserManage.email'),
prop: 'email',
diff --git a/web/src/modules/base/views/permission/user/data/getTableColumns.tsx b/web/src/modules/base/views/permission/user/data/getTableColumns.tsx
index 99f24fc63..2a3ab5b6d 100644
--- a/web/src/modules/base/views/permission/user/data/getTableColumns.tsx
+++ b/web/src/modules/base/views/permission/user/data/getTableColumns.tsx
@@ -86,7 +86,7 @@ export default function getTableColumns(dialog: UseDialogExpose, formRef: any, t
const response = await deleteByIds([row.id])
if (response.code === ResultCode.SUCCESS) {
msg.success(t('crud.delSuccess'))
- proxy.refresh()
+ await proxy.refresh()
}
})
},
diff --git a/web/src/modules/base/views/permission/user/form.vue b/web/src/modules/base/views/permission/user/form.vue
index 253b6a599..c8550f197 100644
--- a/web/src/modules/base/views/permission/user/form.vue
+++ b/web/src/modules/base/views/permission/user/form.vue
@@ -14,17 +14,35 @@ import getFormItems from './data/getFormItems.tsx'
import type { MaFormExpose } from '@mineadmin/form'
import useForm from '@/hooks/useForm.ts'
import { ResultCode } from '@/utils/ResultCode.ts'
+import type { UseDialogExpose } from '@/hooks/useDialog.ts'
+import useDialog from '@/hooks/useDialog.ts'
+import DataScope from '../component/dataScope.vue'
defineOptions({ name: 'permission:user:form' })
-
const { formType = 'add', data = null } = defineProps<{
- formType: 'add' | 'edit'
+ formType?: 'add' | 'edit'
data?: UserVo | null
}>()
const t = useTrans().globalTrans
const userForm = ref()
const userModel = ref({})
+const scopeRef = ref()
+const deptData = inject('deptData')
+
+// 弹窗配置
+const maDialog: UseDialogExpose = useDialog({
+ lgWidth: '750px',
+ ok: () => {
+ if (userModel.value.policy.policy_type === 'CUSTOM_FUNC') {
+ userModel.value.policy.value = [userModel.value.policy.func_name]
+ }
+ if (userModel.value.policy.policy_type === 'CUSTOM_DEPT') {
+ userModel.value.policy.value = scopeRef.value.deptRef.elTree?.getCheckedKeys()
+ }
+ maDialog.close()
+ },
+})
useForm('userForm').then((form: MaFormExpose) => {
if (formType === 'edit' && data) {
@@ -32,9 +50,9 @@ useForm('userForm').then((form: MaFormExpose) => {
userModel.value[key] = data[key]
})
}
- form.setItems(getFormItems(formType, t, userModel.value))
+ form.setItems(getFormItems(formType, t, userModel.value, deptData, maDialog, scopeRef))
form.setOptions({
- labelWidth: '80px',
+ labelWidth: '90px',
})
})
@@ -43,8 +61,6 @@ function add(): Promise {
return new Promise((resolve, reject) => {
create(userModel.value).then((res: any) => {
res.code === ResultCode.SUCCESS ? resolve(res) : reject(res)
- }).catch((err) => {
- reject(err)
})
})
}
@@ -52,10 +68,11 @@ function add(): Promise {
// 更新操作
function edit(): Promise {
return new Promise((resolve, reject) => {
+ if (userModel.value.policy === null) {
+ userModel.value.policy = []
+ }
save(userModel.value.id as number, userModel.value).then((res: any) => {
res.code === ResultCode.SUCCESS ? resolve(res) : reject(res)
- }).catch((err) => {
- reject(err)
})
})
}
@@ -68,7 +85,12 @@ defineExpose({
-
+
+
+
+
+
+