优化推送功能

This commit is contained in:
李志强 2025-07-11 08:50:47 +08:00
parent 6b32df5368
commit 7cd7149091
4 changed files with 438 additions and 121 deletions

View File

@ -30,6 +30,8 @@ use app\admin\model\User\UsersGroup;
use app\admin\model\Banner;
use app\admin\model\ContentPush\ContentPush;
use app\admin\model\ContentPush\ContentPushSetting;
use app\admin\model\Resource\Resource;
use app\admin\model\Article\Articles;
class YunzeradminController extends Base
@ -685,32 +687,57 @@ class YunzeradminController extends Base
public function contentpushlist()
{
if (Request::isGet()) {
$page = intval(input('post.page', 1));
$limit = intval(input('post.limit', 10));
// 获取分页参数
$page = (int) input('get.page', 1);
$limit = (int) input('get.limit', 10);
$query = ContentPush::where('delete_time', null)
->field('id, title, type, status, sort, create_time, update_time');
// 获取总记录数
$count = $query->count();
// 获取分页数据
$lists = $query->order(['sort DESC', 'id DESC'])
->page($page, $limit)
// 1. 获取 Articles 表delete_time为空status=2
$articles = Articles::where('delete_time', null)
->where('status', 2)
->field('id, title, push, create_time')
->select()
->toArray();
// 处理数据
foreach ($lists as &$item) {
$item['create_time'] = is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : $item['create_time'];
$item['update_time'] = is_numeric($item['update_time']) ? date('Y-m-d H:i:s', $item['update_time']) : $item['update_time'];
foreach ($articles as &$a) {
$a['type'] = 'article';
$a['create_time'] = is_numeric($a['create_time']) ? date('Y-m-d H:i:s', $a['create_time']) : $a['create_time'];
}
unset($a);
// 2. 获取 Resource 表delete_time为空status=1
$resources = Resource::where('delete_time', null)
->where('status', 1)
->field('id, title, push, create_time')
->select()
->toArray();
foreach ($resources as &$r) {
$r['type'] = 'resource';
$r['create_time'] = is_numeric($r['create_time']) ? date('Y-m-d H:i:s', $r['create_time']) : $r['create_time'];
}
unset($r);
// 3. 合并
$lists = array_merge($articles, $resources);
// 4. 优先显示push为0的数据再按create_time倒序排序
usort($lists, function ($a, $b) {
// push为0的排在前面
if ($a['push'] != $b['push']) {
return $a['push'] - $b['push'];
}
// push相同则按create_time倒序
return strtotime($b['create_time']) <=> strtotime($a['create_time']);
});
// 5. 分页
$total = count($lists);
$offset = ($page - 1) * $limit;
$data = array_slice($lists, $offset, $limit);
return json([
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $lists
'count' => $total,
'data' => $data
]);
}
return json(['code' => 1, 'msg' => '请求方法无效']);
@ -827,7 +854,7 @@ class YunzeradminController extends Base
$limit = intval(input('get.limit', 10));
$query = ContentPushSetting::where('delete_time', null)
->field('id, title, value, status, sort, create_time');
->field('id, title, value, platformType, status, sort, create_time');
$count = $query->count();
@ -851,6 +878,43 @@ class YunzeradminController extends Base
}
}
//选择列出推送内容列表
public function selectpushcontent()
{
$pushcate = strtolower(trim(input('get.pushcate', '')));
$where = ['push' => 0];
$modelMap = [
'article' => Articles::class,
'resource' => Resource::class,
];
if (!isset($modelMap[$pushcate])) {
return json(['code' => 1, 'msg' => '参数错误: pushcate']);
}
$model = $modelMap[$pushcate];
$query = $model::where($where)->field('title,id');
$count = $query->count();
$list = $query->order('id', 'desc')->select()->toArray();
// 判断是接口请求还是页面请求
if (request()->isAjax() || request()->isJson()) {
return json([
'code' => 0,
'msg' => '获取成功',
'count' => $count,
'data' => $list
]);
} else {
// 这里传递变量到模板
return View::fetch('', [
'data' => $list
]);
}
}
//推送配置添加和编辑通用方法
public function contentpushsettingadd()
{
@ -1054,11 +1118,92 @@ class YunzeradminController extends Base
return json(['code' => 1, 'msg' => '请求方法无效']);
}
//获取推送内容Articles和Resources
//https://www.yunzer.cn/index/articles/detail?id=30
//https://www.yunzer.cn/index/program/index?cateid=2
//https://www.yunzer.cn/index/game/index?cateid=8
//https://www.yunzer.cn/index/resources/detail?id=3
public function gopush()
{
$platformType = input('post.platformType');
$urls = input('post.urls/a', []);
if (empty($platformType)) {
return json(['code' => 1, 'msg' => '平台参数缺失']);
}
if (empty($urls)) {
return json(['code' => 1, 'msg' => '推送内容url为空']);
}
// 查找推送平台配置
$setting = ContentPushSetting::where('platformType', $platformType)->find();
if (!$setting) {
return json(['code' => 1, 'msg' => '未找到该平台的推送配置']);
}
if ($setting['status'] == 0) {
return json(['code' => 1, 'msg' => '该平台推送已禁用']);
}
$api = $setting['value'];
if (empty($api)) {
return json(['code' => 1, 'msg' => '推送API地址未配置']);
}
// 推送
$ch = curl_init();
$options = array(
CURLOPT_URL => $api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $urls), // 多行文本每行一个url
CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
);
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return json(['code' => 1, 'msg' => $error, 'result' => $result]);
}
// 解析推送返回结果
$resultArr = json_decode($result, true);
// 判断返回是否有error字段
if (is_array($resultArr) && isset($resultArr['error'])) {
return json([
'code' => 1,
'msg' => '推送失败: ' . (isset($resultArr['message']) ? $resultArr['message'] : '未知错误'),
'result' => $resultArr
]);
}
// 判断是否有success字段只有有success字段才认为推送成功
if (is_array($resultArr) && isset($resultArr['success'])) {
// 推送成功后更新对应数据的push字段为1
// 解析urls分别更新Articles或Resource表
foreach ($urls as $url) {
// 匹配文章
if (preg_match('/\/index\/articles\/detail\?id=(\d+)/', $url, $matches)) {
$id = intval($matches[1]);
if ($id > 0) {
Articles::where('id', $id)->update(['push' => 1]);
}
}
// 匹配资源
elseif (preg_match('/\/index\/resources\/detail\?id=(\d+)/', $url, $matches)) {
$id = intval($matches[1]);
if ($id > 0) {
Resource::where('id', $id)->update(['push' => 1]);
}
}
}
return json(['code' => 0, 'msg' => '推送成功', 'result' => $resultArr]);
} else {
// 没有success字段视为推送失败把失败的result反馈给前端
return json([
'code' => 1,
'msg' => '推送失败',
'result' => $resultArr !== null ? $resultArr : $result
]);
}
}
}

