mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-07 19:40:49 +08:00
feat(notice): 投递明细/失败重发/只读场景总览
This commit is contained in:
@@ -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 => '公众号',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-field-item detail-field-full">
|
||||
<div class="detail-field-label">投递明细</div>
|
||||
<div class="detail-field-value">
|
||||
{notempty name="target_rows"}
|
||||
<table class="layui-table" style="margin:0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>明细ID</th>
|
||||
<th>接收人</th>
|
||||
<th>渠道</th>
|
||||
<th>状态</th>
|
||||
<th>重试次数</th>
|
||||
<th>错误信息</th>
|
||||
<th>发送时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{volist name="target_rows" id="tvo"}
|
||||
<tr>
|
||||
<td>{$tvo.id}</td>
|
||||
<td>{$tvo.admin_nickname}</td>
|
||||
<td>{$tvo.channel_name}</td>
|
||||
<td><span class="{$tvo.status_class}">{$tvo.status_name}</span></td>
|
||||
<td>{$tvo.retry_count}</td>
|
||||
<td>
|
||||
{notempty name="tvo.error_message"}
|
||||
{$tvo.error_message|htmlspecialchars}
|
||||
{else/}
|
||||
-
|
||||
{/notempty}
|
||||
</td>
|
||||
<td>
|
||||
{if condition="$tvo.send_time gt 0"}
|
||||
{$tvo.send_time|date="Y-m-d H:i:s"}
|
||||
{else/}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{if condition="$tvo.status eq 3"}
|
||||
<button type="button" class="layui-btn layui-btn-warm layui-btn-xs notice-resend-btn" data-target-id="{$tvo.id}">重发</button>
|
||||
{else/}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
</tbody>
|
||||
</table>
|
||||
{else/}
|
||||
<span class="layui-text-em">暂无投递明细</span>
|
||||
{/notempty}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧基础信息 -->
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
43
extend/base/admin/view/system/notice/scene_overview.html
Normal file
43
extend/base/admin/view/system/notice/scene_overview.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="layuimini-container detail-container">
|
||||
<div class="layuimini-main">
|
||||
<div class="layui-card detail-card">
|
||||
<div class="layui-card-header detail-header">
|
||||
<h2 class="detail-title">通知场景总览</h2>
|
||||
</div>
|
||||
<div class="layui-card-body detail-content">
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>场景</th>
|
||||
<th>投递渠道</th>
|
||||
<th>订阅要求</th>
|
||||
<th>订阅统计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{volist name="rows" id="vo"}
|
||||
<tr>
|
||||
<td>
|
||||
{$vo.title}
|
||||
<div style="color:#999;font-size:12px;">{$vo.scene}</div>
|
||||
</td>
|
||||
<td>{$vo.channel_names}</td>
|
||||
<td>
|
||||
{if condition="$vo.locked eq 1"}
|
||||
<span class="layui-badge">必选</span>
|
||||
{else/}
|
||||
<span class="layui-badge layui-bg-blue">自愿</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{$vo.subscribed} / {$vo.total_admins} 位管理员</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="color:#999;font-size:12px;margin-top:8px;">
|
||||
订阅统计口径:该场景订阅状态为启用(status=1)的去重管理员数 / 全体启用管理员数。场景由代码注册,本页仅展示。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
3
extend/base/admin/view/system/notice/scene_overview.js
Normal file
3
extend/base/admin/view/system/notice/scene_overview.js
Normal file
@@ -0,0 +1,3 @@
|
||||
$(function(){
|
||||
ua.listen();
|
||||
})
|
||||
Reference in New Issue
Block a user