mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-08 20:10:50 +08:00
feat(notice): 通知管理 CURD 模块双层化
This commit is contained in:
14
app/admin/controller/system/Notice.php
Normal file
14
app/admin/controller/system/Notice.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\system;
|
||||||
|
|
||||||
|
use app\admin\service\annotation\ControllerAnnotation;
|
||||||
|
use base\admin\controller\system\NoticeBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ControllerAnnotation(title="通知管理",module="系统")
|
||||||
|
* Class Notice
|
||||||
|
*/
|
||||||
|
class Notice extends NoticeBase
|
||||||
|
{
|
||||||
|
}
|
||||||
185
extend/base/admin/controller/system/NoticeBase.php
Normal file
185
extend/base/admin/controller/system/NoticeBase.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace base\admin\controller\system;
|
||||||
|
|
||||||
|
use app\admin\model\SystemAdmin;
|
||||||
|
use app\admin\model\SystemNotice;
|
||||||
|
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 = [
|
||||||
|
NoticeService::CHANNEL_INMAIL => '站内信',
|
||||||
|
NoticeService::CHANNEL_EMAIL => '邮件',
|
||||||
|
NoticeService::CHANNEL_SMS => '短信',
|
||||||
|
NoticeService::CHANNEL_WECHAT => '公众号',
|
||||||
|
];
|
||||||
|
$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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景标题(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);
|
||||||
|
|
||||||
|
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('未做变更');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
extend/base/admin/view/system/notice/_common.js
Normal file
11
extend/base/admin/view/system/notice/_common.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
var init = {
|
||||||
|
tableElem: '#currentTable',
|
||||||
|
tableRenderId: 'currentTableRenderId',
|
||||||
|
indexUrl: 'system.notice/index',
|
||||||
|
addUrl: '',
|
||||||
|
editUrl: '',
|
||||||
|
readUrl: 'system.notice/read',
|
||||||
|
deleteUrl: 'system.notice/delete',
|
||||||
|
exportUrl: '',
|
||||||
|
modifyUrl: 'system.notice/modify',
|
||||||
|
};
|
||||||
7
extend/base/admin/view/system/notice/index.html
Normal file
7
extend/base/admin/view/system/notice/index.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="layuimini-container">
|
||||||
|
<div class="layuimini-main">
|
||||||
|
<table id="currentTable" class="layui-table layui-hide"
|
||||||
|
lay-filter="currentTable">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
54
extend/base/admin/view/system/notice/index.js
Normal file
54
extend/base/admin/view/system/notice/index.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
$(function(){
|
||||||
|
// 场景徽章:标题取 data_brage 下发的 scenes() 动态映射,色标按场景写死(layui-badge 色系)
|
||||||
|
var sceneColorMap = {
|
||||||
|
system: 'layui-badge',
|
||||||
|
common: 'layui-badge layui-bg-blue',
|
||||||
|
admin_login: 'layui-badge layui-bg-green',
|
||||||
|
mall_stock: 'layui-badge layui-bg-orange',
|
||||||
|
};
|
||||||
|
|
||||||
|
var sceneTemplet = function(d) {
|
||||||
|
var titles = ua.getDataBrage('select_list_scene');
|
||||||
|
var text = titles[d.scene] || d.scene;
|
||||||
|
var cls = sceneColorMap[d.scene] || 'layui-badge layui-bg-gray';
|
||||||
|
return '<span class="' + cls + '">' + text + '</span>';
|
||||||
|
};
|
||||||
|
|
||||||
|
var senderTemplet = function(d) {
|
||||||
|
if (!d.send_admin_id) {
|
||||||
|
return '<span style="color:#999">系统</span>';
|
||||||
|
}
|
||||||
|
return d.send_admin_nickname || ('#' + d.send_admin_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
ua.table.render({
|
||||||
|
init: init,
|
||||||
|
toolbar: ['refresh', 'delete'],
|
||||||
|
defaultToolbar: ['filter'],
|
||||||
|
cols: [[
|
||||||
|
{type: 'checkbox'},
|
||||||
|
{field: 'id', title: 'ID', width: 80, sort: true},
|
||||||
|
{field: 'scene', title: '场景', width: 110, search: 'select', searchOp: '=', selectList: ua.getDataBrage('select_list_scene'), templet: sceneTemplet},
|
||||||
|
{field: 'title', title: '标题', minWidth: 220},
|
||||||
|
{field: 'send_admin_nickname', title: '发送者', width: 120, search: false, templet: senderTemplet},
|
||||||
|
{field: 'create_time', title: '创建时间', width: 170, templet: ua.table.date, search: 'range'},
|
||||||
|
{
|
||||||
|
width: 130, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
|
||||||
|
[{
|
||||||
|
class: 'layui-btn layui-btn-primary layui-btn-xs',
|
||||||
|
method: 'tab',
|
||||||
|
field: 'id',
|
||||||
|
text: '详情',
|
||||||
|
title: '查看详情',
|
||||||
|
auth: 'read',
|
||||||
|
url: init.readUrl,
|
||||||
|
icon: ''
|
||||||
|
}],
|
||||||
|
'delete'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]],
|
||||||
|
});
|
||||||
|
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
119
extend/base/admin/view/system/notice/read.html
Normal file
119
extend/base/admin/view/system/notice/read.html
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<div class="layuimini-container detail-container">
|
||||||
|
<div class="layuimini-main">
|
||||||
|
<div class="layui-card detail-card">
|
||||||
|
<div class="layui-card-header detail-header">
|
||||||
|
<div class="layui-row">
|
||||||
|
<div class="layui-col-md9">
|
||||||
|
<h2 class="detail-title">#{$row.id} {$title}</h2>
|
||||||
|
<div class="detail-id">ID: {$row.id}</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-col-md3 text-right detail-actions">
|
||||||
|
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-card-body detail-content">
|
||||||
|
<div class="layui-row layui-col-space12">
|
||||||
|
<!-- 左侧主体内容 -->
|
||||||
|
<div class="layui-col-md8 detail-main">
|
||||||
|
<div class="detail-field-group">
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">场景</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{switch name="row.scene"}
|
||||||
|
{case value="system"}<span class="layui-badge">{$scene_title}</span>{/case}
|
||||||
|
{case value="common"}<span class="layui-badge layui-bg-blue">{$scene_title}</span>{/case}
|
||||||
|
{case value="admin_login"}<span class="layui-badge layui-bg-green">{$scene_title}</span>{/case}
|
||||||
|
{case value="mall_stock"}<span class="layui-badge layui-bg-orange">{$scene_title}</span>{/case}
|
||||||
|
{default /}<span class="layui-badge layui-bg-gray">{$scene_title}</span>
|
||||||
|
{/switch}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">标题</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.title"}
|
||||||
|
{$row.title}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无数据</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item detail-field-full">
|
||||||
|
<div class="detail-field-label">内容</div>
|
||||||
|
<div class="detail-field-value detail-editor-content">
|
||||||
|
{notempty name="row.content"}
|
||||||
|
{$row.content|raw}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无内容</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">业务关联ID</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.biz_id"}
|
||||||
|
{$row.biz_id}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">-</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-field-item detail-field-full">
|
||||||
|
<div class="detail-field-label">投递汇总</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="delivery"}
|
||||||
|
{volist name="delivery" id="vo"}
|
||||||
|
<div style="margin-bottom:4px;">
|
||||||
|
<strong>{$vo.channel_name}</strong>
|
||||||
|
<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#5FB878;background:#e8f8ef;font-size:12px;margin-left:6px;">成功 {$vo.success}</span>
|
||||||
|
<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#FF5722;background:#ffe8e2;font-size:12px;margin-left:4px;">失败 {$vo.failed}</span>
|
||||||
|
<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#1E9FFF;background:#e2f1ff;font-size:12px;margin-left:4px;">待发 {$vo.pending}</span>
|
||||||
|
{if condition="$vo.sending gt 0"}<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#FFB800;background:#fff8e2;font-size:12px;margin-left:4px;">发送中 {$vo.sending}</span>{/if}
|
||||||
|
</div>
|
||||||
|
{/volist}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无投递记录</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 右侧基础信息 -->
|
||||||
|
<div class="layui-col-md4 detail-side">
|
||||||
|
<h3 class="detail-side-title">基础信息</h3>
|
||||||
|
<div class="detail-field-group">
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">ID</div>
|
||||||
|
<div class="detail-field-value">{$row.id}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">发送者</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{if condition="$row.send_admin_id gt 0"}
|
||||||
|
{$sender_nickname|default=''}(#{$row.send_admin_id})
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">系统</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">创建时间</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.create_time"}
|
||||||
|
{$row.create_time|date="Y-m-d H:i:s"}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无数据</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
3
extend/base/admin/view/system/notice/read.js
Normal file
3
extend/base/admin/view/system/notice/read.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
$(function(){
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user