Files
ulthon_admin/extend/base/admin/controller/system/NoticeBase.php
2026-10-12 13:40:40 +00:00

307 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 => '公众号',
];
}
}