model = new AdminModel(); return null; } public function index(Request $request): Response { $response = $this->initializeBackend($request); if ($response !== null) return $response; if ($request->get('select') ?? $request->post('select')) { $selectRes = $this->select($request); if ($selectRes !== null) return $selectRes; } list($where, $alias, $limit, $order) = $this->queryBuilder(); $query = $this->model ->withoutField('login_failure,password,salt') ->withJoin($this->withJoinTable, $this->withJoinType) ->alias($alias) ->where($where); // 仅返回“顶级角色组(pid=0)”下的管理员(用于远程下拉等场景) $topGroup = $request->get('top_group') ?? $request->post('top_group'); if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) { $query = $query ->join('admin_group_access aga', $alias['admin'] . '.id = aga.uid') ->join('admin_group ag', 'aga.group_id = ag.id') ->where('ag.pid', 0) ->distinct(true); } $res = $query ->order($order) ->paginate($limit); $items = $res->items(); $topGroupUids = $this->getTopGroupUserMap(array_column($items, 'id')); foreach ($items as &$item) { $id = $item['id'] ?? null; if ($id === 1 || isset($topGroupUids[$id])) { $item['commission_rate'] = null; } } unset($item); return $this->success('', [ 'list' => $items, 'total' => $res->total(), 'remark' => get_route_remark(), ]); } /** * 远程下拉(重写:支持 top_group=1 仅返回顶级组管理员) */ protected function _select(): Response { if (empty($this->model)) { return $this->success('', [ 'list' => [], 'total' => 0, ]); } $pk = $this->model->getPk(); $fields = [$pk]; $quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField); foreach ($quickSearchArr as $f) { $f = trim((string) $f); if ($f === '') continue; $f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f; if ($f !== '' && !in_array($f, $fields, true)) { $fields[] = $f; } } list($where, $alias, $limit, $order) = $this->queryBuilder(); $modelTable = strtolower($this->model->getTable()); $mainAlias = ($alias[$modelTable] ?? $modelTable) . '.'; // 联表时避免字段歧义:主表字段统一 select 为 "admin.xxx as xxx" $selectFields = []; foreach ($fields as $f) { $f = trim((string) $f); if ($f === '') continue; $selectFields[] = $mainAlias . $f . ' as ' . $f; } // 联表时避免排序字段歧义:无前缀的字段默认加主表前缀 $qualifiedOrder = []; if (is_array($order)) { foreach ($order as $k => $v) { $k = (string) $k; $qualifiedOrder[str_contains($k, '.') ? $k : ($mainAlias . $k)] = $v; } } $query = $this->model ->field($selectFields) ->alias($alias) ->where($where); $topGroup = $this->request ? ($this->request->get('top_group') ?? $this->request->post('top_group')) : null; if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) { $query = $query ->join('admin_group_access aga', $mainAlias . 'id = aga.uid') ->join('admin_group ag', 'aga.group_id = ag.id') ->where('ag.pid', 0) ->distinct(true); } $res = $query ->order($qualifiedOrder ?: $order) ->paginate($limit); return $this->success('', [ 'list' => $res->items(), 'total' => $res->total(), ]); } 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->normalizeSingleGroup($data); if (!$this->hasSingleGroup($data['group_arr'] ?? null)) { return $this->error('请选择且仅选择一个角色组'); } if ($this->modelValidate) { try { $rules = [ 'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username', 'nickname' => 'required|string', 'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/', 'email' => 'email|unique:admin,email', 'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile', 'group_arr' => 'required|array', ]; $messages = [ 'username.regex' => __('Please input correct username'), 'password.regex' => __('Please input correct password'), ]; Validator::make($data, $rules, $messages)->validate(); } catch (ValidationException $e) { return $this->error($e->getMessage()); } } $passwd = $data['password'] ?? ''; $data = $this->excludeFields($data); $creatorChannelId = $this->getCreatorChannelId(); $groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []); if (!$this->auth->isSuperAdmin()) { if ($creatorChannelId === null || $creatorChannelId === '') { return $this->error(__('You have no permission')); } if ($groupChannelId === null || $groupChannelId === '') { return $this->error('所选角色组未绑定渠道'); } if ((string) $groupChannelId !== (string) $creatorChannelId) { return $this->error('所选角色组渠道与当前账号不一致'); } $data['channel_id'] = $creatorChannelId; $data['parent_admin_id'] = $this->auth->id; } else { $data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId; } $data['invite_code'] = $this->generateUniqueInviteCode(); $requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []); if ($requireCommissionRate) { if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) { return $this->error(__('Please enter a valid commission rate for non-top role group')); } $commissionRes = $this->validateAdminCommissionByGroups($data['group_arr'] ?? [], floatval((string)$data['commission_rate'])); if ($commissionRes !== null) return $commissionRes; } else { $data['commission_rate'] = null; } $result = false; if (!empty($data['group_arr'])) { $authRes = $this->checkGroupAuth($data['group_arr']); if ($authRes !== null) return $authRes; } $this->model->startTrans(); try { $result = $this->model->save($data); if (!empty($data['group_arr'])) { $groupAccess = []; foreach ($data['group_arr'] as $datum) { $groupAccess[] = [ 'uid' => $this->model->id, 'group_id' => $datum, ]; } Db::name('admin_group_access')->insertAll($groupAccess); } $this->model->commit(); if (!empty($passwd)) { $this->model->resetPassword($this->model->id, $passwd); } } 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')); } $dataLimitAdminIds = $this->getDataLimitAdminIds(); if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { return $this->error(__('You have no permission')); } if ($request->method() === 'POST') { $data = $request->post(); if (!$data) { return $this->error(__('Parameter %s can not be empty', [''])); } $data = $this->normalizeSingleGroup($data); if (!$this->hasSingleGroup($data['group_arr'] ?? null)) { return $this->error('请选择且仅选择一个角色组'); } // 未提交分红比例时,若角色组未变更则沿用数据库原值(避免表单单项 number 校验把空串判错) $postedGroups = array_map('intval', $data['group_arr'] ?? []); $rowGroups = array_map('intval', $row->group_arr ?? []); sort($postedGroups); sort($rowGroups); $sameGroups = $postedGroups === $rowGroups; $postedCommission = $data['commission_rate'] ?? null; if (($postedCommission === null || $postedCommission === '') && $sameGroups && $this->isValidCommissionRate($row['commission_rate'] ?? null)) { $data['commission_rate'] = $row['commission_rate']; } // 当前管理员编辑自身时,不允许修改角色组和分红比 if ((int)$this->auth->id === (int)$id) { $postedGroups = $data['group_arr'] ?? []; if (!is_array($postedGroups)) { $postedGroups = []; } $originGroups = $row->group_arr ?? []; sort($postedGroups); sort($originGroups); $postedRate = $data['commission_rate'] ?? null; $originRate = $row['commission_rate'] ?? null; if ($postedGroups !== $originGroups || (string)$postedRate !== (string)$originRate) { return $this->error(__('You cannot modify your own management group!')); } } if ($this->modelValidate) { try { $rules = [ 'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username,' . $id, 'nickname' => 'required|string', 'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/', 'email' => 'email|unique:admin,email,' . $id, 'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id, 'group_arr' => 'required|array', ]; $messages = [ 'username.regex' => __('Please input correct username'), 'password.regex' => __('Please input correct password'), ]; Validator::make($data, $rules, $messages)->validate(); } catch (ValidationException $e) { return $this->error($e->getMessage()); } } if ($this->auth->id == $data['id'] && ($data['status'] ?? '') == 'disable') { return $this->error(__('Please use another administrator account to disable the current account!')); } if (!empty($data['password'])) { $this->model->resetPassword($row->id, $data['password']); } $groupAccess = []; if (!empty($data['group_arr'])) { $checkGroups = []; $rowGroupArr = $row->group_arr ?? []; foreach ($data['group_arr'] as $datum) { if (!in_array($datum, $rowGroupArr)) { $checkGroups[] = $datum; } $groupAccess[] = [ 'uid' => $id, 'group_id' => $datum, ]; } $authRes = $this->checkGroupAuth($checkGroups); if ($authRes !== null) return $authRes; } $data = $this->excludeFields($data); unset($data['invite_code']); $creatorChannelId = $this->getCreatorChannelId(); $groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []); if (!$this->auth->isSuperAdmin()) { if ($creatorChannelId === null || $creatorChannelId === '') { return $this->error(__('You have no permission')); } if ($groupChannelId === null || $groupChannelId === '') { return $this->error('所选角色组未绑定渠道'); } if ((string) $groupChannelId !== (string) $creatorChannelId) { return $this->error('所选角色组渠道与当前账号不一致'); } $data['channel_id'] = $creatorChannelId; } else { $data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId; } $requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []); if ($requireCommissionRate) { if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) { return $this->error(__('Please enter a valid commission rate for non-top role group')); } $commissionRes = $this->validateAdminCommissionByGroups($data['group_arr'] ?? [], floatval((string)$data['commission_rate']), intval((string)$id)); if ($commissionRes !== null) return $commissionRes; } else { $data['commission_rate'] = null; } $result = false; $this->model->startTrans(); try { $result = $row->save($data); Db::name('admin_group_access') ->where('uid', $id) ->delete(); if ($groupAccess) { Db::name('admin_group_access')->insertAll($groupAccess); } $this->model->commit(); } catch (Throwable $e) { $this->model->rollback(); return $this->error($e->getMessage()); } if ($result !== false) { return $this->success(__('Update successful')); } return $this->error(__('No rows updated')); } unset($row['salt'], $row['login_failure']); $row['password'] = ''; return $this->success('', [ 'row' => $row ]); } public function del(Request $request): Response { $response = $this->initializeBackend($request); if ($response !== null) return $response; $where = []; $dataLimitAdminIds = $this->getDataLimitAdminIds(); if ($dataLimitAdminIds) { $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; } $ids = $request->get('ids') ?? $request->post('ids') ?? []; $ids = is_array($ids) ? $ids : []; $where[] = [$this->model->getPk(), 'in', $ids]; $data = $this->model->where($where)->select(); $count = 0; $this->model->startTrans(); try { foreach ($data as $v) { if ($v->id != $this->auth->id) { $count += $v->delete(); Db::name('admin_group_access') ->where('uid', $v['id']) ->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')); } /** * 远程下拉(Admin 无自定义,走父类默认列表) */ public function select(Request $request): Response { return parent::select($request); } private function checkGroupAuth(array $groups): ?Response { if ($this->auth->isSuperAdmin()) { return null; } $authGroups = $this->getManageableGroupIds(); foreach ($groups as $group) { if (!in_array($group, $authGroups)) { return $this->error(__('You have no permission to add an administrator to this group!')); } } return null; } private function getManageableGroupIds(): array { if ($this->auth->isSuperAdmin()) { return Db::name('admin_group')->where('status', 1)->column('id'); } $own = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'); $children = $this->auth->getAdminChildGroups(); return array_values(array_unique(array_merge($own, $children))); } 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; } /** * @param array $groupIds */ private function resolveChannelIdFromPrimaryGroup(array $groupIds): mixed { if ($groupIds === []) { return null; } $gid = $groupIds[0]; if ($gid === null || $gid === '') { return null; } return Db::name('admin_group')->where('id', $gid)->value('channel_id'); } private function generateUniqueInviteCode(): string { $tries = 0; do { $tries++; $code = strtoupper(substr(Random::uuid(), 0, 8)); $exists = Db::name('admin')->where('invite_code', $code)->find(); } while ($exists && $tries < 20); return $code; } private function requireCommissionRate(array $groupIds): bool { if (!$groupIds) { return false; } $count = Db::name('admin_group') ->where('id', 'in', $groupIds) ->where('pid', '<>', 0) ->count(); return $count > 0; } private function isValidCommissionRate(mixed $value): bool { if ($value === null || $value === '') { return false; } $rate = trim((string)$value); if (!preg_match('/^(100(\.00?)?|[0-9]{1,2}(\.[0-9]{1,2})?)$/', $rate)) { return false; } return true; } private function validateAdminCommissionByGroups(array $groupIds, float $currentRate, ?int $excludeAdminId = null): ?Response { if (!$groupIds) { return null; } $groups = Db::name('admin_group') ->where('id', 'in', $groupIds) ->where('pid', '<>', 0) ->column('name', 'id'); foreach ($groups as $groupId => $groupName) { $query = Db::name('admin_group_access')->alias('aga') ->join('admin a', 'aga.uid = a.id') ->where('aga.group_id', intval((string)$groupId)); if ($excludeAdminId !== null) { $query = $query->where('a.id', '<>', $excludeAdminId); } $sum = (float)$query->sum('a.commission_rate'); $remaining = 100 - $sum; if ($currentRate > $remaining + 0.000001) { $exceed = $currentRate - $remaining; return $this->error(sprintf('角色组[%s]分红比例总和不能超过100%%,当前剩余 %.2f%%,本次超出 %.2f%%', $groupName, max(0, $remaining), $exceed)); } } return null; } private function getTopGroupUserMap(array $userIds): array { if (!$userIds) { return []; } $uids = Db::name('admin_group_access')->alias('aga') ->join('admin_group ag', 'aga.group_id = ag.id') ->where('aga.uid', 'in', $userIds) ->where('ag.pid', 0) ->column('aga.uid'); $map = []; foreach ($uids as $uid) { $map[$uid] = true; } return $map; } private function normalizeSingleGroup(array $data): array { if (!array_key_exists('group_arr', $data)) { return $data; } $group = $data['group_arr']; if (is_array($group)) { $data['group_arr'] = array_values(array_filter($group, static function ($item) { return $item !== null && $item !== ''; })); return $data; } if ($group === null || $group === '') { $data['group_arr'] = []; return $data; } $data['group_arr'] = [$group]; return $data; } private function hasSingleGroup(mixed $groups): bool { return is_array($groups) && count($groups) === 1; } }