View File

@ -1,27 +1,30 @@
{include file="public/header" /}
<div class="config-container">
<!-- 页面头部样式 -->
<div class="config-header" style="display: flex;flex-direction: column;flex-wrap: wrap;align-items: flex-start;">
<div class="maintitle">
<i class="layui-icon layui-icon-notice"></i>
<span>内容推送列表</span>
</div>
<div style="display: flex;align-items: flex-start;flex-direction: column;gap: 15px;margin-bottom: 10px;">
<div>
<button class="layui-btn layui-bg-blue" onclick="add()">
<i class="layui-icon layui-icon-add-1"></i>添加推送
</button>
<div class="config-header" style="display: flex;flex-direction: column;flex-wrap: wrap;align-items: flex-start;">
<div class="maintitle">
<i class="layui-icon layui-icon-notice"></i>
<span>内容推送列表</span>
</div>
<div style="display: flex;align-items: flex-start;flex-direction: column;gap: 15px;margin-bottom: 10px;">
<div>
<!-- <button class="layui-btn layui-bg-blue" onclick="add()">
<i class="layui-icon layui-icon-add-1"></i>添加推送
</button> -->
<button class="layui-btn layui-bg-blue" onclick="setting()">
<i class="layui-icon layui-icon-set-fill"></i>推送配置
</button>
<button type="button" class="layui-btn layui-btn-primary layui-border-blue" onclick="refresh()">
<i class="layui-icon layui-icon-refresh"></i>刷新
</button>
</div>
</div>
</div>
<i class="layui-icon layui-icon-set-fill"></i>推送配置
</button>
<button type="button" class="layui-btn layui-btn-primary layui-border-green" onclick="pushSelected()">
<i class="layui-icon layui-icon-release"></i>推送
</button>
<button type="button" class="layui-btn layui-btn-primary layui-border-blue" onclick="refresh()">
<i class="layui-icon layui-icon-refresh"></i>刷新
</button>
</div>
</div>
</div>
<table id="contentPushTable" lay-filter="contentPushTable"></table>
<table id="contentPushTable" lay-filter="contentPushTable"></table>
</div>
<script type="text/javascript">
@ -36,32 +39,48 @@
url: '{$config["admin_route"]}yunzeradmin/contentpushlist',
page: true,
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'title', title: '标题', width: 200},
{field: 'type', title: '类型', width: 100, templet: function(d){
return d.type == 1 ? '普通推送' : '重要推送';
}},
{field: 'status', title: '状态', width: 100, templet: function(d){
return d.status == 1 ?
'<span class="layui-badge layui-bg-green">启用</span>' :
'<span class="layui-badge">禁用</span>';
}},
{field: 'sort', title: '排序', width: 100, sort: true},
{field: 'create_time', title: '创建时间', width: 180},
{title: '操作', width: 200, toolbar: '#tableBar', fixed: 'right'}
{ type: 'checkbox', fixed: 'left' }, // 多选全选
// {field: 'id', title: 'ID', width: 80, sort: true},
{
field: 'type', title: '类型', width: 80, templet: function (d) {
return d.type == 'article' ? '文章' : '资源';
}
},
{ field: 'title', title: '标题' },
{
field: 'url', title: '链接', templet: function (d) {
if (d.type == 'article') {
return '<a href="https://www.yunzer.cn/index/articles/detail?id=' + d.id + '" target="_blank">https://www.yunzer.cn/index/articles/detail?id=' + d.id + '</a>';
} else if (d.type == 'resource') {
return '<a href="https://www.yunzer.cn/index/resources/detail?id=' + d.id + '" target="_blank">https://www.yunzer.cn/index/resources/detail?id=' + d.id + '</a>';
} else {
return '';
}
}
},
{
field: 'push', title: '推送状态', width: 100, templet: function (d) {
return d.push == 1 ?
'<span class="layui-badge layui-bg-green">已推送</span>' :
'<span class="layui-badge">未推送</span>';
}
},
// {field: 'sort', title: '排序', width: 100, sort: true},
{ field: 'create_time', title: '创建时间', width: 180 },
// {title: '操作', width: 200, toolbar: '#tableBar', fixed: 'right'}
]],
limit: 10,
limits: [10, 20, 30, 50]
});
// 监听工具条
table.on('tool(contentPushTable)', function(obj){
table.on('tool(contentPushTable)', function (obj) {
var data = obj.data;
if(obj.event === 'edit'){
if (obj.event === 'edit') {
edit(data.id);
} else if(obj.event === 'del'){
} else if (obj.event === 'del') {
del(data.id);
} else if(obj.event === 'status'){
} else if (obj.event === 'status') {
changeStatus(data.id, data.status);
}
});
@ -123,7 +142,7 @@
}
//配置
function setting(){
function setting() {
layer.open({
type: 2,
title: '推送配置',
@ -133,6 +152,70 @@
});
}
//推送数据
function pushSelected() {
var checkStatus = layui.table.checkStatus('contentPushTable');
var data = checkStatus.data;
if (data.length === 0) {
layer.msg('请先选择要推送的数据', { icon: 2 });
return;
}
$.getJSON("{$config['admin_route']}yunzeradmin/contentpushsetting", function (res) {
if (res.code !== 0 || !res.data || res.data.length === 0) {
layer.msg('暂无可用推送平台');
return;
}
var html = '<div style="padding:10px;"><table class="layui-table"><thead><tr><th>ID</th><th>平台名称</th><th>类型标识</th></tr></thead><tbody>';
res.data.forEach(function (item) {
if (item.status == 1) {
html += '<tr style="cursor:pointer" data-type="' + item.platformType + '" data-title="' + item.title.replace(/'/g, "\\'") + '">';
html += '<td>' + item.id + '</td>';
html += '<td>' + item.title + '</td>';
html += '<td>' + item.platformType + '</td>';
html += '</tr>';
}
});
html += '</tbody></table></div>';
layer.open({
type: 1,
title: '选择推送平台',
area: ['500px', '350px'],
content: html,
success: function (layero, index) {
// 事件委托,避免全局变量
$(layero).find('tr[data-type]').on('click', function () {
var platformType = $(this).data('type');
var platformTitle = $(this).data('title');
layer.close(index);
var urls = [];
data.forEach(function (item) {
urls.push('https://www.yunzer.cn/index/' + (item.type === 'resource' ? 'resources' : 'articles') + '/detail?id=' + item.id);
});
$.ajax({
url: "{$config['admin_route']}yunzeradmin/gopush",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
platformType: platformType,
urls: urls
}),
dataType: "json",
success: function (res) {
layer.msg(res.msg, { icon: res.code === 0 ? 1 : 2, time: 5000 });
layui.table.reload('contentPushTable');
}
});
});
}
});
});
}
// 刷新列表
function refresh() {
layui.table.reload('contentPushTable');
@ -140,10 +223,9 @@
</script>
<!-- 表格操作列模板 -->
<script type="text/html" id="tableBar">
<!-- <script type="text/html" id="tableBar">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="status">状态</a>
</script>
</script> -->
{include file="public/tail" /}

View File

@ -1,26 +1,35 @@
{include file="public/header" /}
<div class="config-container">
<form class="layui-form" action="{$config['admin_route']}yunzeradmin/contentpushsave" method="post" lay-filter="contentPushForm">
<form class="layui-form" action="{$config['admin_route']}yunzeradmin/contentpushsave" method="post"
lay-filter="contentPushForm">
<div class="layui-form-item">
<label class="layui-form-label">推送标题</label>
<label class="layui-form-label">推送平台</label>
<div class="layui-input-block">
<input type="text" name="title" required lay-verify="required" placeholder="请输入推送标题" autocomplete="off" class="layui-input">
<select name="push_platform" id="pushPlatformSelect" required lay-verify="required">
<option value="">请选择推送平台</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推送分类</label>
<div class="layui-input-block">
<select name="pushcate" required lay-verify="required">
<option value="article">文章分类</option>
<option value="resource">资源分类</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推送内容</label>
<div class="layui-input-block">
<textarea name="content" placeholder="请输入推送内容" class="layui-textarea" required lay-verify="required"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推送图片</label>
<div class="layui-input-block">
<input type="text" name="image" id="image" placeholder="请上传推送图片" autocomplete="off" class="layui-input">
<button type="button" class="layui-btn" id="uploadImage">
<i class="layui-icon">&#xe67c;</i>上传图片
<div class="layui-input-block" style="display: flex; align-items: center;">
<input type="text" name="title" id="pushTitleInput" required lay-verify="required" placeholder="请输入推送标题"
autocomplete="off" class="layui-input" style="flex:1;">
<button type="button" class="layui-btn layui-btn-primary" id="selectPushContent"
style="margin-left: 8px;">
<i class="layui-icon layui-icon-search"></i>
</button>
</div>
</div>
@ -32,28 +41,38 @@
</div>
</div>
<div class="layui-form-item">
<!-- <div class="layui-form-item">
<label class="layui-form-label">推送图片</label>
<div class="layui-input-block">
<input type="text" name="image" id="image" placeholder="请上传推送图片" autocomplete="off" class="layui-input">
<button type="button" class="layui-btn" id="uploadImage">
<i class="layui-icon">&#xe67c;</i>上传图片
</button>
</div>
</div> -->
<!-- <div class="layui-form-item">
<label class="layui-form-label">推送类型</label>
<div class="layui-input-block">
<input type="radio" name="type" value="1" title="普通推送" checked>
<input type="radio" name="type" value="2" title="重要推送">
</div>
</div>
</div> -->
<div class="layui-form-item">
<!-- <div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="禁用">
</div>
</div>
</div> -->
<div class="layui-form-item">
<!-- <div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-block">
<input type="number" name="sort" value="0" placeholder="请输入排序值" autocomplete="off" class="layui-input">
</div>
</div>
</div> -->
<div class="layui-form-item">
<div class="layui-input-block">
@ -64,50 +83,92 @@
</form>
</div>
<script type="text/javascript">
layui.use(['form', 'upload', 'layer'], function(){
var form = layui.form;
var upload = layui.upload;
var layer = layui.layer;
var $ = layui.jquery;
<script>
layui.use(['layer', 'jquery', 'form', 'upload'], function () {
var $ = layui.jquery,
layer = layui.layer,
form = layui.form,
upload = layui.upload;
// 图片上传
upload.render({
elem: '#uploadImage',
url: '{$config["admin_route"]}yunzeradmin/upload',
accept: 'images',
acceptMime: 'image/*',
done: function(res){
if(res.code == 0){
$('#image').val(res.data.src);
layer.msg('上传成功');
} else {
layer.msg('上传失败');
// 动态加载推送平台
$.getJSON("{$config['admin_route']}yunzeradmin/contentpushsetting", function (res) {
if (res.data.length > 0) {
var $select = $('#pushPlatformSelect');
res.data.forEach(function (item) {
if (item.status == 1) {
$select.append(
$('<option>', {
value: item.id,
text: item.title
})
);
}
});
form.render('select');
}
}
});
});
// 表单提交
form.on('submit(contentPushForm)', function(data){
$.ajax({
url: data.form.action,
type: 'POST',
data: data.field,
success: function(res){
if(res.code == 0){
layer.msg(res.msg, {icon: 1}, function(){
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
parent.layui.table.reload('contentPushTable');
});
// 选择推送内容
$('#selectPushContent').on('click', function () {
var pushCate = $('select[name="pushcate"]').val();
// 弹窗选择内容
layer.open({
type: 2,
title: '选择推送内容',
area: ['800px', '500px'],
content: "{$config['admin_route']}yunzeradmin/selectpushcontent?pushcate=" + pushCate,
success: function (layero, index) {
// 可在弹窗页面通过父页面回调选中内容
window.setPushContent = function (title, url) {
$('#pushTitleInput').val(title);
// 如果需要同步设置跳转链接
if (url) {
$('input[name="url"]').val(url);
}
layer.close(index);
}
}
});
});
// 图片上传
upload.render({
elem: '#uploadImage',
url: '{$config["admin_route"]}yunzeradmin/upload',
accept: 'images',
acceptMime: 'image/*',
done: function (res) {
if (res.code == 0) {
$('#image').val(res.data.src);
layer.msg('上传成功');
} else {
layer.msg(res.msg, {icon: 2});
layer.msg('上传失败');
}
}
});
return false;
// 表单提交
form.on('submit(contentPushForm)', function (data) {
$.ajax({
url: data.form.action,
type: 'POST',
data: data.field,
success: function (res) {
if (res.code == 0) {
layer.msg(res.msg, { icon: 1 }, function () {
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
parent.layui.table.reload('contentPushTable');
});
} else {
layer.msg(res.msg, { icon: 2 });
}
}
});
return false;
});
});
});
</script>
{include file="public/tail" /}
{include file="public/tail" /}

View File

@ -0,0 +1,29 @@
{include file="public/header" /}
<div style="padding: 20px;">
<table class="layui-table">
<thead>
<tr>
<th>标题</th>
</tr>
</thead>
<tbody>
<?php foreach ($data as $item): ?>
<tr>
<td>
<a href="javascript:;" onclick="selectThis('<?php echo addslashes($item['title']); ?>', '<?php echo isset($item['url']) ? addslashes($item['url']) : ''; ?>')">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
function selectThis(title, url) {
if (window.parent && window.parent.setPushContent) {
window.parent.setPushContent(title, url || '');
}
}
</script>
{include file="public/tail" /}