Files
webman-buildadmin/app/admin/controller/auth/Group.php
zhenhui eba80b1bf4 1.修复角色组不能选择权限的报错
2.修复角色创建子角色报权限不够的问题
2026-05-29 10:06:10 +08:00

629 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\admin\controller\auth;
use Throwable;
use ba\Tree;
use support\think\Db;
use support\validation\Validator;
use support\validation\ValidationException;
use app\admin\model\AdminRule;
use app\admin\model\AdminGroup;
use app\common\controller\Backend;
use support\Response;
use Webman\Http\Request;
class Group extends Backend
{
protected string $authMethod = 'allAuthAndOthers';
/**
* 角色组表单分配权限树(仅需登录 + 具备角色组管理相关权限,不依赖菜单规则管理权限)
*/
protected array $noNeedPermission = ['rules'];
protected ?object $model = null;
protected string|array $preExcludeFields = ['create_time', 'update_time'];
protected string|array $quickSearchField = 'name';
protected Tree $tree;
protected array $initValue = [];
protected string $keyword = '';
protected bool $assembleTree = true;
protected array $adminGroups = [];
protected array $manageableGroupIds = [];
protected function initController(Request $request): ?Response
{
$this->model = new AdminGroup();
$this->tree = Tree::instance();
$isTree = $request->get('isTree') ?? $request->post('isTree') ?? true;
$initValue = $request->get('initValue') ?? $request->post('initValue') ?? [];
$this->initValue = is_array($initValue) ? array_filter($initValue) : [];
$this->keyword = $request->get('quickSearch') ?? $request->post('quickSearch') ?? '';
$this->assembleTree = $isTree && !$this->initValue;
$this->adminGroups = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
$this->manageableGroupIds = $this->getManageableGroupIds();
return null;
}
public function index(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($request->get('select') ?? $request->post('select')) {
return $this->select($request);
}
return $this->success('', [
'list' => $this->getGroups($request),
'group' => $this->adminGroups,
'remark' => get_route_remark(),
]);
}
public function add(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($request->method() === 'POST') {
$data = $request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->excludeFields($data);
$pid = $data['pid'] ?? 0;
$pidInt = intval((string)$pid);
if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) {
return $this->error(__('You have no permission'));
}
$inheritRes = $this->applyChannelInheritance($data, $pidInt);
if ($inheritRes !== null) {
return $inheritRes;
}
$rulesRes = $this->handleRules($data, $pidInt);
if ($rulesRes instanceof Response) return $rulesRes;
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
try {
$rules = [
'name' => 'required|string',
'rules' => 'required',
];
$messages = [
'rules.required' => __('Please select rules'),
];
Validator::make($data, $rules, $messages)->validate();
} catch (ValidationException $e) {
throw $e;
}
}
$result = $this->model->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($result !== false) {
return $this->success(__('Added successfully'));
}
return $this->error(__('No rows were added'));
}
return $this->error(__('Parameter error'));
}
public function edit(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$pk = $this->model->getPk();
$id = $request->get($pk) ?? $request->post($pk);
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
$authRes = $this->checkAuth($id);
if ($authRes !== null) return $authRes;
if ($request->method() === 'POST') {
$data = $request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
if (in_array($data['id'], $adminGroup)) {
return $this->error(__('You cannot modify your own management group!'));
}
$data = $this->excludeFields($data);
$pid = $data['pid'] ?? $row['pid'] ?? 0;
$pidInt = intval((string)$pid);
if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) {
return $this->error(__('You have no permission'));
}
$inheritRes = $this->applyChannelInheritance($data, $pidInt);
if ($inheritRes !== null) {
return $inheritRes;
}
$rulesRes = $this->handleRules($data, $pidInt);
if ($rulesRes instanceof Response) return $rulesRes;
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
try {
$rules = [
'name' => 'required|string',
'rules' => 'required',
];
$messages = [
'rules.required' => __('Please select rules'),
];
Validator::make($data, $rules, $messages)->validate();
} catch (ValidationException $e) {
throw $e;
}
}
$result = $row->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($result !== false) {
$this->syncDescendantChannelIds(intval((string)$row['id']));
return $this->success(__('Update successful'));
}
return $this->error(__('No rows updated'));
}
$pidArr = AdminRule::field('pid')
->distinct()
->where('id', 'in', $row->rules)
->select()
->toArray();
$rules = $row->rules ? explode(',', $row->rules) : [];
foreach ($pidArr as $item) {
$ruKey = array_search($item['pid'], $rules);
if ($ruKey !== false) {
unset($rules[$ruKey]);
}
}
$rowData = $row->toArray();
$rowData['rules'] = array_values($rules);
$rowData = $this->enrichChannelDisplay($rowData);
return $this->success('', [
'row' => $rowData
]);
}
/**
* 表单只读展示:根据 channel_id 解析渠道名称与渠道负责人admin.channel_id → admin.username取首个
*/
public function channelBindPreview(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$cid = $request->get('channel_id') ?? $request->post('channel_id');
if ($cid === null || $cid === '') {
return $this->success('', [
'channel_name' => '',
'channel_admin_username' => '',
]);
}
if (!Db::name('channel')->where('id', $cid)->value('id')) {
return $this->error(__('Record not found'));
}
$row = $this->enrichChannelDisplay(['channel_id' => $cid]);
return $this->success('', [
'channel_name' => $row['channel_name'] ?? '',
'channel_admin_username' => $row['channel_admin_username'] ?? '',
]);
}
public function del(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
$ids = is_array($ids) ? $ids : [];
$data = $this->model->where($this->model->getPk(), 'in', $ids)->select();
foreach ($data as $v) {
$authRes = $this->checkAuth($v->id);
if ($authRes !== null) return $authRes;
}
$subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
foreach ($subData as $key => $subDatum) {
if (!in_array($key, $ids)) {
return $this->error(__('Please delete the child element first, or use batch deletion'));
}
}
$adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
$count = 0;
$this->model->startTrans();
try {
foreach ($data as $v) {
if (!in_array($v['id'], $adminGroup)) {
$count += $v->delete();
}
}
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($count) {
return $this->success(__('Deleted successfully'));
}
return $this->error(__('No rows were deleted'));
}
public function select(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$data = $this->getGroups($request, [['status', '=', 1]]);
if ($this->assembleTree) {
$data = $this->tree->assembleTree($this->tree->getTreeArray($data));
}
return $this->success('', [
'options' => $data
]);
}
/**
* 当前登录管理员可分配给下级角色组的菜单权限树(与 Rule::getMenus 一致,走角色组管理权限)
*/
public function rules(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->auth->isSuperAdmin() && !$this->canManageRoleGroups()) {
return $this->error(__('You have no permission'), [], 401);
}
return $this->success('', [
'list' => $this->getAssignableMenuRules($request),
]);
}
/**
* @param int $pidInt 上级角色组 ID大于 0 表示创建/编辑的是下级组,可与当前管理员拥有相同菜单权限
* @return array|Response
*/
private function handleRules(array &$data, int $pidInt = 0)
{
if (!empty($data['rules']) && is_array($data['rules'])) {
$superAdmin = true;
$checkedRules = [];
$allRuleIds = AdminRule::column('id');
foreach ($data['rules'] as $postRuleId) {
if (in_array($postRuleId, $allRuleIds)) {
$checkedRules[] = $postRuleId;
}
}
foreach ($allRuleIds as $ruleId) {
if (!in_array($ruleId, $checkedRules)) {
$superAdmin = false;
}
}
if ($superAdmin && $this->auth->isSuperAdmin()) {
$data['rules'] = '*';
} else {
$ownedRuleIds = $this->normalizeRuleIds($this->auth->getRuleIds());
$checkedRules = $this->normalizeRuleIds($checkedRules);
// 仅限制「非下级」角色组防止子管理员新建与自己平级的全权限组下级组pid>0允许授予相同菜单权限
if (
$pidInt <= 0
&& $ownedRuleIds !== []
&& !array_diff($ownedRuleIds, $checkedRules)
) {
return $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!'));
}
if (array_diff($checkedRules, $ownedRuleIds) && !$this->auth->isSuperAdmin()) {
return $this->error(__('The group permission node exceeds the range that can be allocated'));
}
$data['rules'] = implode(',', $checkedRules);
}
} else {
unset($data['rules']);
}
return $data;
}
/**
* @param array<int|string> $ids
* @return array<int>
*/
private function normalizeRuleIds(array $ids): array
{
$result = [];
foreach ($ids as $id) {
if ($id === '*' || $id === '' || $id === null) {
continue;
}
$result[] = (int) $id;
}
return array_values(array_unique($result));
}
private function getGroups(Request $request, array $where = []): array
{
$pk = $this->model->getPk();
$initKey = $request->get('initKey') ?? $pk;
$absoluteAuth = $request->get('absoluteAuth') ?? false;
if ($this->keyword) {
$keyword = explode(' ', $this->keyword);
foreach ($keyword as $item) {
$where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
}
}
if ($this->initValue) {
$where[] = [$initKey, 'in', $this->initValue];
}
if (!$this->auth->isSuperAdmin()) {
// 仅本人所在角色组 + 其下级子组getManageableGroupIds不包含上级/同级。无父节点在结果集时assembleChild 将该节点作为树根展示,符合「只看我这条线」
$authGroups = $this->manageableGroupIds;
if ($absoluteAuth) {
$authGroups = array_values(array_diff($authGroups, $this->adminGroups));
}
$where[] = ['id', 'in', $authGroups ?: [0]];
}
$data = $this->model->where($where)->select()->toArray();
$channelIds = [];
foreach ($data as $datum) {
$c = $datum['channel_id'] ?? null;
if ($c !== null && $c !== '') {
$channelIds[] = $c;
}
}
$channelNames = [];
if ($channelIds !== []) {
$channelNames = Db::name('channel')->where('id', 'in', array_unique($channelIds))->column('name', 'id');
}
foreach ($data as &$datum) {
$c = $datum['channel_id'] ?? null;
$datum['channel_name'] = ($c !== null && $c !== '') ? ($channelNames[$c] ?? '') : '';
if ($datum['rules']) {
if ($datum['rules'] == '*') {
$datum['rules'] = __('Super administrator');
} else {
$rules = explode(',', $datum['rules']);
if ($rules) {
$rulesFirstTitle = AdminRule::where('id', $rules[0])->value('title');
$datum['rules'] = count($rules) == 1 ? $rulesFirstTitle : __('%first% etc. %count% items', ['%first%' => $rulesFirstTitle, '%count%' => count($rules)]);
}
}
} else {
$datum['rules'] = __('No permission');
}
}
unset($datum);
return $this->assembleTree ? $this->tree->assembleChild($data) : $data;
}
private function checkAuth($groupId): ?Response
{
$authGroups = $this->manageableGroupIds;
if (!$this->auth->isSuperAdmin() && !in_array(intval((string)$groupId), $authGroups, true)) {
return $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~'));
}
return null;
}
private function getManageableGroupIds(): array
{
if ($this->auth->isSuperAdmin()) {
return AdminGroup::where('status', 1)->column('id');
}
$own = array_map('intval', $this->adminGroups);
$children = array_map('intval', $this->auth->getAdminChildGroups());
return array_values(array_unique(array_merge($own, $children)));
}
/**
* 顶级角色组可选渠道;子级继承父级 channel_id不信任客户端提交的子级 channel_id
*
* @param array<string, mixed> $data
*/
private function applyChannelInheritance(array &$data, int $pidInt): ?Response
{
if ($pidInt === 0) {
if (!$this->auth->isSuperAdmin()) {
unset($data['channel_id']);
$cc = $this->getCreatorChannelId();
if ($cc !== null && $cc !== '') {
$data['channel_id'] = $cc;
}
}
$cid = $data['channel_id'] ?? null;
if ($cid !== null && $cid !== '') {
$exists = Db::name('channel')->where('id', $cid)->value('id');
if (!$exists) {
return $this->error(__('Record not found'));
}
}
return null;
}
unset($data['channel_id']);
$parent = Db::name('admin_group')->where('id', $pidInt)->find();
if (!$parent) {
return $this->error(__('Record not found'));
}
$data['channel_id'] = $parent['channel_id'];
return null;
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private function enrichChannelDisplay(array $row): array
{
$row['channel_name'] = '';
$row['channel_admin_username'] = '';
$cid = $row['channel_id'] ?? null;
if ($cid === null || $cid === '') {
return $row;
}
$ch = Db::name('channel')->where('id', $cid)->field(['id', 'name'])->find();
if (!$ch) {
return $row;
}
$row['channel_name'] = $ch['name'] ?? '';
$row['channel_admin_username'] = (string) (Db::name('admin')->where('channel_id', $cid)->order('id', 'asc')->value('username') ?? '');
return $row;
}
private function syncDescendantChannelIds(int $groupId): void
{
$channelId = Db::name('admin_group')->where('id', $groupId)->value('channel_id');
$children = Db::name('admin_group')->where('pid', $groupId)->column('id');
foreach ($children as $childId) {
Db::name('admin_group')->where('id', $childId)->update(['channel_id' => $channelId]);
$this->syncDescendantChannelIds($childId);
}
}
private function getCreatorChannelId(): mixed
{
$currentAdmin = Db::name('admin')
->field(['id', 'channel_id'])
->where('id', $this->auth->id)
->find();
if ($currentAdmin && !empty($currentAdmin['channel_id'])) {
return $currentAdmin['channel_id'];
}
return null;
}
private function canManageRoleGroups(): bool
{
foreach (['auth/group/index', 'auth/group/add', 'auth/group/edit', 'auth/Group/index', 'auth/Group/add', 'auth/Group/edit'] as $routePath) {
if ($this->auth->check($routePath)) {
return true;
}
}
return false;
}
/**
* @return array<int, array<string, mixed>>
*/
private function getAssignableMenuRules(Request $request): array
{
$ids = $this->auth->getRuleIds();
$where = [];
if (!in_array('*', $ids, true)) {
$where[] = ['id', 'in', $ids ?: [0]];
}
$rules = (new AdminRule())
->where($where)
->order(['weigh' => 'desc'])
->select()
->toArray();
$toEnglish = !$this->shouldForceMenuTitleZh($request) && $this->shouldTranslateMenuToEnglish();
foreach ($rules as $idx => $rule) {
$title = $rule['title'] ?? '';
if (is_string($title) && $title !== '') {
$rules[$idx]['title'] = $toEnglish ? $this->menuTitleToEn($title) : $this->menuTitleToZh($title);
}
}
return $this->tree->assembleChild($rules);
}
private function shouldTranslateMenuToEnglish(): bool
{
$lang = function_exists('locale') ? locale() : '';
$normalized = is_string($lang) ? strtolower(str_replace('_', '-', trim($lang))) : '';
return str_starts_with($normalized, 'en');
}
private function shouldForceMenuTitleZh(Request $request): bool
{
$flag = $request->get('force_menu_zh') ?? $request->post('force_menu_zh');
return in_array($flag, [1, '1', true, 'true', 'yes', 'on'], true);
}
private function menuTitleToZh(string $title): string
{
static $zhMap = null;
if (!is_array($zhMap)) {
$mapFile = app_path() . '/common/lang/zh-cn/admin_rule_title.php';
$loaded = is_file($mapFile) ? include $mapFile : [];
$zhMap = is_array($loaded) ? $loaded : [];
}
return isset($zhMap[$title]) && is_string($zhMap[$title]) ? $zhMap[$title] : $title;
}
private function menuTitleToEn(string $title): string
{
static $enMap = null;
if (!is_array($enMap)) {
$mapFile = app_path() . '/common/lang/en/admin_rule_title.php';
$loaded = is_file($mapFile) ? include $mapFile : [];
$enMap = is_array($loaded) ? $loaded : [];
}
return isset($enMap[$title]) && is_string($enMap[$title]) ? $enMap[$title] : $title;
}
}