'desc', ]; /** * 行内属性修改允许的字段(通知主体由系统流程生成,暂不开放字段; * 保留标准白名单结构,下游可按需放开). * @var array */ protected $allowModifyFields = []; public function __construct(App $app) { parent::__construct($app); $this->model = new SystemNotice(); // 场景标题映射(scene 等值搜索下拉与列表徽章共用;data_brage 通道下发) $sceneOptions = []; foreach (NoticeService::scenes() as $scene => $item) { $sceneOptions[$scene] = $item['title']; } $this->assign('select_list_scene', $sceneOptions, true); } /** * @NodeAnotation(title="列表") */ public function index() { if ($this->request->isAjax()) { if (input('selectFields')) { return $this->selectList(); } list($page, $limit, $where) = $this->buildTableParames(); $count = $this->model ->where($where) ->count(); $list = $this->model ->where($where) ->page($page, $limit) ->order($this->sort) ->select(); // 发送者昵称:system_admin 与主表存在同名字段,禁用 withJoin 预载; // 单查昵称缓存后循环附加 $senderIds = array_values(array_filter(array_unique($list->column('send_admin_id')))); $nicknames = []; if (!empty($senderIds)) { $nicknames = SystemAdmin::whereIn('id', $senderIds)->column('nickname', 'id'); } foreach ($list as $row) { $row['send_admin_nickname'] = $nicknames[$row['send_admin_id']] ?? ''; } $data = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $list, ]; return json($data); } return $this->fetch(); } /** * @NodeAnotation(title="详情") */ public function read($id) { $row = $this->model->find($id); empty($row) && $this->error('数据不存在'); // 发送者昵称(单查,勿 withJoin,同列表口径) $senderNickname = ''; if (!empty($row->send_admin_id)) { $senderNickname = SystemAdmin::where('id', $row->send_admin_id)->value('nickname') ?: ''; } // 投递汇总:channel × status 聚合计数(一条 group 查询) $groups = SystemNoticeTarget::where('notice_id', $id) ->field('channel,status,count(*) as cnt') ->group('channel,status') ->select(); $statusCount = []; foreach ($groups as $group) { $statusCount[$group['channel']][$group['status']] = (int) $group['cnt']; } $channelNames = $this->channelNames(); $delivery = []; foreach ($statusCount as $channel => $counts) { $delivery[] = [ 'channel' => $channel, 'channel_name' => $channelNames[$channel] ?? $channel, 'pending' => $counts[SystemNoticeTarget::STATUS_PENDING] ?? 0, 'sending' => $counts[SystemNoticeTarget::STATUS_SENDING] ?? 0, 'success' => $counts[SystemNoticeTarget::STATUS_SUCCESS] ?? 0, 'failed' => $counts[SystemNoticeTarget::STATUS_FAILED] ?? 0, ]; } // 投递明细:全量加载(行数=接收方数,量可控) $targets = SystemNoticeTarget::where('notice_id', $id) ->order('id') ->select(); // 接收人昵称:whereIn 单查映射(禁 withJoin,system_admin 与主表存在同名字段) $adminIds = array_values(array_filter(array_unique($targets->column('admin_id')))); $nicknames = []; if (!empty($adminIds)) { $nicknames = SystemAdmin::whereIn('id', $adminIds)->column('nickname', 'id'); } $statusMeta = [ SystemNoticeTarget::STATUS_PENDING => ['name' => '待发', 'class' => 'layui-badge layui-bg-blue'], SystemNoticeTarget::STATUS_SENDING => ['name' => '发送中', 'class' => 'layui-badge layui-bg-orange'], SystemNoticeTarget::STATUS_SUCCESS => ['name' => '成功', 'class' => 'layui-badge layui-bg-green'], SystemNoticeTarget::STATUS_FAILED => ['name' => '失败', 'class' => 'layui-badge'], ]; $targetRows = []; foreach ($targets as $target) { $status = (int) $target->status; $targetRows[] = [ 'id' => $target->id, 'admin_nickname' => $nicknames[$target->admin_id] ?? ('#' . $target->admin_id), 'target_addr' => $target->target_addr, 'channel_name' => $channelNames[$target->channel] ?? $target->channel, 'status' => $status, 'status_name' => $statusMeta[$status]['name'] ?? (string) $status, 'status_class' => $statusMeta[$status]['class'] ?? 'layui-badge layui-bg-gray', 'retry_count' => (int) $target->retry_count, 'error_message' => (string) $target->error_message, 'send_time' => (int) $target->send_time, ]; } // 场景标题(scenes() 动态映射,未注册场景回退原值) $sceneTitle = NoticeService::scenes()[$row->scene]['title'] ?? $row->scene; $this->assign('row', $row); $this->assign('title', $row->title); $this->assign('sender_nickname', $senderNickname); $this->assign('scene_title', $sceneTitle); $this->assign('delivery', $delivery); $this->assign('target_rows', $targetRows); return $this->fetch(); } /** * @NodeAnotation(title="删除") */ public function delete($id) { $this->checkPostRequest(); $row = $this->model->whereIn('id', $id)->select(); $row->isEmpty() && $this->error('数据不存在'); try { // 模型启用软删(delete_time),delete 即软删;投递行保留不级联 $save = $row->delete(); } catch (\Exception $e) { $this->error('删除失败:' . $e->getMessage()); } $save ? $this->success('删除成功') : $this->error('删除失败'); } /** * @NodeAnotation(title="属性修改") */ public function modify() { $this->checkPostRequest(); $post = $this->request->post(); $rule = [ 'id|ID' => 'require', 'field|字段' => 'require', 'value|值' => 'require', ]; $this->validate($post, $rule); if (!in_array($post['field'], $this->allowModifyFields)) { $this->error('该字段不允许修改:' . $post['field']); } $row = $this->model->find($post['id']); empty($row) && $this->error('数据不存在'); try { $save = $row->save([$post['field'] => $post['value']]); } catch (\Exception $e) { $this->error($e->getMessage()); } $save ? $this->success('保存成功') : $this->error('未做变更'); } /** * @NodeAnotation(title="失败重发") * * POST notice_id(批量重置该通知全部失败投递)或 target_id(单行)。 * 原子条件 UPDATE:仅命中 status=3 的失败行(已成功/发送中行不受影响), * 影响行数即真实重置数。重置后由 timer 消费者按退避策略重新消费。 */ public function resend() { $this->checkPostRequest(); $noticeId = $this->request->post('notice_id/d', 0); $targetId = $this->request->post('target_id/d', 0); if (empty($noticeId) && empty($targetId)) { $this->error('缺少参数:notice_id 或 target_id'); } $query = SystemNoticeTarget::where('status', SystemNoticeTarget::STATUS_FAILED); if ($noticeId > 0) { $query->where('notice_id', $noticeId); } else { $query->where('id', $targetId); } $count = $query->update([ 'status' => SystemNoticeTarget::STATUS_PENDING, 'retry_count' => 0, 'next_retry_time' => 0, 'error_message' => '', ]); $count > 0 ? $this->success('已将 ' . $count . ' 条失败投递重置为待发送') : $this->error('没有可重发的失败投递(仅失败状态可重发)'); } /** * @NodeAnotation(title="场景总览") * * 只读渲染 scenes() 注册表:标题/渠道/是否必选/每场景订阅统计(分式展示)。 * 场景为代码级注册表,本页不提供任何写操作。 */ public function sceneOverview() { // 每场景订阅统计:一条 group 查询(scene, count(distinct admin_id)) $subGroups = SystemNoticeSubscribe::where('status', 1) ->field('scene, count(distinct admin_id) as admin_cnt') ->group('scene') ->select(); $subscribeCounts = []; foreach ($subGroups as $item) { $subscribeCounts[$item['scene']] = (int) $item['admin_cnt']; } // 全体启用管理员数(软删自动排除)一次单查 $totalAdmins = SystemAdmin::where('status', 1)->count(); $channelNames = $this->channelNames(); $rows = []; foreach (NoticeService::scenes() as $scene => $item) { $rows[] = [ 'scene' => $scene, 'title' => $item['title'], 'channel_names' => implode('、', array_map(function ($channel) use ($channelNames) { return $channelNames[$channel] ?? $channel; }, $item['channels'])), 'locked' => (int) $item['locked'], 'subscribed' => $subscribeCounts[$scene] ?? 0, 'total_admins' => $totalAdmins, ]; } $this->assign('rows', $rows); $this->assign('total_admins', $totalAdmins); return $this->fetch(); } /** * 渠道标识到中文名的映射(详情汇总/明细/场景总览共用). */ private function channelNames(): array { return [ NoticeService::CHANNEL_INMAIL => '站内信', NoticeService::CHANNEL_EMAIL => '邮件', NoticeService::CHANNEL_SMS => '短信', NoticeService::CHANNEL_WECHAT => '公众号', ]; } }