Purchasecontent.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  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. $orderDir = strtoupper((string)$order) === 'ASC' ? 'ASC' : 'DESC';
  152. $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? (string)$sort : 'id';
  153. $query = $this->model->where($where);
  154. if (!$isSuper) {
  155. $recvIds = Db::table('purchase_content_recipient')
  156. ->where('admin_id', $adminId)
  157. ->column('content_id');
  158. $recvIds = is_array($recvIds) ? array_values(array_unique(array_filter(array_map('intval', $recvIds)))) : [];
  159. $query->where(function ($q) use ($adminId, $recvIds) {
  160. $q->where('creator_id', $adminId);
  161. if ($recvIds !== []) {
  162. $q->whereOr('id', 'in', $recvIds);
  163. }
  164. });
  165. }
  166. $this->applyPurchaseContentSort($query, $sortField, $orderDir, $adminId);
  167. $list = $query->paginate($limit);
  168. $rows = $list->items();
  169. $this->enrichContentRows($rows, $adminId);
  170. return json(['total' => $list->total(), 'rows' => $rows]);
  171. }
  172. /**
  173. * 列表排序(支持投递人/投递给/状态等衍生字段)
  174. *
  175. * @param mixed $query
  176. */
  177. protected function applyPurchaseContentSort($query, string $sortField, string $orderDir, int $adminId): void
  178. {
  179. $table = $this->model->getTable();
  180. if ($sortField === 'sender_name') {
  181. $query->orderRaw(
  182. '(SELECT COALESCE(NULLIF(TRIM(sa.nickname), \'\'), sa.username, \'\') '
  183. . 'FROM admin sa WHERE sa.id = `' . $table . '`.sender_id LIMIT 1) ' . $orderDir
  184. )->order($table . '.id', 'DESC');
  185. return;
  186. }
  187. if ($sortField === 'recipient_names') {
  188. $query->orderRaw(
  189. '(SELECT COUNT(*) FROM purchase_content_recipient rcnt WHERE rcnt.content_id = `' . $table . '`.id) ' . $orderDir
  190. )->order($table . '.id', 'DESC');
  191. return;
  192. }
  193. if ($sortField === 'is_read') {
  194. $aid = max(0, $adminId);
  195. $query->orderRaw(
  196. '(SELECT CASE '
  197. . 'WHEN COUNT(*) = 0 THEN 2 '
  198. . 'WHEN SUM(CASE WHEN rr.read_time IS NULL OR rr.read_time = \'\' '
  199. . 'OR CAST(rr.read_time AS CHAR) LIKE \'0000-%\' THEN 1 ELSE 0 END) > 0 THEN 0 '
  200. . 'ELSE 1 END '
  201. . 'FROM purchase_content_recipient rr '
  202. . 'WHERE rr.content_id = `' . $table . '`.id AND rr.admin_id = ' . $aid . ') ' . $orderDir
  203. )->order($table . '.id', 'DESC');
  204. return;
  205. }
  206. $allow = ['id', 'createtime', 'subject', 'content', 'sender_id', 'creator_id', 'updatetime'];
  207. if (!in_array($sortField, $allow, true)) {
  208. $sortField = 'id';
  209. }
  210. $query->order($table . '.' . $sortField, $orderDir);
  211. }
  212. public function add()
  213. {
  214. if (!$this->request->isPost()) {
  215. $admin = $this->auth->getUserinfo();
  216. $this->view->assign('admin', [
  217. 'id' => (int)($admin['id'] ?? $this->auth->id),
  218. 'nickname' => (string)($admin['nickname'] ?? $admin['username'] ?? ''),
  219. ]);
  220. $this->view->assign('adminList', $this->loadNormalAdminList());
  221. return $this->view->fetch();
  222. }
  223. $params = $this->request->post('row/a');
  224. if (empty($params)) {
  225. $this->error(__('Parameter %s can not be empty', ''));
  226. }
  227. $subject = trim((string)($params['subject'] ?? ''));
  228. $content = trim((string)($params['content'] ?? ''));
  229. if ($subject === '') {
  230. $this->error('请填写主题');
  231. }
  232. if ($content === '') {
  233. $this->error('请填写内容');
  234. }
  235. $senderId = (int)$this->auth->id;
  236. if ($senderId <= 0 || !$this->adminExists($senderId)) {
  237. $this->error('当前登录用户无效');
  238. }
  239. $recipientIds = $this->parseRecipientIdList($params['recipient_ids'] ?? '');
  240. if ($recipientIds === []) {
  241. $this->error('请选择投递接收人');
  242. }
  243. $creatorId = (int)$this->auth->id;
  244. $now = date('Y-m-d H:i:s');
  245. Db::startTrans();
  246. try {
  247. $contentId = (int)Db::table('purchase_content')->insertGetId([
  248. 'creator_id' => $creatorId,
  249. 'sender_id' => $senderId,
  250. 'subject' => $subject,
  251. 'content' => $content,
  252. 'createtime' => $now,
  253. 'updatetime' => $now,
  254. ]);
  255. if ($contentId <= 0) {
  256. throw new Exception('保存公告失败');
  257. }
  258. $recvRows = [];
  259. foreach ($recipientIds as $aid) {
  260. $recvRows[] = [
  261. 'content_id' => $contentId,
  262. 'admin_id' => $aid,
  263. 'createtime' => $now,
  264. ];
  265. }
  266. Db::table('purchase_content_recipient')->insertAll($recvRows);
  267. Db::commit();
  268. } catch (ValidateException|PDOException|Exception $e) {
  269. Db::rollback();
  270. $this->error($e->getMessage());
  271. }
  272. $this->success();
  273. }
  274. public function detail($ids = null)
  275. {
  276. $row = $this->getContentRowForAdmin((int)$ids);
  277. if (!$row) {
  278. $this->error(__('No Results were found'));
  279. }
  280. $adminId = (int)$this->auth->id;
  281. $contentId = (int)$row['id'];
  282. $isCreator = (int)($row['creator_id'] ?? 0) === $adminId;
  283. $recvRow = Db::table('purchase_content_recipient')
  284. ->where('content_id', $contentId)
  285. ->where('admin_id', $adminId)
  286. ->find();
  287. $isReceiver = (bool)$recvRow;
  288. if (!$this->auth->isSuperAdmin() && !$isCreator && !$isReceiver) {
  289. $this->error('无权查看该通知');
  290. }
  291. // 当前用户是接收人时,首次查看标记为已读
  292. if ($recvRow) {
  293. $rt = $recvRow['read_time'] ?? null;
  294. $unread = ($rt === null || $rt === '' || stripos((string)$rt, '0000-00-00') === 0);
  295. if ($unread) {
  296. Db::table('purchase_content_recipient')
  297. ->where('id', (int)$recvRow['id'])
  298. ->update(['read_time' => date('Y-m-d H:i:s')]);
  299. }
  300. }
  301. $items = [$row];
  302. $this->enrichContentRows($items, $adminId);
  303. $row = $items[0];
  304. $this->view->assign('row', $row);
  305. return $this->view->fetch();
  306. }
  307. public function del($ids = null)
  308. {
  309. if (!$this->request->isPost()) {
  310. $this->error(__('Invalid parameters'));
  311. }
  312. $ids = $ids ?: $this->request->post('ids');
  313. $idList = $this->parseIdList($ids);
  314. if ($idList === []) {
  315. $this->error(__('Parameter %s can not be empty', 'ids'));
  316. }
  317. $adminId = (int)$this->auth->id;
  318. Db::startTrans();
  319. try {
  320. foreach ($idList as $cid) {
  321. $row = $this->model->get($cid);
  322. if (!$row) {
  323. continue;
  324. }
  325. if ((int)$row['sender_id'] !== $adminId) {
  326. throw new Exception('仅可删除本人投递的公告');
  327. }
  328. Db::table('purchase_content_recipient')->where('content_id', $cid)->delete();
  329. $row->delete();
  330. }
  331. Db::commit();
  332. } catch (PDOException|Exception $e) {
  333. Db::rollback();
  334. $this->error($e->getMessage());
  335. }
  336. $this->success();
  337. }
  338. /**
  339. * 安装菜单(超级管理员执行一次)
  340. */
  341. public function install()
  342. {
  343. if (!$this->auth->isSuperAdmin()) {
  344. $this->error('仅超级管理员可执行');
  345. }
  346. $t = time();
  347. $pid = 0;
  348. $parent = Db::name('auth_rule')->where('name', 'procuremenroot')->find();
  349. if ($parent) {
  350. $pid = (int)$parent['id'];
  351. } else {
  352. $pick = Db::name('auth_rule')->where('name', 'procuremen/pick')->find();
  353. if ($pick) {
  354. $pid = (int)($pick['pid'] ?? 0);
  355. }
  356. }
  357. if ($pid <= 0) {
  358. $this->error('未找到协助采购父菜单,请先在权限规则中配置 procuremenroot 或 procuremen/pick');
  359. }
  360. $added = 0;
  361. $menuName = 'purchasecontent/index';
  362. $menuRule = Db::name('auth_rule')->where('name', $menuName)->find();
  363. if ($menuRule) {
  364. $menuId = (int)$menuRule['id'];
  365. } else {
  366. $menuId = Db::name('auth_rule')->insertGetId([
  367. 'type' => 'file',
  368. 'pid' => $pid,
  369. 'name' => $menuName,
  370. 'title' => '通知公告',
  371. 'icon' => 'fa fa-bullhorn',
  372. 'url' => '',
  373. 'ismenu' => 1,
  374. 'menutype' => 'addtabs',
  375. 'weigh' => 84,
  376. 'status' => 'normal',
  377. 'createtime' => $t,
  378. 'updatetime' => $t,
  379. ]);
  380. $added++;
  381. }
  382. foreach (['purchasecontent/add' => '新增', 'purchasecontent/detail' => '查看', 'purchasecontent/del' => '删除', 'purchasecontent/adminselect' => '选择用户'] as $name => $title) {
  383. if (Db::name('auth_rule')->where('name', $name)->find()) {
  384. continue;
  385. }
  386. Db::name('auth_rule')->insert([
  387. 'type' => 'file',
  388. 'pid' => $menuId,
  389. 'name' => $name,
  390. 'title' => $title,
  391. 'icon' => 'fa fa-circle-o',
  392. 'ismenu' => 0,
  393. 'status' => 'normal',
  394. 'createtime' => $t,
  395. 'updatetime' => $t,
  396. ]);
  397. $added++;
  398. }
  399. \think\Cache::rm('__menu__');
  400. $this->success('通知公告菜单安装完成,新增节点 ' . $added . ' 个。请刷新后台并为角色勾选权限。');
  401. }
  402. /**
  403. * @param array<int, mixed> $rows
  404. */
  405. protected function enrichContentRows(array &$rows, int $currentAdminId): void
  406. {
  407. if ($rows === []) {
  408. return;
  409. }
  410. $contentIds = [];
  411. $adminIds = [];
  412. foreach ($rows as $r) {
  413. if (!is_array($r) && !is_object($r)) {
  414. continue;
  415. }
  416. $arr = is_array($r) ? $r : $r->toArray();
  417. $cid = (int)($arr['id'] ?? 0);
  418. if ($cid > 0) {
  419. $contentIds[$cid] = true;
  420. }
  421. foreach (['creator_id', 'sender_id'] as $fk) {
  422. $aid = (int)($arr[$fk] ?? 0);
  423. if ($aid > 0) {
  424. $adminIds[$aid] = true;
  425. }
  426. }
  427. }
  428. $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds));
  429. $recvMap = $this->loadRecipientMap(array_keys($contentIds));
  430. foreach ($rows as &$row) {
  431. if (is_object($row) && method_exists($row, 'getData')) {
  432. $data = $row->getData();
  433. } elseif (is_object($row) && method_exists($row, 'toArray')) {
  434. $data = $row->toArray();
  435. } else {
  436. $data = (array)$row;
  437. }
  438. $cid = (int)($data['id'] ?? 0);
  439. $creatorId = (int)($data['creator_id'] ?? 0);
  440. $senderId = (int)($data['sender_id'] ?? 0);
  441. $recv = $recvMap[$cid] ?? ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null];
  442. $isSender = ($senderId === $currentAdminId);
  443. $extra = [
  444. 'creator_name' => $nameMap[$creatorId] ?? '',
  445. 'sender_name' => $nameMap[$senderId] ?? '',
  446. 'sender_id' => $senderId,
  447. 'recipient_names' => implode('、', $recv['names']),
  448. 'recipient_count' => $recv['count'],
  449. 'is_receiver' => $recv['is_receiver'] ? 1 : 0,
  450. 'is_sender' => $isSender ? 1 : 0,
  451. 'is_read' => (!empty($recv['read_time'])) ? 1 : 0,
  452. 'read_time' => $recv['read_time'],
  453. 'send_time_text' => $this->formatPurchaseContentTime($data['createtime'] ?? ''),
  454. 'can_delete' => $isSender ? 1 : 0,
  455. ];
  456. if (is_object($row)) {
  457. foreach ($extra as $k => $v) {
  458. $row->$k = $v;
  459. }
  460. } else {
  461. $row = array_merge($data, $extra);
  462. }
  463. }
  464. unset($row);
  465. }
  466. /**
  467. * @param int[] $adminIds
  468. * @return array<int, string>
  469. */
  470. protected function loadAdminNicknameMap(array $adminIds): array
  471. {
  472. $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds))));
  473. if ($adminIds === []) {
  474. return [];
  475. }
  476. $rows = Db::name('admin')
  477. ->where('id', 'in', $adminIds)
  478. ->field('id,nickname,username')
  479. ->select();
  480. if (!is_array($rows)) {
  481. return [];
  482. }
  483. $out = [];
  484. foreach ($rows as $r) {
  485. if (!is_array($r)) {
  486. continue;
  487. }
  488. $id = (int)($r['id'] ?? 0);
  489. $nick = trim((string)($r['nickname'] ?? ''));
  490. if ($nick === '') {
  491. $nick = trim((string)($r['username'] ?? ''));
  492. }
  493. $out[$id] = $nick !== '' ? $nick : ('#' . $id);
  494. }
  495. return $out;
  496. }
  497. /**
  498. * @param int[] $contentIds
  499. * @return array<int, array{names: string[], count: int, is_receiver: bool, read_time: int|null}>
  500. */
  501. protected function loadRecipientMap(array $contentIds): array
  502. {
  503. $contentIds = array_values(array_unique(array_filter(array_map('intval', $contentIds))));
  504. $out = [];
  505. if ($contentIds === []) {
  506. return $out;
  507. }
  508. $recvRows = Db::table('purchase_content_recipient')
  509. ->where('content_id', 'in', $contentIds)
  510. ->field('content_id,admin_id,read_time')
  511. ->select();
  512. if (!is_array($recvRows)) {
  513. return $out;
  514. }
  515. $adminIds = [];
  516. foreach ($recvRows as $r) {
  517. if (!is_array($r)) {
  518. continue;
  519. }
  520. $aid = (int)($r['admin_id'] ?? 0);
  521. if ($aid > 0) {
  522. $adminIds[$aid] = true;
  523. }
  524. }
  525. $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds));
  526. $currentAdminId = (int)$this->auth->id;
  527. foreach ($recvRows as $r) {
  528. if (!is_array($r)) {
  529. continue;
  530. }
  531. $cid = (int)($r['content_id'] ?? 0);
  532. $aid = (int)($r['admin_id'] ?? 0);
  533. if ($cid <= 0) {
  534. continue;
  535. }
  536. if (!isset($out[$cid])) {
  537. $out[$cid] = ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null];
  538. }
  539. $nm = $nameMap[$aid] ?? ('#' . $aid);
  540. if ($nm !== '' && !in_array($nm, $out[$cid]['names'], true)) {
  541. $out[$cid]['names'][] = $nm;
  542. }
  543. if ($aid === $currentAdminId) {
  544. $out[$cid]['is_receiver'] = true;
  545. $rt = $r['read_time'] ?? null;
  546. if ($rt !== null && $rt !== '' && stripos((string)$rt, '0000-00-00') !== 0) {
  547. $out[$cid]['read_time'] = $this->formatPurchaseContentTime($rt);
  548. }
  549. }
  550. }
  551. foreach ($out as $cid => &$item) {
  552. sort($item['names'], SORT_STRING);
  553. $item['count'] = count($item['names']);
  554. }
  555. unset($item);
  556. return $out;
  557. }
  558. protected function getContentRowForAdmin(int $id): ?array
  559. {
  560. if ($id <= 0) {
  561. return null;
  562. }
  563. $row = $this->model->get($id);
  564. return $row ? $row->toArray() : null;
  565. }
  566. protected function adminExists(int $adminId): bool
  567. {
  568. if ($adminId <= 0) {
  569. return false;
  570. }
  571. return (bool)Db::name('admin')->where('id', $adminId)->where('status', 'normal')->count();
  572. }
  573. /**
  574. * 接收人是否属于「供应商证」角色组(id=10)及其子组
  575. */
  576. protected function recipientAdminExists(int $adminId): bool
  577. {
  578. if ($adminId <= 0 || !$this->adminExists($adminId)) {
  579. return false;
  580. }
  581. $groupIds = $this->loadRecipientAuthGroupIds();
  582. if ($groupIds === []) {
  583. return false;
  584. }
  585. return (bool)Db::name('auth_group_access')
  586. ->where('uid', $adminId)
  587. ->where('group_id', 'in', $groupIds)
  588. ->count();
  589. }
  590. /**
  591. * auth_group id=10 及其全部子组 id
  592. *
  593. * @return int[]
  594. */
  595. protected function loadRecipientAuthGroupIds(): array
  596. {
  597. static $memo = null;
  598. if ($memo !== null) {
  599. return $memo;
  600. }
  601. $cacheKey = 'purchase_content_group_ids_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  602. $cached = Cache::get($cacheKey);
  603. if (is_array($cached) && $cached !== []) {
  604. $memo = $cached;
  605. return $memo;
  606. }
  607. $rootId = self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  608. try {
  609. $groups = Db::name('auth_group')->where('status', 'normal')->field('id,pid')->select();
  610. } catch (\Throwable $e) {
  611. return [$rootId];
  612. }
  613. if (!is_array($groups) || $groups === []) {
  614. return [$rootId];
  615. }
  616. $groupList = [];
  617. foreach ($groups as $g) {
  618. if (is_array($g)) {
  619. $groupList[] = $g;
  620. }
  621. }
  622. if ($groupList === []) {
  623. return [$rootId];
  624. }
  625. $tree = \fast\Tree::instance();
  626. $tree->init($groupList);
  627. $ids = $tree->getChildrenIds($rootId, true);
  628. if (!is_array($ids) || $ids === []) {
  629. $memo = [$rootId];
  630. return $memo;
  631. }
  632. $memo = array_values(array_unique(array_filter(array_map('intval', $ids))));
  633. Cache::set($cacheKey, $memo, self::RECIPIENT_LIST_CACHE_TTL);
  634. return $memo;
  635. }
  636. /**
  637. * 新增公告:可选接收人列表(仅角色组 id=10 及其子组下的用户)
  638. *
  639. * @return array<int, array{id:int, username:string, nickname:string, label:string}>
  640. */
  641. protected function loadNormalAdminList(): array
  642. {
  643. $cacheKey = 'purchase_content_admin_list_v2_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID;
  644. $cached = Cache::get($cacheKey);
  645. if (is_array($cached)) {
  646. return $cached;
  647. }
  648. $groupIds = $this->loadRecipientAuthGroupIds();
  649. if ($groupIds === []) {
  650. return [];
  651. }
  652. try {
  653. $adminIds = Db::name('auth_group_access')
  654. ->where('group_id', 'in', $groupIds)
  655. ->column('uid');
  656. } catch (\Throwable $e) {
  657. $adminIds = [];
  658. }
  659. $adminIds = is_array($adminIds)
  660. ? array_values(array_unique(array_filter(array_map('intval', $adminIds))))
  661. : [];
  662. if ($adminIds === []) {
  663. return [];
  664. }
  665. try {
  666. $rows = Db::name('admin')
  667. ->where('status', 'normal')
  668. ->where('id', 'in', $adminIds)
  669. ->field('id,username,nickname')
  670. ->order('id', 'asc')
  671. ->select();
  672. } catch (\Throwable $e) {
  673. $rows = [];
  674. }
  675. if (!is_array($rows)) {
  676. return [];
  677. }
  678. $out = [];
  679. foreach ($rows as $r) {
  680. if (!is_array($r)) {
  681. continue;
  682. }
  683. $id = (int)($r['id'] ?? 0);
  684. if ($id <= 0) {
  685. continue;
  686. }
  687. $username = trim((string)($r['username'] ?? ''));
  688. $nickname = trim((string)($r['nickname'] ?? ''));
  689. if ($nickname === '') {
  690. $nickname = $username;
  691. }
  692. if ($nickname === '') {
  693. $nickname = '#' . $id;
  694. }
  695. $out[] = [
  696. 'id' => $id,
  697. 'username' => $username,
  698. 'nickname' => $nickname,
  699. 'label' => $nickname,
  700. ];
  701. }
  702. Cache::set($cacheKey, $out, self::RECIPIENT_LIST_CACHE_TTL);
  703. return $out;
  704. }
  705. /**
  706. * @param mixed $raw
  707. * @return int[]
  708. */
  709. protected function parseIdList($raw): array
  710. {
  711. if (is_array($raw)) {
  712. $parts = $raw;
  713. } else {
  714. $parts = preg_split('/\s*,\s*/', trim((string)$raw), -1, PREG_SPLIT_NO_EMPTY);
  715. }
  716. if (!is_array($parts)) {
  717. return [];
  718. }
  719. $out = [];
  720. foreach ($parts as $p) {
  721. $id = (int)$p;
  722. if ($id > 0) {
  723. $out[$id] = $id;
  724. }
  725. }
  726. return array_values($out);
  727. }
  728. /**
  729. * 解析并校验接收人 id(须为角色组 id=10 及其子组下的正常用户)
  730. *
  731. * @param mixed $raw
  732. * @return int[]
  733. */
  734. protected function parseRecipientIdList($raw): array
  735. {
  736. $out = [];
  737. foreach ($this->parseIdList($raw) as $id) {
  738. if ($this->recipientAdminExists($id)) {
  739. $out[$id] = $id;
  740. }
  741. }
  742. return array_values($out);
  743. }
  744. }