Backend.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // |
  4. // +----------------------------------------------------------------------
  5. // |
  6. // +----------------------------------------------------------------------
  7. // |
  8. // +----------------------------------------------------------------------
  9. // |
  10. // +----------------------------------------------------------------------
  11. // | Original reference: https://gitee.com/karson/fastadmin
  12. // +----------------------------------------------------------------------
  13. // +----------------------------------------------------------------------
  14. // | 后台控制模块
  15. // +----------------------------------------------------------------------
  16. namespace app\common\controller;
  17. use app\admin\library\Auth;
  18. use think\facade\Config;
  19. use think\facade\Event;
  20. use think\facade\Session;
  21. use think\Model;
  22. //定义是后台
  23. define('IN_ADMIN', true);
  24. class Backend extends BaseController
  25. {
  26. //无需登录的方法,同时也就不需要鉴权了
  27. protected $noNeedLogin = [];
  28. //无需鉴权的方法,但需要登录
  29. protected $noNeedRight = [];
  30. //当前登录账号信息
  31. //public $_userinfo;
  32. //Multi方法可批量修改的字段
  33. protected $multiFields = 'status,listorder';
  34. /**
  35. * 权限控制类
  36. * @var Auth
  37. */
  38. protected $auth = null;
  39. //模型对象
  40. protected $modelClass = null;
  41. //快速搜索时执行查找的字段
  42. protected $searchFields = 'id';
  43. //Selectpage可显示的字段
  44. protected $selectpageFields = '*';
  45. //是否是关联查询
  46. protected $relationSearch = false;
  47. //前台提交过来,需要排除的字段数据
  48. protected $excludeFields = "";
  49. /**
  50. * 是否开启数据限制
  51. * 支持auth/personal
  52. * 表示按权限判断/仅限个人
  53. * 默认为禁用,若启用请务必保证表中存在admin_id字段
  54. */
  55. protected $dataLimit = false;
  56. //数据限制字段
  57. protected $dataLimitField = 'admin_id';
  58. //数据限制开启时自动填充限制字段值
  59. protected $dataLimitFieldAutoFill = true;
  60. //是否开启Validate验证
  61. protected $modelValidate = false;
  62. //是否开启模型场景验证
  63. protected $modelSceneValidate = false;
  64. //表查询参数预处理规则(用于 buildTableParames)
  65. protected $tableParamsFilters = ['trim', 'strip_tags'];
  66. /*
  67. * 引入后台控制器的traits
  68. */
  69. use \app\admin\library\traits\Backend;
  70. //初始化
  71. protected function initialize()
  72. {
  73. parent::initialize();
  74. $this->auth = Auth::instance();
  75. $modulename = app()->http->getName();
  76. $controller = preg_replace_callback('/\.[A-Z]/', function ($d) {
  77. return strtolower($d[0]);
  78. }, $this->request->controller());
  79. $controllername = parse_name($controller);
  80. $actionname = strtolower($this->request->action());
  81. $path = $controllername . '/' . $actionname;
  82. // 定义是否Dialog请求
  83. !defined('IS_DIALOG') && define('IS_DIALOG', $this->request->param("dialog") ? true : false);
  84. // 设置当前请求的URI
  85. $this->auth->setRequestUri($path);
  86. // 检测是否需要验证登录
  87. if (!$this->auth->match($this->noNeedLogin)) {
  88. if (!$this->auth->isLogin()) {
  89. Event::trigger('admin_nologin', $this);
  90. $url = Session::get('referer');
  91. $url = $url ?: $this->request->url();
  92. if (in_array($this->request->pathinfo(), ['', '/', 'index/index'])) {
  93. $this->redirect((string) url('index/login'), 302, ['referer' => $url]);
  94. exit;
  95. }
  96. $this->error('请先登陆', (string) url('index/login', ['url' => $url]));
  97. }
  98. // 是否是超级管理员
  99. define('IS_ROOT', $this->auth->isAdministrator());
  100. $forbiddenIps = config::get('site.admin_forbid_ip');
  101. if ($forbiddenIps) {
  102. $ip = $this->request->ip();
  103. // 检查IP地址访问
  104. $arr = explode(',', $forbiddenIps);
  105. foreach ($arr as $val) {
  106. //是否是IP段
  107. if (strpos($val, '*')) {
  108. if (str_contains($ip, str_replace('.*', '', $val))) {
  109. $this->error('403:你在IP禁止段内,禁止访问!');
  110. }
  111. } else {
  112. //不是IP段,用绝对匹配
  113. if ($ip == $val) {
  114. $this->error('403:IP地址绝对匹配,禁止访问!');
  115. }
  116. }
  117. }
  118. }
  119. if (!IS_ROOT && !$this->auth->match($this->noNeedRight)) {
  120. //检测访问权限
  121. if (!$this->auth->check($path)) {
  122. Event::trigger('admin_nopermission', $this);
  123. $this->error('未授权访问!');
  124. }
  125. }
  126. }
  127. $site = Config::get("site");
  128. // 上传信息配置后
  129. $upload = array_merge(\app\common\model\Config::upload(), ...Event::trigger("upload_config_init"));
  130. $config = [
  131. 'site' => array_intersect_key($site, array_flip(['name', 'indexurl', 'cdnurl', 'version'])),
  132. 'upload' => $upload,
  133. 'modulename' => $modulename,
  134. 'controllername' => $controllername,
  135. 'actionname' => $actionname,
  136. 'jsname' => 'backend/' . str_replace('.', '/', $controllername) . (in_array($actionname, ['index', '_empty']) ? '' : '/' . $actionname),
  137. 'moduleurl' => rtrim((string) url("/", [], false), '/'),
  138. 'referer' => Session::get("referer")
  139. ];
  140. $controllerJsName = 'backend/' . str_replace('.', '/', $controllername);
  141. $actionJsPath = root_path() . 'public/assets/js/' . $config['jsname'] . '.js';
  142. $controllerJsPath = root_path() . 'public/assets/js/' . $controllerJsName . '.js';
  143. if (!in_array($actionname, ['index', '_empty']) && !is_file($actionJsPath) && is_file($controllerJsPath)) {
  144. $config['jsname'] = $controllerJsName;
  145. }
  146. $config = array_merge($config, Config::get("view.tpl_replace_string"), ...Event::trigger("config_init"));
  147. Config::set(array_merge(Config::get('upload'), $upload), 'upload');
  148. //渲染站点配置
  149. $this->assign('site', $site);
  150. //渲染配置信息
  151. $this->assign('config', $config);
  152. //渲染权限对象
  153. $this->assign('auth', $this->auth);
  154. //渲染管理员对象
  155. $this->assign('admin', Session::get('admin'));
  156. $this->view->filter(function ($content) {
  157. return Event::trigger("admin_view_filter", $content, true) ?: $content;
  158. });
  159. }
  160. /**
  161. * 渲染配置信息
  162. * @param mixed $name 键名或数组
  163. * @param mixed $value 值
  164. */
  165. protected function assignconfig($name, $value = '')
  166. {
  167. $this->view->config = array_merge($this->view->config ? $this->view->config : [], is_array($name) ? $name : [$name => $value]);
  168. }
  169. /**
  170. * 构建请求参数
  171. * @param mixed $searchfields 快速查询的字段
  172. * @param array $excludeFields 忽略构建搜索的字段
  173. * @param boolean $relationSearch 是否关联查询
  174. * @return array
  175. */
  176. protected function buildTableParames($searchfields = null, $excludeFields = [], $relationSearch = null)
  177. {
  178. //设置过滤方法
  179. $this->request->filter($this->tableParamsFilters);
  180. $searchfields = is_null($searchfields) ? $this->searchFields : $searchfields;
  181. $relationSearch = is_null($relationSearch) ? $this->relationSearch : $relationSearch;
  182. $search = $this->request->get("search", '');
  183. $filter = $this->request->get("filter", '');
  184. $op = $this->request->get("op", '');
  185. $sort = $this->request->get("sort", !empty($this->modelClass) && $this->modelClass->getPk() ? $this->modelClass->getPk() : 'id');
  186. $order = $this->request->get("order", "DESC");
  187. $limit = $this->request->get("limit/d", 999999);
  188. $page = $this->request->get("page/d", 1);
  189. //新增自动计算分页偏移
  190. $offset = $page ? ($page - 1) * $limit : 0;
  191. if ($this->request->has("offset")) {
  192. $offset = $this->request->get("offset/d", 0);
  193. }
  194. $this->request->withGet([config::get('paginate.var_page') => $page]);
  195. $filter = (array) json_decode($filter, true);
  196. $op = (array) json_decode($op, true);
  197. $filter = $filter ? $filter : [];
  198. $where = [];
  199. $alias = [];
  200. $bind = [];
  201. $name = '';
  202. $aliasName = '';
  203. if (!empty($this->modelClass) && $relationSearch) {
  204. $name = $this->modelClass->getTable();
  205. $alias[$name] = parse_name(basename(str_replace('\\', '/', get_class($this->modelClass))));
  206. $aliasName = $alias[$name] . '.';
  207. }
  208. $sortArr = explode(',', $sort);
  209. foreach ($sortArr as $index => &$item) {
  210. $item = stripos($item, ".") === false ? $aliasName . trim($item) : $item;
  211. }
  212. unset($item);
  213. $sort = implode(',', $sortArr);
  214. $adminIds = $this->getDataLimitAdminIds();
  215. if (is_array($adminIds)) {
  216. $where[] = [$aliasName . $this->dataLimitField, 'in', $adminIds];
  217. }
  218. if ($search) {
  219. $searcharr = is_array($searchfields) ? $searchfields : explode(',', $searchfields);
  220. foreach ($searcharr as $k => &$v) {
  221. $v = stripos($v, ".") === false ? $aliasName . $v : $v;
  222. }
  223. unset($v);
  224. $where[] = [implode("|", $searcharr), "LIKE", "%{$search}%"];
  225. }
  226. $index = 0;
  227. foreach ($filter as $k => $v) {
  228. if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $k)) {
  229. continue;
  230. }
  231. $sym = $op[$k] ?? '=';
  232. if (stripos($k, ".") === false) {
  233. $k = $aliasName . $k;
  234. }
  235. $v = !is_array($v) ? trim($v) : $v;
  236. $sym = strtoupper($op[$k] ?? $sym);
  237. //null和空字符串特殊处理
  238. if (!is_array($v)) {
  239. if (in_array(strtoupper($v), ['NULL', 'NOT NULL'])) {
  240. $sym = strtoupper($v);
  241. }
  242. if (in_array($v, ['""', "''"])) {
  243. $v = '';
  244. $sym = '=';
  245. }
  246. }
  247. $sym = $this->getMappedOperator($sym);
  248. switch ($sym) {
  249. case '=':
  250. case '<>':
  251. $where[] = [$k, $sym, (string) $v];
  252. break;
  253. case 'LIKE':
  254. case 'NOT LIKE':
  255. case 'LIKE %...%':
  256. case 'NOT LIKE %...%':
  257. $where[] = [$k, trim(str_replace('%...%', '', $sym)), "%{$v}%"];
  258. break;
  259. case '>':
  260. case '>=':
  261. case '<':
  262. case '<=':
  263. $where[] = [$k, $sym, intval($v)];
  264. break;
  265. case 'FINDIN':
  266. case 'FINDINSET':
  267. case 'FIND_IN_SET':
  268. $v = is_array($v) ? $v : explode(',', str_replace(' ', ',', $v));
  269. $findArr = array_values($v);
  270. foreach ($findArr as $idx => $item) {
  271. $bindName = "item_" . $index . "_" . $idx;
  272. $bind[$bindName] = $item;
  273. $where[] = ["FIND_IN_SET(:{$bindName}, `" . str_replace('.', '`.`', $k) . "`)", [$bindName => $item]];
  274. }
  275. break;
  276. case 'IN':
  277. case 'IN(...)':
  278. case 'NOT IN':
  279. case 'NOT IN(...)':
  280. $where[] = [$k, str_replace('(...)', '', $sym), is_array($v) ? $v : explode(',', $v)];
  281. break;
  282. case 'BETWEEN':
  283. case 'NOT BETWEEN':
  284. $arr = array_slice(explode(',', $v), 0, 2);
  285. if (stripos($v, ',') === false || !array_filter($arr)) {
  286. continue 2;
  287. }
  288. //当出现一边为空时改变操作符
  289. if ($arr[0] === '') {
  290. $sym = $sym == 'BETWEEN' ? '<=' : '>';
  291. $arr = $arr[1];
  292. } elseif ($arr[1] === '') {
  293. $sym = $sym == 'BETWEEN' ? '>=' : '<';
  294. $arr = $arr[0];
  295. }
  296. $where[] = [$k, $sym, $arr];
  297. break;
  298. case 'RANGE':
  299. case 'NOT RANGE':
  300. $v = str_replace(' - ', ',', $v);
  301. $arr = array_slice(explode(',', $v), 0, 2);
  302. if (stripos($v, ',') === false || !array_filter($arr)) {
  303. continue 2;
  304. }
  305. //当出现一边为空时改变操作符
  306. if ($arr[0] === '') {
  307. $sym = $sym == 'RANGE' ? '<=' : '>';
  308. $arr = $arr[1];
  309. } elseif ($arr[1] === '') {
  310. $sym = $sym == 'RANGE' ? '>=' : '<';
  311. $arr = $arr[0];
  312. }
  313. $tableArr = explode('.', $k);
  314. if (count($tableArr) > 1 && $tableArr[0] != $name && !in_array($tableArr[0], $alias) && !empty($this->modelClass)) {
  315. //修复关联模型下时间无法搜索的BUG
  316. $relation = parse_name($tableArr[0], 1, false);
  317. $alias[$this->modelClass->$relation()->getTable()] = $tableArr[0];
  318. }
  319. $where[] = [$k, str_replace('RANGE', 'BETWEEN', $sym) . ' TIME', $arr];
  320. break;
  321. case 'NULL':
  322. case 'IS NULL':
  323. case 'NOT NULL':
  324. case 'IS NOT NULL':
  325. $where[] = [$k, strtolower(str_replace('IS ', '', $sym))];
  326. break;
  327. default:
  328. break;
  329. }
  330. $index++;
  331. }
  332. if (!empty($this->modelClass)) {
  333. $this->modelClass->alias($alias);
  334. }
  335. $where = function ($query) use ($where, $alias, $bind) {
  336. $query->alias($alias);
  337. foreach ($where as $v) {
  338. if (is_array($v)) {
  339. call_user_func_array([$query, 'where'], $v);
  340. } else {
  341. $query->where($v);
  342. }
  343. }
  344. };
  345. return [$page, $limit, $where, $sort, $order, $offset, $alias, $bind];
  346. }
  347. /**
  348. * 解析操作符
  349. */
  350. protected function getMappedOperator($operator)
  351. {
  352. $operatorMap = [
  353. 'EQ' => '=',
  354. 'NEQ' => '<>',
  355. 'GT' => '>',
  356. 'EGT' => '>=',
  357. 'LT' => '<',
  358. 'ELT' => '<=',
  359. ];
  360. return $operatorMap[$operator] ?? $operator;
  361. }
  362. /**
  363. * 获取数据限制的管理员ID
  364. * 禁用数据限制时返回的是null
  365. * @return mixed
  366. */
  367. protected function getDataLimitAdminIds()
  368. {
  369. if (!$this->dataLimit) {
  370. return null;
  371. }
  372. if ($this->auth->isAdministrator()) {
  373. return null;
  374. }
  375. $adminIds = [];
  376. if (in_array($this->dataLimit, ['auth', 'personal'])) {
  377. $adminIds = $this->dataLimit == 'auth' ? $this->auth->getChildrenAdminIds(true) : [$this->auth->id];
  378. }
  379. return $adminIds;
  380. }
  381. /**
  382. *
  383. * 当前方法只是一个比较通用的搜索匹配,请按需重载此方法来编写自己的搜索逻辑,$where按自己的需求写即可
  384. * 这里示例了所有的参数,所以比较复杂,实现上自己实现只需简单的几行即可
  385. *
  386. */
  387. protected function selectpage()
  388. {
  389. //设置过滤方法
  390. $this->request->filter(['trim', 'strip_tags', 'htmlspecialchars']);
  391. //搜索关键词,客户端输入以空格分开,这里接收为数组
  392. $word = (array) $this->request->request("q_word/a");
  393. //当前页
  394. $page = $this->request->request("pageNumber", 0);
  395. //分页大小
  396. $pagesize = $this->request->request("pageSize");
  397. //搜索条件
  398. $andor = $this->request->request("andOr", "and", "strtoupper");
  399. //排序方式
  400. $orderby = (array) $this->request->request("orderBy/a");
  401. //显示的字段
  402. $field = $this->request->request("showField");
  403. //主键
  404. $primarykey = $this->request->request("keyField");
  405. //主键值
  406. $primaryvalue = $this->request->request("keyValue");
  407. //搜索字段
  408. $searchfield = (array) $this->request->request("searchField/a");
  409. //自定义搜索条件
  410. $custom = (array) $this->request->request("custom/a");
  411. //是否返回树形结构
  412. $istree = $this->request->request("isTree", 0);
  413. $ishtml = $this->request->request("isHtml", 0);
  414. if ($istree) {
  415. $word = [];
  416. $pagesize = 99999;
  417. }
  418. $order = [];
  419. foreach ($orderby as $k => $v) {
  420. $order[$v[0]] = $v[1];
  421. }
  422. $field = $field ?: 'name';
  423. //如果有primaryvalue,说明当前是初始化传值
  424. if ($primaryvalue !== null) {
  425. $where = [$primarykey => explode(',', $primaryvalue)];
  426. $pagesize = 99999;
  427. } else {
  428. $where = function ($query) use ($word, $andor, $field, $searchfield, $custom) {
  429. $logic = $andor == 'AND' ? '&' : '|';
  430. $searchfield = is_array($searchfield) ? implode($logic, $searchfield) : $searchfield;
  431. $searchfield = str_replace(',', $logic, $searchfield);
  432. $word = array_filter(array_unique($word));
  433. if (count($word) == 1) {
  434. $query->where($searchfield, "like", "%" . reset($word) . "%");
  435. } else {
  436. $query->where(function ($query) use ($word, $searchfield) {
  437. foreach ($word as $item) {
  438. $query->whereOr(function ($query) use ($item, $searchfield) {
  439. $query->where($searchfield, "like", "%{$item}%");
  440. });
  441. }
  442. });
  443. }
  444. if ($custom && is_array($custom)) {
  445. foreach ($custom as $k => $v) {
  446. if (is_array($v) && 2 == count($v)) {
  447. $query->where($k, trim($v[0]), $v[1]);
  448. } else {
  449. $query->where($k, '=', $v);
  450. }
  451. }
  452. }
  453. };
  454. }
  455. $adminIds = $this->getDataLimitAdminIds();
  456. if (is_array($adminIds)) {
  457. $this->modelClass = $this->modelClass->where($this->dataLimitField, 'in', $adminIds);
  458. }
  459. $list = [];
  460. $total = $this->modelClass->where($where)->count();
  461. if ($total > 0) {
  462. $fields = is_array($this->selectpageFields) ? $this->selectpageFields : ($this->selectpageFields && $this->selectpageFields != '*' ? explode(',', $this->selectpageFields) : []);
  463. //如果有primaryvalue,说明当前是初始化传值,按照选择顺序排序
  464. if ($primaryvalue !== null && preg_match("/^[a-z0-9_\-]+$/i", $primarykey)) {
  465. $primaryvalue = array_unique(is_array($primaryvalue) ? $primaryvalue : explode(',', $primaryvalue));
  466. //修复自定义data-primary-key为字符串内容时,给排序字段添加上引号
  467. $primaryvalue = array_map(function ($value) {
  468. return '\'' . $value . '\'';
  469. }, $primaryvalue);
  470. $primaryvalue = implode(',', $primaryvalue);
  471. $this->modelClass->orderRaw("FIELD(`{$primarykey}`, {$primaryvalue})");
  472. } else {
  473. $this->modelClass->order($order);
  474. }
  475. $this->modelClass->removeOption('where');
  476. if (is_array($adminIds)) {
  477. $this->modelClass->where($this->dataLimitField, 'in', $adminIds);
  478. }
  479. $datalist = $this->modelClass->where($where)
  480. ->page($page, $pagesize)
  481. ->select();
  482. foreach ($datalist as $item) {
  483. unset($item['password'], $item['salt']);
  484. if ($this->selectpageFields == '*') {
  485. $result = [
  486. $primarykey => $item[$primarykey] ?? '',
  487. $field => $item[$field] ?? '',
  488. ];
  489. } else {
  490. $result = array_intersect_key(($item instanceof Model ? $item->toArray() : (array) $item), array_flip($fields));
  491. }
  492. $result['pid'] = $item['pid'] ?? ($item['parentid'] ?? 0);
  493. //安全处理字符
  494. $result = array_map(function ($value) {
  495. return $value === null ? '' : htmlentities((string) $value);
  496. }, $result);
  497. $list[] = $result;
  498. }
  499. if ($istree && !$primaryvalue) {
  500. $tree = \util\Tree::instance();
  501. $tree->init($list, 'pid');
  502. $list = $tree->getTreeList($tree->getTreeArray(0), $field);
  503. if (!$ishtml) {
  504. foreach ($list as &$item) {
  505. $item = str_replace('&nbsp;', ' ', $item);
  506. }
  507. unset($item);
  508. }
  509. }
  510. }
  511. //这里一定要返回有list这个字段,total是可选的,如果total<=list的数量,则会隐藏分页按钮
  512. return json(['data' => $list, 'count' => $total]);
  513. }
  514. //刷新Token
  515. protected function token()
  516. {
  517. $check = $this->request->checkToken('__token__');
  518. // 刷新token
  519. $token = $this->request->buildToken();
  520. if ($this->request->isAjax()) {
  521. header('__token__: ' . $token);
  522. }
  523. if (false === $check) {
  524. $this->error('令牌错误!', '', ['__token__' => $token]);
  525. }
  526. }
  527. }