diff --git a/extend/base/admin/controller/system/NoticeBase.php b/extend/base/admin/controller/system/NoticeBase.php index f1f24a3..504c950 100644 --- a/extend/base/admin/controller/system/NoticeBase.php +++ b/extend/base/admin/controller/system/NoticeBase.php @@ -4,6 +4,7 @@ namespace base\admin\controller\system; use app\admin\model\SystemAdmin; use app\admin\model\SystemNotice; +use app\admin\model\SystemNoticeSubscribe; use app\admin\model\SystemNoticeTarget; use app\admin\service\annotation\ControllerAnnotation; use app\admin\service\annotation\NodeAnotation; @@ -110,12 +111,7 @@ class NoticeBase extends AdminController $statusCount[$group['channel']][$group['status']] = (int) $group['cnt']; } - $channelNames = [ - NoticeService::CHANNEL_INMAIL => '站内信', - NoticeService::CHANNEL_EMAIL => '邮件', - NoticeService::CHANNEL_SMS => '短信', - NoticeService::CHANNEL_WECHAT => '公众号', - ]; + $channelNames = $this->channelNames(); $delivery = []; foreach ($statusCount as $channel => $counts) { $delivery[] = [ @@ -128,6 +124,41 @@ class NoticeBase extends AdminController ]; } + // 投递明细:全量加载(行数=接收方数,量可控) + $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; @@ -136,6 +167,7 @@ class NoticeBase extends AdminController $this->assign('sender_nickname', $senderNickname); $this->assign('scene_title', $sceneTitle); $this->assign('delivery', $delivery); + $this->assign('target_rows', $targetRows); return $this->fetch(); } @@ -182,4 +214,93 @@ class NoticeBase extends AdminController } $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 => '公众号', + ]; + } } diff --git a/extend/base/admin/view/system/notice/read.html b/extend/base/admin/view/system/notice/read.html index e464b2c..4154671 100644 --- a/extend/base/admin/view/system/notice/read.html +++ b/extend/base/admin/view/system/notice/read.html @@ -79,6 +79,62 @@ +
+
投递明细
+
+ {notempty name="target_rows"} + + + + + + + + + + + + + + + {volist name="target_rows" id="tvo"} + + + + + + + + + + + {/volist} + +
明细ID接收人渠道状态重试次数错误信息发送时间操作
{$tvo.id}{$tvo.admin_nickname}{$tvo.channel_name}{$tvo.status_name}{$tvo.retry_count} + {notempty name="tvo.error_message"} + {$tvo.error_message|htmlspecialchars} + {else/} + - + {/notempty} + + {if condition="$tvo.send_time gt 0"} + {$tvo.send_time|date="Y-m-d H:i:s"} + {else/} + - + {/if} + + {if condition="$tvo.status eq 3"} + + {else/} + - + {/if} +
+ {else/} + 暂无投递明细 + {/notempty} +
+
+ diff --git a/extend/base/admin/view/system/notice/read.js b/extend/base/admin/view/system/notice/read.js index 81a35d4..32f9473 100644 --- a/extend/base/admin/view/system/notice/read.js +++ b/extend/base/admin/view/system/notice/read.js @@ -1,3 +1,21 @@ $(function(){ ua.listen(); + + // 无 resend 权限时隐藏行内重发按钮(与框架按钮保守隐藏口径一致) + if (ua.auth('resend') === false) { + $('.notice-resend-btn').remove(); + } + + // 失败投递行内重发:POST target_id,服务端原子重置 status=3 行后刷新明细 + $(document).on('click', '.notice-resend-btn', function () { + var btn = $(this); + ua.request.post({ + url: ua.url('system.notice/resend'), + data: {target_id: btn.data('target-id')}, + }, function (res) { + ua.msg.success(res.msg || '已重发', function () { + location.reload(); + }); + }); + }); }) diff --git a/extend/base/admin/view/system/notice/scene_overview.html b/extend/base/admin/view/system/notice/scene_overview.html new file mode 100644 index 0000000..e950caf --- /dev/null +++ b/extend/base/admin/view/system/notice/scene_overview.html @@ -0,0 +1,43 @@ +
+
+
+
+

通知场景总览

+
+
+ + + + + + + + + + + {volist name="rows" id="vo"} + + + + + + + {/volist} + +
场景投递渠道订阅要求订阅统计
+ {$vo.title} +
{$vo.scene}
+
{$vo.channel_names} + {if condition="$vo.locked eq 1"} + 必选 + {else/} + 自愿 + {/if} + {$vo.subscribed} / {$vo.total_admins} 位管理员
+
+ 订阅统计口径:该场景订阅状态为启用(status=1)的去重管理员数 / 全体启用管理员数。场景由代码注册,本页仅展示。 +
+
+
+
+
diff --git a/extend/base/admin/view/system/notice/scene_overview.js b/extend/base/admin/view/system/notice/scene_overview.js new file mode 100644 index 0000000..81a35d4 --- /dev/null +++ b/extend/base/admin/view/system/notice/scene_overview.js @@ -0,0 +1,3 @@ +$(function(){ + ua.listen(); +})