Backend.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. namespace app\common\controller;
  3. use Throwable;
  4. use think\Model;
  5. use think\facade\Event;
  6. use app\admin\library\Auth;
  7. use app\common\library\token\TokenExpirationException;
  8. class Backend extends Api
  9. {
  10. /**
  11. * 无需登录的方法,访问本控制器的此方法,无需管理员登录
  12. * @var array
  13. */
  14. protected array $noNeedLogin = [];
  15. /**
  16. * 无需鉴权的方法
  17. * @var array
  18. */
  19. protected array $noNeedPermission = [];
  20. /**
  21. * 新增/编辑时,对前端发送的字段进行排除(忽略不入库)
  22. * @var array|string
  23. */
  24. protected array|string $preExcludeFields = [];
  25. /**
  26. * 权限类实例
  27. * @var Auth
  28. */
  29. protected Auth $auth;
  30. /**
  31. * 模型类实例
  32. * @var object
  33. * @phpstan-var Model
  34. */
  35. protected object $model;
  36. /**
  37. * 权重字段
  38. * @var string
  39. */
  40. protected string $weighField = 'weigh';
  41. /**
  42. * 默认排序
  43. * @var string|array id,desc 或 ['id' => 'desc']
  44. */
  45. protected string|array $defaultSortField = [];
  46. /**
  47. * 有序保证
  48. * 查询数据时总是需要指定 ORDER BY 子句,否则 MySQL 不保证排序,即先查到哪行就输出哪行且不保证多次查询中的输出顺序
  49. * 将以下配置作为数据有序保证(用于无排序字段时、默认排序字段相同时继续保持数据有序),不设置将自动使用 pk 字段
  50. * @var string|array id,desc 或 ['id' => 'desc'](有更方便的格式,此处为了保持和 $defaultSortField 属性的配置格式一致)
  51. */
  52. protected string|array $orderGuarantee = [];
  53. /**
  54. * 快速搜索字段
  55. * @var string|array
  56. */
  57. protected string|array $quickSearchField = 'id';
  58. /**
  59. * 是否开启模型验证
  60. * @var bool
  61. */
  62. protected bool $modelValidate = true;
  63. /**
  64. * 是否开启模型场景验证
  65. * @var bool
  66. */
  67. protected bool $modelSceneValidate = false;
  68. /**
  69. * 关联查询方法名,方法应定义在模型中
  70. * @var array
  71. */
  72. protected array $withJoinTable = [];
  73. /**
  74. * 关联查询JOIN方式
  75. * @var string
  76. */
  77. protected string $withJoinType = 'LEFT';
  78. /**
  79. * 开启数据限制
  80. * false=关闭
  81. * personal=仅限个人
  82. * allAuth=拥有某管理员所有的权限时
  83. * allAuthAndOthers=拥有某管理员所有的权限并且还有其他权限时
  84. * parent=上级分组中的管理员可查
  85. * 指定分组中的管理员可查,比如 $dataLimit = 2;
  86. * 启用请确保数据表内存在 admin_id 字段,可以查询/编辑数据的管理员为admin_id对应的管理员+数据限制所表示的管理员们
  87. * @var bool|string|int
  88. */
  89. protected bool|string|int $dataLimit = false;
  90. /**
  91. * 数据限制字段
  92. * @var string
  93. */
  94. protected string $dataLimitField = 'admin_id';
  95. /**
  96. * 数据限制开启时自动填充字段值为当前管理员id
  97. * @var bool
  98. */
  99. protected bool $dataLimitFieldAutoFill = true;
  100. /**
  101. * 查看请求返回的主表字段控制
  102. * @var string|array
  103. */
  104. protected string|array $indexField = ['*'];
  105. /**
  106. * 引入traits
  107. * traits内实现了index、add、edit等方法
  108. */
  109. use \app\admin\library\traits\Backend;
  110. /**
  111. * 初始化
  112. * @throws Throwable
  113. */
  114. public function initialize(): void
  115. {
  116. parent::initialize();
  117. $needLogin = !action_in_arr($this->noNeedLogin);
  118. try {
  119. // 初始化管理员鉴权实例
  120. $this->auth = Auth::instance();
  121. $token = get_auth_token();
  122. if ($token) $this->auth->init($token);
  123. } catch (TokenExpirationException) {
  124. if ($needLogin) {
  125. $this->error(__('Token expiration'), [], 409);
  126. }
  127. }
  128. if ($needLogin) {
  129. if (!$this->auth->isLogin()) {
  130. $this->error(__('Please login first'), [
  131. 'type' => $this->auth::NEED_LOGIN
  132. ], $this->auth::LOGIN_RESPONSE_CODE);
  133. }
  134. if (!action_in_arr($this->noNeedPermission)) {
  135. $routePath = ($this->app->request->controllerPath ?? '') . '/' . $this->request->action(true);
  136. if (!$this->auth->check($routePath)) {
  137. $this->error(__('You have no permission'), [], 401);
  138. }
  139. }
  140. }
  141. // 管理员验权和登录标签位
  142. Event::trigger('backendInit', $this->auth);
  143. }
  144. /**
  145. * 查询参数构建器
  146. * @throws Throwable
  147. */
  148. public function queryBuilder(): array
  149. {
  150. if (empty($this->model)) {
  151. return [];
  152. }
  153. $pk = $this->model->getPk();
  154. $quickSearch = $this->request->get("quickSearch/s", '');
  155. $limit = $this->request->get("limit/d", 10);
  156. $search = $this->request->get("search/a", []);
  157. $initKey = $this->request->get("initKey/s", $pk);
  158. $initValue = $this->request->get("initValue", '');
  159. $initOperator = $this->request->get("initOperator/s", 'in');
  160. $where = [];
  161. $modelTable = strtolower($this->model->getTable());
  162. $alias[$modelTable] = parse_name(basename(str_replace('\\', '/', get_class($this->model))));
  163. $mainTableAlias = $alias[$modelTable] . '.';
  164. // 快速搜索
  165. if ($quickSearch) {
  166. $quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', $this->quickSearchField);
  167. foreach ($quickSearchArr as $k => $v) {
  168. $quickSearchArr[$k] = str_contains($v, '.') ? $v : $mainTableAlias . $v;
  169. }
  170. $where[] = [implode("|", $quickSearchArr), "LIKE", '%' . str_replace('%', '\%', $quickSearch) . '%'];
  171. }
  172. if ($initValue) {
  173. $where[] = [$initKey, $initOperator, $initValue];
  174. $limit = 999999;
  175. }
  176. // 通用搜索组装
  177. foreach ($search as $field) {
  178. if (!is_array($field) || !isset($field['operator']) || !isset($field['field']) || !isset($field['val'])) {
  179. continue;
  180. }
  181. $field['operator'] = $this->getOperatorByAlias($field['operator']);
  182. // 查询关联表字段,转换表别名(驼峰转小写下划线)
  183. if (str_contains($field['field'], '.')) {
  184. $fieldNameParts = explode('.', $field['field']);
  185. $fieldNamePartsLastKey = array_key_last($fieldNameParts);
  186. // 忽略最后一个元素(字段名)
  187. foreach ($fieldNameParts as $fieldNamePartsKey => $fieldNamePart) {
  188. if ($fieldNamePartsKey !== $fieldNamePartsLastKey) {
  189. $fieldNameParts[$fieldNamePartsKey] = parse_name($fieldNamePart);
  190. }
  191. }
  192. $fieldName = implode('.', $fieldNameParts);
  193. } else {
  194. $fieldName = $mainTableAlias . $field['field'];
  195. }
  196. // 日期时间
  197. if (isset($field['render']) && $field['render'] == 'datetime') {
  198. if ($field['operator'] == 'RANGE') {
  199. $datetimeArr = explode(',', $field['val']);
  200. if (!isset($datetimeArr[1])) {
  201. continue;
  202. }
  203. $datetimeArr = array_filter(array_map("strtotime", $datetimeArr));
  204. $where[] = [$fieldName, str_replace('RANGE', 'BETWEEN', $field['operator']), $datetimeArr];
  205. continue;
  206. }
  207. $where[] = [$fieldName, '=', strtotime($field['val'])];
  208. continue;
  209. }
  210. // 范围查询
  211. if ($field['operator'] == 'RANGE' || $field['operator'] == 'NOT RANGE') {
  212. $arr = explode(',', $field['val']);
  213. // 重新确定操作符
  214. if (!isset($arr[0]) || $arr[0] === '') {
  215. $operator = $field['operator'] == 'RANGE' ? '<=' : '>';
  216. $arr = $arr[1];
  217. } elseif (!isset($arr[1]) || $arr[1] === '') {
  218. $operator = $field['operator'] == 'RANGE' ? '>=' : '<';
  219. $arr = $arr[0];
  220. } else {
  221. $operator = str_replace('RANGE', 'BETWEEN', $field['operator']);
  222. }
  223. $where[] = [$fieldName, $operator, $arr];
  224. continue;
  225. }
  226. switch ($field['operator']) {
  227. case '=':
  228. case '<>':
  229. $where[] = [$fieldName, $field['operator'], (string)$field['val']];
  230. break;
  231. case 'LIKE':
  232. case 'NOT LIKE':
  233. $where[] = [$fieldName, $field['operator'], '%' . str_replace('%', '\%', $field['val']) . '%'];
  234. break;
  235. case '>':
  236. case '>=':
  237. case '<':
  238. case '<=':
  239. $where[] = [$fieldName, $field['operator'], intval($field['val'])];
  240. break;
  241. case 'FIND_IN_SET':
  242. if (is_array($field['val'])) {
  243. foreach ($field['val'] as $val) {
  244. $where[] = [$fieldName, 'find in set', $val];
  245. }
  246. } else {
  247. $where[] = [$fieldName, 'find in set', $field['val']];
  248. }
  249. break;
  250. case 'IN':
  251. case 'NOT IN':
  252. $where[] = [$fieldName, $field['operator'], is_array($field['val']) ? $field['val'] : explode(',', $field['val'])];
  253. break;
  254. case 'NULL':
  255. case 'NOT NULL':
  256. $where[] = [$fieldName, strtolower($field['operator']), ''];
  257. break;
  258. }
  259. }
  260. // 数据权限
  261. $dataLimitAdminIds = $this->getDataLimitAdminIds();
  262. if ($dataLimitAdminIds) {
  263. $where[] = [$mainTableAlias . $this->dataLimitField, 'in', $dataLimitAdminIds];
  264. }
  265. return [$where, $alias, $limit, $this->queryOrderBuilder()];
  266. }
  267. /**
  268. * 查询的排序参数构建器
  269. */
  270. public function queryOrderBuilder()
  271. {
  272. $pk = $this->model->getPk();
  273. $order = $this->request->get("order/s") ?: $this->defaultSortField;
  274. if ($order && is_string($order)) {
  275. $order = explode(',', $order);
  276. $order = [$order[0] => $order[1] ?? 'asc'];
  277. }
  278. if (!$this->orderGuarantee) {
  279. $this->orderGuarantee = [$pk => 'desc'];
  280. } elseif (is_string($this->orderGuarantee)) {
  281. $this->orderGuarantee = explode(',', $this->orderGuarantee);
  282. $this->orderGuarantee = [$this->orderGuarantee[0] => $this->orderGuarantee[1] ?? 'asc'];
  283. }
  284. $orderGuaranteeKey = array_key_first($this->orderGuarantee);
  285. if (!array_key_exists($orderGuaranteeKey, $order)) {
  286. $order[$orderGuaranteeKey] = $this->orderGuarantee[$orderGuaranteeKey];
  287. }
  288. return $order;
  289. }
  290. /**
  291. * 数据权限控制-获取有权限访问的管理员Ids
  292. * @throws Throwable
  293. */
  294. protected function getDataLimitAdminIds(): array
  295. {
  296. if (!$this->dataLimit || $this->auth->isSuperAdmin()) {
  297. return [];
  298. }
  299. $adminIds = [];
  300. if ($this->dataLimit == 'parent') {
  301. // 取得当前管理员的下级分组们
  302. $parentGroups = $this->auth->getAdminChildGroups();
  303. if ($parentGroups) {
  304. // 取得分组内的所有管理员
  305. $adminIds = $this->auth->getGroupAdmins($parentGroups);
  306. }
  307. } elseif (is_numeric($this->dataLimit) && $this->dataLimit > 0) {
  308. // 在组内,可查看所有,不在组内,可查看自己的
  309. $adminIds = $this->auth->getGroupAdmins([$this->dataLimit]);
  310. return in_array($this->auth->id, $adminIds) ? [] : [$this->auth->id];
  311. } elseif ($this->dataLimit == 'allAuth' || $this->dataLimit == 'allAuthAndOthers') {
  312. // 取得拥有他所有权限的分组
  313. $allAuthGroups = $this->auth->getAllAuthGroups($this->dataLimit);
  314. // 取得分组内的所有管理员
  315. $adminIds = $this->auth->getGroupAdmins($allAuthGroups);
  316. }
  317. $adminIds[] = $this->auth->id;
  318. return array_unique($adminIds);
  319. }
  320. /**
  321. * 从别名获取原始的逻辑运算符
  322. * @param string $operator 逻辑运算符别名
  323. * @return string 原始的逻辑运算符,无别名则原样返回
  324. */
  325. protected function getOperatorByAlias(string $operator): string
  326. {
  327. $alias = [
  328. 'ne' => '<>',
  329. 'eq' => '=',
  330. 'gt' => '>',
  331. 'egt' => '>=',
  332. 'lt' => '<',
  333. 'elt' => '<=',
  334. ];
  335. return $alias[$operator] ?? $operator;
  336. }
  337. }