Purchasecontent.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use think\Cache;
  5. use think\Db;
  6. use think\exception\PDOException;
  7. use think\exception\ValidateException;
  8. use Exception;
  9. /**
  10. * 协助采购 — 通知公告
  11. *
  12. * @icon fa fa-bullhorn
  13. */
  14. class Purchasecontent extends Backend
  15. {
  16. /** 投递接收人可选范围:auth_group 根组 id(含其全部子组) */
  17. protected const RECIPIENT_AUTH_GROUP_ROOT_ID = 10;
  18. /** 表结构已就绪缓存键(避免每次请求重复 SHOW COLUMNS / 探表) */
  19. protected const SCHEMA_CACHE_KEY = 'purchase_content_schema_v2';
  20. /** 可选接收人列表缓存秒数 */
  21. protected const RECIPIENT_LIST_CACHE_TTL = 300;
  22. /** @var \app\admin\model\Purchasecontent */
  23. protected $model = null;
  24. protected $searchFields = 'subject,content';
  25. protected $noNeedRight = ['adminselect', 'install'];
  26. public function _initialize()
  27. {
  28. parent::_initialize();
  29. $this->model = new \app\admin\model\Purchasecontent;
  30. if (!Cache::get(self::SCHEMA_CACHE_KEY)) {
  31. $this->ensurePurchaseContentSchemaOnce();
  32. }
  33. }
  34. /** 首次访问时建表/迁移,完成后写入缓存 */
  35. protected function ensurePurchaseContentSchemaOnce(): void
  36. {
  37. $this->ensurePurchaseContentTables();
  38. $this->ensurePurchaseContentDatetimeColumns();
  39. Cache::set(self::SCHEMA_CACHE_KEY, 1);
  40. }
  41. protected function ensurePurchaseContentTables(): void
  42. {
  43. try {
  44. Db::query('SELECT 1 FROM `purchase_content` LIMIT 1');
  45. Db::query('SELECT 1 FROM `purchase_content_recipient` LIMIT 1');
  46. } catch (\Throwable $e) {
  47. $sqlFile = APP_PATH . 'extra' . DS . 'purchase_content_install.sql';
  48. if (is_file($sqlFile)) {
  49. $sql = file_get_contents($sqlFile);
  50. if (is_string($sql) && $sql !== '') {
  51. foreach (preg_split('/;\s*[\r\n]+/', $sql) as $stmt) {
  52. $stmt = trim($stmt);
  53. if ($stmt === '' || stripos($stmt, 'CREATE TABLE') === false) {
  54. continue;
  55. }
  56. try {
  57. Db::execute($stmt);
  58. } catch (\Throwable $ignore) {
  59. }
  60. }
  61. }
  62. }
  63. }
  64. }
  65. /** 旧版 int 时间戳列迁移为 datetime */
  66. protected function ensurePurchaseContentDatetimeColumns(): void
  67. {
  68. try {
  69. $this->migrateTableDatetimeColumn('purchase_content', 'createtime', '投递时间');
  70. $this->migrateTableDatetimeColumn('purchase_content', 'updatetime', '更新时间');
  71. $this->migrateTableDatetimeColumn('purchase_content_recipient', 'createtime', '创建时间');
  72. $this->migrateTableDatetimeColumn('purchase_content_recipient', 'read_time', '首次阅读时间');
  73. } catch (\Throwable $e) {
  74. }
  75. }
  76. protected function migrateTableDatetimeColumn(string $table, string $column, string $comment): void
  77. {
  78. $rows = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'");
  79. if (!is_array($rows) || !isset($rows[0]['Type'])) {
  80. return;
  81. }
  82. $type = strtolower((string)$rows[0]['Type']);
  83. if (strpos($type, 'int') === false) {
  84. return;
  85. }
  86. $tmp = $column . '_dt';
  87. Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$tmp}` datetime DEFAULT NULL COMMENT '{$comment}'");
  88. Db::execute("UPDATE `{$table}` SET `{$tmp}` = IF(`{$column}` > 0, FROM_UNIXTIME(`{$column}`), NULL)");
  89. Db::execute("ALTER TABLE `{$table}` DROP COLUMN `{$column}`");
  90. Db::execute("ALTER TABLE `{$table}` CHANGE `{$tmp}` `{$column}` datetime DEFAULT NULL COMMENT '{$comment}'");
  91. }
  92. /**
  93. * 投递/阅读时间展示:YYYY-MM-DD HH:mm:ss
  94. *
  95. * @param mixed $value
  96. */
  97. protected function formatPurchaseContentTime($value): string
  98. {
  99. if ($value === null || $value === '') {
  100. return '';
  101. }
  102. if (is_numeric($value)) {
  103. $ts = (int)$value;
  104. if ($ts > 946684800) {
  105. return date('Y-m-d H:i:s', $ts);
  106. }
  107. return '';
  108. }
  109. $s = trim((string)$value);
  110. if ($s === '' || stripos($s, '0000-00-00') === 0) {
  111. return '';
  112. }
  113. if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/', $s, $m)) {
  114. return $m[1];
  115. }
  116. if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
  117. return $m[1] . ' 00:00:00';
  118. }
  119. $ts = strtotime($s);
  120. return ($ts !== false && $ts > 0) ? date('Y-m-d H:i:s', $ts) : $s;
  121. }
  122. /**
  123. * 选择后台用户(发送人 / 接收人)
  124. */
  125. public function adminselect()
  126. {
  127. $this->model = model('Admin');
  128. $this->selectpageFields = 'id,username,nickname';
  129. $this->searchFields = 'id,username,nickname';
  130. $groupIds = $this->loadRecipientAuthGroupIds();
  131. $this->model->where('status', 'normal');
  132. if ($groupIds !== []) {
  133. $this->model->where('id', 'in', function ($query) use ($groupIds) {
  134. $query->name('auth_group_access')->where('group_id', 'in', $groupIds)->field('uid');
  135. });
  136. } else {
  137. $this->model->where('id', 0);
  138. }
  139. return $this->selectpage();
  140. }
  141. public function index()
  142. {
  143. $this->relationSearch = false;
  144. $this->request->filter(['strip_tags', 'trim']);
  145. if (!$this->request->isAjax()) {
  146. return $this->view->fetch();
  147. }
  148. $adminId = (int)$this->auth->id;
  149. $isSuper = $this->auth->isSuperAdmin();
  150. list($where, $sort, $order, $offset, $limit) = $this->buildparams();
  151. $query = $this->model->where($where);
  152. if (!$isSuper) {
  153. $recvIds = Db::table('purchase_content_recipient')
  154. ->where('admin_id', $adminId)
  155. ->column('content_id');
  156. $recvIds = is_array($recvIds) ? array_values(array_unique(array_filter(array_map('intval', $recvIds)))) : [];
  157. $query->where(function ($q) use ($adminId, $recvIds) {
  158. $q->where('creator_id', $adminId);
  159. if ($recvIds !== []) {
  160. $q->whereOr('id', 'in', $recvIds);
  161. }
  162. });
  163. }
  164. $list = $query->order($sort, $order)->paginate($limit);
  165. $rows = $list->items();
  166. $this->enrichContentRows($rows, $adminId);
  167. return json(['total' => $list->total(), 'rows' => $rows]);
  168. }
  169. public function add()
  170. {
  171. if (!$this->request->isPost()) {
  172. $admin = $this->auth->getUserinfo();
  173. $this->view->assign('admin', [
  174. 'id' => (int)($admin['id'] ?? $this->auth->id),
  175. 'nickname' => (string)($admin['nickname'] ?? $admin['username'] ?? ''),
  176. ]);
  177. $this->view->assign('adminList', $this->loadNormalAdminList());
  178. return $this->view->fetch();
  179. }
  180. $params = $this->request->post('row/a');
  181. if (empty($params)) {
  182. $this->error(__('Parameter %s can not be empty', ''));
  183. }
  184. $subject = trim((string)($params['subject'] ?? ''));
  185. $content = trim((string)($params['content'] ?? ''));
  186. if ($subject === '') {
  187. $this->error('请填写主题');
  188. }
  189. if ($content === '') {
  190. $this->error('请填写内容');
  191. }
  192. $senderId = (int)$this->auth->id;
  193. if ($senderId <= 0 || !$this->adminExists($senderId)) {
  194. $this->error('当前登录用户无效');
  195. }
  196. $recipientIds = $this->parseRecipientIdList($params['recipient_ids'] ?? '');
  197. if ($recipientIds === []) {
  198. $this->error('请选择投递接收人');
  199. }
  200. $creatorId = (int)$this->auth->id;
  201. $now = date('Y-m-d H:i:s');
  202. Db::startTrans();
  203. try {
  204. $contentId = (int)Db::table('purchase_content')->insertGetId([
  205. 'creator_id' => $creatorId,
  206. 'sender_id' => $senderId,
  207. 'subject' => $subject,
  208. 'content' => $content,
  209. 'createtime' => $now,
  210. 'updatetime' => $now,
  211. ]);
  212. if ($contentId <= 0) {
  213. throw new Exception('保存公告失败');
  214. }
  215. $recvRows = [];
  216. foreach ($recipientIds as $aid) {
  217. $recvRows[] = [
  218. 'content_id' => $contentId,
  219. 'admin_id' => $aid,
  220. 'createtime' => $now,
  221. ];
  222. }
  223. Db::table('purchase_content_recipient')->insertAll($recvRows);
  224. Db::commit();
  225. } catch (ValidateException|PDOException|Exception $e) {
  226. Db::rollback();
  227. $this->error($e->getMessage());
  228. }
  229. $this->success();
  230. }
  231. public function detail($ids = null)
  232. {
  233. $row = $this->getContentRowForAdmin((int)$ids);
  234. if (!$row) {
  235. $this->error(__('No Results were found'));
  236. }
  237. $adminId = (int)$this->auth->id;
  238. if (!$this->auth->isSuperAdmin() && (int)$row['creator_id'] !== $adminId) {
  239. $isRecv = Db::table('purchase_content_recipient')
  240. ->where('content_id', (int)$row['id'])
  241. ->where('admin_id', $adminId)
  242. ->count();
  243. if (!$isRecv) {
  244. $this->error('无权查看该通知');
  245. }
  246. $recvRow = Db::table('purchase_content_recipient')
  247. ->where('content_id', (int)$row['id'])
  248. ->where('admin_id', $adminId)
  249. ->find();
  250. if ($recvRow && empty($recvRow['read_time'])) {
  251. Db::table('purchase_content_recipient')
  252. ->where('id', (int)$recvRow['id'])
  253. ->update(['read_time' => date('Y-m-d H:i:s')]);
  254. }
  255. }
  256. $items = [$row];
  257. $this->enrichContentRows($items, $adminId);
  258. $row = $items[0];
  259. $this->view->assign('row', $row);
  260. return $this->view->fetch();
  261. }
  262. public function del($ids = null)
  263. {
  264. if (!$this->request->isPost()) {
  265. $this->error(__('Invalid parameters'));
  266. }
  267. $ids = $ids ?: $this->request->post('ids');
  268. $idList = $this->parseIdList($ids);
  269. if ($idList === []) {
  270. $this->error(__('Parameter %s can not be empty', 'ids'));
  271. }
  272. $adminId = (int)$this->auth->id;
  273. Db::startTrans();
  274. try {
  275. foreach ($idList as $cid) {
  276. $row = $this->model->get($cid);
  277. if (!$row) {
  278. continue;
  279. }
  280. if ((int)$row['sender_id'] !== $adminId) {
  281. throw new Exception('仅可删除本人投递的公告');
  282. }
  283. Db::table('purchase_content_recipient')->where('content_id', $cid)->delete();
  284. $row->delete();
  285. }
  286. Db::commit();
  287. } catch (PDOException|Exception $e) {
  288. Db::rollback();
  289. $this->error($e->getMessage());
  290. }
  291. $this->success();
  292. }
  293. /**
  294. * 安装菜单(超级管理员执行一次)
  295. */
  296. public function install()
  297. {
  298. if (!$this->auth->isSuperAdmin()) {
  299. $this->error('仅超级管理员可执行');
  300. }
  301. $t = time();
  302. $pid = 0;
  303. $parent = Db::name('auth_rule')->where('name', 'procuremenroot')->find();
  304. if ($parent) {
  305. $pid = (int)$parent['id'];
  306. } else {
  307. $pick = Db::name('auth_rule')->where('name', 'procuremen/pick')->find();
  308. if ($pick) {
  309. $pid = (int)($pick['pid'] ?? 0);
  310. }
  311. }
  312. if ($pid <= 0) {
  313. $this->error('未找到协助采购父菜单,请先在权限规则中配置 procuremenroot 或 procuremen/pick');
  314. }
  315. $added = 0;
  316. $menuName = 'purchasecontent/index';
  317. $menuRule = Db::name('auth_rule')->where('name', $menuName)->find();
  318. if ($menuRule) {
  319. $menuId = (int)$menuRule['id'];
  320. } else {
  321. $menuId = Db::name('auth_rule')->insertGetId([
  322. 'type' => 'file',
  323. 'pid' => $pid,
  324. 'name' => $menuName,
  325. 'title' => '通知公告',
  326. 'icon' => 'fa fa-bullhorn',
  327. 'url' => '',
  328. 'ismenu' => 1,
  329. 'menutype' => 'addtabs',
  330. 'weigh' => 84,
  331. 'status' => 'normal',
  332. 'createtime' => $t,
  333. 'updatetime' => $t,
  334. ]);
  335. $added++;
  336. }
  337. foreach (['purchasecontent/add' => '新增', 'purchasecontent/detail' => '查看', 'purchasecontent/del' => '删除', 'purchasecontent/adminselect' => '选择用户'] as $name => $title) {
  338. if (Db::name('auth_rule')->where('name', $name)->find()) {
  339. continue;
  340. }
  341. Db::name('auth_rule')->insert([
  342. 'type' => 'file',
  343. 'pid' => $menuId,
  344. 'name' => $name,
  345. 'title' => $title,
  346. 'icon' => 'fa fa-circle-o',
  347. 'ismenu' => 0,
  348. 'status' => 'normal',
  349. 'createtime' => $t,
  350. 'updatetime' => $t,
  351. ]);
  352. $added++;
  353. }
  354. \think\Cache::rm('__menu__');
  355. $this->success('通知公告菜单安装完成,新增节点 ' . $added . ' 个。请刷新后台并为角色勾选权限。');
  356. }
  357. /**
  358. * @param array<int, mixed> $rows
  359. */
  360. protected function enrichContentRows(array &$rows, int $currentAdminId): void
  361. {
  362. if ($rows === []) {
  363. return;
  364. }
  365. $contentIds = [];
  366. $adminIds = [];
  367. foreach ($rows as $r) {
  368. if (!is_array($r) && !is_object($r)) {
  369. continue;
  370. }
  371. $arr = is_array($r) ? $r : $r->toArray();
  372. $cid = (int)($arr['id'] ?? 0);
  373. if ($cid > 0) {
  374. $contentIds[$cid] = true;
  375. }
  376. foreach (['creator_id', 'sender_id'] as $fk) {
  377. $aid = (int)($arr[$fk] ?? 0);
  378. if ($aid > 0) {
  379. $adminIds[$aid] = true;
  380. }
  381. }
  382. }
  383. $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds));
  384. $recvMap = $this->loadRecipientMap(array_keys($contentIds));
  385. foreach ($rows as &$row) {
  386. if (is_object($row) && method_exists($row, 'getData')) {
  387. $data = $row->getData();
  388. } elseif (is_object($row) && method_exists($row, 'toArray')) {
  389. $data = $row->toArray();
  390. } else {
  391. $data = (array)$row;
  392. }
  393. $cid = (int)($data['id'] ?? 0);
  394. $creatorId = (int)($data['creator_id'] ?? 0);
  395. $senderId = (int)($data['sender_id'] ?? 0);
  396. $recv = $recvMap[$cid] ?? ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null];
  397. $isSender = ($senderId === $currentAdminId);
  398. $extra = [
  399. 'creator_name' => $nameMap[$creatorId] ?? '',
  400. 'sender_name' => $nameMap[$senderId] ?? '',
  401. 'sender_id' => $senderId,
  402. 'recipient_names' => implode('、', $recv['names']),
  403. 'recipient_count' => $recv['count'],
  404. 'is_receiver' => $recv['is_receiver'] ? 1 : 0,
  405. 'is_sender' => $isSender ? 1 : 0,
  406. 'read_time' => $recv['read_time'],
  407. 'send_time_text' => $this->formatPurchaseContentTime($data['createtime'] ?? ''),
  408. 'can_delete' => $isSender ? 1 : 0,
  409. ];
  410. if (is_object($row)) {
  411. foreach ($extra as $k => $v) {
  412. $row->$k = $v;
  413. }
  414. } else {
  415. $row = array_merge($data, $extra);
  416. }
  417. }
  418. unset($row);
  419. }
  420. /**
  421. * @param int[] $adminIds
  422. * @return array<int, string>
  423. */
  424. protected function loadAdminNicknameMap(array $adminIds): array
  425. {
  426. $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds))));
  427. if ($adminIds === []) {
  428. return [];
  429. }
  430. $rows = Db::name('admin')
  431. ->where('id', 'in', $adminIds)
  432. ->field('id,nickname,username')
  433. ->select();
  434. if (!is_array($rows)) {
  435. return [];
  436. }
  437. $out = [];
  438. foreach ($rows as $r) {
  439. if (!is_array($r)) {
  440. continue;
  441. }
  442. $id = (int)($r['id'] ?? 0);
  443. $nick = trim((string)($r['nickname'] ?? ''));
  444. if ($nick === '') {
  445. $nick = trim((string)($r['username'] ?? ''));
  446. }
  447. $out[$id] = $nick !== '' ? $nick : ('#' . $id);
  448. }
  449. return $out;
  450. }
  451. /**
  452. * @param int[] $contentIds
  453. * @return array<int, array{names: string[], count: int, is_receiver: bool, read_time: int|null}>
  454. */
  455. protected function loadRecipientMap(array $contentIds): array
  456. {
  457. $contentIds = array_values(array_unique(array_filter(array_map('intval', $contentIds))));
  458. $out = [];
  459. if ($contentIds === []) {
  460. return $out;
  461. }
  462. $recvRows = Db::table('purchase_content_recipient')
  463. ->where('content_id', 'in', $contentIds)
  464. ->field('content_id,admin_id,read_time')
  465. ->select();
  466. if (!is_array($recvRows)) {
  467. return $out;
  468. }
  469. $adminIds = [];
  470. foreach ($recvRows as $r) {
  471. if (!is_array($r)) {
  472. continue;
  473. }
  474. $aid = (int)($r['admin_id'] ?? 0);
  475. if ($aid > 0) {
  476. $adminIds[$aid] = true;
  477. }
  478. }
  479. $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds));
  480. $currentAdminId = (int)$this->auth->id;
  481. foreach ($recvRows as $r) {
  482. if (!is_array($r)) {
  483. continue;
  484. }
  485. $cid = (int)($r['content_id'] ?? 0);
  486. $aid = (int)($r['admin_id'] ?? 0);
  487. if ($cid <= 0) {
  488. continue;
  489. }
  490. if (!isset($out[$cid])) {
  491. $out[$cid] = ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null];
  492. }
  493. $nm = $nameMap[$aid] ?? ('#' . $aid);
  494. if ($nm !== '' && !in_array($nm, $out[$cid]['names'], true)) {
  495. $out[$cid]['names'][] = $nm;
  496. }
  497. if ($aid === $currentAdminId) {
  498. $out[$cid]['is_receiver'] = true;
  499. $rt = $r['read_time'] ?? null;
  500. if ($rt !== null && $rt !== '' && stripos((string)$rt, '0000-00-00') !== 0) {
  501. $out[$cid]['read_time'] = $this->formatPurchaseContentTime($rt);
  502. }
  503. }
  504. }
  505. foreach ($out as $cid => &$item) {
  506. sort($item['names'], SORT_STRING);
  507. $item['count'] = count($item['names']);
  508. }
  509. unset($item);
  510. return $out;
  511. }
  512. protected function getContentRowForAdmin(int $id): ?array
  513. {
  514. if ($id <= 0) {
  515. return null;
  516. }
  517. $row = $this->model->get($id);
  518. return $row ? $row->toArray() : null;
  519. }
  520. protected function adminExists(int $adminId): bool
  521. {
  522. if ($adminId <= 0) {
  523. return false;
  524. }
  525. return (bool)Db::name('admin')->where('id', $adminId)->where('status', 'normal')->count();
  526. }
  527. /**
  528. * 接收人是否属于「供应商证」角色组(id=10)及其子组
  529. */
  530. protected function recipientAdminExists(int $adminId): bool
  531. {
  532. if ($adminId <= 0 || !$this->adminExists($adminId)) {
  533. return false;
  534. }
  535. $groupIds = $this->loadRecipientAuthGroupIds();
  536. if ($groupIds === []) {
  537. return false;
  538. }
  539. return (bool)Db::name('auth_group_access')
  540. ->where('uid', $adminId)
  541. ->where('group_id', 'in', $groupIds)
  542. ->count();
  543. }
  544. /**
  545. * auth_group id=10 及其全部子组 id
  546. *
  547. * @return int[]
  548. */
  549. protected function loadRecipientAuthGroupIds(): array
  550. {
  551. static $memo = null;
  552. if ($memo !== null) {
  553. return $memo;
  554. }
  555. $cacheKey = 'purchase_content_group_ids_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  556. $cached = Cache::get($cacheKey);
  557. if (is_array($cached) && $cached !== []) {
  558. $memo = $cached;
  559. return $memo;
  560. }
  561. $rootId = self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  562. try {
  563. $groups = Db::name('auth_group')->where('status', 'normal')->field('id,pid')->select();
  564. } catch (\Throwable $e) {
  565. return [$rootId];
  566. }
  567. if (!is_array($groups) || $groups === []) {
  568. return [$rootId];
  569. }
  570. $groupList = [];
  571. foreach ($groups as $g) {
  572. if (is_array($g)) {
  573. $groupList[] = $g;
  574. }
  575. }
  576. if ($groupList === []) {
  577. return [$rootId];
  578. }
  579. $tree = \fast\Tree::instance();
  580. $tree->init($groupList);
  581. $ids = $tree->getChildrenIds($rootId, true);
  582. if (!is_array($ids) || $ids === []) {
  583. $memo = [$rootId];
  584. return $memo;
  585. }
  586. $memo = array_values(array_unique(array_filter(array_map('intval', $ids))));
  587. Cache::set($cacheKey, $memo, self::RECIPIENT_LIST_CACHE_TTL);
  588. return $memo;
  589. }
  590. /**
  591. * 新增公告:可选接收人列表(仅角色组 id=10 及其子组下的用户)
  592. *
  593. * @return array<int, array{id:int, username:string, nickname:string, label:string}>
  594. */
  595. protected function loadNormalAdminList(): array
  596. {
  597. $cacheKey = 'purchase_content_admin_list_v2_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  598. $cached = Cache::get($cacheKey);
  599. if (is_array($cached)) {
  600. return $cached;
  601. }
  602. $groupIds = $this->loadRecipientAuthGroupIds();
  603. if ($groupIds === []) {
  604. return [];
  605. }
  606. try {
  607. $adminIds = Db::name('auth_group_access')
  608. ->where('group_id', 'in', $groupIds)
  609. ->column('uid');
  610. } catch (\Throwable $e) {
  611. $adminIds = [];
  612. }
  613. $adminIds = is_array($adminIds)
  614. ? array_values(array_unique(array_filter(array_map('intval', $adminIds))))
  615. : [];
  616. if ($adminIds === []) {
  617. return [];
  618. }
  619. try {
  620. $rows = Db::name('admin')
  621. ->where('status', 'normal')
  622. ->where('id', 'in', $adminIds)
  623. ->field('id,username,nickname')
  624. ->order('id', 'asc')
  625. ->select();
  626. } catch (\Throwable $e) {
  627. $rows = [];
  628. }
  629. if (!is_array($rows)) {
  630. return [];
  631. }
  632. $out = [];
  633. foreach ($rows as $r) {
  634. if (!is_array($r)) {
  635. continue;
  636. }
  637. $id = (int)($r['id'] ?? 0);
  638. if ($id <= 0) {
  639. continue;
  640. }
  641. $username = trim((string)($r['username'] ?? ''));
  642. $nickname = trim((string)($r['nickname'] ?? ''));
  643. if ($nickname === '') {
  644. $nickname = $username;
  645. }
  646. if ($nickname === '') {
  647. $nickname = '#' . $id;
  648. }
  649. $out[] = [
  650. 'id' => $id,
  651. 'username' => $username,
  652. 'nickname' => $nickname,
  653. 'label' => $nickname,
  654. ];
  655. }
  656. Cache::set($cacheKey, $out, self::RECIPIENT_LIST_CACHE_TTL);
  657. return $out;
  658. }
  659. /**
  660. * @param mixed $raw
  661. * @return int[]
  662. */
  663. protected function parseIdList($raw): array
  664. {
  665. if (is_array($raw)) {
  666. $parts = $raw;
  667. } else {
  668. $parts = preg_split('/\s*,\s*/', trim((string)$raw), -1, PREG_SPLIT_NO_EMPTY);
  669. }
  670. if (!is_array($parts)) {
  671. return [];
  672. }
  673. $out = [];
  674. foreach ($parts as $p) {
  675. $id = (int)$p;
  676. if ($id > 0) {
  677. $out[$id] = $id;
  678. }
  679. }
  680. return array_values($out);
  681. }
  682. /**
  683. * 解析并校验接收人 id(须为角色组 id=10 及其子组下的正常用户)
  684. *
  685. * @param mixed $raw
  686. * @return int[]
  687. */
  688. protected function parseRecipientIdList($raw): array
  689. {
  690. $out = [];
  691. foreach ($this->parseIdList($raw) as $id) {
  692. if ($this->recipientAdminExists($id)) {
  693. $out[$id] = $id;
  694. }
  695. }
  696. return array_values($out);
  697. }
  698. }