chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+917
View File
@@ -0,0 +1,917 @@
<?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;
}
}