修改
This commit is contained in:
parent
aa030e770d
commit
87a37460bd
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,4 +4,5 @@ runtime/*
|
|||
vendor/*
|
||||
404.html
|
||||
public/.well-known/*
|
||||
.env
|
||||
.env
|
||||
public/ueditor/*
|
||||
|
|
@ -87,4 +87,30 @@ class Danye extends Base
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改图片优化状态
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function change_image_optimizer()
|
||||
{
|
||||
$id = input('post.id/d', 0);
|
||||
$status = input('post.status/d', 0);
|
||||
|
||||
if (empty($id)) {
|
||||
return $this->renderError('参数错误');
|
||||
}
|
||||
|
||||
$info = DanyeModel::getInfo(['id' => $id], 'id');
|
||||
if (!$info) {
|
||||
return $this->renderError('数据不存在');
|
||||
}
|
||||
|
||||
$result = DanyeModel::where(['id' => $id])->update(['is_image_optimizer' => $status, 'update_time' => time()]);
|
||||
if ($result) {
|
||||
return $this->renderSuccess('操作成功');
|
||||
} else {
|
||||
return $this->renderError('操作失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ Route::rule('send_order_dandufahuo', 'Order/send_order_dandufahuo');
|
|||
Route::rule('danye', 'Danye/index', 'GET|POST');
|
||||
Route::rule('danye_edit', 'Danye/edit', 'GET|POST');
|
||||
Route::rule('gonggao', 'Danye/gonggao', 'GET|POST');
|
||||
Route::rule('change_image_optimizer', 'Danye/change_image_optimizer', 'POST');
|
||||
|
||||
#============================
|
||||
#Advert.php轮播图
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@
|
|||
condition="$info['id'] lt 20" }readonly{/if}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图片优化</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="is_image_optimizer" value="1" lay-skin="switch" lay-text="开启|关闭" {if condition="$info['is_image_optimizer'] eq 1"}checked{/if}>
|
||||
<div class="layui-word-aux">开启后将只显示图片,同时图片可以长按识别</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">内容</label>
|
||||
<div class="layui-input-block">
|
||||
|
|
@ -48,6 +55,8 @@
|
|||
<script type="text/javascript">
|
||||
layui.use(['layer', 'form'], function () {
|
||||
var $ = layui.$;
|
||||
var form = layui.form;
|
||||
|
||||
$(function () {
|
||||
// window.UEDITOR_CONFIG = {
|
||||
// // 其他配置...
|
||||
|
|
@ -60,6 +69,8 @@
|
|||
// };
|
||||
var ue = UE.getEditor('editor');
|
||||
|
||||
// 初始化表单
|
||||
form.render();
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@
|
|||
<input type="text" name="title" value="{$info['title']}" lay-verify="required" placeholder="请输入标题" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图片优化</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="is_image_optimizer" value="1" lay-skin="switch" lay-text="开启|关闭" {if condition="$info['is_image_optimizer'] eq 1"}checked{/if}>
|
||||
<div class="layui-word-aux">开启后将只显示图片,同时图片可以长按识别</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">内容</label>
|
||||
<div class="layui-input-block" >
|
||||
|
|
@ -42,8 +49,13 @@
|
|||
<script type="text/javascript">
|
||||
layui.use(['layer','form'], function(){
|
||||
var $ = layui.$;
|
||||
var form = layui.form;
|
||||
|
||||
$(function(){
|
||||
var ue = UE.getEditor('editor');
|
||||
|
||||
// 初始化表单
|
||||
form.render();
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,34 +5,41 @@
|
|||
<div class="layui-card-body">
|
||||
<xblock>
|
||||
<div style="padding-bottom: 10px;">
|
||||
<span style="line-height:40px;float:left;">图片优化:开启后将只显示图片,同时图片可以长按识别,建议带二维码的图片开启。</span>
|
||||
<span style="line-height:40px;float:right;">共有数据: {$count}条</span>
|
||||
</div>
|
||||
</xblock>
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{volist name="data" id="vo"}
|
||||
<tr>
|
||||
<td>{$vo['id']}</td>
|
||||
<td>{$vo['title']}</td>
|
||||
<td >
|
||||
<a style="text-decoration:none" title="编辑" onclick="danye_edit({$vo.id})" class="layui-btn layui-btn-xs">
|
||||
<i class="layui-icon layui-icon-edit"></i>编辑
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
{if condition="$count eq 0"}
|
||||
<tr><td colspan='3' style="text-align:center;">暂时没有数据!</td></tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<form class="layui-form">
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>图片优化</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{volist name="data" id="vo"}
|
||||
<tr>
|
||||
<td>{$vo['id']}</td>
|
||||
<td>{$vo['title']}</td>
|
||||
<td>
|
||||
<input type="checkbox" name="switch" lay-skin="switch" lay-filter="is_image_optimizer" value="{$vo.id}" lay-text="开启|关闭" {if condition="$vo['is_image_optimizer'] eq 1"}checked{/if}>
|
||||
</td>
|
||||
<td >
|
||||
<a style="text-decoration:none" title="编辑" onclick="danye_edit({$vo.id})" class="layui-btn layui-btn-xs">
|
||||
<i class="layui-icon layui-icon-edit"></i>编辑
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
{if condition="$count eq 0"}
|
||||
<tr><td colspan='4' style="text-align:center;">暂时没有数据!</td></tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<div class="layui-box layui-laypage layui-laypage-default">
|
||||
{$page|raw}
|
||||
</div>
|
||||
|
|
@ -41,8 +48,41 @@
|
|||
</div>
|
||||
{include file="Public:footer"/}
|
||||
<script type="text/javascript">
|
||||
layui.use(['table'], function(){
|
||||
|
||||
layui.use(['table', 'form', 'jquery'], function(){
|
||||
var form = layui.form;
|
||||
var $ = layui.jquery;
|
||||
|
||||
// 必须要初始化表单,否则开关不会显示
|
||||
form.render();
|
||||
|
||||
// 监听开关事件
|
||||
form.on('switch(is_image_optimizer)', function(obj){
|
||||
var id = this.value;
|
||||
var status = obj.elem.checked ? 1 : 0;
|
||||
|
||||
// 发送AJAX请求更新状态
|
||||
$.ajax({
|
||||
url: "{:url('/admin/change_image_optimizer')}",
|
||||
type: 'POST',
|
||||
data: {id: id, status: status},
|
||||
success: function(res){
|
||||
if(res.status == 1){
|
||||
layer.msg(res.msg, {icon: 1});
|
||||
}else{
|
||||
layer.msg(res.msg, {icon: 2});
|
||||
// 如果更新失败,恢复开关状态
|
||||
obj.elem.checked = !obj.elem.checked;
|
||||
form.render();
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.msg('网络错误', {icon: 2});
|
||||
// 如果请求失败,恢复开关状态
|
||||
obj.elem.checked = !obj.elem.checked;
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use app\common\model\UserCoupon;
|
|||
use app\common\model\GoodsType;
|
||||
use app\common\service\CommonService;
|
||||
use app\common\model\GoodsExtend;
|
||||
use think\Collection;
|
||||
class Goods extends Base
|
||||
{
|
||||
|
||||
|
|
@ -61,36 +62,35 @@ class Goods extends Base
|
|||
$paginate = 15;
|
||||
$type_str = request()->param('type', -1);
|
||||
// 1一番赏 2无限赏 3擂台赏 4抽卡机 5积分赏 6全局赏 7福利盲盒 8领主赏 9连击赏 10 商品赏
|
||||
if ($type_str == 1) {
|
||||
$whe[] = ['type', '=', 1];
|
||||
} elseif ($type_str == 2) {
|
||||
$whe[] = ['type', '=', 2];
|
||||
} elseif ($type_str == 3) {
|
||||
$whe[] = ['type', '=', 3];
|
||||
} elseif ($type_str == 5) {
|
||||
$whe[] = ['type', '=', 5];
|
||||
} elseif ($type_str == 6) {
|
||||
$whe[] = ['type', '=', 6];
|
||||
} elseif ($type_str == 7) {
|
||||
$whe[] = ['type', '=', 7];
|
||||
} elseif ($type_str == 8) {
|
||||
$whe[] = ['type', '=', 8];
|
||||
} elseif ($type_str == 9) {
|
||||
$whe[] = ['type', 'in', [9]];
|
||||
} elseif ($type_str == 10) {
|
||||
$paginate = 999;
|
||||
$whe[] = ['type', '=', 10];
|
||||
} elseif ($type_str == 11) {
|
||||
|
||||
$whe[] = ['type', '=', 11];
|
||||
} elseif ($type_str == 12) {
|
||||
$whe[] = ['type', '=', 12];
|
||||
} elseif ($type_str == 15) {
|
||||
$whe[] = ['type', '=', 15];
|
||||
} elseif ($type_str == 16) {
|
||||
$whe[] = ['type', '=', 16];
|
||||
|
||||
// 使用映射数组简化类型条件判断
|
||||
$typeMapping = [
|
||||
1 => ['type' => 1],
|
||||
2 => ['type' => 2],
|
||||
3 => ['type' => 3],
|
||||
5 => ['type' => 5],
|
||||
6 => ['type' => 6],
|
||||
7 => ['type' => 7],
|
||||
8 => ['type' => 8],
|
||||
9 => ['type' => 'in', 'value' => [9]],
|
||||
10 => ['type' => 10, 'paginate' => 999],
|
||||
11 => ['type' => 11],
|
||||
12 => ['type' => 12],
|
||||
15 => ['type' => 15],
|
||||
16 => ['type' => 16],
|
||||
];
|
||||
|
||||
if (isset($typeMapping[$type_str])) {
|
||||
if (isset($typeMapping[$type_str]['paginate'])) {
|
||||
$paginate = $typeMapping[$type_str]['paginate'];
|
||||
}
|
||||
|
||||
if (isset($typeMapping[$type_str]['type']) && $typeMapping[$type_str]['type'] === 'in') {
|
||||
$whe[] = ['type', 'in', $typeMapping[$type_str]['value']];
|
||||
} else {
|
||||
$whe[] = ['type', '=', $typeMapping[$type_str]['type']];
|
||||
}
|
||||
} else {
|
||||
// $whe[] = ['type', 'not in', [4, 10, 15]];
|
||||
$whe[] = ['type', 'in', [2, 6, 8, 16]];
|
||||
}
|
||||
|
||||
|
|
@ -123,80 +123,108 @@ class Goods extends Base
|
|||
#盒子
|
||||
$goods = GoodsModel::where($whe)
|
||||
->field("id,title,imgurl,price,type,stock,sale_stock,status,lock_is,is_shou_zhe,new_is")
|
||||
->order("sort desc,id desc")->paginate($paginate)->each(function ($itme) {
|
||||
$itme['imgurl'] = imageUrl($itme['imgurl']);
|
||||
#剩余
|
||||
$itme['sale_stock'] = $itme['stock'] - $itme['sale_stock'];
|
||||
if ($itme['type'] == 10) {
|
||||
$goods_id = $itme['id'];
|
||||
#本箱子余量
|
||||
$goodslist = GoodsList::field('sum(`stock`) as stock, sum(`surplus_stock`) as surplus_stock')
|
||||
->where('goods_id', '=', $goods_id)
|
||||
->where('num', '=', 1)
|
||||
->where('shang_id', 'between', self::$shang_prize_id)
|
||||
->find();
|
||||
$stock1 = intval($goodslist['stock']);
|
||||
$surplus_stock1 = intval($goodslist['surplus_stock']);
|
||||
//库存-剩余库存
|
||||
// $surplus_stock =$stock1 - $surplus_stock1 ;
|
||||
$itme['sale_stock'] = $surplus_stock1;
|
||||
$itme['stock'] = $stock1;
|
||||
}
|
||||
#参与次数
|
||||
$join_count = OrderList::field('id')
|
||||
->where('shang_id', 'between', self::$shang_count_id)
|
||||
->where('goods_id', '=', $itme['id'])
|
||||
->where('parent_goods_list_id', '=', 0)
|
||||
->where('order_type', '=', $itme['type'])
|
||||
->count();
|
||||
$itme['join_count'] = $join_count;
|
||||
$itme['need_draw_num'] = 0;
|
||||
if ($itme['type'] == 7) {
|
||||
$itme['need_draw_num'] = 1;
|
||||
}
|
||||
$type_text = '';
|
||||
->order("sort desc,id desc")
|
||||
->paginate($paginate);
|
||||
|
||||
if ($itme['type'] == 1) {
|
||||
$type_text = '一番赏';
|
||||
} elseif ($itme['type'] == 2) {
|
||||
$type_text = '无限赏';
|
||||
} elseif ($itme['type'] == 3) {
|
||||
$type_text = '擂台赏';
|
||||
} elseif ($itme['type'] == 5) {
|
||||
$type_text = '积分赏';
|
||||
} elseif ($itme['type'] == 6) {
|
||||
$type_text = '全局赏';
|
||||
} elseif ($itme['type'] == 8) {
|
||||
$type_text = '领主赏';
|
||||
} elseif ($itme['type'] == 9) {
|
||||
$type_text = '连击赏';
|
||||
} elseif ($itme['type'] == 10) {
|
||||
$type_text = '商品赏';
|
||||
} elseif ($itme['type'] == 11) {
|
||||
$type_text = '自制赏';
|
||||
} elseif ($itme['type'] == 16) {
|
||||
$type_text = '翻倍赏';
|
||||
}
|
||||
// elseif ($itme['type'] == 9) {
|
||||
// $type_text = '冲冲赏';
|
||||
// }
|
||||
$itme['type_text'] = $type_text;
|
||||
});
|
||||
//循环盒子内容,将角标文字写入到$itme['corner_text']中
|
||||
foreach ($goods->items() as &$itme) {
|
||||
//根据$itme['type']值,找到对应的角标文字,goods_types_arr 是数组value,corner_text,
|
||||
//$goods_types_arr 查找对应的value
|
||||
if (isset($goods_types_map[$itme['type']])) {
|
||||
$corner_text = $goods_types_map[$itme['type']];
|
||||
$itme['type_text'] = $corner_text;
|
||||
// 获取所有商品项
|
||||
$goodsItems = $goods->items();
|
||||
|
||||
// 如果没有商品,直接返回
|
||||
if (empty($goodsItems)) {
|
||||
$new_data = [
|
||||
'data' => [],
|
||||
'last_page' => $goods->lastPage(),
|
||||
];
|
||||
return $this->renderSuccess('请求成功', $new_data);
|
||||
}
|
||||
|
||||
// 获取所有商品ID和类型
|
||||
$goodsIds = [];
|
||||
$goodsTypes = [];
|
||||
$type10GoodsIds = [];
|
||||
|
||||
foreach ($goodsItems as $item) {
|
||||
$goodsIds[] = $item['id'];
|
||||
$goodsTypes[$item['id']] = $item['type'];
|
||||
|
||||
// 记录type=10的商品ID
|
||||
if ($item['type'] == 10) {
|
||||
$type10GoodsIds[] = $item['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询所有商品的参与次数
|
||||
$joinCountMap = [];
|
||||
if (!empty($goodsIds)) {
|
||||
$joinCounts = OrderList::field('goods_id, order_type, COUNT(1) as count')
|
||||
->where('shang_id', 'between', self::$shang_count_id)
|
||||
->where('goods_id', 'in', $goodsIds)
|
||||
->where('parent_goods_list_id', '=', 0)
|
||||
->group('goods_id, order_type')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 转为映射关系,键为"goods_id_order_type"格式
|
||||
foreach ($joinCounts as $joinCount) {
|
||||
$key = $joinCount['goods_id'] . '_' . $joinCount['order_type'];
|
||||
$joinCountMap[$key] = $joinCount['count'];
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询type=10的商品库存
|
||||
$goodslistMap = [];
|
||||
if (!empty($type10GoodsIds)) {
|
||||
$goodslists = GoodsList::field('goods_id, sum(`stock`) as stock, sum(`surplus_stock`) as surplus_stock')
|
||||
->where('goods_id', 'in', $type10GoodsIds)
|
||||
->where('num', '=', 1)
|
||||
->where('shang_id', 'between', self::$shang_prize_id)
|
||||
->group('goods_id')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($goodslists as $goodslist) {
|
||||
$goodslistMap[$goodslist['goods_id']] = [
|
||||
'stock' => intval($goodslist['stock']),
|
||||
'surplus_stock' => intval($goodslist['surplus_stock'])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理所有商品数据
|
||||
foreach ($goodsItems as &$item) {
|
||||
$item['imgurl'] = imageUrl($item['imgurl']);
|
||||
|
||||
// 计算剩余库存
|
||||
$item['sale_stock'] = $item['stock'] - $item['sale_stock'];
|
||||
|
||||
// 处理type=10的商品
|
||||
if ($item['type'] == 10 && isset($goodslistMap[$item['id']])) {
|
||||
$goodslistData = $goodslistMap[$item['id']];
|
||||
$item['sale_stock'] = $goodslistData['surplus_stock'];
|
||||
$item['stock'] = $goodslistData['stock'];
|
||||
}
|
||||
|
||||
// 设置参与次数
|
||||
$joinCountKey = $item['id'] . '_' . $item['type'];
|
||||
$item['join_count'] = isset($joinCountMap[$joinCountKey]) ? $joinCountMap[$joinCountKey] : 0;
|
||||
|
||||
// 设置需要抽奖的数量
|
||||
$item['need_draw_num'] = $item['type'] == 7 ? 1 : 0;
|
||||
|
||||
// 设置角标文字,合并之前的第二个循环逻辑
|
||||
if (isset($goods_types_map[$item['type']])) {
|
||||
$item['type_text'] = $goods_types_map[$item['type']];
|
||||
}
|
||||
}
|
||||
|
||||
// 将处理后的数据设置回分页对象
|
||||
$goods->setCollection(new Collection($goodsItems));
|
||||
|
||||
$new_data = [
|
||||
'data' => $goods->items(),
|
||||
'last_page' => $goods->lastPage(),
|
||||
];
|
||||
return $this->renderSuccess('请求成功', $new_data);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -76,14 +76,21 @@ class Index extends Base
|
|||
*/
|
||||
public function danye()
|
||||
{
|
||||
// 设置header
|
||||
|
||||
|
||||
$type = \request()->param('type/d', 0);
|
||||
$info = Danye::where(['id' => $type])->find();
|
||||
$is_image_optimizer = 0;
|
||||
if ($info) {
|
||||
$content = contentUrl($info['content']);
|
||||
$is_image_optimizer = $info['is_image_optimizer'];
|
||||
// header('Access-Control-Allow-Headers: Content-Type');
|
||||
} else {
|
||||
$content = '';
|
||||
}
|
||||
return $this->renderSuccess("请求成功", $content);
|
||||
// return $this->renderSuccess("请求成功", $content);
|
||||
return json(['status' => 1, 'msg' => '请求成功', 'data' => $content, 'is_image_optimizer' => $is_image_optimizer]);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -114,7 +121,7 @@ class Index extends Base
|
|||
{
|
||||
$type = request()->param('type_id/d', 1);
|
||||
#首页轮播图
|
||||
$advert = Advert::field('imgurl,ttype,coupon_id,goods_id')->where(['type' => $type])->order('sort desc,id desc')->select();
|
||||
$advert = Advert::field('imgurl,ttype,coupon_id,goods_id,url')->where(['type' => $type])->order('sort desc,id desc')->select();
|
||||
foreach ($advert as &$advert_value) {
|
||||
$advert_value['imgurl'] = imageUrl($advert_value['imgurl']);
|
||||
}
|
||||
|
|
@ -206,22 +213,142 @@ class Index extends Base
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成带用户推广二维码的海报图片
|
||||
* @return \think\response\Json|void
|
||||
*/
|
||||
public function generate_urllinks()
|
||||
{
|
||||
// 获取并验证用户ID
|
||||
$userId = request()->param('userId/d', 0);
|
||||
$wxServer = new \app\common\server\Wx($this->app);
|
||||
$user_base = $wxServer->generateUrlLinks($userId);
|
||||
$autoload = new \app\common\server\autoload();
|
||||
$currentDir = getcwd();
|
||||
if ($userId <= 0) {
|
||||
return $this->renderError('无效的用户ID');
|
||||
}
|
||||
|
||||
$imageData = $autoload->generatePosterWithQR($currentDir . '/img_poster.jpg', $user_base);
|
||||
if ($imageData) {
|
||||
// 检查用户是否存在
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
return $this->renderError('用户不存在');
|
||||
}
|
||||
|
||||
// 获取配置信息
|
||||
$config = getConfig('base');
|
||||
$posterPath = $config['poster_template'] ?? '/img_poster.jpg';
|
||||
$cacheExpire = $config['poster_cache_expire'] ?? 86400; // 默认缓存1天
|
||||
|
||||
// 创建缓存目录结构(用户ID分组,避免单目录文件过多)
|
||||
$userGroup = floor($userId / 1000); // 每1000个用户一个目录
|
||||
$cacheDir = runtime_path() . 'poster/' . $userGroup . '/';
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0755, true);
|
||||
}
|
||||
|
||||
// 缓存文件名(基于用户ID和模板文件的哈希)
|
||||
$templateFile = getcwd() . $posterPath;
|
||||
$templateHash = md5_file($templateFile); // 使用文件内容哈希而不是修改时间
|
||||
$cacheFile = $cacheDir . 'user_' . $userId . '_' . $templateHash . '.png';
|
||||
|
||||
// 清理该用户的旧缓存文件(保留当前有效的)
|
||||
$this->cleanUserPosterCache($cacheDir, $userId, $templateHash);
|
||||
|
||||
// 定期清理过期缓存(每100次请求执行一次,避免频繁IO)
|
||||
if (mt_rand(1, 100) === 1) {
|
||||
$this->cleanExpiredPosterCache($cacheExpire);
|
||||
}
|
||||
|
||||
// 如果缓存存在且未过期,直接使用缓存
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheExpire)) {
|
||||
$imageData = file_get_contents($cacheFile);
|
||||
} else {
|
||||
// 生成URL链接
|
||||
$wxServer = new \app\common\server\Wx($this->app);
|
||||
$user_base = $wxServer->generateUrlLinks($userId);
|
||||
|
||||
// 生成海报
|
||||
$autoload = new \app\common\server\autoload();
|
||||
$imageData = $autoload->generatePosterWithQR($templateFile, $user_base);
|
||||
|
||||
// 保存到缓存
|
||||
if ($imageData) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
// 更新最后访问时间文件(用于清理判断)
|
||||
touch($cacheFile);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要直接输出图片
|
||||
$outputImage = request()->param('output/d', 1);
|
||||
|
||||
if ($imageData && $outputImage) {
|
||||
header('Content-Type: image/png');
|
||||
header('Content-Length: ' . strlen($imageData)); // 设置图像长度,帮助浏览器处理流式内容
|
||||
header('Content-Length: ' . strlen($imageData));
|
||||
echo $imageData;
|
||||
exit();
|
||||
} elseif ($imageData) {
|
||||
// 生成可访问的URL(使用相对路径,更安全且灵活)
|
||||
$relativePath = 'runtime/poster/' . $userGroup . '/user_' . $userId . '_' . $templateHash . '.png';
|
||||
return $this->renderSuccess('海报生成成功', [
|
||||
'image_url' => request()->domain() . '/' . $relativePath
|
||||
]);
|
||||
} else {
|
||||
// 生成失败,返回错误信息
|
||||
return $this->renderError('海报生成失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定用户的旧海报缓存
|
||||
* @param string $cacheDir 缓存目录
|
||||
* @param int $userId 用户ID
|
||||
* @param string $currentHash 当前模板哈希
|
||||
*/
|
||||
private function cleanUserPosterCache($cacheDir, $userId, $currentHash)
|
||||
{
|
||||
// 查找该用户的所有缓存文件
|
||||
$pattern = $cacheDir . 'user_' . $userId . '_*.png';
|
||||
$files = glob($pattern);
|
||||
|
||||
foreach ($files as $file) {
|
||||
// 如果不是当前使用的缓存文件,则删除
|
||||
if (strpos($file, 'user_' . $userId . '_' . $currentHash . '.png') === false) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有过期的海报缓存
|
||||
* @param int $expireTime 过期时间(秒)
|
||||
*/
|
||||
private function cleanExpiredPosterCache($expireTime)
|
||||
{
|
||||
// 获取缓存根目录
|
||||
$rootCacheDir = runtime_path() . 'poster/';
|
||||
if (!is_dir($rootCacheDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 当前时间
|
||||
$now = time();
|
||||
|
||||
// 遍历所有用户组目录
|
||||
$groupDirs = glob($rootCacheDir . '*/');
|
||||
foreach ($groupDirs as $groupDir) {
|
||||
// 获取组目录中的所有PNG文件
|
||||
$files = glob($groupDir . '*.png');
|
||||
foreach ($files as $file) {
|
||||
// 如果文件过期(最后修改时间超过过期时间)
|
||||
if ($now - filemtime($file) > $expireTime) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果目录为空,删除目录
|
||||
$remainingFiles = glob($groupDir . '*');
|
||||
if (empty($remainingFiles)) {
|
||||
@rmdir($groupDir);
|
||||
}
|
||||
}
|
||||
return $this->renderSuccess('请求成功', $user_base);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
return [
|
||||
// 域名绑定检查
|
||||
\app\api\middleware\DomainBind::class,
|
||||
//h5跨域
|
||||
\app\api\middleware\Allow::class,
|
||||
|
||||
// 跨域处理中间件
|
||||
\app\api\middleware\CorsMiddleware::class,
|
||||
// OPTIONS预检请求处理中间件
|
||||
\app\api\middleware\OptionsRequestMiddleware::class,
|
||||
// GET请求签名验证中间件
|
||||
\app\api\middleware\SignatureVerifyMiddleware::class,
|
||||
// 注意:原来的Allow中间件已被拆分为上面三个专门的中间件,不再需要
|
||||
// \app\api\middleware\Allow::class,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,191 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
*
|
||||
* User: anker
|
||||
* Date: 4/22/22
|
||||
* Email: <13408046898@163.com>
|
||||
**/
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
use think\facade\Config;
|
||||
use think\exception\HttpResponseException;
|
||||
use think\Response;
|
||||
|
||||
class Allow
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
|
||||
// 处理跨域
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Max-Age: 1800');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE');
|
||||
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With,access-token,token,adid,clickid,client');
|
||||
|
||||
// 第一次预请求不管
|
||||
if (strtoupper($request->method()) == "OPTIONS") {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 对GET请求进行签名验证
|
||||
if (strtoupper($request->method()) == "GET") {
|
||||
$this->verifySignature($request);
|
||||
}
|
||||
|
||||
// 处理跨域
|
||||
// 后置中间件
|
||||
$response = $next($request);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求签名
|
||||
* @param \think\Request $request
|
||||
* @return void
|
||||
*/
|
||||
protected function verifySignature($request)
|
||||
{
|
||||
// 获取所有GET参数
|
||||
$params = $request->get();
|
||||
// 获取当前请求路径
|
||||
$path = $request->pathinfo();
|
||||
|
||||
// 白名单路径检查 - 不需要验证域名的路径
|
||||
$whitelistPaths = $this->getWhitelistPaths();
|
||||
foreach ($whitelistPaths as $whitePath) {
|
||||
// 支持简单的路径匹配,如 'notify/*' 匹配所有通知路径
|
||||
if ($this->pathMatch($whitePath, $path)) {
|
||||
// 白名单路径,跳过域名检查
|
||||
// \think\facade\Log::info('白名单路径访问: ' . $path . ', 域名: ' . $);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 如果请求中携带is_test=true参数,则跳过签名验证
|
||||
if (isset($params['is_test']) && $params['is_test'] === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有必要的签名参数
|
||||
if (!isset($params['timestamp']) || !isset($params['sign'])) {
|
||||
$this->error('缺少必要的签名参数');
|
||||
}
|
||||
|
||||
// 检查请求时间戳是否在合理范围内(例如5分钟内)
|
||||
if (time() - intval($params['timestamp']) > 300) {
|
||||
$this->error('请求已过期');
|
||||
}
|
||||
|
||||
// 从请求中获取签名
|
||||
$requestSign = $params['sign'];
|
||||
//移除url
|
||||
unset($params['s']);
|
||||
// 从参数中移除签名,用于生成本地签名
|
||||
unset($params['sign']);
|
||||
|
||||
// 按照键名对参数进行排序
|
||||
ksort($params);
|
||||
|
||||
// 组合参数为字符串
|
||||
$signStr = '';
|
||||
foreach ($params as $key => $value) {
|
||||
$signStr .= $key . '=' . $value . '&';
|
||||
}
|
||||
|
||||
// 获取当前请求的域名和时间戳,组合为密钥
|
||||
$host = $request->host();
|
||||
$timestamp = $params['timestamp'];
|
||||
$appSecret = $host . $timestamp;
|
||||
|
||||
// 添加密钥
|
||||
$signStr = rtrim($signStr, '&') . $appSecret;
|
||||
|
||||
// 生成本地签名(使用MD5签名算法)
|
||||
$localSign = md5($signStr);
|
||||
|
||||
// 比对签名
|
||||
if ($requestSign !== $localSign) {
|
||||
$this->error('签名验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误信息
|
||||
* @param string $msg 错误信息
|
||||
* @param int $code 错误码
|
||||
* @return void
|
||||
*/
|
||||
protected function error($msg, $code = 0)
|
||||
{
|
||||
$result = [
|
||||
'status' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => null
|
||||
];
|
||||
|
||||
$response = Response::create($result, 'json', $code);
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件结束调度
|
||||
* @param \think\Response $response
|
||||
* @return void
|
||||
*/
|
||||
public function end(\think\Response $response)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径白名单
|
||||
*
|
||||
* @return array 白名单路径列表
|
||||
*/
|
||||
protected function getWhitelistPaths()
|
||||
{
|
||||
// 1. 默认白名单路径(如支付回调通知等)
|
||||
$defaultWhitelist = [
|
||||
'notify/*', // 支付回调等通知
|
||||
'health', // 健康检查
|
||||
'debug', // 调试接口
|
||||
'generate_urllinks',
|
||||
];
|
||||
|
||||
// 2. 从配置文件中获取白名单路径
|
||||
try {
|
||||
$configWhitelist = Config::get('api.whitelist_paths', []);
|
||||
if (!empty($configWhitelist) && is_array($configWhitelist)) {
|
||||
return array_merge($defaultWhitelist, $configWhitelist);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('获取API白名单路径配置失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $defaultWhitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径匹配检查
|
||||
*
|
||||
* @param string $pattern 白名单路径模式
|
||||
* @param string $path 请求路径
|
||||
* @return bool 是否匹配
|
||||
*/
|
||||
protected function pathMatch($pattern, $path)
|
||||
{
|
||||
// 完全匹配
|
||||
if ($pattern === $path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通配符匹配 (例如: 'notify/*')
|
||||
if (strpos($pattern, '*') !== false) {
|
||||
$pattern = str_replace('*', '.*', $pattern);
|
||||
$pattern = '/^' . str_replace('/', '\/', $pattern) . '$/i';
|
||||
return preg_match($pattern, $path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
44
app/api/middleware/CorsMiddleware.php
Normal file
44
app/api/middleware/CorsMiddleware.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/**
|
||||
* 处理跨域请求的中间件
|
||||
*
|
||||
* User: system
|
||||
* Date: 2024/06/18
|
||||
**/
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
class CorsMiddleware
|
||||
{
|
||||
/**
|
||||
* 处理跨域请求
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 处理跨域
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Max-Age: 1800');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE');
|
||||
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With,access-token,token,adid,clickid,client');
|
||||
|
||||
// 继续执行下一个中间件
|
||||
$response = $next($request);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件结束调度
|
||||
*
|
||||
* @param \think\Response $response
|
||||
* @return void
|
||||
*/
|
||||
public function end(\think\Response $response)
|
||||
{
|
||||
// 可以在这里对响应做额外处理
|
||||
}
|
||||
}
|
||||
43
app/api/middleware/OptionsRequestMiddleware.php
Normal file
43
app/api/middleware/OptionsRequestMiddleware.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/**
|
||||
* 处理OPTIONS预检请求的中间件
|
||||
*
|
||||
* User: system
|
||||
* Date: 2024/06/18
|
||||
**/
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
class OptionsRequestMiddleware
|
||||
{
|
||||
/**
|
||||
* 处理OPTIONS预检请求
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 处理OPTIONS预检请求
|
||||
if (strtoupper($request->method()) == "OPTIONS") {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 继续执行下一个中间件
|
||||
$response = $next($request);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件结束调度
|
||||
*
|
||||
* @param \think\Response $response
|
||||
* @return void
|
||||
*/
|
||||
public function end(\think\Response $response)
|
||||
{
|
||||
// 可以在这里对响应做额外处理
|
||||
}
|
||||
}
|
||||
70
app/api/middleware/RateLimit.php
Normal file
70
app/api/middleware/RateLimit.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
namespace app\api\middleware;
|
||||
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use think\exception\HttpResponseException;
|
||||
use app\common\server\RedisHelper;
|
||||
|
||||
class RateLimit
|
||||
{
|
||||
// 默认配置:每60秒最多20次
|
||||
protected $limit = 20;
|
||||
protected $ttl = 60;
|
||||
|
||||
/**
|
||||
* 限流处理入口
|
||||
* @param Request $request
|
||||
* @param \Closure $next
|
||||
* @param string $keyPrefix 可传入 'ip' 或 'token' 等标识字段
|
||||
* @return Response
|
||||
*/
|
||||
public function handle($request, \Closure $next, $keyPrefix = 'ip')
|
||||
{
|
||||
$identifier = $this->getIdentifier($request, $keyPrefix);
|
||||
|
||||
$redisKey = "rate_limit:{$keyPrefix}:" . $identifier;
|
||||
$redis = (new RedisHelper())->getRedis();
|
||||
|
||||
$count = $redis->incr($redisKey);
|
||||
|
||||
if ($count == 1) {
|
||||
// 第一次设置过期时间
|
||||
$redis->expire($redisKey, $this->ttl);
|
||||
}
|
||||
|
||||
if ($count > $this->limit) {
|
||||
$this->error("请求频率过高,请稍后再试");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求标识符(支持IP/token)
|
||||
*/
|
||||
protected function getIdentifier(Request $request, string $keyPrefix)
|
||||
{
|
||||
switch ($keyPrefix) {
|
||||
case 'token':
|
||||
return $request->header('token') ?? $request->param('token') ?? 'guest';
|
||||
case 'ip':
|
||||
default:
|
||||
return $request->ip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误输出统一格式
|
||||
*/
|
||||
protected function error($msg, $code = 429)
|
||||
{
|
||||
$result = [
|
||||
'status' => 0,
|
||||
'msg' => $msg,
|
||||
'data' => null
|
||||
];
|
||||
$response = Response::create($result, 'json', $code);
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
}
|
||||
291
app/api/middleware/SignatureVerifyMiddleware.php
Normal file
291
app/api/middleware/SignatureVerifyMiddleware.php
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
<?php
|
||||
/**
|
||||
* 请求签名验证中间件
|
||||
*
|
||||
* User: system
|
||||
* Date: 2024/06/18
|
||||
* Update: 2024/06/19 - 增加POST请求签名验证和防重放攻击功能
|
||||
**/
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
use think\facade\Config;
|
||||
use think\exception\HttpResponseException;
|
||||
use think\Response;
|
||||
|
||||
class SignatureVerifyMiddleware
|
||||
{
|
||||
/**
|
||||
* Redis实例
|
||||
* @var \Redis
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
/**
|
||||
* Redis键前缀
|
||||
*/
|
||||
const REDIS_KEY_PREFIX = 'api_nonce_';
|
||||
|
||||
/**
|
||||
* Nonce过期时间(秒)
|
||||
*/
|
||||
const NONCE_EXPIRE_TIME = 600; // 10分钟
|
||||
|
||||
/**
|
||||
* 时间戳允许的误差(秒)
|
||||
*/
|
||||
const TIMESTAMP_TOLERANCE = 60; // 1分钟
|
||||
|
||||
/**
|
||||
* 构造函数,初始化Redis连接
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->redis = (new \app\common\server\RedisHelper())->getRedis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求签名验证
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 获取当前请求路径
|
||||
$path = $request->pathinfo();
|
||||
|
||||
// 检查是否在白名单内
|
||||
if ($this->isWhitelistedPath($path, $request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 根据请求方法进行签名验证
|
||||
$method = strtoupper($request->method());
|
||||
if ($method == "GET") {
|
||||
$this->verifySignature($request, $request->get());
|
||||
} elseif ($method == "POST") {
|
||||
$this->verifySignature($request, $request->post());
|
||||
}
|
||||
|
||||
// 继续执行下一个中间件
|
||||
$response = $next($request);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求路径是否在白名单中
|
||||
*
|
||||
* @param string $path 请求路径
|
||||
* @param \think\Request $request 请求对象
|
||||
* @return bool 是否在白名单中
|
||||
*/
|
||||
protected function isWhitelistedPath($path, $request)
|
||||
{
|
||||
// 检查是否有内部标识
|
||||
$params = $request->param();
|
||||
if (isset($params['is_test']) && $params['is_test'] === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取白名单路径
|
||||
$whitelistPaths = $this->getWhitelistPaths();
|
||||
|
||||
// 检查路径是否在白名单内
|
||||
foreach ($whitelistPaths as $whitePath) {
|
||||
if ($this->pathMatch($whitePath, $path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 检查IP白名单
|
||||
$ipWhitelist = $this->getIpWhitelist();
|
||||
$clientIp = $request->ip();
|
||||
if (in_array($clientIp, $ipWhitelist)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求签名
|
||||
* @param \think\Request $request 请求对象
|
||||
* @param array $params 请求参数
|
||||
* @return void
|
||||
*/
|
||||
protected function verifySignature($request, $params)
|
||||
{
|
||||
// 检查是否有必要的签名参数
|
||||
if (!isset($params['timestamp']) || !isset($params['sign']) || !isset($params['nonce'])) {
|
||||
$this->error('缺少必要的签名参数');
|
||||
}
|
||||
|
||||
// 检查时间戳是否在允许范围内(1分钟误差)
|
||||
$timestamp = intval($params['timestamp']);
|
||||
$now = time();
|
||||
if (abs($now - $timestamp) > self::TIMESTAMP_TOLERANCE) {
|
||||
$this->error('请求时间戳超出允许范围');
|
||||
}
|
||||
|
||||
// 检查nonce是否被使用过(防重放攻击)
|
||||
$nonce = $params['nonce'];
|
||||
$nonceKey = self::REDIS_KEY_PREFIX . $nonce;
|
||||
if ($this->redis->exists($nonceKey)) {
|
||||
$this->error('无效的请求(nonce已被使用)');
|
||||
}
|
||||
|
||||
// 记录nonce到Redis,有效期10分钟(足够覆盖时间戳可接受的误差范围)
|
||||
$this->redis->setex($nonceKey, self::NONCE_EXPIRE_TIME, $timestamp);
|
||||
|
||||
// 从请求中获取签名
|
||||
$requestSign = $params['sign'];
|
||||
|
||||
// 拷贝参数,移除不需要的参数
|
||||
$signParams = $params;
|
||||
unset($signParams['s']); // 移除URL参数
|
||||
unset($signParams['sign']); // 移除签名参数
|
||||
|
||||
// 按照键名对参数进行排序
|
||||
ksort($signParams);
|
||||
|
||||
// 组合参数为字符串
|
||||
$signStr = '';
|
||||
foreach ($signParams as $key => $value) {
|
||||
// 处理数组或对象类型的参数
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$signStr .= $key . '=' . $value . '&';
|
||||
}
|
||||
|
||||
// 获取当前请求的域名和时间戳,组合为密钥
|
||||
$host = $request->host();
|
||||
$timestamp = $signParams['timestamp'];
|
||||
$appSecret = $host . $timestamp;
|
||||
|
||||
// 添加密钥
|
||||
$signStr = rtrim($signStr, '&') . $appSecret;
|
||||
|
||||
// 生成本地签名(使用MD5签名算法)
|
||||
$localSign = md5($signStr);
|
||||
|
||||
// 比对签名
|
||||
if ($requestSign !== $localSign) {
|
||||
$this->error('签名验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误信息
|
||||
* @param string $msg 错误信息
|
||||
* @param int $code 错误码
|
||||
* @return void
|
||||
*/
|
||||
protected function error($msg, $code = 0)
|
||||
{
|
||||
$result = [
|
||||
'status' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => null
|
||||
];
|
||||
|
||||
$response = Response::create($result, 'json', $code);
|
||||
throw new HttpResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径白名单
|
||||
*
|
||||
* @return array 白名单路径列表
|
||||
*/
|
||||
protected function getWhitelistPaths()
|
||||
{
|
||||
// 1. 默认白名单路径(如支付回调通知等)
|
||||
$defaultWhitelist = [
|
||||
'notify/*', // 支付回调等通知
|
||||
'health', // 健康检查
|
||||
'debug', // 调试接口
|
||||
'generate_urllinks',
|
||||
'webhook/*', // 添加webhook路径
|
||||
'internal/*', // 内部接口
|
||||
];
|
||||
|
||||
// 2. 从配置文件中获取白名单路径
|
||||
try {
|
||||
$configWhitelist = Config::get('api.whitelist_paths', []);
|
||||
if (!empty($configWhitelist) && is_array($configWhitelist)) {
|
||||
return array_merge($defaultWhitelist, $configWhitelist);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('获取API白名单路径配置失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $defaultWhitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP白名单
|
||||
*
|
||||
* @return array IP白名单列表
|
||||
*/
|
||||
protected function getIpWhitelist()
|
||||
{
|
||||
// 默认IP白名单
|
||||
$defaultIpWhitelist = [
|
||||
'127.0.0.1', // 本地回环地址
|
||||
'::1', // IPv6本地回环地址
|
||||
];
|
||||
|
||||
// 从配置文件中获取IP白名单
|
||||
try {
|
||||
$configIpWhitelist = Config::get('api.ip_whitelist', []);
|
||||
if (!empty($configIpWhitelist) && is_array($configIpWhitelist)) {
|
||||
return array_merge($defaultIpWhitelist, $configIpWhitelist);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('获取API白名单IP配置失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $defaultIpWhitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径匹配检查
|
||||
*
|
||||
* @param string $pattern 白名单路径模式
|
||||
* @param string $path 请求路径
|
||||
* @return bool 是否匹配
|
||||
*/
|
||||
protected function pathMatch($pattern, $path)
|
||||
{
|
||||
// 完全匹配
|
||||
if ($pattern === $path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通配符匹配 (例如: 'notify/*')
|
||||
if (strpos($pattern, '*') !== false) {
|
||||
$pattern = str_replace('*', '.*', $pattern);
|
||||
$pattern = '/^' . str_replace('/', '\/', $pattern) . '$/i';
|
||||
return preg_match($pattern, $path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件结束调度
|
||||
*
|
||||
* @param \think\Response $response
|
||||
* @return void
|
||||
*/
|
||||
public function end(\think\Response $response)
|
||||
{
|
||||
// 可以在这里对响应做额外处理
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,11 @@ return [
|
|||
'debug', // 调试接口
|
||||
'wechat/callback', // 微信回调
|
||||
],
|
||||
// IP白名单 - 不需要进行签名验证的IP地址
|
||||
'ip_whitelist' => [
|
||||
'127.0.0.1', // 本地测试
|
||||
'::1', // IPv6本地
|
||||
'192.168.0.1', // 内网测试服务器
|
||||
'10.0.0.1', // 内网开发服务器
|
||||
],
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user