Files
YwxAppThink/ywxapp/controller/BackendBase.php
T

918 lines
33 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
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
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 复用核心 layoutfetch 叠加皮肤层与配色变量。
*
* 公共逻辑(属性 / 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, '</head>')) !== 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/<name>/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\<插件>\... -> <插件>)。
* 由 appPathaddon/<插件>/)剥 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;
}
}