diff --git a/application/common/controller/Backend.php b/application/common/controller/Backend.php index fa6e7905..54e6242a 100644 --- a/application/common/controller/Backend.php +++ b/application/common/controller/Backend.php @@ -3,6 +3,7 @@ namespace app\common\controller; use app\admin\library\Auth; +use app\common\library\SelectPage; use think\Config; use think\Controller; use think\Hook; @@ -10,7 +11,6 @@ use think\Lang; use think\Loader; use think\Model; use think\Session; -use fast\Tree; use think\Validate; /** @@ -468,144 +468,27 @@ class Backend extends Controller /** * Selectpage的实现方法 - * - * 当前方法只是一个比较通用的搜索匹配,请按需重载此方法来编写自己的搜索逻辑,$where按自己的需求写即可 - * 这里示例了所有的参数,所以比较复杂,实现上自己实现只需简单的几行即可 - * */ protected function selectpage() { //设置过滤方法 $this->request->filter(['trim', 'strip_tags', 'htmlspecialchars']); - //搜索关键词,客户端输入以空格分开,这里接收为数组 - $word = (array)$this->request->request("q_word/a"); - //当前页 - $page = $this->request->request("pageNumber"); - //分页大小 - $pagesize = $this->request->request("pageSize"); - //搜索条件 - $andor = $this->request->request("andOr", "and", "strtoupper"); - //排序方式 - $orderby = (array)$this->request->request("orderBy/a"); - //显示的字段 - $field = $this->request->request("showField"); - //主键 - $primarykey = $this->request->request("keyField"); - //主键值 - $primaryvalue = $this->request->request("keyValue"); - //搜索字段 - $searchfield = (array)$this->request->request("searchField/a"); - //自定义搜索条件 - $custom = (array)$this->request->request("custom/a"); - //是否返回树形结构 - $istree = $this->request->request("isTree", 0); - $ishtml = $this->request->request("isHtml", 0); - if ($istree) { - $word = []; - $pagesize = 999999; - } - $order = []; - foreach ($orderby as $k => $v) { - $order[$v[0]] = $v[1]; - } - $field = $field ? $field : 'name'; + $selectPage = new SelectPage($this->model, $this->selectpageFields); - //如果有primaryvalue,说明当前是初始化传值 - if ($primaryvalue !== null) { - $where = [$primarykey => ['in', $primaryvalue]]; - $pagesize = 999999; - } else { - $where = function ($query) use ($word, $andor, $field, $searchfield, $custom) { - $logic = $andor == 'AND' ? '&' : '|'; - $searchfield = is_array($searchfield) ? implode($logic, $searchfield) : $searchfield; - $searchfield = str_replace(',', $logic, $searchfield); - $word = array_filter(array_unique($word)); - if (count($word) == 1) { - $query->where($searchfield, "like", "%" . reset($word) . "%"); - } else { - $query->where(function ($query) use ($word, $searchfield) { - foreach ($word as $index => $item) { - $query->whereOr(function ($query) use ($item, $searchfield) { - $query->where($searchfield, "like", "%{$item}%"); - }); - } - }); - } - if ($custom && is_array($custom)) { - foreach ($custom as $k => $v) { - if (is_array($v) && 2 == count($v)) { - $query->where($k, trim($v[0]), $v[1]); - } else { - $query->where($k, '=', $v); - } - } - } - }; - } + // 数据限制 $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { - $this->model->where($this->dataLimitField, 'in', $adminIds); + $selectPage->setDataLimit($this->dataLimit, $this->dataLimitField, $adminIds); } - $list = []; - $total = $this->model->where($where)->count(); - if ($total > 0) { - if (is_array($adminIds)) { - $this->model->where($this->dataLimitField, 'in', $adminIds); - } - $fields = is_array($this->selectpageFields) ? $this->selectpageFields : ($this->selectpageFields && $this->selectpageFields != '*' ? explode(',', $this->selectpageFields) : []); - - //如果有primaryvalue,说明当前是初始化传值,按照选择顺序排序 - if ($primaryvalue !== null && preg_match("/^[a-z0-9_\-]+$/i", $primarykey)) { - $primaryvalue = array_unique(is_array($primaryvalue) ? $primaryvalue : explode(',', $primaryvalue)); - //修复自定义data-primary-key为字符串内容时,给排序字段添加上引号 - $primaryvalue = array_map(function ($value) { - return \think\Db::quote($value); - }, $primaryvalue); - - $primaryvalue = implode(',', $primaryvalue); - - $this->model->orderRaw("FIELD(`{$primarykey}`, {$primaryvalue})"); - } else { - $this->model->order($order); - } - - $datalist = $this->model->where($where) - ->page($page, $pagesize) - ->select(); - - foreach ($datalist as $index => $item) { - unset($item['password'], $item['salt']); - if ($this->selectpageFields == '*') { - $result = [ - $primarykey => $item[$primarykey] ?? '', - $field => $item[$field] ?? '', - ]; - } else { - $result = array_intersect_key(($item instanceof Model ? $item->toArray() : (array)$item), array_flip($fields)); - } - $result['pid'] = isset($item['pid']) ? $item['pid'] : (isset($item['parent_id']) ? $item['parent_id'] : 0); - // 修改为安全的htmlentities调用,兼容php8+版本 - $result = array_map(function ($value) { - return $value === null ? '' : htmlentities((string)$value); - }, $result); - $list[] = $result; - } - if ($istree && !$primaryvalue) { - $tree = Tree::instance(); - $tree->init(collection($list)->toArray(), 'pid'); - $list = $tree->getTreeList($tree->getTreeArray(0), $field); - if (!$ishtml) { - foreach ($list as &$item) { - $item = str_replace(' ', ' ', $item); - } - unset($item); - } - } + try { + $result = $selectPage->execute($this->request->request()); + } catch (\think\Exception $e) { + $this->error($e->getMessage()); } - //这里一定要返回有list这个字段,total是可选的,如果total<=list的数量,则会隐藏分页按钮 - return json(['list' => $list, 'total' => $total]); + + return json($result); } /** diff --git a/application/common/library/SelectPage.php b/application/common/library/SelectPage.php new file mode 100644 index 00000000..fbc47066 --- /dev/null +++ b/application/common/library/SelectPage.php @@ -0,0 +1,433 @@ +', '>', '>=', '<', '<=', + 'like', 'not like', 'notlike', + 'in', 'not in', 'notin', + 'between', 'not between', 'notbetween', + 'null', 'not null', 'notnull', + 'exists', 'not exists', 'notexists', + '> time', '< time', '>= time', '<= time', + 'between time', 'not between time', 'notbetween time', + ]; + + /** + * 允许排序的字段 + * @var array + */ + protected $orderFields = []; + + /** + * @param Model $model 模型实例 + * @param string $fields SelectPage可显示的字段 + */ + public function __construct(Model $model, $fields = '*') + { + $this->model = $model; + $this->selectpageFields = $fields; + $this->allowedFields = array_map('strtolower', $model->getTableFields()); + $this->orderFields = $this->allowedFields; + } + + /** + * 设置数据限制 + * @param bool|string $dataLimit auth/personal/false + * @param string $field 限制字段 + * @param array $adminIds 允许的管理员ID列表 + * @return $this + */ + public function setDataLimit($dataLimit, $field = 'admin_id', array $adminIds = []) + { + $this->dataLimit = $dataLimit; + $this->dataLimitField = $field; + + if (is_array($adminIds) && !empty($adminIds)) { + $this->model->where($this->dataLimitField, 'in', $adminIds); + } + + return $this; + } + + /** + * 执行查询 + * @param array $params 请求参数 + * @return array ['list' => [...], 'total' => int] + */ + public function execute(array $params) + { + $keywordWords = $this->getArrayParam($params, 'q_word'); + $page = $params['pageNumber'] ?? 1; + $pageSize = $params['pageSize'] ?? 10; + $andor = strtoupper($params['andOr'] ?? 'AND'); + $orderBy = $this->getArrayParam($params, 'orderBy'); + $showField = $params['showField'] ?? 'name'; + $keyField = $params['keyField'] ?? ''; + $keyValue = $params['keyValue'] ?? null; + $searchField = $this->getArrayParam($params, 'searchField'); + $custom = $this->getArrayParam($params, 'custom'); + $isTree = (bool)($params['isTree'] ?? 0); + $isHtml = (bool)($params['isHtml'] ?? 0); + + // 树形模式强制参数 + if ($isTree) { + $keywordWords = []; + $pageSize = 999999; + } + + // 验证字段 + $showFields = $this->normalizeField($showField); + $keyFields = $keyField ? $this->normalizeField($keyField) : []; + foreach ($showFields as $f) { + $this->validateField($f); + } + foreach ($keyFields as $f) { + $this->validateField($f); + } + + // 验证搜索字段 + foreach ($searchField as $f) { + $this->validateField($f); + } + + // 验证自定义条件的字段和操作符 + $this->validateCustomConditions($custom); + + // 构建排序 + $order = $this->buildOrder($orderBy); + + // 构建查询条件 + $where = $this->buildWhere( + $keywordWords, + $andor, + $showField, + $searchField, + $custom, + $keyField, + $keyValue + ); + + // 执行总数统计 + $total = $this->model->where($where)->count(); + + if ($total <= 0) { + return ['list' => [], 'total' => 0]; + } + + // 排序处理 + if ($keyValue !== null && $keyField) { + $this->applyPrimaryKeyOrder($keyField, $keyValue); + } else { + $this->model->order($order); + } + + // 执行查询 + $dataList = $this->model->where($where) + ->page($page, $pageSize) + ->select(); + + // 构建结果集 + $list = $this->buildResultList($dataList, $showField, $keyField); + + // 树形结构处理 + if ($isTree && !$keyValue) { + $list = $this->buildTreeList($list, $showField, $isHtml); + } + + return ['list' => $list, 'total' => $total]; + } + + /** + * 标准化字段为数组(支持逗号分隔字符串) + */ + protected function normalizeField($field): array + { + if (is_array($field)) { + return $field; + } + if (is_string($field) && strpos($field, ',') !== false) { + return array_map('trim', explode(',', $field)); + } + return $field !== '' ? [$field] : []; + } + + /** + * 获取数组参数 + */ + protected function getArrayParam(array $params, string $key): array + { + $value = $params[$key] ?? []; + if (is_array($value)) { + return $value; + } + if (is_string($value) && strpos($value, ',') !== false) { + return array_map('trim', explode(',', $value)); + } + if ($value === '' || $value === null) { + return []; + } + return [$value]; + } + + /** + * 验证字段名是否在允许列表中 + */ + protected function validateField(string $field) + { + $field = strtolower($field); + if (!in_array($field, $this->allowedFields, true)) { + throw new Exception('Invalid parameters'); + } + } + + /** + * 验证自定义搜索条件 + */ + protected function validateCustomConditions(array $custom) + { + foreach ($custom as $k => $v) { + $field = strtolower($k); + if (!in_array($field, $this->allowedFields, true)) { + throw new Exception('Invalid parameters'); + } + // 如果操作符是数组形式传入,校验操作符合法性 + if (is_array($v) && count($v) >= 2) { + $operator = strtolower(trim($v[0])); + if (!in_array($operator, self::$allowedOperators, true)) { + throw new Exception('Invalid parameters'); + } + } + } + } + + /** + * 构建排序 + */ + protected function buildOrder(array $orderBy): array + { + $order = []; + foreach ($orderBy as $v) { + if (!isset($v[0], $v[1])) { + continue; + } + $field = strtolower($v[0]); + $direction = strtoupper($v[1]) === 'ASC' ? 'ASC' : 'DESC'; + if (in_array($field, $this->orderFields, true)) { + $order[$field] = $direction; + } + } + return $order; + } + + /** + * 构建查询条件 + */ + protected function buildWhere( + array $keywordWords, + string $andor, + string $showField, + array $searchField, + array $custom, + string $keyField, + $keyValue + ) + { + // 如果有 keyValue,按主键值精确查询 + if ($keyValue !== null && $keyField) { + return [$keyField => ['in', is_array($keyValue) ? $keyValue : explode(',', (string)$keyValue)]]; + } + + return function ($query) use ($keywordWords, $andor, $showField, $searchField, $custom) { + // 关键词搜索 + $searchFields = $this->resolveSearchFields($searchField, $showField, $andor); + $words = array_filter(array_unique($keywordWords)); + if (!empty($words)) { + if (count($words) === 1) { + $query->where($searchFields, 'like', '%' . reset($words) . '%'); + } else { + $query->where(function ($query) use ($words, $searchFields) { + foreach ($words as $word) { + $query->whereOr($searchFields, 'like', '%' . $word . '%'); + } + }); + } + } + + // 自定义条件 + foreach ($custom as $k => $v) { + if (is_array($v) && count($v) >= 2) { + $operator = strtolower(trim($v[0])); + $value = $v[1]; + $query->where(strtolower($k), $operator, $value); + } else { + $query->where(strtolower($k), '=', $v); + } + } + }; + } + + /** + * 解析搜索字段 + */ + protected function resolveSearchFields(array $searchField, string $showField, string $andor): string + { + // 过滤掉不在允许列表中的字段 + $validFields = []; + $inputFields = array_filter(array_map('trim', $searchField)); + + foreach ($inputFields as $field) { + $lowerField = strtolower($field); + if (in_array($lowerField, $this->allowedFields, true)) { + $validFields[] = $lowerField; + } + } + + if (empty($validFields)) { + $lowerShow = strtolower($showField); + if (in_array($lowerShow, $this->allowedFields, true)) { + return $lowerShow; + } + return 'id'; + } + + $logic = $andor === 'AND' ? '&' : '|'; + return implode($logic, $validFields); + } + + /** + * 应用主键排序 + */ + protected function applyPrimaryKeyOrder(string $keyField, $keyValue) + { + $values = is_array($keyValue) ? $keyValue : explode(',', (string)$keyValue); + $values = array_unique(array_filter(array_map(function ($v) { + return trim((string)$v); + }, $values))); + + if (empty($values)) { + return; + } + + $quotedValues = implode(',', array_map(function ($v) { + return Db::quote($v); + }, $values)); + + $this->model->orderRaw("FIELD(`{$keyField}`, {$quotedValues})"); + } + + /** + * 构建结果列表 + */ + protected function buildResultList($dataList, string $showField, string $keyField): array + { + $list = []; + $fields = $this->resolveSelectpageFields(); + + foreach ($dataList as $item) { + $row = $item instanceof Model ? $item->toArray() : (array)$item; + + // 移除敏感字段 + unset($row['password'], $row['salt']); + + if ($this->selectpageFields === '*') { + $result = [ + $keyField => $row[$keyField] ?? '', + $showField => $row[$showField] ?? '', + ]; + } else { + $result = array_intersect_key($row, array_flip($fields)); + } + + // 添加父级ID + $result['pid'] = $row['pid'] ?? ($row['parent_id'] ?? 0); + + // HTML 转义 + $result = array_map(function ($value) { + return $value === null ? '' : htmlentities((string)$value, ENT_QUOTES, 'UTF-8'); + }, $result); + + $list[] = $result; + } + + return $list; + } + + /** + * 构建树形列表 + */ + protected function buildTreeList(array $list, string $showField, bool $isHtml): array + { + $tree = Tree::instance(); + $tree->init($list, 'pid'); + $result = $tree->getTreeList($tree->getTreeArray(0), $showField); + + if (!$isHtml) { + foreach ($result as &$item) { + $item = str_replace(' ', ' ', $item); + } + unset($item); + } + + return $result; + } + + /** + * 解析 SelectPage 显示字段 + */ + protected function resolveSelectpageFields(): array + { + if (is_array($this->selectpageFields)) { + return $this->selectpageFields; + } + if ($this->selectpageFields && $this->selectpageFields !== '*') { + return explode(',', $this->selectpageFields); + } + return []; + } +}