// +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\controller; use think\App; use think\Exception; use think\facade\Db; use think\exception\ValidateException; use think\facade\View; use app\common\library\Token; use ywxapp\library\AdminAuth; use ywxapp\library\SkinOverlay; use ywxapp\library\SkinVariables; use ywxapp\AddonBase; /** * 后台控制器基类(统一核心后台与插件后台). * * - 核心后台(app\backend\...):afterAuth 启用核心 layout 并注入管理员。 * - 插件后台(addon\*\...):beforeAuth 强制 AdminAuth 与插件视图目录, * afterAuth 复用核心 layout,fetch 叠加皮肤层与配色变量。 * * 公共逻辑(属性 / buildparams / 数据权限 / CRUD / 登录登出)统一落地于此, * 原 Backend、AddonBackend、traits\Backend 已并入并删除。 */ abstract class BackendBase extends BaseController { /** * 快速搜索时执行查找的字段. */ protected $searchFields = 'id'; /** * 是否是关联查询. */ protected $relationSearch = false; /** * 是否开启数据限制 * 支持auth/personal * 表示按权限判断/仅限个人 * 默认为禁用,若启用请务必保证表中存在admin_id字段. */ protected $dataLimit = false; /** * 数据限制字段. */ protected $dataLimitField = 'admin_id'; /** * 数据限制开启时自动填充限制字段值 */ protected $dataLimitFieldAutoFill = true; /** * 是否开启Validate验证 */ protected $modelValidate = false; /** * 是否开启模型场景验证 */ protected $modelSceneValidate = false; /** * Multi方法可批量修改的字段. */ protected $multiFields = 'status'; /** * Selectpage可显示的字段. */ protected $selectpageFields = '*'; /** * 前台提交过来,需要排除的字段数据. */ protected $excludeFields = ''; /** * 导入文件首行类型 * 支持comment/name * 表示注释或字段名. */ protected $importHeadType = 'comment'; /** * 视图类实例 * @var \think\View */ protected $view; /** * 构造方法:在 parent 解析容器 auth 之前,先判定本控制器是否为「插件后台」。 * * 插件后台路由经全局分发落在 frontend 应用,容器 auth 默认是前台 Auth。 * 这里用 static::class(实际子类类名,构造前即可获取,不依赖路由 dispatch 时机) * 匹配 addon\<插件>\controller\backend\* 目录约定,命中则把容器 auth 绑成 AdminAuth, * 使 parent 构造取到的 $app->auth 即为后台鉴权实例(isAdmin=true)。 * 核心后台(backend 应用)由 AppService 闭包直接给 AdminAuth,无需此处处理。 * * @param App $app */ public function __construct(App $app) { // 插件后台判定:getNamespace() 只到 addon\<插件> 层(不含 controller\backend 子段), // 无法区分后台/前台控制器,故插件后台必须用 static::class(完整类名构造前即可获取, // 含 addon\<插件>\controller\backend\ 段)来匹配;getNamespace() 仅用于插件上下文兜底。 $class = static::class; if (strpos($class, 'addon\\') === 0 && strpos($class, '\\controller\\backend\\') !== false) { $app->bind('auth', AdminAuth::class); } parent::__construct($app); } /** * 控制器初始化骨架:准备视图实例 -> beforeAuth -> 校验 -> afterAuth. * @return void */ public function _initialize() { $this->view = $this->app->view; $this->beforeAuth(); // 登录与权限校验:未登录时整页请求 302 跳登录页,AJAX 返回 401 由前端跳转 $this->auth->verifyAuth($this->noNeedLogin, $this->noNeedVerify); // 鉴权后置钩子(启用 layout / 注入登录管理员 / 修正 $site 等) $this->afterAuth(); // 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用 $this->assignSite(); // 子类初始化钩子 $this->initialize(); } /** * 控制器初始化方法,子类可通过重写该方法实现自己的初始化逻辑. * @return void */ protected function initialize() {} /** * 鉴权前置钩子:根据上下文自动适配核心 / 插件后台. * @return void */ protected function beforeAuth() { if ($this->isAddonContext()) { // 插件后台安全基线:必须登录,杜绝继承来的免登录白名单 $this->noNeedLogin = []; // 鉴权实例由容器统一解析:本类控制器命名空间为 addon\*, // AppService 的 auth 闭包已据此返回 AdminAuth(无需手动 new)。 // 视图目录切到插件自身 view/backend/ $this->switchAddonViewPath(); } else { // 核心后台处理 } } /** * 鉴权后置钩子:根据上下文自动适配核心 / 插件后台. * @return void */ protected function afterAuth() { if ($this->isAddonContext()) { // 插件后台复用核心后台外壳(裸内容由 layout 包裹 {__CONTENT__}) $this->view->config([ "layout_on" => true, "layout_name" => $this->app->getRootPath() . 'app/backend/view/common/layout.html' ]); } else { $this->view->config([ "layout_on" => true, "layout_name" => 'common/layout' ]); // 核心后台:layout 由后台模板自行 {extend common/layout} 控制(保持原 Backend 注释态,不强制包裹) if ($this->auth && $this->auth->isLogin) { $this->view->assign('member', $this->auth->info); } } } /** * 渲染模板(基类入口). * 插件后台叠加皮肤覆盖层与配色变量;核心后台走默认。 */ protected function fetch($template = '', $vars = [], $replace = [], $config = []) { if ($this->isAddonContext()) { $addon = $this->currentAddon(); $restore = null; if ($addon && $overlay = SkinOverlay::resolve($addon, 'backend')) { $restore = View::getConfig('view_path'); View::config(['view_path' => $overlay]); } $result = View::fetch($template, $vars, $replace, $config); if ($restore !== null) { View::config(['view_path' => $restore]); } // 注入皮肤变量 CSS(Discuz 式配色层,无需改 HTML) $style = SkinVariables::styleTag($addon, 'backend'); if ($style !== '' && ($pos = stripos($result, '')) !== false) { $result = substr($result, 0, $pos) . $style . "\n" . substr($result, $pos); } elseif ($style !== '') { $result = $style . $result; } return $result; } return View::fetch($template, $vars, $replace, $config); } /** * 将站点上下文修正为后台(插件页复用核心 layout 时 $site.app/module 需为 backend). * @return void */ protected function applyBackendSite() { $site = $this->view->getConfig('site') ?? []; if (is_array($site)) { $site['module'] = 'backend'; $site['app'] = 'backend'; $this->view->assign('site', $site); } } /** * 从控制器类名反推插件目录并切换视图根目录到 addon//view/backend/. * @return void */ protected function switchAddonViewPath() { // 重构版 MultiApp 已将 appPath 设为 addon/<插件>/,直接拼视图目录,无需正则 $this->view->config([ 'view_path' => $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR . 'backend' . DIRECTORY_SEPARATOR, ]); } /** * 是否插件后台(addon\*\controller\backend\ 命名空间 + AdminAuth 鉴权). * @return bool */ protected function isAddonAdmin(): bool { return strpos($this->app->getNamespace(), 'addon\\') === 0 && strpos($this->app->getNamespace(), '\\controller\\backend\\') !== false && $this->auth instanceof AdminAuth; } /** * 判断当前控制器是否属于插件上下文(addon\ 命名空间). * @return bool */ protected function isAddonContext(): bool { return strpos($this->app->getNamespace(), 'addon\\') === 0; } /** * 从当前控制器命名空间提取插件名(addon\<插件>\... -> <插件>)。 * 由 appPath(addon/<插件>/)剥 rootPath 取首段,无需正则。 * @return string */ protected function currentAddon(): string { $rel = trim(substr($this->app->getAppPath(), strlen($this->app->getRootPath())), DIRECTORY_SEPARATOR); $seg = explode(DIRECTORY_SEPARATOR, $rel); return $seg[0] === 'addon' && isset($seg[1]) ? $seg[1] : ''; } /** * 赋值到模板. */ protected function assign($name, $value = '') { $this->view->assign($name, $value); return $this; } /** * 是否已登录 * @return bool */ public function isLogin() { return $this->auth->isLogin; } /** * 后台登录入口(免登录). * 渲染登录页(layout(false) 独立整页)。 */ public function login() { if ($this->request->isPost()) { $account = $this->request->post('account/s', ''); $password = $this->request->post('password/s', ''); $captcha = $this->request->post('captcha/s', ''); if (! $this->auth->login($account, $password, $captcha)) { $this->result->error($this->auth->getError()); } $this->result->success('登录成功', ['url' => url('backend/index/index')->build()]); } View::layout(false); return View::fetch(); } /** * 退出登录 */ public function logout() { $this->auth->logout(); $this->result->success('退出成功', ['url' => url('backend/login/login')->build()]); } /** * 设置页面标题(核心 layout 通过 {$title} 显示). * @param string $title */ protected function setTitle(string $title) { $this->view->assign('title', $title); } /** * 排除前台提交过来的字段 * @param $params * @return array */ protected function preExcludeFields($params) { if (is_array($this->excludeFields)) { foreach ($this->excludeFields as $field) { if (array_key_exists($field, $params)) unset($params[$field]); } } else { if (array_key_exists($this->excludeFields, $params)) unset($params[$this->excludeFields]); } return $params; } /** * 查看 */ public function index() { //设置过滤方法 $this->request->filter(['strip_tags']); //如果发送的来源是Selectpage,则转发到Selectpage if ($this->request->request('keyField')) return $this->selectpage(); [$where, $sort, $order, $offset, $limit] = $this->buildparams(); $total = $this->model ->where($where) ->order($sort, $order) ->count(); $list = $this->model ->where($where) ->order($sort, $order) ->limit($offset, $limit) ->select(); $list = $list->toArray(); $result = ['total' => $total, 'rows' => $list]; return $this->result->success($result); } /** * 回收站 */ public function recyclebin() { //设置过滤方法 $this->request->filter(['strip_tags']); if ($this->request->isAjax()) { [$where, $sort, $order, $offset, $limit] = $this->buildparams(); $total = $this->model ->onlyTrashed() ->where($where) ->order($sort, $order) ->count(); $list = $this->model ->onlyTrashed() ->where($where) ->order($sort, $order) ->limit($offset, $limit) ->select(); $result = ['total' => $total, 'rows' => $list]; $this->result->success($result); } } /** * 添加 */ public function create() { if ($this->request->isPost()) { $params = $this->request->post('row/a'); if ($params) { $params = $this->preExcludeFields($params); if ($this->dataLimit && $this->dataLimitFieldAutoFill) $params[$this->dataLimitField] = $this->auth->id; $result = false; Db::startTrans(); try { //是否采用模型验证 if ($this->modelValidate) { $name = str_replace('\\model\\', '\\validate\\', get_class($this->model)); $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate; validate($validate)->scene($this->modelSceneValidate ? 'edit' : $name)->check($params); } $result = $this->model->save($params); Db::commit(); } catch (ValidateException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($result !== false) $this->result->success(); $this->result->error(lang('No rows were inserted')); } $this->result->error(lang('Parameter %s can not be empty', '')); } $this->result->error(lang('Parameter %s can not be empty')); } /** * 编辑 */ public function edit($id = null) { $row = $this->model->get($ids); if (!$row) $this->result->error(lang('No Results were found')); $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { if (!in_array($row[$this->dataLimitField], $adminIds)) $this->result->error(lang('You have no permission')); } if ($this->request->isPost()) { $params = $this->request->post('row/a'); if ($params) { $params = $this->preExcludeFields($params); $result = false; Db::startTrans(); try { //是否采用模型验证 if ($this->modelValidate) { $name = str_replace('\\model\\', '\\validate\\', get_class($this->model)); $validate = is_bool($this->modelValidate) ? $name : $this->modelValidate; $pk = $row->getPk(); if (!isset($params[$pk])) { $params[$pk] = $row->$pk; } validate($validate)->scene($this->modelSceneValidate ? 'edit' : $name)->check($params); } $result = $row->save($params); Db::commit(); } catch (ValidateException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($result !== false) $this->result->success(); $this->result->error(lang('No rows were updated')); } $this->result->error(lang('Parameter %s can not be empty', '')); } $this->view->assign('row', $row); return $this->view->fetch(); } /** * 删除 */ public function del($ids = '') { if ($ids) { $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) $this->model->where($this->dataLimitField, 'in', $adminIds); $list = $this->model->where($pk, 'in', $ids)->select(); $count = 0; Db::startTrans(); try { foreach ($list as $k => $v) { $count += $v->delete(); } Db::commit(); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($count) $this->result->success(); $this->result->error(lang('No rows were deleted')); } $this->result->error(lang('Parameter %s can not be empty')); } /** * 真实删除 */ public function destroy($ids = '') { $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); $where = []; if (is_array($adminIds)) $where[$this->dataLimitField] = $adminIds; if ($ids) $where[$pk] = explode(',', $ids); $count = 0; Db::startTrans(); try { $list = $this->model->onlyTrashed()->where($where)->select(); foreach ($list as $k => $v) { $count += $v->force()->delete(); } Db::commit(); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($count) $this->result->success(); $this->result->error(lang('Parameter %s can not be empty', 'ids')); } /** * 还原 */ public function restore($ids = '') { $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); $where = []; if (is_array($adminIds)) $where[$this->dataLimitField] = $adminIds; if ($ids) $where[$pk] = explode(',', $ids); $count = 0; Db::startTrans(); try { $list = $this->model->onlyTrashed()->where($where)->select(); foreach ($list as $index => $item) { $count += $item->restore(); } Db::commit(); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($count) $this->result->success(); $this->result->error(lang('No rows were updated')); } /** * 批量更新 */ public function multi($ids = '') { $ids = $ids ? $ids : $this->request->param('ids'); if ($ids) { if ($this->request->has('params')) { parse_str($this->request->post('params'), $values); $values = $this->auth->isSuperAdmin() ? $values : array_intersect_key( $values, array_flip(is_array($this->multiFields) ? $this->multiFields : explode(',', $this->multiFields)) ); if ($values) { $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { $this->model->where($this->dataLimitField, 'in', $adminIds); } $count = 0; Db::startTrans(); try { $list = $this->model->where($this->model->getPk(), 'in', $ids)->select(); foreach ($list as $index => $item) { $count += $item->save($values); } Db::commit(); } catch (\PDOException $e) { Db::rollback(); $this->result->error($e->getMessage()); } catch (Exception $e) { Db::rollback(); $this->result->error($e->getMessage()); } if ($count) { $this->result->success(); } else { $this->result->error(lang('No rows were updated')); } } else { $this->result->error(lang('You have no permission')); } } } $this->result->error(lang('Parameter %s can not be empty', 'ids')); } /** * 导入 */ protected function import() { $file = $this->request->request('file'); if (!$file) { $this->result->error(lang('Parameter %s can not be empty', 'file')); } $filePath = app()->getRootPath() . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $file; if (!is_file($filePath)) { $this->result->error(lang('No results were found')); } //实例化reader $ext = pathinfo($filePath, PATHINFO_EXTENSION); if (!in_array($ext, ['csv', 'xls', 'xlsx'])) { $this->result->error(lang('Unknown data format')); } if ($ext === 'csv') { $file = fopen($filePath, 'r'); $filePath = tempnam(sys_get_temp_dir(), 'import_csv'); $fp = fopen($filePath, 'w'); $n = 0; while ($line = fgets($file)) { $line = rtrim($line, "\n\r\0"); $encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']); if ($encoding != 'utf-8') { $line = mb_convert_encoding($line, 'utf-8', $encoding); } if ($n == 0 || preg_match('/^".*"$/', $line)) { fwrite($fp, $line . "\n"); } else { fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n"); } $n++; } fclose($file) || fclose($fp); $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); } elseif ($ext === 'xls') { $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls(); } else { $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); } //导入文件首行类型,默认是注释,如果需要使用字段名称请使用name $importHeadType = isset($this->importHeadType) ? $this->importHeadType : 'comment'; $table = $this->model->db()->getTable(); $database = \think\facade\Config::get('database.database'); $fieldArr = []; $list = Db::query( 'SELECT COLUMN_NAME,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?', [$table, $database] ); foreach ($list as $k => $v) { if ($importHeadType == 'comment') $fieldArr[$v['COLUMN_COMMENT']] = $v['COLUMN_NAME']; else $fieldArr[$v['COLUMN_NAME']] = $v['COLUMN_NAME']; } //加载文件 $insert = []; try { if (!$PHPExcel = $reader->load($filePath)) { $this->result->error(lang('Unknown data format')); } $currentSheet = $PHPExcel->getSheet(0); //读取文件中的第一个工作表 $allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号 $allRow = $currentSheet->getHighestRow(); //取得一共有多少行 $maxColumnNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($allColumn); $fields = []; for ($currentRow = 1; $currentRow <= 1; $currentRow++) { for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) { $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue(); $fields[] = $val; } } for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) { $values = []; for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) { $val = $currentSheet->getCellByColumnAndColumn($currentColumn, $currentRow)->getValue(); $values[] = is_null($val) ? '' : $val; } $row = []; $temp = array_combine($fields, $values); foreach ($temp as $k => $v) { if (isset($fieldArr[$k]) && $k !== '') { $row[$fieldArr[$k]] = $v; } } if ($row) { $insert[] = $row; } } } catch (Exception $exception) { $this->result->error($exception->getMessage()); } if (!$insert) { $this->result->error(lang('No rows were updated')); } try { //是否包含admin_id字段 $has_admin_id = false; foreach ($fieldArr as $name => $key) { if ($key == 'admin_id') { $has_admin_id = true; break; } } if ($has_admin_id) { $auth = $this->auth; foreach ($insert as &$val) { if (!isset($val['admin_id']) || empty($val['admin_id'])) { $val['admin_id'] = $auth->isLogin ? $auth->id : 0; } } } $this->model->saveAll($insert); } catch (\PDOException $exception) { $msg = $exception->getMessage(); if ( preg_match( "/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is", $msg, $matches ) ) { $msg = "导入失败,包含【{$matches[1]}】的记录已存在"; } $this->result->error($msg); } catch (Exception $e) { $this->result->error($e->getMessage()); } $this->result->success(); } /** * 生成查询所需要的条件,排序,分页等信息(含安全加固). * * @param bool|array $searchfields 快速搜索字段 * @return array */ protected function buildparams($searchfields = false) { $searchfields = is_array($searchfields) ? $searchfields : (is_bool($searchfields) && $searchfields !== false ? $this->searchFields : $searchfields); $searchfields = is_string($searchfields) ? explode(',', $searchfields) : $searchfields; $filter = $this->request->get('filter', ''); $op = $this->request->get('op', '', 'trim'); $sort = $this->request->get('sort', 'id'); $order = $this->request->get('order', 'desc'); $offset = $this->request->get('offset', 0); $limit = $this->request->get('limit', 0); $filter = (array)json_decode($filter, true); $op = (array)json_decode($op, true); $filter = $filter ? $filter : []; $where = []; $tableName = ''; $model = $this->model; if (! empty($model)) { // 兼容模型别名 $tableName = $model->getQuery()->getTable(); } $alias = $model && method_exists($model, 'getTable') ? $model->getTable() : ''; $pkField = $model && method_exists($model, 'getPk') ? $model->getPk() : 'id'; foreach ($filter as $k => $v) { // 安全加固:仅允许白名单字段(字母/数字/下划线/点,禁止表达式注入) if (! preg_match('/^[A-Za-z_][A-Za-z0-9_.]*$/', $k)) { continue; } $sym = isset($op[$k]) ? $op[$k] : '='; // 安全加固:限定操作符白名单 if (! in_array($sym, ['=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'RANGE', 'NOT RANGE', 'NULL', 'IS NULL', 'NOT NULL', 'IS NOT NULL', 'FIND_IN_SET'])) { $sym = '='; } if (strtoupper($sym) === 'FIND_IN_SET') { // FIND_IN_SET(col, val) —— 列名同样需校验 if (preg_match('/^[A-Za-z_][A-Za-z0-9_.]*$/', $k)) { $where[] = ['', 'EXP', Db::raw("FIND_IN_SET(`{$k}`, '" . addslashes($v) . "')")]; } continue; } if (stripos($k, '.') === false) { $k = $alias ? ($alias . '.' . $k) : $k; } switch (strtoupper($sym)) { case '=': case '>': case '>=': case '<': case '<=': case 'LIKE': case 'NOT LIKE': $where[] = [$k, $sym, $v]; break; case 'IN': case 'NOT IN': $arr = is_array($v) ? $v : (strpos($v, ',') !== false ? explode(',', $v) : [$v]); $where[] = [$k, $sym, $arr]; break; case 'BETWEEN': case 'NOT BETWEEN': $arr = array_slice(explode(',', $v), 0, 2); if (stripos($v, ',') === false || ! array_filter($arr)) { continue 2; } $where[] = [$k, $sym, $arr]; break; case 'RANGE': case 'NOT RANGE': $v = str_replace(' - ', ',', $v); $arr = array_slice(explode(',', $v), 0, 2); if (stripos($v, ',') === false || ! array_filter($arr)) { continue 2; } //当出现一边为空时改变操作符 if ($arr[0] === '') { $sym = $sym == 'RANGE' ? ' <= ' : '>'; $arr = $arr[1]; } elseif ($arr[1] === '') { $sym = $sym == 'RANGE' ? ' >= ' : '<'; $arr = $arr[0]; } $where[] = [$k, str_replace('RANGE', 'BETWEEN', $sym) . ' time', $arr]; break; case 'NULL': case 'IS NULL': case 'NOT NULL': case 'IS NOT NULL': $where[] = [$k, strtolower(str_replace('IS ', '', $sym))]; break; default: break; } } if (! empty($where)) { $where = function ($query) use ($where) { foreach ($where as $k => $v) { if (is_array($v)) { call_user_func_array([$query, 'where'], $v); } else { $query->where($v); } } }; } return [$where, trim($sort), trim($order), $offset, $limit]; } /** * 获取数据限制的管理员 ID 集合(用于数据权限隔离). * * - dataLimit 为 false:返回 null,表示不限制(默认) * - 'personal':仅当前管理员本人 * - 'auth':可按组织树扩展,此处简化为当前管理员 * * @return array|null */ protected function getDataLimitAdminIds() { if (! $this->dataLimit) { return null; } $adminId = $this->auth->model->id ?? null; return $adminId !== null ? [$adminId] : null; } }