mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-07 19:40:49 +08:00
307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
||
|
||
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;
|
||
use app\common\controller\AdminController;
|
||
use app\common\service\NoticeService;
|
||
use think\App;
|
||
|
||
/**
|
||
* 通知管理(系统流程写入,不提供 add/edit;删除为软删).
|
||
*
|
||
* @ControllerAnnotation(title="通知管理")
|
||
*/
|
||
class NoticeBase extends AdminController
|
||
{
|
||
protected $sort = [
|
||
'id' => '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 => '公众号',
|
||
];
|
||
}
|
||
}
|