Menu.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <?php
  2. namespace common\models\backend;
  3. use common\enums\AppEnum;
  4. use common\enums\StatusEnum;
  5. use common\enums\WhetherEnum;
  6. use common\events\menu\MenuEvent;
  7. use common\helpers\ArrayHelper;
  8. use common\helpers\TreeHelper;
  9. use common\models\base\BaseActiveRecord;
  10. use common\models\backend\MenuCate;
  11. use common\models\rbac\AuthItem;
  12. use PHPUnit\Util\Exception;
  13. use Yii;
  14. use yii\db\Expression;
  15. /**
  16. * This is the model class for table "{{%common_menu}}".
  17. *
  18. * @property int $id
  19. * @property string $title 标题
  20. * @property string $app_id 应用id
  21. * @property int $cate_id 菜单分类
  22. * @property string $addons_name 插件名称
  23. * @property int|null $is_addon 是否插件
  24. * @property int|null $pid 上级id
  25. * @property string|null $url 路由
  26. * @property string|null $icon 样式
  27. * @property int|null $level 级别
  28. * @property int|null $dev 开发者[0:都可见;开发模式可见]
  29. * @property int|null $sort 排序
  30. * @property string|null $params 参数
  31. * @property string $tree 树
  32. * @property int|null $status 状态[-1:删除;0:禁用;1启用]
  33. * @property int|null $created_at 添加时间
  34. * @property int|null $updated_at 修改时间
  35. */
  36. class Menu extends BaseActiveRecord
  37. {
  38. const MENU_CAT_PLATFORM = 1;
  39. const MENU_CAT_MALL = 2;
  40. const STATUS_ARR = ['-1' => '删除', '0' => '禁用', '1' => '启用'];
  41. /**
  42. * {@inheritdoc}
  43. */
  44. public static function tableName()
  45. {
  46. return '{{%menu}}';
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. public function rules()
  52. {
  53. return [
  54. [['cate_id'], 'required'],
  55. [['cate_id', 'is_addon', 'pid', 'level', 'dev', 'sort', 'status', 'created_at', 'updated_at'], 'integer'],
  56. [['params', 'addons_name'], 'safe'],
  57. [['title', 'icon'], 'string', 'max' => 50],
  58. [['app_id'], 'string', 'max' => 20],
  59. [['addons_name', 'url'], 'string', 'max' => 100],
  60. [['tree'], 'string', 'max' => 300],
  61. ];
  62. }
  63. /**
  64. * {@inheritdoc}
  65. */
  66. public function attributeLabels()
  67. {
  68. return [
  69. 'id' => 'ID',
  70. 'title' => '标题',
  71. 'app_id' => '应用id',
  72. 'cate_id' => '菜单分类',
  73. 'addons_name' => '插件名称',
  74. 'is_addon' => '是否插件',
  75. 'pid' => '上级id',
  76. 'url' => '路由',
  77. 'icon' => '样式',
  78. 'level' => '级别',
  79. 'dev' => '开发者[0:都可见;开发模式可见]',
  80. 'sort' => '排序',
  81. 'params' => '参数',
  82. 'tree' => '树',
  83. 'status' => '状态[-1:删除;0:禁用;1启用]',
  84. 'created_at' => '添加时间',
  85. 'updated_at' => '修改时间',
  86. ];
  87. }
  88. //关联模型
  89. public function getChild()
  90. {
  91. return $this->hasMany(self::class, ['pid' => 'id'])->where(['<>', 'status', StatusEnum::DELETE])->orderBy('sort asc,id asc');
  92. }
  93. /**
  94. * @param Menu $parent
  95. */
  96. public function setParent(Menu $parent)
  97. {
  98. $this->parent = $parent;
  99. }
  100. /**
  101. * @param bool $insert
  102. * @return bool
  103. */
  104. public function beforeSave($insert)
  105. {
  106. if ($this->isNewRecord) {
  107. if ($this->pid == 0) {
  108. $this->tree = TreeHelper::defaultTreeKey();
  109. } else {
  110. $parent = $this->parent;
  111. $this->cate_id = $parent->cate_id;
  112. $this->level = $parent->level + 1;
  113. !$this->tree && $this->tree = $parent->tree . TreeHelper::prefixTreeKey($parent->id);
  114. }
  115. !$this->app_id && $this->app_id = $this->cate->app_id;
  116. // !$this->addons_name && $this->addons_name = $this->cate->addons_name;
  117. !isset($this->is_addon) && $this->is_addon = $this->cate->is_addon;
  118. } else {
  119. // 修改父类
  120. if ($this->oldAttributes['pid'] != $this->pid) {
  121. $parent = $this->parent;
  122. if ($this->pid == 0) {
  123. $parent = new self();
  124. $parent = $parent->loadDefaultValues();
  125. $parent->cate_id = $this->cate_id;
  126. }
  127. $this->cate_id = $parent->cate_id;
  128. $level = $parent->level + 1;
  129. $tree = $parent->tree . TreeHelper::prefixTreeKey($parent->id ?? 0);
  130. // 查找所有子级
  131. $list = Yii::$app->services->menu->findChildByID($this->tree, $this->id);
  132. $distanceLevel = $level - $this->level;
  133. // 递归修改
  134. $data = ArrayHelper::itemsMerge($list, $this->id);
  135. $this->recursionUpdate($data, $parent->cate_id, $distanceLevel, $tree);
  136. $this->level = $level;
  137. $this->tree = $tree;
  138. }
  139. }
  140. return parent::beforeSave($insert);
  141. }
  142. /**
  143. * 递归更新数据
  144. *
  145. * @param $data
  146. * @param $distanceLevel
  147. * @param $tree
  148. */
  149. protected function recursionUpdate($data, $cate_id, $distanceLevel, $tree)
  150. {
  151. $updateIds = [];
  152. $itemLevel = '';
  153. $itemTree = '';
  154. foreach ($data as $item) {
  155. $updateIds[] = $item['id'];
  156. empty($itemLevel) && $itemLevel = $item['level'] + $distanceLevel;
  157. empty($itemTree) && $itemTree = str_replace($this->tree, $tree, $item['tree']);
  158. !empty($item['-']) && $this->recursionUpdate($item['-'], $cate_id, $distanceLevel, $tree);
  159. unset($item);
  160. }
  161. !empty($updateIds) && self::updateAll(['cate_id' => $cate_id, 'level' => $itemLevel, 'tree' => $itemTree],
  162. ['in', 'id', $updateIds]);
  163. }
  164. /**
  165. *
  166. * @Author bing
  167. * @DateTime 2021-08-26 10:56:02
  168. * @param integer $cate_id
  169. * @param integer $pid
  170. * @param string $app_id
  171. * @return array|null
  172. * @copyright: Copyright (c) 2020 广东七件事集团
  173. */
  174. public static function getMenus(array $cond, array $with = [], bool $is_array = true, string $select = '*')
  175. {
  176. $query = self::find()->select($select);
  177. self::paramPack(['where' => $cond, 'with' => $with, 'order' => 'sort asc, id asc'], $query);
  178. $menu = $query->asArray($is_array)->all();
  179. return $menu;
  180. }
  181. /**
  182. * 获取树状菜单
  183. * @Author bing
  184. * @DateTime 2021-08-10 18:22:16
  185. * @param array $menu 菜单
  186. * @param integer $parentId
  187. * @param string $app_id APP_ID
  188. * @return array
  189. * @copyright: Copyright (c) 2020 广东七件事集团
  190. */
  191. public static function getTreeMenu(array $menu, int $pid = 0, string $route = '')
  192. {
  193. $tree = [];
  194. foreach ($menu as $k => $v) {
  195. $v['value'] = $v['id'];
  196. $v['label'] = $v['title'];
  197. if ($v['pid'] == $pid) {
  198. $tmp = $v;
  199. // 当前路由的菜单被选中
  200. if ($route == $v['url']) $tmp['is_active'] = true;
  201. $children = self::getTreeMenu($menu, $v['id'], $route);
  202. $children && $tmp['children'] = $children;
  203. //判断父级菜单是否被选中
  204. if (isset($tmp['children']) && !empty($tmp['children'])) {
  205. foreach ($tmp['children'] as $child) {
  206. if (isset($child['is_active']) && $child['is_active'] == true) {
  207. $tmp['is_active'] = true;
  208. }
  209. }
  210. }
  211. $tree[] = $tmp;
  212. }
  213. }
  214. return $tree;
  215. }
  216. public static function getDropDown($menuCate, $app_id = 'backend', $id = '')
  217. {
  218. $list = Menu::find()
  219. ->where(['>=', 'status', StatusEnum::DISABLED])
  220. ->andWhere(['app_id' => $app_id])
  221. ->andWhere(['is_addon' => $menuCate->is_addon])
  222. ->andFilterWhere(['addons_name' => $menuCate->addons_name])
  223. ->andFilterWhere(['<>', 'id', $id])
  224. ->select(['id', 'title', 'pid', 'level'])
  225. ->orderBy('cate_id asc, sort asc')
  226. ->asArray()
  227. ->all();
  228. $models = ArrayHelper::itemsMerge($list);
  229. $data = ArrayHelper::map(ArrayHelper::itemsMergeDropDown($models), 'id', 'title');
  230. return ArrayHelper::merge([0 => '顶级菜单'], $data);
  231. }
  232. /**
  233. * 添加菜单
  234. * @param $params
  235. * @return bool
  236. */
  237. public static function createMenu($params)
  238. {
  239. try {
  240. if (!empty($params['id'])) {
  241. $model = Menu::findOne($params['id']);
  242. if (empty($model)) throw new Exception('菜单不存在或已删除');
  243. } else {
  244. $model = new self();
  245. }
  246. if (!$model->load($params, '')) throw new Exception($model->getErrorMessage());
  247. $pid = !empty($params['pid']) ? $params['pid'] : 0;
  248. $model->pid = $pid;
  249. $model->level = 1;
  250. $model->tree = 'tr_0';
  251. $model->addons_name = '';
  252. if (!empty($model->pid)) {
  253. $rbac_item = Menu::findOne($model->pid);
  254. $model->level = $rbac_item->level;
  255. $model->tree = $rbac_item->tree . ' tr_' . $rbac_item['id'];
  256. }
  257. if (in_array($params['cate_id'], [AppEnum::PLATFORM, AppEnum::MALL])) $model->is_addon = WhetherEnum::DISABLED;
  258. if (in_array($params['cate_id'], [AppEnum::ADDON])) $model->is_addon = 1;
  259. $model->setParent($model);
  260. $res = $model->save();
  261. if ($res === false) throw new Exception($model->getErrorMessage());
  262. // 触发添加菜单成功事件
  263. if (empty($params['id'])) {
  264. $event = new MenuEvent();
  265. $data = [
  266. 'name' => $params['url'],
  267. 'title' => $params['title'],
  268. 'cate_id' => $params['cate_id'],
  269. 'sort' => $params['sort'],
  270. 'menu_pid' => $params['pid'],
  271. 'app_id' => $params['app_id'],
  272. ];
  273. Yii::$app->services->events->dispatch($event, $data, $event->listeners);
  274. }
  275. return $res;
  276. } catch (\Exception $e) {
  277. self::$static_error = $e->getMessage();
  278. return false;
  279. }
  280. }
  281. /**
  282. * 保存数据
  283. * @param array $params
  284. * @return bool|Menu
  285. */
  286. public static function setData(array $params)
  287. {
  288. try {
  289. if (!empty($params['id'])) {
  290. $model = self::findOne(['id' => $params['id'], 'status' => [StatusEnum::ENABLED, StatusEnum::DISABLED]]);
  291. if (empty($model)) throw new Exception('服务不存在或已删除');
  292. } else {
  293. $model = new self();
  294. $model->loadDefaultValues();
  295. }
  296. isset($params['sort']) && $model->sort = $params['sort'];
  297. isset($params['status']) && $model->status = $params['status'];
  298. if (!$model->load($params, '') || !$model->save()) throw new Exception($model->getErrorMessage());
  299. return $model;
  300. } catch (Exception $e) {
  301. self::$static_error = $e->getMessage();
  302. return false;
  303. }
  304. }
  305. /**
  306. * 获取编辑菜单数据
  307. * @param $id
  308. * @param $type
  309. * @return array
  310. */
  311. public static function getEditData($id, $type)
  312. {
  313. $where = ['and',
  314. ['id' => $id],
  315. ['<>', 'status', -1]
  316. ];
  317. $menus = Menu::find()->where($where)->asArray()->one();
  318. if (!empty($menus)) {
  319. if ($menus['status'] == -1 || $menus['status'] == 0) {
  320. $menus['status'] = 0;
  321. } else {
  322. $menus['status'] = 1;
  323. }
  324. }
  325. if (!empty($id) && $type == 'add') {
  326. $parent_title = $menus['title'];
  327. } else {
  328. $parent_title = '顶级菜单';
  329. if (!empty($menus['pid'])) {
  330. $parent_item = Menu::find()->where(['id' => $menus['pid']])->andWhere(['<>', 'status', -1])->asArray()->one();
  331. $parent_title = $parent_item['title'];
  332. }
  333. }
  334. $list = Menu::getDropDown(1, AppEnum::BACKEND);
  335. return [
  336. 'menus' => !empty($menus) ? $menus : [],
  337. 'parent_title' => $parent_title,
  338. 'list' => $list
  339. ];
  340. }
  341. /**
  342. * 删除
  343. * @param $id
  344. * @param $status
  345. * @return bool
  346. */
  347. public static function delMenu($id, $status)
  348. {
  349. try {
  350. $query = Menu::find()->where(['and',
  351. ['id' => $id],
  352. ['<>', 'status', StatusEnum::DELETE]
  353. ]);
  354. $menus = $query->one();
  355. $menus->status = $status;
  356. $menus->save();
  357. Menu::updateAll(['status' => $status], ['pid' => $id]);
  358. return true;
  359. } catch (\Exception $e) {
  360. return false;
  361. }
  362. }
  363. /**
  364. * 关联分类
  365. *
  366. * @return \yii\db\ActiveQuery
  367. */
  368. public function getCate()
  369. {
  370. return $this->hasOne(MenuCate::class, ['id' => 'cate_id']);
  371. }
  372. /**
  373. * 迁移生成菜单
  374. * @Author: lun
  375. * @DateTime: 2023/7/13 0013 16:36
  376. * @Copyright: copyright (c) 2021 广东七件事集团
  377. * @param $params
  378. * @return bool|Menu
  379. */
  380. public static function migrateCreateMenu($params)
  381. {
  382. try {
  383. foreach ($params as $menu) {
  384. $pidByRoute = 0;
  385. if (empty($menu['pid']) && !empty($menu['p_route'])) {
  386. $pidByRoute = self::find()
  387. ->where(['url' => $menu['p_route'], 'status' => StatusEnum::ENABLED])
  388. ->select('id')
  389. ->scalar();
  390. }
  391. $menu['pid'] = $pidByRoute ?: $menu['pid'];
  392. $model = Menu::findOne(['url' => $menu['route'], 'cate_id' => $menu['cate_id'], 'pid' => $menu['pid'], 'status' => StatusEnum::ENABLED]);
  393. if (empty($model)) {
  394. $model = new self();
  395. $model->cate_id = $menu['cate_id'];
  396. $model->title = $menu['title'];
  397. $model->url = $menu['route'];
  398. $model->app_id = 'backend';
  399. $model->is_addon = 0;
  400. }
  401. $model->pid = $menu['pid'];
  402. $model->level = 1;
  403. $model->icon = $menu['icon'] ?? '';
  404. $model->tree = 'tr_0';
  405. if (!empty($model->pid)) {
  406. $rbac_item = Menu::findOne($model->pid);
  407. $model->level = $rbac_item->level;
  408. $model->tree = $rbac_item->tree . ' tr_' . $rbac_item['id'];
  409. // 获取上级权限
  410. $where = [];
  411. $where[] = ['name' => $rbac_item->url];
  412. $where[] = ['cate_id' => $rbac_item->cate_id];
  413. $where[] = ['status' => StatusEnum::ENABLED];
  414. $auth_parent = AuthItem::getOne($where);
  415. $auth_pid = empty($auth_parent) ? 0 : $auth_parent['id'];
  416. }
  417. $model->setParent($model);
  418. $res = $model->save();
  419. if ($res === false) throw new Exception($model->getErrorMessage());
  420. // 设置权限
  421. $auth = [
  422. 'name' => $menu['route'],
  423. 'title' => $menu['title'],
  424. 'cate_id' => $menu['cate_id'],
  425. 'app_id' => 'backend',
  426. 'pid' => $auth_pid ?? 0,
  427. ];
  428. $auth_item = AuthItem::createAuthItem($auth);
  429. if ($auth_item === false) throw new Exception(AuthItem::getStaticError());
  430. // 设置子权限
  431. if (!empty($menu['authItemChild'])) {
  432. foreach ($menu['authItemChild'] as $child) {
  433. $auth_child = [
  434. 'name' => $child['name'],
  435. 'title' => $child['title'],
  436. 'cate_id' => $menu['cate_id'],
  437. 'app_id' => 'backend',
  438. 'pid' => $auth_item['id'] ?? 0,
  439. ];
  440. $res = AuthItem::createAuthItem($auth_child);
  441. if ($res === false) throw new Exception(AuthItem::getStaticError());
  442. }
  443. }
  444. }
  445. return true;
  446. } catch (\Exception $e) {
  447. self::$static_error = $e->getFile().',第'.$e->getLine().'行,'.$e->getMessage();
  448. return false;
  449. }
  450. }
  451. /**
  452. * 根据菜单补全权限
  453. * @Author: liuzhantao
  454. * @DateTime: 2024/6/22 11:29
  455. * @Copyright: copyright (c) 2021 广东七件事集团
  456. * @param $params
  457. * @return bool|Menu
  458. */
  459. public static function migrateUpdateMenu()
  460. {
  461. try {
  462. // menu菜单表的字段“url” 对应 qimall_rbac_auth_item权限表的字段“name”,根据两表该字段的差异,找到缺少的权限记录
  463. $missingUrls = Menu::find()
  464. ->alias('m')
  465. ->leftJoin(AuthItem::tableName() . ' a', 'm.url = a.name')
  466. ->where(['a.name' => null])
  467. ->andWhere(['m.status' => 1]) // status = 1,状态:启用
  468. ->andWhere(['m.is_addon' => 0]) // is_addon = 0, 不是插件
  469. ->andWhere([ // 前缀只能是'mall/'或'admin/'
  470. 'or',
  471. ['like', 'm.url', 'mall/%', false],
  472. ['like', 'm.url', 'admin/%', false],
  473. ])
  474. ->orderBy(['m.id' => SORT_ASC])
  475. ->asArray()
  476. ->all();
  477. foreach ($missingUrls as $k => $v) {
  478. $rbac_item = Menu::findOne($v['pid']);
  479. // 获取上级权限
  480. $where = [];
  481. $where[] = ['name' => $rbac_item['url']];
  482. $where[] = ['cate_id' => $rbac_item['cate_id']];
  483. $auth_parent = AuthItem::getOne($where);
  484. $auth_pid = empty($auth_parent) ? 0 : $auth_parent['id'];
  485. // 设置权限
  486. $auth = [
  487. 'name' => $v['url'],
  488. 'title' => $v['title'],
  489. 'cate_id' => $v['cate_id'],
  490. 'app_id' => 'backend',
  491. 'pid' => $auth_pid ?? 0,
  492. ];
  493. AuthItem::createAuthItem($auth);
  494. }
  495. return true;
  496. } catch (\Exception $e) {
  497. self::$static_error = $e->getFile() . ',第' . $e->getLine() . '行,' . $e->getMessage();
  498. return false;
  499. }
  500. }
  501. /**
  502. * 权限表所有记录的所有字段进行重复对比,删除重复的菜单
  503. * @Author: liuzhantao
  504. * @DateTime: 2024/6/27 10:29
  505. * @Copyright: copyright (c) 2021 广东七件事集团
  506. * @param $params
  507. * @return bool|Menu
  508. */
  509. public static function migrateDeleteMenu()
  510. {
  511. try {
  512. // 找到权限表中记录相同的记录
  513. $query = AuthItem::find()
  514. ->select([
  515. 'name',
  516. 'title',
  517. 'app_id',
  518. 'addons_name',
  519. 'pid',
  520. 'level',
  521. 'is_addon',
  522. 'sort',
  523. 'tree',
  524. 'status',
  525. 'cate_id',
  526. new Expression('COUNT(*) AS duplicate_count')
  527. ])
  528. ->groupBy([
  529. 'name',
  530. 'title',
  531. 'app_id',
  532. 'addons_name',
  533. 'pid',
  534. 'level',
  535. 'is_addon',
  536. 'sort',
  537. 'tree',
  538. 'status',
  539. 'cate_id'
  540. ])
  541. ->having('COUNT(*) > 1');
  542. $results = $query->asArray()->all();
  543. // 循环记录
  544. foreach ($results as $record) {
  545. $duplicates = AuthItem::find()
  546. ->where([
  547. 'name' => $record['name'],
  548. 'title' => $record['title'],
  549. 'app_id' => $record['app_id'],
  550. 'addons_name' => $record['addons_name'],
  551. 'pid' => $record['pid'],
  552. 'level' => $record['level'],
  553. 'is_addon' => $record['is_addon'],
  554. 'sort' => $record['sort'],
  555. 'tree' => $record['tree'],
  556. 'status' => $record['status'],
  557. 'cate_id' => $record['cate_id'],
  558. ])
  559. ->all();
  560. // 保留第一条记录,删除其余记录
  561. foreach (array_slice($duplicates, 1) as $duplicate) {
  562. $duplicate->delete();
  563. }
  564. }
  565. } catch (\Exception $e) {
  566. self::$static_error = $e->getFile() . ',第' . $e->getLine() . '行,' . $e->getMessage();
  567. return false;
  568. }
  569. }
  570. }