model = new \app\admin\model\Purchasecontent; if (!Cache::get(self::SCHEMA_CACHE_KEY)) { $this->ensurePurchaseContentSchemaOnce(); } } /** 首次访问时建表/迁移,完成后写入缓存 */ protected function ensurePurchaseContentSchemaOnce(): void { $this->ensurePurchaseContentTables(); $this->ensurePurchaseContentDatetimeColumns(); Cache::set(self::SCHEMA_CACHE_KEY, 1); } protected function ensurePurchaseContentTables(): void { try { Db::query('SELECT 1 FROM `purchase_content` LIMIT 1'); Db::query('SELECT 1 FROM `purchase_content_recipient` LIMIT 1'); } catch (\Throwable $e) { $sqlFile = APP_PATH . 'extra' . DS . 'purchase_content_install.sql'; if (is_file($sqlFile)) { $sql = file_get_contents($sqlFile); if (is_string($sql) && $sql !== '') { foreach (preg_split('/;\s*[\r\n]+/', $sql) as $stmt) { $stmt = trim($stmt); if ($stmt === '' || stripos($stmt, 'CREATE TABLE') === false) { continue; } try { Db::execute($stmt); } catch (\Throwable $ignore) { } } } } } } /** 旧版 int 时间戳列迁移为 datetime */ protected function ensurePurchaseContentDatetimeColumns(): void { try { $this->migrateTableDatetimeColumn('purchase_content', 'createtime', '投递时间'); $this->migrateTableDatetimeColumn('purchase_content', 'updatetime', '更新时间'); $this->migrateTableDatetimeColumn('purchase_content_recipient', 'createtime', '创建时间'); $this->migrateTableDatetimeColumn('purchase_content_recipient', 'read_time', '首次阅读时间'); } catch (\Throwable $e) { } } protected function migrateTableDatetimeColumn(string $table, string $column, string $comment): void { $rows = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'"); if (!is_array($rows) || !isset($rows[0]['Type'])) { return; } $type = strtolower((string)$rows[0]['Type']); if (strpos($type, 'int') === false) { return; } $tmp = $column . '_dt'; Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$tmp}` datetime DEFAULT NULL COMMENT '{$comment}'"); Db::execute("UPDATE `{$table}` SET `{$tmp}` = IF(`{$column}` > 0, FROM_UNIXTIME(`{$column}`), NULL)"); Db::execute("ALTER TABLE `{$table}` DROP COLUMN `{$column}`"); Db::execute("ALTER TABLE `{$table}` CHANGE `{$tmp}` `{$column}` datetime DEFAULT NULL COMMENT '{$comment}'"); } /** * 投递/阅读时间展示:YYYY-MM-DD HH:mm:ss * * @param mixed $value */ protected function formatPurchaseContentTime($value): string { if ($value === null || $value === '') { return ''; } if (is_numeric($value)) { $ts = (int)$value; if ($ts > 946684800) { return date('Y-m-d H:i:s', $ts); } return ''; } $s = trim((string)$value); if ($s === '' || stripos($s, '0000-00-00') === 0) { return ''; } if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/', $s, $m)) { return $m[1]; } if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) { return $m[1] . ' 00:00:00'; } $ts = strtotime($s); return ($ts !== false && $ts > 0) ? date('Y-m-d H:i:s', $ts) : $s; } /** * 选择后台用户(发送人 / 接收人) */ public function adminselect() { $this->model = model('Admin'); $this->selectpageFields = 'id,username,nickname'; $this->searchFields = 'id,username,nickname'; $groupIds = $this->loadRecipientAuthGroupIds(); $this->model->where('status', 'normal'); if ($groupIds !== []) { $this->model->where('id', 'in', function ($query) use ($groupIds) { $query->name('auth_group_access')->where('group_id', 'in', $groupIds)->field('uid'); }); } else { $this->model->where('id', 0); } return $this->selectpage(); } public function index() { $this->relationSearch = false; $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { return $this->view->fetch(); } $adminId = (int)$this->auth->id; $isSuper = $this->auth->isSuperAdmin(); list($where, $sort, $order, $offset, $limit) = $this->buildparams(); $query = $this->model->where($where); if (!$isSuper) { $recvIds = Db::table('purchase_content_recipient') ->where('admin_id', $adminId) ->column('content_id'); $recvIds = is_array($recvIds) ? array_values(array_unique(array_filter(array_map('intval', $recvIds)))) : []; $query->where(function ($q) use ($adminId, $recvIds) { $q->where('creator_id', $adminId); if ($recvIds !== []) { $q->whereOr('id', 'in', $recvIds); } }); } $list = $query->order($sort, $order)->paginate($limit); $rows = $list->items(); $this->enrichContentRows($rows, $adminId); return json(['total' => $list->total(), 'rows' => $rows]); } public function add() { if (!$this->request->isPost()) { $admin = $this->auth->getUserinfo(); $this->view->assign('admin', [ 'id' => (int)($admin['id'] ?? $this->auth->id), 'nickname' => (string)($admin['nickname'] ?? $admin['username'] ?? ''), ]); $this->view->assign('adminList', $this->loadNormalAdminList()); return $this->view->fetch(); } $params = $this->request->post('row/a'); if (empty($params)) { $this->error(__('Parameter %s can not be empty', '')); } $subject = trim((string)($params['subject'] ?? '')); $content = trim((string)($params['content'] ?? '')); if ($subject === '') { $this->error('请填写主题'); } if ($content === '') { $this->error('请填写内容'); } $senderId = (int)$this->auth->id; if ($senderId <= 0 || !$this->adminExists($senderId)) { $this->error('当前登录用户无效'); } $recipientIds = $this->parseRecipientIdList($params['recipient_ids'] ?? ''); if ($recipientIds === []) { $this->error('请选择投递接收人'); } $creatorId = (int)$this->auth->id; $now = date('Y-m-d H:i:s'); Db::startTrans(); try { $contentId = (int)Db::table('purchase_content')->insertGetId([ 'creator_id' => $creatorId, 'sender_id' => $senderId, 'subject' => $subject, 'content' => $content, 'createtime' => $now, 'updatetime' => $now, ]); if ($contentId <= 0) { throw new Exception('保存公告失败'); } $recvRows = []; foreach ($recipientIds as $aid) { $recvRows[] = [ 'content_id' => $contentId, 'admin_id' => $aid, 'createtime' => $now, ]; } Db::table('purchase_content_recipient')->insertAll($recvRows); Db::commit(); } catch (ValidateException|PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } $this->success(); } public function detail($ids = null) { $row = $this->getContentRowForAdmin((int)$ids); if (!$row) { $this->error(__('No Results were found')); } $adminId = (int)$this->auth->id; if (!$this->auth->isSuperAdmin() && (int)$row['creator_id'] !== $adminId) { $isRecv = Db::table('purchase_content_recipient') ->where('content_id', (int)$row['id']) ->where('admin_id', $adminId) ->count(); if (!$isRecv) { $this->error('无权查看该通知'); } $recvRow = Db::table('purchase_content_recipient') ->where('content_id', (int)$row['id']) ->where('admin_id', $adminId) ->find(); if ($recvRow && empty($recvRow['read_time'])) { Db::table('purchase_content_recipient') ->where('id', (int)$recvRow['id']) ->update(['read_time' => date('Y-m-d H:i:s')]); } } $items = [$row]; $this->enrichContentRows($items, $adminId); $row = $items[0]; $this->view->assign('row', $row); return $this->view->fetch(); } public function del($ids = null) { if (!$this->request->isPost()) { $this->error(__('Invalid parameters')); } $ids = $ids ?: $this->request->post('ids'); $idList = $this->parseIdList($ids); if ($idList === []) { $this->error(__('Parameter %s can not be empty', 'ids')); } $adminId = (int)$this->auth->id; Db::startTrans(); try { foreach ($idList as $cid) { $row = $this->model->get($cid); if (!$row) { continue; } if ((int)$row['sender_id'] !== $adminId) { throw new Exception('仅可删除本人投递的公告'); } Db::table('purchase_content_recipient')->where('content_id', $cid)->delete(); $row->delete(); } Db::commit(); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } $this->success(); } /** * 安装菜单(超级管理员执行一次) */ public function install() { if (!$this->auth->isSuperAdmin()) { $this->error('仅超级管理员可执行'); } $t = time(); $pid = 0; $parent = Db::name('auth_rule')->where('name', 'procuremenroot')->find(); if ($parent) { $pid = (int)$parent['id']; } else { $pick = Db::name('auth_rule')->where('name', 'procuremen/pick')->find(); if ($pick) { $pid = (int)($pick['pid'] ?? 0); } } if ($pid <= 0) { $this->error('未找到协助采购父菜单,请先在权限规则中配置 procuremenroot 或 procuremen/pick'); } $added = 0; $menuName = 'purchasecontent/index'; $menuRule = Db::name('auth_rule')->where('name', $menuName)->find(); if ($menuRule) { $menuId = (int)$menuRule['id']; } else { $menuId = Db::name('auth_rule')->insertGetId([ 'type' => 'file', 'pid' => $pid, 'name' => $menuName, 'title' => '通知公告', 'icon' => 'fa fa-bullhorn', 'url' => '', 'ismenu' => 1, 'menutype' => 'addtabs', 'weigh' => 84, 'status' => 'normal', 'createtime' => $t, 'updatetime' => $t, ]); $added++; } foreach (['purchasecontent/add' => '新增', 'purchasecontent/detail' => '查看', 'purchasecontent/del' => '删除', 'purchasecontent/adminselect' => '选择用户'] as $name => $title) { if (Db::name('auth_rule')->where('name', $name)->find()) { continue; } Db::name('auth_rule')->insert([ 'type' => 'file', 'pid' => $menuId, 'name' => $name, 'title' => $title, 'icon' => 'fa fa-circle-o', 'ismenu' => 0, 'status' => 'normal', 'createtime' => $t, 'updatetime' => $t, ]); $added++; } \think\Cache::rm('__menu__'); $this->success('通知公告菜单安装完成,新增节点 ' . $added . ' 个。请刷新后台并为角色勾选权限。'); } /** * @param array $rows */ protected function enrichContentRows(array &$rows, int $currentAdminId): void { if ($rows === []) { return; } $contentIds = []; $adminIds = []; foreach ($rows as $r) { if (!is_array($r) && !is_object($r)) { continue; } $arr = is_array($r) ? $r : $r->toArray(); $cid = (int)($arr['id'] ?? 0); if ($cid > 0) { $contentIds[$cid] = true; } foreach (['creator_id', 'sender_id'] as $fk) { $aid = (int)($arr[$fk] ?? 0); if ($aid > 0) { $adminIds[$aid] = true; } } } $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds)); $recvMap = $this->loadRecipientMap(array_keys($contentIds)); foreach ($rows as &$row) { if (is_object($row) && method_exists($row, 'getData')) { $data = $row->getData(); } elseif (is_object($row) && method_exists($row, 'toArray')) { $data = $row->toArray(); } else { $data = (array)$row; } $cid = (int)($data['id'] ?? 0); $creatorId = (int)($data['creator_id'] ?? 0); $senderId = (int)($data['sender_id'] ?? 0); $recv = $recvMap[$cid] ?? ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null]; $isSender = ($senderId === $currentAdminId); $extra = [ 'creator_name' => $nameMap[$creatorId] ?? '', 'sender_name' => $nameMap[$senderId] ?? '', 'sender_id' => $senderId, 'recipient_names' => implode('、', $recv['names']), 'recipient_count' => $recv['count'], 'is_receiver' => $recv['is_receiver'] ? 1 : 0, 'is_sender' => $isSender ? 1 : 0, 'read_time' => $recv['read_time'], 'send_time_text' => $this->formatPurchaseContentTime($data['createtime'] ?? ''), 'can_delete' => $isSender ? 1 : 0, ]; if (is_object($row)) { foreach ($extra as $k => $v) { $row->$k = $v; } } else { $row = array_merge($data, $extra); } } unset($row); } /** * @param int[] $adminIds * @return array */ protected function loadAdminNicknameMap(array $adminIds): array { $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds)))); if ($adminIds === []) { return []; } $rows = Db::name('admin') ->where('id', 'in', $adminIds) ->field('id,nickname,username') ->select(); if (!is_array($rows)) { return []; } $out = []; foreach ($rows as $r) { if (!is_array($r)) { continue; } $id = (int)($r['id'] ?? 0); $nick = trim((string)($r['nickname'] ?? '')); if ($nick === '') { $nick = trim((string)($r['username'] ?? '')); } $out[$id] = $nick !== '' ? $nick : ('#' . $id); } return $out; } /** * @param int[] $contentIds * @return array */ protected function loadRecipientMap(array $contentIds): array { $contentIds = array_values(array_unique(array_filter(array_map('intval', $contentIds)))); $out = []; if ($contentIds === []) { return $out; } $recvRows = Db::table('purchase_content_recipient') ->where('content_id', 'in', $contentIds) ->field('content_id,admin_id,read_time') ->select(); if (!is_array($recvRows)) { return $out; } $adminIds = []; foreach ($recvRows as $r) { if (!is_array($r)) { continue; } $aid = (int)($r['admin_id'] ?? 0); if ($aid > 0) { $adminIds[$aid] = true; } } $nameMap = $this->loadAdminNicknameMap(array_keys($adminIds)); $currentAdminId = (int)$this->auth->id; foreach ($recvRows as $r) { if (!is_array($r)) { continue; } $cid = (int)($r['content_id'] ?? 0); $aid = (int)($r['admin_id'] ?? 0); if ($cid <= 0) { continue; } if (!isset($out[$cid])) { $out[$cid] = ['names' => [], 'count' => 0, 'is_receiver' => false, 'read_time' => null]; } $nm = $nameMap[$aid] ?? ('#' . $aid); if ($nm !== '' && !in_array($nm, $out[$cid]['names'], true)) { $out[$cid]['names'][] = $nm; } if ($aid === $currentAdminId) { $out[$cid]['is_receiver'] = true; $rt = $r['read_time'] ?? null; if ($rt !== null && $rt !== '' && stripos((string)$rt, '0000-00-00') !== 0) { $out[$cid]['read_time'] = $this->formatPurchaseContentTime($rt); } } } foreach ($out as $cid => &$item) { sort($item['names'], SORT_STRING); $item['count'] = count($item['names']); } unset($item); return $out; } protected function getContentRowForAdmin(int $id): ?array { if ($id <= 0) { return null; } $row = $this->model->get($id); return $row ? $row->toArray() : null; } protected function adminExists(int $adminId): bool { if ($adminId <= 0) { return false; } return (bool)Db::name('admin')->where('id', $adminId)->where('status', 'normal')->count(); } /** * 接收人是否属于「供应商证」角色组(id=10)及其子组 */ protected function recipientAdminExists(int $adminId): bool { if ($adminId <= 0 || !$this->adminExists($adminId)) { return false; } $groupIds = $this->loadRecipientAuthGroupIds(); if ($groupIds === []) { return false; } return (bool)Db::name('auth_group_access') ->where('uid', $adminId) ->where('group_id', 'in', $groupIds) ->count(); } /** * auth_group id=10 及其全部子组 id * * @return int[] */ protected function loadRecipientAuthGroupIds(): array { static $memo = null; if ($memo !== null) { return $memo; } $cacheKey = 'purchase_content_group_ids_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID; $cached = Cache::get($cacheKey); if (is_array($cached) && $cached !== []) { $memo = $cached; return $memo; } $rootId = self::RECIPIENT_AUTH_GROUP_ROOT_ID; try { $groups = Db::name('auth_group')->where('status', 'normal')->field('id,pid')->select(); } catch (\Throwable $e) { return [$rootId]; } if (!is_array($groups) || $groups === []) { return [$rootId]; } $groupList = []; foreach ($groups as $g) { if (is_array($g)) { $groupList[] = $g; } } if ($groupList === []) { return [$rootId]; } $tree = \fast\Tree::instance(); $tree->init($groupList); $ids = $tree->getChildrenIds($rootId, true); if (!is_array($ids) || $ids === []) { $memo = [$rootId]; return $memo; } $memo = array_values(array_unique(array_filter(array_map('intval', $ids)))); Cache::set($cacheKey, $memo, self::RECIPIENT_LIST_CACHE_TTL); return $memo; } /** * 新增公告:可选接收人列表(仅角色组 id=10 及其子组下的用户) * * @return array */ protected function loadNormalAdminList(): array { $cacheKey = 'purchase_content_admin_list_v2_' . self::RECIPIENT_AUTH_GROUP_ROOT_ID; $cached = Cache::get($cacheKey); if (is_array($cached)) { return $cached; } $groupIds = $this->loadRecipientAuthGroupIds(); if ($groupIds === []) { return []; } try { $adminIds = Db::name('auth_group_access') ->where('group_id', 'in', $groupIds) ->column('uid'); } catch (\Throwable $e) { $adminIds = []; } $adminIds = is_array($adminIds) ? array_values(array_unique(array_filter(array_map('intval', $adminIds)))) : []; if ($adminIds === []) { return []; } try { $rows = Db::name('admin') ->where('status', 'normal') ->where('id', 'in', $adminIds) ->field('id,username,nickname') ->order('id', 'asc') ->select(); } catch (\Throwable $e) { $rows = []; } if (!is_array($rows)) { return []; } $out = []; foreach ($rows as $r) { if (!is_array($r)) { continue; } $id = (int)($r['id'] ?? 0); if ($id <= 0) { continue; } $username = trim((string)($r['username'] ?? '')); $nickname = trim((string)($r['nickname'] ?? '')); if ($nickname === '') { $nickname = $username; } if ($nickname === '') { $nickname = '#' . $id; } $out[] = [ 'id' => $id, 'username' => $username, 'nickname' => $nickname, 'label' => $nickname, ]; } Cache::set($cacheKey, $out, self::RECIPIENT_LIST_CACHE_TTL); return $out; } /** * @param mixed $raw * @return int[] */ protected function parseIdList($raw): array { if (is_array($raw)) { $parts = $raw; } else { $parts = preg_split('/\s*,\s*/', trim((string)$raw), -1, PREG_SPLIT_NO_EMPTY); } if (!is_array($parts)) { return []; } $out = []; foreach ($parts as $p) { $id = (int)$p; if ($id > 0) { $out[$id] = $id; } } return array_values($out); } /** * 解析并校验接收人 id(须为角色组 id=10 及其子组下的正常用户) * * @param mixed $raw * @return int[] */ protected function parseRecipientIdList($raw): array { $out = []; foreach ($this->parseIdList($raw) as $id) { if ($this->recipientAdminExists($id)) { $out[$id] = $id; } } return array_values($out); } }