提交代码

This commit is contained in:
焦钰锟 2025-03-06 08:59:31 +08:00
parent 2c0293d3b1
commit dc6021174c
54 changed files with 230544 additions and 105 deletions

1
addons/apilog/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["application\/admin\/view\/apilog\/data\/index.html","application\/admin\/view\/apilog\/trend\/index.html","application\/admin\/view\/apilog\/index\/index.html","application\/admin\/view\/apilog\/index\/detail.html","application\/admin\/lang\/zh-cn\/apilog\/index.php","application\/admin\/controller\/apilog\/Index.php","application\/admin\/controller\/apilog\/Data.php","application\/admin\/controller\/apilog\/Trend.php","public\/assets\/js\/backend\/apilog\/trend.js","public\/assets\/js\/backend\/apilog\/index.js","public\/assets\/js\/backend\/apilog\/data.js"],"license":"regular","licenseto":"16018","licensekey":"5Qktp3hqLnbw20TW ElHTrcZnhWDG7oG4adh9Fw==","domains":["lyzhcs.com"],"licensecodes":[],"validations":["e024ae94bdaa4a38c8b385df093954db"],"menus":["apilog","apilog\/data","apilog\/data\/index","apilog\/trend","apilog\/trend\/index","apilog\/index","apilog\/index\/index","apilog\/index\/del","apilog\/index\/detail","apilog\/index\/banip"]}

264
addons/apilog/Apilog.php Normal file
View File

@ -0,0 +1,264 @@
<?php
namespace addons\apilog;
use app\common\library\Menu;
use think\Addons;
use think\addons\Service;
use think\Request;
use app\common\library\Auth;
use addons\apilog\model\Apilog as ModelApilog;
use think\Cache;
use app\common\library\Email;
/**
* 插件
*/
class Apilog extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu = [
[
'name' => 'apilog',
'title' => 'API访问监测分析',
'icon' => 'fa fa-pie-chart',
'ismenu' => 1,
'sublist' => [
[
"name" => "apilog/data",
"title" => "基础数据",
"ismenu" => 1,
"icon" => "fa fa-dashboard",
'sublist' => [
['name' => 'apilog/data/index', 'title' => '查看'],
]
],
[
"name" => "apilog/trend",
"title" => "趋势数据",
"ismenu" => 1,
"icon" => "fa fa-area-chart",
'sublist' => [
['name' => 'apilog/trend/index', 'title' => '查看'],
]
],
[
"name" => "apilog/index",
"title" => "请求列表",
"ismenu" => 1,
"icon" => "fa fa-list",
'sublist' => [
['name' => 'apilog/index/index', 'title' => '查看'],
['name' => 'apilog/index/del', 'title' => '删除'],
['name' => 'apilog/index/detail', 'title' => '详情'],
['name' => 'apilog/index/banip', 'title' => '禁用IP'],
]
],
]
]
];
Menu::create($menu);
Service::refresh();
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
Menu::delete("apilog");
Service::refresh();
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
Menu::enable('apilog');
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
Menu::disable("apilog");
return true;
}
public function responseSend(&$params)
{
if (Request::instance()->module() == "api" && Request::instance()->baseUrl() !="/api/deepseek.deepseek/answer_list") {
$log['time'] = (microtime(true) - Request::instance()->time(true)) * 1000;
$auth = Auth::instance();
$user_id = $auth->isLogin() ? $auth->id : 0;
$username = $auth->isLogin() ? $auth->username : __('Unknown');
$log['url'] = Request::instance()->baseUrl();
$log['method'] = Request::instance()->method();
$log['param'] = json_encode(Request::instance()->param());
$log['ip'] = Request::instance()->ip();
$log['ua'] = Request::instance()->header('user-agent');
$log['controller'] = Request::instance()->controller();
$log['action'] = Request::instance()->action();
$log['code'] = $params->getCode();
$log['user_id'] = $user_id;
$log['username'] = $username;
$log['response'] = $params->getContent();
(new ModelApilog)->save($log);
$config = get_addon_config('apilog');
//状态码记录
if ($config['error']['open'] == 1) {
$count_code = Cache::get('countcode', null);
if (is_null($count_code)) {
Cache::set('countcode', 0, $config['error']['pl']);
$tagkey = Cache::get('tag_' . md5('code'));
$keys = $tagkey ? array_filter(explode(',', $tagkey)) : [];
foreach ($keys as $k => $v) {
Cache::rm($v);
}
Cache::rm($tagkey);
}
$count_code = Cache::inc('countcode');
$k_code = 'code:' . $params->getCode();
$yj_code = Cache::get($k_code, null);
if (is_null($yj_code)) {
Cache::set($k_code, 0, 0);
Cache::tag('code', $k_code);
}
Cache::inc($k_code);
$codes = array_filter(explode(',', $config['error']['sj']));
$now = 0;
foreach ($codes as $k => $v) {
$now += Cache::get('code:' . $v, 0);
}
if ($now / $count_code >= $config['error']['zb'] / 100) {
// echo '触发错误预警' . $now / $count_code;
$this->emailnotify($config['base']['email'], '请求错误监控', '当前api请求错误率已达到【' . round($now / $count_code * 100, 2) . '%】,请及时关注!');
}
}
//超时记录数
if ($config['time']['open'] == 1) {
$count_time = Cache::get('counttime', null);
if (is_null($count_time)) {
Cache::set('counttime', 0, $config['time']['pl']);
Cache::rm('time');
}
$tot_time = Cache::inc('counttime');
if ($log['time'] > $config['time']['sj']) {
$yj_time = Cache::get('time', null);
if (is_null($yj_time)) {
Cache::set('time', 0, 0);
}
$now_time = Cache::inc('time');
if ($now_time / $tot_time >= $config['time']['zb'] / 100) {
// echo '触发超时预警' . $now_time / $tot_time;
$this->emailnotify($config['base']['email'], '响应超时监控', '当前api响应超时请求占比已达到【' . round($now_time / $tot_time * 100, 2) . '%】,请及时关注!');
}
}
}
}
}
public function moduleInit(&$params)
{
if (Request::instance()->module() == "api") {
$ip = 'banip:' . Request::instance()->ip();
$cacheIp = Cache::get($ip);
if ($cacheIp !== false) {
$this->respone(500, '抱歉您的IP已被禁止访问');
}
$config = get_addon_config('apilog');
//总请求数
if ($config['count']['open'] == 1) {
$yj_count = Cache::get('count', null);
if (is_null($yj_count)) {
Cache::set('count', 0, $config['count']['pl']);
}
Cache::inc('count');
//预警
if ($yj_count + 1 >= $config['count']['max']) {
Cache::rm('count');
// $this->respone(500, '触发请求量预警');
$this->emailnotify($config['base']['email'], '请求量监控', '当前最大请求数量已达到【' . ++$yj_count . '次】,请及时关注!');
}
}
//IP访问请求数
if ($config['ip']['open'] == 1) {
$count_ip = Cache::get('countip', null);
if (is_null($count_ip)) {
Cache::set('countip', 0, $config['ip']['pl']);
$tagkey = Cache::get('tag_' . md5('ip'));
$keys = $tagkey ? array_filter(explode(',', $tagkey)) : [];
foreach ($keys as $k => $v) {
Cache::rm($v);
}
Cache::rm($tagkey);
}
$count_ip = Cache::inc('countip');
$k_ip = 'ip:' . Request::instance()->ip();
$yj_ip = Cache::get($k_ip, null);
if (is_null($yj_ip)) {
Cache::set($k_ip, 0, 0);
Cache::tag('ip', $k_ip);
}
$this_ip = Cache::inc($k_ip);
//白名单
$white = array_filter(explode(',', $config['ip']['white']));
//预警
if (!in_array(Request::instance()->ip(), $white) && $this_ip / $count_ip >= $config['ip']['zb'] / 100) {
//$this->respone(500, '触发IP预警');
$this->emailnotify($config['base']['email'], 'IP异常监控', 'IP【' . Request::instance()->ip()
. '】的访问请求占比已达到【' . round($this_ip / $count_ip * 100, 2) . '%】,请及时关注!');
}
}
}
}
protected function respone($code, $msg)
{
$result = [
'code' => $code,
'msg' => $msg,
'time' => Request::instance()->server('REQUEST_TIME'),
'data' => null,
];
$type = Request::instance()->param(config('var_jsonp_handler')) ? 'jsonp' : 'json';
$response = \think\Response::create($result, $type, 500);
throw new \think\exception\HttpResponseException($response);
}
/**
* 发送邮件预警
* 同类型预警半小时最多发一次
*/
protected function emailnotify($receiver, $subject, $content)
{
$cache = Cache::get('notify:' . $subject, null);
if (is_null($cache)) {
$email = new Email;
$result = $email
->to($receiver)
->subject('【API预警】' . $subject)
->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . $content . '</div>')
->send();
if ($result) {
Cache::set('notify:' . $subject, 1, 1800);
}
}
}
}

412
addons/apilog/config.html Normal file
View File

@ -0,0 +1,412 @@
<style>
.row .input-group{
float: none;
padding-left: 15px;
padding-right: 15px;
margin-right: 15px;
}
</style>
<div class="panel panel-default panel-intro">
<div class="panel-heading">
<div class="panel-lead"><em>API预警配置</em>可在此对API的运行情况进行监控并发送通知</div>
<ul class="nav nav-tabs">
<li class="active"><a href="#tab-base" data-toggle="tab">基础配置</a></li>
<li><a href="#tab-time" data-toggle="tab">响应超时监控</a></li>
<li><a href="#tab-error" data-toggle="tab">请求错误监控</a></li>
<li><a href="#tab-ip" data-toggle="tab">IP异常监控</a></li>
<li><a href="#tab-count" data-toggle="tab">请求量监控</a></li>
</ul>
</div>
<div class="panel-body">
<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST"
action="">
<div id="myTabContent" class="tab-content">
<!--基础配置-->
<div class="tab-pane fade active in" id="tab-base">
<div class="widget-body no-padding">
<table class="table table-striped">
<thead>
<tr>
<th width="15%">配置项</th>
<th width="85%">配置值</th>
</tr>
</thead>
<tbody>
<tr>
<td>预警邮箱</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[base][email]" placeholder="请输入接收预警的邮箱,多个使用英文逗号分隔"
value='{$addon.config[0]["value"]["email"]}' class="form-control"
data-tip="">
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info-light" style="margin-bottom:10px;">
<b>基础配置:</b><br>
1、预警邮箱:接收预警监控通知的邮箱;<br>
请务必先在 [常规管理-系统配置-邮件配置] 中配置并测试邮件发送是否正常<br>
2、相同预警通知在30分钟内仅会发送一次请及时关注API运行情况<br>
3、建议使用Redis作为系统缓存
</div>
</div>
<!--响应超时-->
<div class="tab-pane fade" id="tab-time">
<div class="widget-body no-padding">
<div class="widget-body no-padding">
<table class="table table-striped">
<thead>
<tr>
<th width="15%">配置项</th>
<th width="85%">配置值</th>
</tr>
</thead>
<tbody>
<tr>
<td>监控频率</td>
<td>
<div class="row">
<div class="col-sm-12 col-xs-12">
<select name="row[time][pl]" class="selectpicker">
<option value ="60" {if $addon.config[1]["value"]["pl"]==60} selected {/if}>1分钟</option>
<option value ="180" {if $addon.config[1]["value"]["pl"]==180} selected {/if}>3分钟</option>
<option value ="300" {if $addon.config[1]["value"]["pl"]==300} selected {/if}>5分钟</option>
<option value ="600" {if $addon.config[1]["value"]["pl"]==600} selected {/if}>10分钟</option>
<option value ="1800" {if $addon.config[1]["value"]["pl"]==1800} selected {/if}>30分钟</option>
<option value="3600" {if $addon.config[1]["value"]["pl"]==3600} selected {/if}>1小时</option>
<option value="7200" {if $addon.config[1]["value"]["pl"]==7200} selected {/if}>2小时</option>
<option value="14400" {if $addon.config[1]["value"]["pl"]==14400} selected {/if}>4小时</option>
<option value="21600" {if $addon.config[1]["value"]["pl"]==21600} selected {/if}>6小时</option>
<option value="28800" {if $addon.config[1]["value"]["pl"]==28800} selected {/if}>8小时</option>
<option value="43200" {if $addon.config[1]["value"]["pl"]==43200} selected {/if}>12小时</option>
<option value="86400" {if $addon.config[1]["value"]["pl"]==86400} selected {/if}>1天</option>
</select>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>超时时间</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12 input-group">
<input type="text" name="row[time][sj]"
value='{$addon.config[1]["value"]["sj"]}'
class="form-control" data-tip="" aria-describedby="time_t">
<span class="input-group-addon" id="time_t">毫秒</span>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>百分比</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12 input-group">
<input type="text" name="row[time][zb]"
value='{$addon.config[1]["value"]["zb"]}'
class="form-control" data-tip="" aria-describedby="time_zb">
<span class="input-group-addon" id="time_zb">%</span>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>是否开启预警</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="radio" name="row[time][open]" value="0" {if
$addon.config[1]["value"]["open"]==0} checked {/if}> 关闭
<input type="radio" name="row[time][open]" value="1" {if
$addon.config[1]["value"]["open"]==1} checked {/if}> 开启
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info-light" style="margin-bottom:10px;">
<b>响应超时监控:</b><br>主要针对一段时间内接口调用超时率过高<br>
1、设定接口超时时间(毫秒)<br>
2、设置接口超时百分比。支持两位小数超时率达到或者大于设定值时会触发预警。
</div>
</div>
</div>
<!--请求错误-->
<div class="tab-pane fade" id="tab-error">
<div class="widget-body no-padding">
<div class="widget-body no-padding">
<table class="table table-striped">
<thead>
<tr>
<th width="15%">配置项</th>
<th width="85%">配置值</th>
</tr>
</thead>
<tbody>
<tr>
<td>监控频率</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<select name="row[error][pl]" class="selectpicker">
<option value ="60" {if $addon.config[2]["value"]["pl"]==60} selected {/if}>1分钟</option>
<option value ="180" {if $addon.config[2]["value"]["pl"]==180} selected {/if}>3分钟</option>
<option value ="300" {if $addon.config[2]["value"]["pl"]==300} selected {/if}>5分钟</option>
<option value ="600" {if $addon.config[2]["value"]["pl"]==600} selected {/if}>10分钟</option>
<option value ="1800" {if $addon.config[2]["value"]["pl"]==1800} selected {/if}>30分钟</option>
<option value="3600" {if $addon.config[2]["value"]["pl"]==3600} selected {/if}>1小时</option>
<option value="7200" {if $addon.config[2]["value"]["pl"]==7200} selected {/if}>2小时</option>
<option value="14400" {if $addon.config[2]["value"]["pl"]==14400} selected {/if}>4小时</option>
<option value="21600" {if $addon.config[2]["value"]["pl"]==21600} selected {/if}>6小时</option>
<option value="28800" {if $addon.config[2]["value"]["pl"]==28800} selected {/if}>8小时</option>
<option value="43200" {if $addon.config[2]["value"]["pl"]==43200} selected {/if}>12小时</option>
<option value="86400" {if $addon.config[2]["value"]["pl"]==86400} selected {/if}>1天</option>
</select>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>HTTP状态码</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[error][sj]"
value='{$addon.config[2]["value"]["sj"]}'
class="form-control" placeholder="请输入需要监控的状态码,多个使用英文逗号分隔" data-tip="多个状态码用英文逗号隔开">
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>百分比</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12 input-group">
<input type="text" name="row[error][zb]"
value='{$addon.config[2]["value"]["zb"]}'
class="form-control" data-tip="" aria-describedby="erro_zb">
<span class="input-group-addon" id="erro_zb">%</span>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>是否开启预警</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="radio" name="row[error][open]" value="0" {if
$addon.config[2]["value"]["open"]==0} checked {/if}> 关闭
<input type="radio" name="row[error][open]" value="1" {if
$addon.config[2]["value"]["open"]==1} checked {/if}> 开启
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info-light" style="margin-bottom:10px;">
<b>请求错误监控:</b><br>主要针对一段时间内接口调用错误率过高<br>
1、设定Http状态码,500,503,404,多个状态码用英文逗号隔开<br>
2、设置命中率,支持 两位小数点,命中率达到或大于设定值时会触发预警。
</div>
</div>
</div>
<!--IP异常-->
<div class="tab-pane fade" id="tab-ip">
<div class="widget-body no-padding">
<div class="widget-body no-padding">
<table class="table table-striped">
<thead>
<tr>
<th width="15%">配置项</th>
<th width="85%">配置值</th>
</tr>
</thead>
<tbody>
<tr>
<td>监控频率</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<select name="row[ip][pl]" class="selectpicker">
<option value ="60" {if $addon.config[3]["value"]["pl"]==60} selected {/if}>1分钟</option>
<option value ="180" {if $addon.config[3]["value"]["pl"]==180} selected {/if}>3分钟</option>
<option value ="300" {if $addon.config[3]["value"]["pl"]==300} selected {/if}>5分钟</option>
<option value ="600" {if $addon.config[3]["value"]["pl"]==600} selected {/if}>10分钟</option>
<option value ="1800" {if $addon.config[3]["value"]["pl"]==1800} selected {/if}>30分钟</option>
<option value="3600" {if $addon.config[3]["value"]["pl"]==3600} selected {/if}>1小时</option>
<option value="7200" {if $addon.config[3]["value"]["pl"]==7200} selected {/if}>2小时</option>
<option value="14400" {if $addon.config[3]["value"]["pl"]==14400} selected {/if}>4小时</option>
<option value="21600" {if $addon.config[3]["value"]["pl"]==21600} selected {/if}>6小时</option>
<option value="28800" {if $addon.config[3]["value"]["pl"]==28800} selected {/if}>8小时</option>
<option value="43200" {if $addon.config[3]["value"]["pl"]==43200} selected {/if}>12小时</option>
<option value="86400" {if $addon.config[3]["value"]["pl"]==86400} selected {/if}>1天</option>
</select>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>IP白名单</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[ip][white]"
value='{$addon.config[3]["value"]["white"]}' placeholder="请输入IP白名单"
class="form-control" data-tip="多个IP地址中间用英文逗号分开">
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>百分比</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12 input-group">
<input type="text" name="row[ip][zb]"
value='{$addon.config[3]["value"]["zb"]}'
class="form-control" data-tip="" aria-describedby="ip_zb">
<span class="input-group-addon" id="ip_zb">%</span>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>是否开启预警</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="radio" name="row[ip][open]" value="0" {if
$addon.config[3]["value"]["open"]==0} checked {/if}> 关闭
<input type="radio" name="row[ip][open]" value="1" {if
$addon.config[3]["value"]["open"]==1} checked {/if}> 开启
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info-light" style="margin-bottom:10px;">
<b>IP异常监控</b><br>主要针对一段时间内大量固定IP请求类似机器人请求;<br>
1、设置IP白名单(可不填),多个IP地址中间用英文逗号分开<br>
2、设置重复率支持两位小数点,当IP重复率达到或大于设定值时,触发预警。
</div>
</div>
</div>
<!--请求量-->
<div class="tab-pane fade" id="tab-count">
<div class="widget-body no-padding">
<div class="widget-body no-padding">
<table class="table table-striped">
<thead>
<tr>
<th width="15%">配置项</th>
<th width="85%">配置值</th>
</tr>
</thead>
<tbody>
<tr>
<td>监控频率</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<select name="row[count][pl]" class="selectpicker">
<option value ="60" {if $addon.config[4]["value"]["pl"]==60} selected {/if}>1分钟</option>
<option value ="180" {if $addon.config[4]["value"]["pl"]==180} selected {/if}>3分钟</option>
<option value ="300" {if $addon.config[4]["value"]["pl"]==300} selected {/if}>5分钟</option>
<option value ="600" {if $addon.config[4]["value"]["pl"]==600} selected {/if}>10分钟</option>
<option value ="1800" {if $addon.config[4]["value"]["pl"]==1800} selected {/if}>30分钟</option>
<option value="3600" {if $addon.config[4]["value"]["pl"]==3600} selected {/if}>1小时</option>
<option value="7200" {if $addon.config[4]["value"]["pl"]==7200} selected {/if}>2小时</option>
<option value="14400" {if $addon.config[4]["value"]["pl"]==14400} selected {/if}>4小时</option>
<option value="21600" {if $addon.config[4]["value"]["pl"]==21600} selected {/if}>6小时</option>
<option value="28800" {if $addon.config[4]["value"]["pl"]==28800} selected {/if}>8小时</option>
<option value="43200" {if $addon.config[4]["value"]["pl"]==43200} selected {/if}>12小时</option>
<option value="86400" {if $addon.config[4]["value"]["pl"]==86400} selected {/if}>1天</option>
</select>
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>最大请求数量</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="text" name="row[count][max]"
value='{$addon.config[4]["value"]["max"]}'
class="form-control" data-tip="">
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
<tr>
<td>是否开启预警</td>
<td>
<div class="row">
<div class="col-sm-8 col-xs-12">
<input type="radio" name="row[count][open]" value="0" {if
$addon.config[4]["value"]["open"]==0} checked {/if}> 关闭
<input type="radio" name="row[count][open]" value="1" {if
$addon.config[4]["value"]["open"]==1} checked {/if}> 开启
</div>
<div class="col-sm-4"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info-light" style="margin-bottom:10px;">
请求量监控:</b><br>主要针对一段时间内接口大量请求<br>
1、设置单位时间内接口最大请求量,当请求量达到或者大于设定时触发预警
</div>
</div>
</div>
<!--footer-->
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
</div>
</div>
</div>
</form>
</div>
</div>

105
addons/apilog/config.php Normal file
View File

@ -0,0 +1,105 @@
<?php
return array (
0 =>
array (
'name' => 'base',
'title' => '基础配置',
'type' => 'array',
'content' =>
array (
),
'value' =>
array (
'email' => '',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
1 =>
array (
'name' => 'time',
'title' => '响应时间',
'type' => 'array',
'content' =>
array (
),
'value' =>
array (
'pl' => '1800',
'sj' => '5000',
'zb' => '10',
'open' => '0',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
2 =>
array (
'name' => 'error',
'title' => '错误',
'type' => 'array',
'content' =>
array (
),
'value' =>
array (
'pl' => '1800',
'sj' => '500',
'zb' => '10',
'open' => '0',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
3 =>
array (
'name' => 'ip',
'title' => 'ip',
'type' => 'array',
'content' =>
array (
),
'value' =>
array (
'pl' => '1800',
'white' => '127.0.0.1',
'zb' => '20',
'open' => '0',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
4 =>
array (
'name' => 'count',
'title' => '最大请求',
'type' => 'array',
'content' =>
array (
),
'value' =>
array (
'pl' => '300',
'max' => '500',
'open' => '0',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
);

View File

@ -0,0 +1,16 @@
<?php
namespace addons\apilog\controller;
use addons\apilog\model\Apilog;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
// $this->error("当前插件暂无前台页面");
return $this->view->fetch();
}
}

10
addons/apilog/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = apilog
title = API访问监测分析
intro = API访问监测分析快速了解接口运行情况
author = xiaoyu5062
website = https://www.fastadmin.net
version = 1.0.2
state = 1
url = /addons/apilog
license = regular
licenseto = 16018

23
addons/apilog/install.sql Normal file
View File

@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS `__PREFIX__apilog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(255) DEFAULT NULL COMMENT 'IP',
`url` varchar(255) DEFAULT NULL COMMENT '请求地址',
`method` enum('GET','POST','PUT','DELETE') DEFAULT NULL COMMENT '请求方法',
`param` text COMMENT '参数',
`ua` varchar(255) DEFAULT '' COMMENT 'UA',
`controller` varchar(255) DEFAULT NULL COMMENT '控制器',
`action` varchar(255) DEFAULT NULL COMMENT '操作',
`time` float(11,6) DEFAULT '0.000000' COMMENT '耗时',
`code` int(11) DEFAULT '200' COMMENT '状态码',
`createtime` int(11) DEFAULT NULL COMMENT '请求时间',
`user_id` int(11) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
`response` text COMMENT '响应内容',
PRIMARY KEY (`id`),
KEY `createtime` (`createtime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
--1.0.1
--
ALTER TABLE `__PREFIX__apilog` ADD COLUMN `response` text COMMENT '响应内容';

View File

@ -0,0 +1,278 @@
<?php
namespace addons\apilog\model;
use think\Model;
class Apilog extends Model
{
protected $table = 'fa_apilog';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
protected $append = [
'method_text',
'time_text'
];
public function getMethodList()
{
return ['GET' => 'GET', 'POST' => 'POST', 'PUT' => 'PUT', 'DELETE' => 'DELETE'];
}
public function getMethodTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['method']) ? $data['method'] : '');
$list = $this->getMethodList();
return isset($list[$value]) ? $list[$value] : '';
}
public function getTimeTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['time']) ? $data['time'] : '');
return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
}
protected function setTimeAttr($value)
{
return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
}
/**
* 基本数据
*
* @param [type] $start
* @param [type] $end
* @return void
*/
public static function getBaseInfo($start, $end)
{
//请求次数
$count_request = Apilog::whereTime('createtime', 'between', [$start, $end])->count();
//平均处理时间
$avg_time = Apilog::whereTime('createtime', 'between', [$start, $end])->avg('time');
//404
$count_404 = Apilog::whereTime('createtime', 'between', [$start, $end])->where('code', 404)->count();
//500
$count_500 = Apilog::whereTime('createtime', 'between', [$start, $end])->where('code', 500)->count();
//错误率占比
$error_rank = $count_request > 0 ? $count_500 / $count_request : 0;
//接口总数(已请求)
$count_api = Apilog::whereTime('createtime', 'between', [$start, $end])->group('controller,action')->count();
//echo Apilog::getLastSql();
return [
'count_request' => $count_request,
'avg_time' => $avg_time,
'count_404' => $count_404,
'count_500' => $count_500,
'error_rank' => $error_rank,
'count_api' => $count_api
];
}
/**
* 请求状态码 饼图
*
* @return void
*/
public static function getHttpCodePie($start, $end)
{
$list = Apilog::whereTime('createtime', 'between', [$start, $end])->group('code')->field('count(1) num,code')->select();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['code'];
$data['y'][] = $v['num'];
$data['kv'][] = ['name' => $v['code'], 'value' => $v['num']];
}
return $data;
}
/**
* 请求处理时间ms饼图
* 按0-100 100-500500-10001000-30003000-50005000以上划分
*
* @return void
*/
public static function getResponseTimePie($start, $end)
{
$row = Apilog::whereTime('createtime', 'between', [$start, $end])
->field("sum(CASE WHEN TIME<100 THEN 1 ELSE 0 END) AS '0-100' ,
sum(CASE WHEN TIME>=100 and TIME<500 THEN 1 ELSE 0 END) AS '100-500' ,
sum(CASE WHEN TIME>=500 and TIME<1000 THEN 1 ELSE 0 END) AS '500-1000' ,
sum(CASE WHEN TIME>=1000 and TIME<3000 THEN 1 ELSE 0 END) AS '1000-3000' ,
sum(CASE WHEN TIME>=3000 and TIME<5000 THEN 1 ELSE 0 END) AS '3000-5000' ,
sum(CASE WHEN TIME>=5000 THEN 1 ELSE 0 END) AS '5000以上'
")
->find();
// echo Apilog::getLastSql();
$data['x'] = ['0-100', '100-500', '500-1000', '1000-3000', '3000-5000', '5000以上'];
$data['y'] = [$row['0-100'], $row['100-500'], $row['500-1000'], $row['1000-3000'], $row['3000-5000'], $row['5000以上']];
foreach ($data['x'] as $k => $v) {
$data['kv'][] = ['name' => $v, 'value' => $data['y'][$k]];
}
return $data;
}
/**
* 最多请求 Top n展现接口名称
*
* @return void
*/
public static function getMaxRequestTop($start, $end)
{
$list = Apilog::whereTime('createtime', 'between', [$start, $end])
->group('url')->field('count(1) num, url')->order('num desc')->limit(0, 15)->select();
// echo Apilog::getLastSql();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['url'];
$data['y'][] = $v['num'];
}
return $data;
}
/**
* 请求错误 Top n
*
* @return void
*/
public static function getMaxErrorTop($start, $end)
{
$list = Apilog::whereTime('createtime', 'between', [$start, $end])
->where('code', 500)
->group('url')->field('count(1) num, url')->order('num desc')->limit(0, 15)->select();
// echo Apilog::getLastSql();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['url'];
$data['y'][] = $v['num'];
}
return $data;
}
/**
* 平均处理时间最快 Top n
*
* @return void
*/
public static function getDoFastTop($start, $end)
{
$list = Apilog::whereTime('createtime', 'between', [$start, $end])
->group('url')->field('avg(time) num, url')->order('num')->limit(0, 15)->select();
// echo Apilog::getLastSql();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['url'];
$data['y'][] = $v['num'];
}
return $data;
}
/**
* 平均处理时间最慢 Top n
*
* @return void
*/
public static function getDoSlowTop($start, $end)
{
$list = Apilog::whereTime('createtime', 'between', [$start, $end])
->group('url')->field('avg(time) num, url')->order('num desc')->limit(0, 15)->select();
// echo Apilog::getLastSql();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['url'];
$data['y'][] = $v['num'];
}
return $data;
}
/**
* 请求次数 近一个小时,按分钟
*
* @param int $type 0:每分钟 1:每小时 2:每天
* @return void
*/
public static function getRequestCountLine($type)
{
$now = time();
$where = $type == 0 ? [$now - 3600, $now] : ($type == 1 ? [$now - 3600 * 24, $now] : 'month');
$format = $type == 0 ? 'i' : ($type == 1 ? 'H' : 'd');
$group = "FROM_UNIXTIME(createtime,'%" . $format . "')";
$list = Apilog::whereTime('createtime', $where)->group($group)->field('count(1) num,' . $group . ' as time')->select();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['time'];
$data['y'][] = $v['num'];
}
if ($type == 2) {
return $data;
}
$max = $type == 0 ? 60 : ($type == 1 ? 24 : 0);
$s = $type == 0 ? getdate()['minutes'] + 1 : ($type == 1 ? getdate()['hours'] + 1 : 0);
$tmp = null;
for ($i = 0; $i < $max; $i++) {
$k = $s + $i >= $max ? $s + $i - $max : $s + $i;
$tmp['x'][] = $k;
if (($idx = array_search($k, $data['x'])) !== false) {
$tmp['y'][] = $data['y'][$idx];
} else {
$tmp['y'][] = 0;
}
}
return $tmp;
}
/**
* 平均处理时间 近一个小时,按分钟
*
* @param int $type 0:每分钟 1:每小时 2:每天
* @return void
*/
public static function getDoTimeLine($type)
{
$now = time();
$where = $type == 0 ? [$now - 3600, $now] : ($type == 1 ? [$now - 3600 * 24, $now] : 'month');
$format = $type == 0 ? 'i' : ($type == 1 ? 'H' : 'd');
$group = "FROM_UNIXTIME(createtime,'%" . $format . "')";
$list = Apilog::whereTime('createtime', $where)->group($group)->field('avg(time) num,' . $group . ' as time')->select();
$data['x'] = [];
$data['y'] = [];
foreach ($list as $k => $v) {
$data['x'][] = $v['time'];
$data['y'][] = $v['num'];
}
if ($type == 2) {
return $data;
}
$max = $type == 0 ? 60 : ($type == 1 ? 24 : 0);
$s = $type == 0 ? getdate()['minutes'] + 1 : ($type == 1 ? getdate()['hours'] + 1 : 0);
$tmp = null;
for ($i = 0; $i < $max; $i++) {
$k = $s + $i >= $max ? $s + $i - $max : $s + $i;
$tmp['x'][] = $k;
if (($idx = array_search($k, $data['x'])) !== false) {
$tmp['y'][] = $data['y'][$idx];
} else {
$tmp['y'][] = 0;
}
}
return $tmp;
}
}

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>API访问监测分析</title>
<!-- Bootstrap Core CSS -->
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="__CDN__/assets/css/frontend.css" rel="stylesheet">
<!-- Plugin CSS -->
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://cdn.staticfile.org/simple-line-icons/2.4.1/css/simple-line-icons.min.css" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="well" style="margin-top:30px;">
<h3>欢迎使用【API访问监测分析】工具</h3>
<br>
<br><p>如您在使用过程有遇到问题可通过以下三种方式解决:</p>
<p>1、通过问答社区提问(推荐)</p>
<p>2、通过个人中心的售后工单提交工单</p>
<p>3、联系插件作者(QQ170515071注明插件售后)。</p>
</div>
</div>
<!-- jQuery -->
<script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

1
addons/webscan/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["application\/common\/model\/webscan\/WebscanVerifies.php","application\/common\/model\/webscan\/WebscanLog.php","application\/admin\/view\/webscan\/webscanlog\/index.html","application\/admin\/view\/webscan\/webscanlog\/dashboard.html","application\/admin\/view\/webscan\/verifies\/index.html","application\/admin\/controller\/webscan\/Verifies.php","application\/admin\/controller\/webscan\/Webscanlog.php","public\/assets\/js\/backend\/webscan\/webscanlog.js","public\/assets\/js\/backend\/webscan\/verifies.js"],"license":"regular","licenseto":"16018","licensekey":"H2E7qcP9Fwo4aYXl \/8FV42uRPBZ8yX6w92fSAQ==","domains":["lyzhcs.com"],"licensecodes":[],"validations":["e024ae94bdaa4a38c8b385df093954db"],"menus":["webscan","webscan\/webscanlog\/dashboard","webscan\/verifies","webscan\/verifies\/index","webscan\/verifies\/show","webscan\/verifies\/trust","webscan\/verifies\/trusts","webscan\/verifies\/build","webscan\/verifies\/bianli","webscan\/webscanlog","webscan\/webscanlog\/index","webscan\/webscanlog\/black"]}

100
addons/webscan/Webscan.php Normal file
View File

@ -0,0 +1,100 @@
<?php
namespace addons\webscan;
use addons\webscan\library\ChallengeCollapsar;
use addons\webscan\library\Webscan as WebscanService;
use app\common\library\Menu;
use think\Addons;
use think\Exception;
/**
* 插件
*/
class Webscan extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu=[];
$config_file= ADDON_PATH ."webscan" . DS.'config'.DS. "menu.php";
if (is_file($config_file)) {
$menu = include $config_file;
}
if($menu){
Menu::create($menu);
}
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
$info=get_addon_info('webscan');
Menu::delete(isset($info['first_menu'])?$info['first_menu']:'webscan');
return true;
}
/**
* 插件启用方法
*/
public function enable()
{
$info=get_addon_info('webscan');
Menu::enable(isset($info['first_menu'])?$info['first_menu']:'webscan');
}
/**
* 插件禁用方法
*/
public function disable()
{
$info=get_addon_info('webscan');
Menu::disable(isset($info['first_menu'])?$info['first_menu']:'webscan');
}
/**
* 应用初始化标钩子
*/
public function appInit(){
//判断如果是cli模式直接返回
if (preg_match("/cli/i", php_sapi_name())) return true;
$config= $this->getConfig();
//黑名单处理
$ip=\request()->ip();
if ($ip&&$config['webscan_black_ip']){
$webscan_black_ip_arr=explode(PHP_EOL,$config['webscan_black_ip']);
if (in_array($ip,$webscan_black_ip_arr)){
$config['webscan_warn']=$config['black_warn'];
(new WebscanService($config))->webscanPape();
}
}
//是否开启CC过滤
if ($config['ccopen']==1){
try{
//CC攻击
$ChallengeCollapsar=new ChallengeCollapsar($config);
$ChallengeCollapsar->start();
}catch (Exception $exception){
}
}
if ($config['webscan_switch']==1){
$webscan=new WebscanService($config);
$webscan->start();
}
}
}

166
addons/webscan/config.php Normal file
View File

@ -0,0 +1,166 @@
<?php
return array (
0 =>
array (
'type' => 'radio',
'name' => 'webscan_switch',
'title' => '【ZR】开关',
'value' => '1',
'content' =>
array (
1 => '开启',
0 => '关闭',
),
'tip' => 'SQL注入防防XSS拦截开关',
'rule' => 'required',
'extend' => '',
),
1 =>
array (
'type' => 'string',
'name' => 'webscan_warn',
'title' => '【ZR】提示',
'value' => '检测有非法攻击代码,请停止攻击,否则将加入黑名单,如有疑问请联系我们',
'content' => '',
'tip' => '',
'rule' => '',
'extend' => '',
),
2 =>
array (
'type' => 'text',
'name' => 'webscan_white_url',
'title' => '【ZR】放行url',
'value' => 'admin|index.php/admin',
'content' => '',
'tip' => '多个以|隔开前置优先原则如输入admin,admin开头的所有访问连接都不拦截',
'rule' => '',
'extend' => '',
),
3 =>
array (
'type' => 'radio',
'name' => 'ccopen',
'title' => '【CC】攻击开启',
'value' => '1',
'content' =>
array (
1 => '开启',
0 => '关闭',
),
'tip' => 'CC攻击拦截开关',
'rule' => '',
'extend' => '',
),
4 =>
array (
'type' => 'text',
'name' => 'white_url',
'title' => '【CC】放行URL',
'value' => 'index.php?s=/captcha|addons/webscan|captcha|index.php/addons/webscan',
'content' => '',
'tip' => '多个以|隔开前置优先原则如输入admin,admin开头的所有访问连接都不拦截',
'rule' => '',
'extend' => '',
),
5 =>
array (
'type' => 'number',
'name' => 'seconds',
'title' => '【CC】秒数',
'value' => '60',
'content' => '',
'tip' => '规定时间内配合下面的访问次数触发CC',
'rule' => '',
'extend' => '',
),
6 =>
array (
'type' => 'number',
'name' => 'refresh',
'title' => '【CC】访问次数',
'value' => '100',
'content' => '',
'tip' => '在上面的时间内访问次数触发CC',
'rule' => '',
'extend' => '',
),
7 =>
array (
'type' => 'string',
'name' => 'return_json',
'title' => '需返回json的目录',
'value' => 'api',
'content' => '',
'tip' => '报错需要返回json的接口目录多个以|隔开 如api或api|admin/index/login',
'rule' => '',
'extend' => '',
),
8 =>
array (
'type' => 'text',
'name' => 'webscan_white_ip',
'title' => '白名单ip',
'value' => '127.0.0.1',
'content' => '',
'tip' => '//白名单ip 一行一条记录 如127.0.0.1',
'rule' => '',
'extend' => '',
),
9 =>
array (
'type' => 'text',
'name' => 'webscan_black_ip',
'title' => '黑名单IP',
'value' => '',
'content' => '',
'tip' => ' 一行一条记录',
'rule' => '',
'extend' => '',
),
10 =>
array (
'type' => 'string',
'name' => 'black_warn',
'title' => '黑名单提示',
'value' => '您已被加入黑名单,如有疑问请联系我们',
'content' => '',
'tip' => '黑名单IP访问时提示',
'rule' => '',
'extend' => '',
),
11 =>
array (
'type' => 'number',
'name' => 'black_auto',
'title' => '自动黑名单',
'value' => '0',
'content' => '',
'tip' => '攻击多少次/天自动加入黑名单。为0不加入黑名单',
'rule' => '',
'extend' => '',
),
12 =>
array (
'type' => 'string',
'name' => 'files_suffix',
'title' => '【校验】文件后缀',
'value' => 'php|js|css|html',
'content' => '',
'tip' => '需要校验的文件后缀,|隔开',
'rule' => '',
'extend' => '',
),
13 =>
array (
'type' => 'string',
'name' => 'ignore_dir',
'title' => '【校验】忽略文件',
'value' => '.idea|.settings|.svn|runtime',
'content' => '',
'tip' => '忽略文件或文件夹校验,|隔开',
'rule' => '',
'extend' => '',
),
);

View File

@ -0,0 +1,5 @@
<?php
return [
"table_name" => "fa_webscan_log,fa_webscan_verifies",
"self_path" => ""
];

View File

@ -0,0 +1,121 @@
<?php
/**
* 菜单配置文件
*/
return [
[
"type" => "file",
"name" => "webscan",
"title" => "安全防护",
"icon" => "fa fa-shield",
"condition" => "",
"remark" => "",
"ismenu" => 1,
"sublist" => [
[
"type" => "file",
"name" => "webscan/webscanlog/dashboard",
"title" => "攻击概括",
"icon" => "fa fa-bar-chart-o",
"condition" => "",
"remark" => "",
"ismenu" => 1
],
[
"type" => "file",
"name" => "webscan/verifies",
"title" => "文件校验",
"icon" => "fa fa-history",
"condition" => "",
"remark" => "",
"ismenu" => 1,
"sublist" => [
[
"type" => "file",
"name" => "webscan/verifies/index",
"title" => "校验首页",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/verifies/show",
"title" => "查看文件",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/verifies/trust",
"title" => "加入信任",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/verifies/trusts",
"title" => "批量信任",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/verifies/build",
"title" => "初始化数据",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/verifies/bianli",
"title" => "遍历检查",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
]
]
],
[
"type" => "file",
"name" => "webscan/webscanlog",
"title" => "攻击日志",
"icon" => "fa fa-warning",
"condition" => "",
"remark" => "",
"ismenu" => 1,
"sublist" => [
[
"type" => "file",
"name" => "webscan/webscanlog/index",
"title" => "攻击日志",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
],
[
"type" => "file",
"name" => "webscan/webscanlog/black",
"title" => "加入黑名单",
"icon" => "fa fa-circle-o",
"condition" => "",
"remark" => "",
"ismenu" => 0
]
]
]
]
]
];

View File

@ -0,0 +1,15 @@
<?php
namespace addons\webscan\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
return $this->view->fetch('',['from'=>$this->request->param('from'),'captcha'=>$this->request->param('captcha','','')]);
}
}

11
addons/webscan/info.ini Normal file
View File

@ -0,0 +1,11 @@
name = webscan
title = 安全防护
intro = 防SQL注入防CC攻击防XSS校验恶意修改文件
author = amplam
website = /
version = 1.0.2
state = 1
url = /addons/webscan
first_menu = webscan
license = regular
licenseto = 16018

View File

@ -0,0 +1,24 @@
CREATE TABLE `__PREFIX__webscan_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`create_time` int(10) NOT NULL,
`page` varchar(100) DEFAULT NULL,
`method` varchar(20) DEFAULT NULL,
`rkey` varchar(50) DEFAULT NULL,
`rdata` varchar(100) DEFAULT NULL,
`user_agent` varchar(200) DEFAULT NULL,
`request_url` varchar(200) DEFAULT NULL,
`user_id` int(11) DEFAULT '0',
`ip` varchar(50) DEFAULT NULL,
`type` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=229 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `__PREFIX__webscan_verifies` (
`nameid` int(32) NOT NULL AUTO_INCREMENT,
`md5` varchar(32) NOT NULL DEFAULT '',
`method` enum('local','official') NOT NULL DEFAULT 'official',
`filename` varchar(254) NOT NULL DEFAULT '',
`mktime` int(11) NOT NULL DEFAULT '0' COMMENT '最后修改时间',
PRIMARY KEY (`nameid`)
) ENGINE=MyISAM AUTO_INCREMENT=36329 DEFAULT CHARSET=utf8;

View File

@ -0,0 +1,119 @@
<?php
namespace addons\webscan\library;
use addons\webscan\model\WebscanLog;
use think\Cache;
use think\Validate;
/**
* CC攻击助手
* @author amplam 122795200@qq.com
* @date 2019年10月30日 16:21:52
*/
class ChallengeCollapsar extends Server
{
private $cachename = 'ChallengeCollapsar';
protected $config = [
'seconds' => 60,//多少秒以内
'refresh' => 60,//刷新、访问次数
'white_url' => "",
];
/**
* 构造函数
* WxPay constructor.
* @param $config
*/
public function __construct($config = [])
{
$this->config = array_merge($this->config, $config);
}
/**
* CC攻击防护开始
* @return bool
*/
public function start()
{
//CC攻击URL白名单
if ($this->whiteUrl($this->config['white_url'])) return true;
//CC攻击URL白名单
//ip白名单
if ($this->whiteIp($this->config['webscan_white_ip'])) return true;
$now_time = time();
$ip = request()->ip();
$data = Cache::get($this->cachename . md5($ip));
if ($data) {
$data['refresh_times'] = $data['refresh_times'] + 1;
} else {
$data['refresh_times'] = 1;
$data['last_time'] = $now_time;
}
if (($now_time - $data['last_time']) < $this->config['seconds']) {
if ($data['refresh_times'] >= $this->config['refresh']) {
$captcha = request()->param('captcha');
if (!$captcha) {
//保存访问日志 相等才保存,不然可能会很多日志
if ($data['refresh_times'] == $this->config['refresh']) {
$logs = array('ip' => $ip, 'page' => $_SERVER["PHP_SELF"], 'method' => request()->method(), 'rkey' => "CC攻击", 'rdata' => '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'request_url' => $_SERVER["REQUEST_URI"], 'type' => 'cc');
WebscanLog::create($logs);
Cache::set($this->cachename . md5($ip), $data, 3600);
}
if ($this->config['return_json']) {
$this->config['return_json'] = str_replace("/", "\\/", $this->config['return_json']);
if (preg_match("/^" . $this->config['return_json'] . "/is", request()->pathinfo())) {
return $this->result("请输入验证码", [], '-1101', 'json');
}
}
if ($this->getResponseType() !== 'html') {
return $this->result("请输入验证码", [], '-1101', $this->getResponseType());
}
header('Location: ' . addon_url('webscan/index/index', ['from' => $_SERVER['REQUEST_URI']]));//跳转到输入验证码界面
exit;
}
$rule['captcha'] = 'require|captcha';
$validate = new Validate($rule, [], ['captcha' => "验证码"]);
$result = $validate->check(['captcha' => $captcha]);
if (!$result) {
if ($this->config['return_json']) {
$this->config['return_json'] = str_replace("/", "\\/", $this->config['return_json']);
if (preg_match("/^" . $this->config['return_json'] . "/is", request()->pathinfo())) {
return $this->result("验证码错误", [], '-1102', 'json');
}
}
if ($this->getResponseType() !== 'html') {
return $this->result("验证码错误", [], '-1102', $this->getResponseType());
}
header('Location:' . addon_url('webscan/index/index', ['from' => $_SERVER['REQUEST_URI']]));//跳转到输入验证码界面
exit();
}
$data['refresh_times'] = 1;
$data['last_time'] = $now_time;
}
} else {
$data['refresh_times'] = 1;
$data['last_time'] = $now_time;
}
Cache::set($this->cachename . md5($ip), $data, 3600);
return true;
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace addons\webscan\library;
use addons\webscan\model\WebscanLog;
use think\Config;
use think\exception\HttpResponseException;
use think\Request;
use think\Response;
use think\Url;
/**
* Class server
* @package addons\webscan\library
*/
abstract class Server
{
/**
* 错误信息
* @var
*/
protected $error;
/**
* 返回错误信息
* @return mixed
*/
public function getError()
{
return $this->error;
}
/**
* url白名单
* @param $white_url
* @return bool
*/
protected function whiteUrl($white_url, $url_var = '')
{
if (!$white_url) return false;
$url_var = $url_var ?: isset($_SERVER['REQUEST_URI'])?$_SERVER['REQUEST_URI']:'';
$url_var = strpos($url_var, '/') != 0 ?: substr($url_var, 1);
$search = ["/", "?", "=", ".", "&", '|'];
$replace = ["\/", "\?", "\=", "\.", "\&", '|^'];
$white_url = str_replace($search, $replace, $white_url);
if (preg_match("/^" . $white_url . "/is", $url_var)) {
return true;
}
return false;
}
/**
* ip白名单
* @param $white_ip
* @param string $ip
* @return bool
*/
protected function whiteIp($white_ip, $ip = '')
{
$ip = $ip ?: \request()->ip();
if ($ip && $white_ip) {
$webscan_white_ip_arr = explode(PHP_EOL, $white_ip);
if (count($webscan_white_ip_arr) > 0) {
if (in_array($ip, $webscan_white_ip_arr)) {
return true;
}
}
}
return false;
}
/**
* 防护提示
*/
protected function result($msg, $data = null, $code = 0, $type = null, array $header = [])
{
$url = '';
if (is_null($url)) {
$url = Request::instance()->isAjax() ? '' : 'javascript:history.back(-1);';
} elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) {
$url = Url::build($url);
}
$type = $type ?: $this->getResponseType();
$result = [
'code' => $code,
'msg' => $msg,
'data' => [],
'url' => $url,
];
if ('html' == strtolower($type)) {
$template = Config::get('template');
$view = Config::get('view_replace_str');
$result = \think\View::instance($template, $view)->fetch(Config::get('dispatch_error_tmpl'), $result);
}
$response = Response::create($result, $type)->header($header);
throw new HttpResponseException($response);
}
/**
* 获取当前的 response 输出类型
* @access protected
* @return string
*/
protected function getResponseType()
{
return Request::instance()->isAjax()
? Config::get('default_ajax_return')
: Config::get('default_return_type');
}
/**
* 日记记录
*/
protected function webscanSlog($logs)
{
WebscanLog::create($logs);
if ($this->config['black_auto'] > 0) {
$beginToday = mktime(0, 0, 0, date('m'), date('d'), date('Y'));
if ((new WebscanLog())->where('ip', $logs['ip'])->where('create_time', '>', $beginToday)->count() >= $this->config['black_auto']) {
//加入黑名单
$config = get_addon_config('webscan');
//更新配置文件
$config['webscan_black_ip'] = $config['webscan_black_ip'] . PHP_EOL . $logs['ip'];
set_addon_config('webscan', $config);
\think\addons\Service::refresh();
}
}
}
}

View File

@ -0,0 +1,186 @@
<?php
namespace addons\webscan\library;
use addons\webscan\model\WebscanLog;
use think\Config;
use think\exception\HttpResponseException;
use think\Request;
use think\Response;
use think\Url;
/**
* SQL注入XSS攻击助手
* @author amplam 122795200@qq.com
* @date 2019年10月29日 17:43:27
*/
class Webscan extends Server
{
protected $error = "";
//get拦截规则
private $getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//post拦截规则
private $postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//cookie拦截规则
private $cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
protected $config = [
'webscan_switch' => 1,//拦截开关(1为开启0关闭)
//提交方式拦截(1开启拦截,0关闭拦截,post,get,cookie,referre选择需要拦截的方式)
'webscan_post' => 1,
'webscan_get' => 1,
'webscan_cookie' => 1,
'webscan_referre' => 1,
'black_auto' => 0,//攻击多少次/天自动加入黑名单。为0不加入黑名单
'webscan_warn' => "检测有非法攻击代码,请停止攻击,否则将加入黑名单,如有疑问请联系我们",//提示
'webscan_white_module' => '',//放行指定模块 多个以|隔开 如admin|admin1
'webscan_white_url' => '',//模块/控制器/方法 一行一条记录 如admin/index/login
'webscan_white_ip' => '',//白名单ip 一行一条记录 如127.0.0.1
];
/**
* 构造函数
* WxPay constructor.
* @param $config
*/
public function __construct($config = [])
{
$this->config = array_merge($this->config, $config);
}
/**
* 开始拦截
*/
public function start()
{
if ($this->config['webscan_switch'] && $this->webscanWhite($this->config['webscan_white_module'], $this->config['webscan_white_url'])) {
if ($this->config['webscan_get']) {
$_GET['temp_url_path'] = \request()->pathinfo();//请求的路径也加入检测
foreach ($_GET as $key => $value) {
$this->webscanStopAttack($key, $value, $this->getfilter, "GET");
}
}
if ($this->config['webscan_post']) {
foreach ($_POST as $key => $value) {
$this->webscanStopAttack($key, $value, $this->postfilter, "POST");
}
}
if ($this->config['webscan_cookie']) {
foreach ($_COOKIE as $key => $value) {
$this->webscanStopAttack($key, $value, $this->cookiefilter, "COOKIE");
}
}
if ($this->config['webscan_referre']) {
//referer获取
$webscan_referer = empty($_SERVER['HTTP_REFERER']) ? array() : array('HTTP_REFERER' => $_SERVER['HTTP_REFERER']);
foreach ($webscan_referer as $key => $value) {
$this->webscanStopAttack($key, $value, $this->postfilter, "REFERRER");
}
}
//其他类似put,delete的检测
$method = \request()->method(true);
// 自动获取请求变量
switch ($method) {
case 'PUT':
case 'DELETE':
case 'PATCH':
$put_arr = \request()->put();
foreach ($put_arr as $key => $value) {
$this->webscanStopAttack($key, $value, $this->postfilter, $method);
}
break;
default:
}
}
}
/**
* 拦截白名单
* true需要拦截false 不需要
*/
private function webscanWhite($webscan_white_module, $webscan_white_url)
{
//ip白名单
if ($this->whiteIp($this->config['webscan_white_ip'])) return false;
//URL白名单
if ($this->whiteUrl($webscan_white_url)) return false;
return true;
}
/**
* 攻击检查拦截
*/
private function webscanStopAttack($StrFiltKey, $StrFiltValue, $ArrFiltReq, $method)
{
$StrFiltValue = $this->webscanArrForeach($StrFiltValue);
if (preg_match("/" . $ArrFiltReq . "/is", $StrFiltValue) == 1) {
$this->webscanSlog(array('ip' => \request()->ip(), 'page' => $_SERVER["PHP_SELF"], 'method' => \request()->method(), 'rkey' => $StrFiltKey, 'rdata' => $StrFiltValue, 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'request_url' => $_SERVER["REQUEST_URI"], 'type' => 'webscan'));
return $this->webscanPape();
}
if (preg_match("/" . $ArrFiltReq . "/is", $StrFiltKey) == 1) {
$this->webscanSlog(array('ip' => \request()->ip(), 'page' => $_SERVER["PHP_SELF"], 'method' => \request()->method(), 'rkey' => $StrFiltKey, 'rdata' => $StrFiltKey, 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'request_url' => $_SERVER["REQUEST_URI"], 'type' => 'webscan'));
return $this->webscanPape();
}
}
/**
* 参数拆分
*/
private function webscanArrForeach($arr)
{
static $str;
static $keystr;
if (!is_array($arr)) {
return $arr;
}
foreach ($arr as $key => $val) {
$keystr = $keystr . $key;
if (is_array($val)) {
return $this->webscanArrForeach($val);
} else {
$str[] = $val . $keystr;
}
}
return implode($str);
}
/**
* 防护提示
*/
public function webscanPape()
{
if ($this->config['return_json']) {
$url_var = $_SERVER['REQUEST_URI'];
$url_var = strpos($url_var, '/') != 0 ?: substr($url_var, 1);
$search = ["/", "?", "=", ".", "&", '|'];
$replace = ["\/", "\?", "\=", "\.", "\&", '|^'];
$this->config['return_json'] = str_replace($search, $replace, $this->config['return_json']);
if (preg_match("/^" . $this->config['return_json'] . "/is", $url_var)) {
return $this->result($this->config['webscan_warn'], [], '-110', 'json');
}
}
return $this->result($this->config['webscan_warn'], [], '-110', $this->getResponseType());
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace addons\webscan\model;
use app\common\model\webscan\WebscanLog as WebscanLogModel;
/**
* 攻击日志
*/
class WebscanLog extends WebscanLogModel
{
}

View File

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>温馨提示</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
*{box-sizing:border-box;margin:0;padding:0;font-family:Lantinghei SC,Open Sans,Arial,Hiragino Sans GB,Microsoft YaHei,"微软雅黑",STHeiti,WenQuanYi Micro Hei,SimSun,sans-serif;-webkit-font-smoothing:antialiased}
body{padding:70px 0;background:#edf1f4;font-weight:400;font-size:1pc;-webkit-text-size-adjust:none;color:#333}
a{outline:0;color:#3498db;text-decoration:none;cursor:pointer}
.system-message{margin:20px 5%;padding:40px 20px;background:#fff;box-shadow:1px 1px 1px hsla(0,0%,39%,.1);text-align:center}
.system-message h1{margin:0;margin-bottom:9pt;color:#444;font-weight:400;font-size:40px}
.system-message .jump,.system-message .image{margin:20px 0;padding:0;padding:10px 0;font-weight:400}
.system-message .jump{font-size:14px}
.system-message .jump a{color:#333}
.system-message p{font-size:9pt;line-height:20px}
.system-message .btn{display:inline-block;margin-right:10px;width:138px;height:2pc;border:1px solid #44a0e8;border-radius:30px;color:#44a0e8;text-align:center;font-size:1pc;line-height:2pc;margin-bottom:5px;}
.success .btn{border-color:#69bf4e;color:#69bf4e}
.error .btn{border-color:#ff8992;color:#ff8992}
.info .btn{border-color:#3498db;color:#3498db}
.copyright p{width:100%;color:#919191;text-align:center;font-size:10px}
.system-message .btn-grey{border-color:#bbb;color:#bbb}
.clearfix:after{clear:both;display:block;visibility:hidden;height:0;content:"."}
@media (max-width:768px){body {padding:20px 0;}}
@media (max-width:480px){.system-message h1{font-size:30px;}}
.form-control {
width: 100%;
height: 31px;
padding: 6px 12px;
font-size: 12px;
line-height: 1.42857143;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.container {
margin-right: auto;
margin-left: auto;
padding-left: 15px;
padding-right: 15px;
}
@media (min-width: 768px) {
.container {
width: 750px;
}
}
@media (min-width: 992px) {
.container {
width: 970px;
}
}
@media (min-width: 1200px) {
.container {
width: 1170px;
}
}
.screen {
max-width: 400px;
padding: 0;
margin: 100px auto 0 auto;
}
</style>
</head>
<body>
<div class="system-message info container">
<h1>{php}echo $captcha?"验证码错误,请重新输入":"经系统检测,您需要输入验证码才可以继续访问";{/php}</h1>
<div class="screen">
<div class="input-group" style="margin-bottom: 15px;">
<div class="input-group-addon" style="padding:0;border:none;cursor:pointer;margin-bottom: 15px;">
<img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="200" height="60" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
</div>
<input type="text" name="captcha" id="captcha" class="form-control" placeholder="{:__('输入验证码,继续访问')}" />
</div>
<p class="clearfix">
<a href="javascript:post()" class="btn btn-grey">确定</a>
</p>
</div>
</div>
<script>
function post(){
var captcha=document.getElementById('captcha').value;
if(!captcha){
alert("请输入图片验证码");
return false;
}
var referrer="{$from}";
if(referrer==''){
alert("找不到上一页");
return false;
}
referrer=referrer+(referrer.indexOf("?",0)>0?"&captcha="+captcha:"?captcha="+captcha);
window.location.href=referrer;
}
</script>
</body>
</html>

View File

@ -0,0 +1,56 @@
<?php
namespace app\admin\controller\apilog;
use app\common\controller\Backend;
use addons\apilog\model\Apilog;
class Data extends Backend
{
//基础数据
public function index()
{
if (IS_AJAX) {
$start = input('start', strtotime(date("Y-m-d", time())));
$end = input('end', $start + 60 * 60 * 24);
$baseinfo = Apilog::getBaseInfo($start, $end);
$code = Apilog::getHttpCodePie($start, $end);
$time = Apilog::getResponseTimePie($start, $end);
$requesttop = Apilog::getMaxRequestTop($start, $end);
$errortop = Apilog::getMaxErrorTop($start, $end);
$fasttop = Apilog::getDoFastTop($start, $end);
$slowtop = Apilog::getDoSlowTop($start, $end);
$data['base'] = $baseinfo;
$data['code'] = $code;
$data['requesttop'] = $requesttop;
$data['time'] = $time;
$data['errortop'] = $errortop;
$data['fasttop'] = $fasttop;
$data['slowtop'] = $slowtop;
return json($data);
}
return $this->view->fetch();
}
//趋势数据
public function qushi()
{
if (IS_AJAX) {
$count_m = Apilog::getRequestCountLine(0);
$count_h = Apilog::getRequestCountLine(1);
$count_d = Apilog::getRequestCountLine(2);
$time_m = Apilog::getDoTimeLine(0);
$time_h = Apilog::getDoTimeLine(1);
return json([
'count_m' => $count_m,
'count_h' => $count_h,
'count_d' => $count_d,
'time_m' => $time_m,
'time_h' => $time_h
]);
}
return $this->view->fetch();
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace app\admin\controller\apilog;
use app\common\controller\Backend;
use think\Cache;
class Index extends Backend
{
protected $model = null;
public function _initialize()
{
parent::_initialize();
$this->model = new \addons\apilog\model\Apilog;
$this->view->assign("methodList", $this->model->getMethodList());
}
public function index()
{
$this->request->filter(['strip_tags']);
if ($this->request->isAjax()) {
if ($this->request->request('keyField')) {
return $this->selectpage();
}
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
$total = $this->model
->where($where)
->order($sort, $order)
->count();
$list = $this->model
->where($where)
->order($sort, $order)
->limit($offset, $limit)
->select();
foreach ($list as $k => $v) {
$v['banip'] = Cache::has('banip:' . $v['ip']);
}
$list = collection($list)->toArray();
$result = array("total" => $total, "rows" => $list);
return json($result);
}
return $this->view->fetch();
}
public function detail($ids)
{
$row = $this->model->get(['id' => $ids]);
if (!$row)
$this->error(__('No Results were found'));
$this->view->assign("row", $row->toArray());
return $this->view->fetch();
}
public function banip($status, $ip, $time = 0)
{
if ($status == 0) {
Cache::set('banip:' . $ip, 1, $time * 60);
} else {
Cache::rm('banip:' . $ip);
}
$this->success('succ', null, Cache::has('banip:' . $ip));
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace app\admin\controller\apilog;
use app\common\controller\Backend;
use addons\apilog\model\Apilog;
class Trend extends Backend
{
//趋势数据
public function index()
{
if (IS_AJAX) {
$count_m = Apilog::getRequestCountLine(0);
$count_h = Apilog::getRequestCountLine(1);
$count_d = Apilog::getRequestCountLine(2);
$time_m = Apilog::getDoTimeLine(0);
$time_h = Apilog::getDoTimeLine(1);
return json([
'count_m' => $count_m,
'count_h' => $count_h,
'count_d' => $count_d,
'time_m' => $time_m,
'time_h' => $time_h
]);
}
return $this->view->fetch();
}
}

View File

@ -0,0 +1,237 @@
<?php
namespace app\admin\controller\webscan;
use app\common\controller\Backend;
use think\Db;
/**
*文件校验
*/
class Verifies extends Backend
{
//文件校验结果缓存名
private $result_cache_name = 'InconformityCacheArr';
//本地需要校验的文件缓存名
private $loca_cache_name = "WebscanVerifiesCache";
public function _initialize()
{
parent::_initialize();
$this->model = new \app\common\model\webscan\WebscanVerifies();
}
/**
* 校验首页
* @return string|\think\response\Json
* @throws \think\Exception
*/
public function index()
{
if ($this->request->isAjax()) {
/*$search=$this->request->param('search');
$limit=$this->request->param('limit','10','intval');
$offset=$this->request->param('offset','0','intval');
$sort=$this->request->param('sort');
$order=$this->request->param('order');*/
$files_arr = cache($this->result_cache_name);
$count = count($files_arr);
//分页处理
//$files_arr=array_slice($files_arr,$offset,$limit);
$result = array("total" => $count, "rows" => $files_arr);
return json($result);
} else {
$verifi_data = $this->model->where('method', 'local')->count();
if (!$verifi_data) cache($this->loca_cache_name, null);//删除缓存
return $this->view->fetch('', ['verifi_data' => $verifi_data]);
}
}
/**
* 查看文件
*/
public function show($filename)
{
if (file_exists(ROOT_PATH . $filename)) {
show_source(ROOT_PATH . $filename);
} else {
$this->error("文件不存在");
}
}
/**
*加入信任
* @param $filename
*/
public function trust($index)
{
$files_arr = cache($this->result_cache_name);
$temp = $files_arr[$index];
if ($temp) {
unset($files_arr[$index]);
$info = $this->model->where('method', $temp['method'])->where('filename', $temp['filename'])->find();
if ($info) {
$info->md5 = $temp['md5'];
$info->save();
} else {
$this->model->insert($temp);
}
}
cache($this->result_cache_name, array_values($files_arr));
$this->success();
}
/**
*批量信任
* @param $filename
*/
public function trusts($ids)
{
if (!$ids) $this->error("请选择");
$ids = explode(',', $ids);
$files_arr = cache($this->result_cache_name);
//TODO 待优化
foreach ($ids as $index) {
$temp = $files_arr[$index];
if ($temp) {
unset($files_arr[$index]);
$info = $this->model->where('method', $temp['method'])->where('filename', $temp['filename'])->find();
if ($info) {
$info->md5 = $temp['md5'];
$info->save();
} else {
$this->model->insert($temp);
}
}
}
cache($this->result_cache_name, array_values($files_arr));
$this->success();
}
/**
* 初始化数据
*/
public function build()
{
$verifi_data = $this->model->where('method', 'local')->count();
if (!$verifi_data) {
cache($this->loca_cache_name, null);//删除缓存
} else {
$this->error("已经初始化");
}
$files_arr = $this->readFiles();
if ($files_arr) {
$this->model->insertAll($files_arr);
$this->success("初始化数据成功");
} else {
$this->error("初始化失败");
}
}
/**
*遍历检查
* @param $dir
* @return array
*/
public function bianli()
{
$limit = $this->request->param('limit', '1000', 'intval');
$offset = $this->request->param('offset', '0', 'intval');
if ($offset == 0) {
cache($this->result_cache_name, null);//清空之前的结果
}
$files_arr = $this->readFiles();
$count = count($files_arr);
//分页处理
$files_arr = array_slice($files_arr, $offset, $limit);
if ($files_arr) {
$md5_arr = array_column($files_arr, 'md5');
$subQuery = Db::name('webscan_verifies')->field('md5')->where('md5', 'in', $md5_arr)->buildSql();
$sql = "SELECT md5,method,filename,mktime FROM (";
$tempsql = "";
foreach ($files_arr as $row) {
//TODO待优化
$tempsql .= $tempsql ? " UNION SELECT '" . $row['md5'] . "' as md5, '" . $row['method'] . "' as method ,'" . addslashes($row['filename']) . "' as filename ,'" . $row['mktime'] . "' as mktime " : " SELECT '" . $row['md5'] . "' as md5, '" . $row['method'] . "' as method ,'" . addslashes($row['filename']) . "' as filename ,'" . $row['mktime'] . "' as mktime ";
}
$sql = $sql . $tempsql . ")a WHERE md5 NOT IN(" . $subQuery . ")";
$result = Db::query($sql);
$InconformityCacheArr = cache($this->result_cache_name);
$InconformityCacheArr = $InconformityCacheArr ?: [];
if ($result) {
$InconformityCacheArr = $InconformityCacheArr ? array_merge($InconformityCacheArr, $result) : $result;
cache($this->result_cache_name, $InconformityCacheArr);
}
$this->success("正在检测中,已检测" . ($offset + $limit) . "个文件,有" . count($InconformityCacheArr) . "个文件不一致", url('webscan.verifies/bianli', ['offset' => ($offset + $limit)]));
}
$this->success("检测完成{$count}", url('webscan.verifies/index'));
}
/**
* 读取本地的文件数据
* @return mixed
*/
protected function readFiles()
{
$files_arr = cache($this->loca_cache_name);
if (!$files_arr) {
$config = get_addon_config('webscan');
$suffix = explode('|', $config['files_suffix']);
$dir = $config['ignore_dir'];//忽略的文件夹
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(ROOT_PATH), \RecursiveIteratorIterator::LEAVES_ONLY
);
$i = 0;
foreach ($files as $name => $file) {
$temp_str = str_replace(ROOT_PATH, '', $name);
$search = ["/", "?", "=", ".", "&", '|', '\\'];
$replace = ["\/", "\?", "\=", "\.", "\&", '|^', '\/'];
$white_url = str_replace($search, $replace, $dir);
if (preg_match("/^" . $white_url . "/is", $temp_str)) {
continue;
}
if (!$file->isDir()) {
if (in_array($file->getExtension(), $suffix)) {
$files_arr[$i]['md5'] = md5_file($name);
$files_arr[$i]['filename'] = $temp_str;
$files_arr[$i]['method'] = 'local';
$files_arr[$i]['mktime'] = $file->getMTime();
}
$i++;
}
}
unset($files);
if ($files_arr) {
cache($this->loca_cache_name, $files_arr, 86400);//缓存一天
}
}
return $files_arr;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace app\admin\controller\webscan;
use app\common\controller\Backend;
/**
*攻击日志
*/
class Webscanlog extends Backend
{
public function _initialize()
{
parent::_initialize();
$this->model = new \app\common\model\webscan\WebscanLog();
}
/**
* 攻击日志
* @return string|\think\response\Json
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function index()
{
if ($this->request->isAjax()) {
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
$limit = $limit ? $limit : 10;
$total = $this->model->where($where)->count();
$list = $this->model->where($where)->order($sort, $order)->limit($offset, $limit)->fetchSql(false)->select();
$result = array("total" => $total, "rows" => $list);
return json($result);
} else {
return $this->view->fetch();
}
}
/**
* 加入黑名单
* @param null $ip
*/
public function black($ip = null)
{
if (!$ip) {
$this->error("请输入ip");
}
if ($ip == $this->request->ip()) {
$this->error("不能把自己的IP加入黑名单");
}
//加入黑名单
$config = get_addon_config('webscan');
if ($config['webscan_black_ip']) {
$webscan_black_ip_arr = explode(PHP_EOL, $config['webscan_black_ip']);
if (in_array($ip, $webscan_black_ip_arr)) {
$this->error("{$ip}已是黑名单");
}
}
//更新配置文件
$config['webscan_black_ip'] = $config['webscan_black_ip'] . PHP_EOL . $ip;
set_addon_config('webscan', $config);
\think\addons\Service::refresh();
$this->success("已成功把{$ip}加入了黑名单");
}
/**
* 攻击概括
*/
public function dashboard()
{
//今天开始时间
$beginToday = mktime(0, 0, 0, date('m'), date('d'), date('Y'));
$todaytimes = $this->model->where('create_time', '>', $beginToday)->count();
$todayips = $this->model->where('create_time', '>', $beginToday)->group('ip')->count();
$totaltimes = $this->model->count();
$totalips = $this->model->group('ip')->count();
$seventtime = \fast\Date::unixtime('day', -7);
$totallist = [];
for ($i = 0; $i < 7; $i++) {
$day = date("Y-m-d", $seventtime + ($i * 86400));
//开始时间
$beginDay = strtotime($day);
$endDay = strtotime($day . " 23:59:59");
$totallist[$day] = $this->model->where('create_time', 'between', [$beginDay, $endDay])->count();
}
$rankingips = $this->model->group('ip')->field('ip,count(ip) as count')->group('ip')->limit(10)->order('count desc')->select();
$todayranking = $this->model->group('ip')->where('create_time', '>', $beginToday)->field('ip,count(ip) as count')->group('ip')->limit(10)->order('count desc')->select();
$this->view->assign([
'todaytimes' => $todaytimes,//今日攻击次数
'todayips' => $todayips,//今日攻击ip
'totallist' => $totallist,//
'rankingips' => $rankingips,//历史攻击排行
'totaltimes' => $totaltimes,//总攻击
'totalips' => $totalips,//总ip
'todayranking' => $todayranking,//今日攻击排行
]);
return $this->view->fetch();
}
}

View File

@ -8,5 +8,6 @@ return [
'Endtime' => '答完时间',
'User.nickname' => '昵称',
'User.mobile' => '手机号',
'User.avatar' => '头像'
'User.avatar' => '头像',
"Edit" => '查看详情',
];

View File

@ -2,6 +2,7 @@
namespace app\admin\model\deepseek;
use app\common\model\deepseek\Stream;
use think\Model;
@ -25,9 +26,25 @@ class Question extends Model
// 追加属性
protected $append = [
'endtime_text'
'endtime_text',
'reasoning_text',
'answer_text'
];
public function getReasoningTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['id']) ? $data['id'] : '');
//Stream model拼接所有question_id=$value =list中的所有content字段成完整字符串文本
$contents = Stream::where('question_id', $value)->where('reasoning', 1)->order('created_time asc,reasoning desc,id asc')->column("content");
return implode("", $contents);
}
public function getAnswerTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['id']) ? $data['id'] : '');
$contents = Stream::where('question_id', $value)->where('reasoning', 0)->order('created_time asc,reasoning desc,id asc')->column("content");
return implode("", $contents);
}

View File

@ -0,0 +1,145 @@
<style type="text/css">
.smallstat {
border-radius: 8px;
box-shadow: 2px 2px 4px #ccc;
position: relative;
margin-bottom: 30px;
height: 120px;
padding: 15px;
}
.teal-bg {
color: #fff;
background: #97d3c5;
background-color: #97d3c5;
}
.smallstat .value {
text-align: center;
font-size: 26px;
padding-top: 5px;
}
.smallstat .title,
.smallstat .value {
display: block;
width: 100%;
}
.smallstat h4 {
text-align: center;
font-size: 16px;
margin-top: 20px;
letter-spacing: 0.05rem;
}
</style>
<div class="panel" style="background-color: transparent;">
<div class="panel-body">
<div class="btn-group datefilter" role="group" aria-label="..." style="padding-bottom: 10px;">
<button type="button" data-type="1" class="btn btn-default">15分钟</button>
<button type="button" data-type="2" class="btn btn-default">30分钟</button>
<button type="button" data-type="3" class="btn btn-default">1小时</button>
<button type="button" data-type="4" class="btn btn-default">4小时</button>
<button type="button" data-type="5" class="btn btn-default">12小时</button>
<button type="button" data-type="6" class="btn btn-default btn-24hours">24小时</button>
<div class="input-group " style="padding-left: 10px; width: 340px;">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="form-control input-inline datetimerange" id="createtime" placeholder="指定日期">
</div>
</div>
</div>
</div>
<div class="panel" style="background-color: transparent;">
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#dd6b66">
<b><span class="value" id="bs_request">0</span></b>
<h4>请求次数</h4>
</div>
</div>
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#759aa0">
<b><span class="value" id="bs_time">0</span></b>
<h4>平均处理时间(ms)</h4>
</div>
</div>
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#e69d87">
<b><span class="value" id="bs_404">0</span></b>
<h4>404</h4>
</div>
</div>
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#eedd78">
<b><span class="value" id="bs_500">0</span></b>
<h4>500</h4>
</div>
</div>
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#73a373">
<b><span class="value" id="bs_error">0%</span></b>
<h4>错误率占比</h4>
</div>
</div>
<div class="col-lg-2 col-sm-6 col-xs-6 col-xxs-12">
<div class="smallstat teal-bg" style="background-color:#7289ab">
<b><span class="value" id="bs_api">0</span></b>
<h4>接口总数(已请求)</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in">
<div class="row">
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="code-chart" style="position: relative; height: 300px;">
</div>
</div>
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="time-chart" style="position: relative; height: 300px;">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="request-chart" style="position: relative; height: 350px;">
</div>
</div>
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="error-chart" style="position: relative; height: 350px;">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="fast-chart" style="position: relative; height: 350px;">
</div>
</div>
<div class="col-md-6 nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="slow-chart" style="position: relative; height: 350px;">
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
<table class="table table-striped">
<thead>
<tr>
<th>{:__('Title')}</th>
<th>{:__('Content')}</th>
</tr>
</thead>
<tbody>
{volist name="row" id="vo" }
<tr>
<td>{:__($key)}</td>
<td>{if $key=='createtime'}{$vo|datetime}{else/}{$vo|htmlentities}{/if}</td>
</tr>
{/volist}
</tbody>
</table>
<div class="hide layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="reset" class="btn btn-primary btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
</div>
</div>

View File

@ -0,0 +1,24 @@
<div class="panel panel-default panel-intro">
{:build_heading()}
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i
class="fa fa-refresh"></i> </a>
<a href="javascript:;"
class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('apilog/index/del')?'':'hide'}"
title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-del="{:$auth->check('apilog/index/del')}" width="100%">
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,40 @@
<div class="panel">
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in">
<div class="row">
<div class="nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="count-m-chart" style="position: relative; height: 300px;">
</div>
</div>
<div class="nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="time-m-chart" style="position: relative; height: 300px;">
</div>
</div>
</div>
<div class="row">
<div class="nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="count-h-chart" style="position: relative; height: 300px;">
</div>
</div>
<div class="nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="time-h-chart" style="position: relative; height: 300px;">
</div>
</div>
</div>
<div class="row">
<div class="nav-tabs-custom charts-custom">
<div class="chart tab-pane" id="count-d-chart" style="position: relative; height: 300px;">
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,27 +1,69 @@
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('User_id')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-user_id" data-rule="required" data-source="user/user/index" data-field="nickname" class="form-control selectpage" name="row[user_id]" type="text" value="{$row.user_id|htmlentities}">
</div>
</div>
<!-- <div class="form-group">-->
<!-- <label class="control-label col-xs-12 col-sm-2">{:__('User_id')}:</label>-->
<!-- <div class="col-xs-12 col-sm-8">-->
<!-- <input id="c-user_id" data-rule="required" data-source="user/user/index" data-field="nickname" class="form-control selectpage" name="row[user_id]" type="text" value="{$row.user_id|htmlentities}">-->
<!-- </div>-->
<!-- </div>-->
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-content" class="form-control" name="row[content]" type="text" value="{$row.content|htmlentities}">
<!-- <input id="c-content" class="form-control" name="row[content]" type="text" value="{$row.content|htmlentities}">-->
<textarea id="c-content" class="form-control editor" rows="5" name="row[content]">{$row.content|htmlentities}</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Endtime')}:</label>
<div class="form-group" style="display: {$row.err_msg ? 'block' : 'none'}">
<label class="control-label col-xs-12 col-sm-2">{:__('模型报错啦!')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-endtime" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[endtime]" type="text" value="{:$row.endtime?datetime($row.endtime):''}">
<span class="label label-danger">{$row.err_msg|htmlentities}</span>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('请求地址')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-ip_addr" class="form-control" name="row[ip_addr]" type="text" value="{$row.ip_addr|htmlentities}">
<!-- <textarea id="c-content" class="form-control editor" rows="5" name="row[content]">{$row.content|htmlentities}</textarea>-->
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('思考(空为未思考)')}:</label>
<div class="col-xs-12 col-sm-8">
<!-- <input id="c-content" class="form-control" name="row[content]" type="text" value="{$row.content|htmlentities}">-->
<textarea id="c-reasoning_text" class="form-control editor" rows="5" name="row[reasoning_text]">{$row.reasoning_text|htmlentities}</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('回答')}:</label>
<div class="col-xs-12 col-sm-8">
<!-- <input id="c-content" class="form-control" name="row[content]" type="text" value="{$row.content|htmlentities}">-->
<textarea id="c-answer_text" class="form-control editor" rows="5" name="row[answer_text]">{$row.answer_text|htmlentities}</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('本次请求问答链')}:</label>
<div class="col-xs-12 col-sm-8">
<!-- <input id="c-q_json" class="form-control" name="row[q_json]" type="text" value="{$row.q_json|htmlentities}">-->
<textarea id="c-q_json" class="form-control editor" rows="5" name="row[q_json]">{$row.q_json|htmlentities}</textarea>
</div>
</div>
<!-- <div class="form-group">-->
<!-- <label class="control-label col-xs-12 col-sm-2">{:__('Endtime')}:</label>-->
<!-- <div class="col-xs-12 col-sm-8">-->
<!-- <input id="c-endtime" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[endtime]" type="text" value="{:$row.endtime?datetime($row.endtime):''}">-->
<!-- </div>-->
<!-- </div>-->
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
<!-- <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>-->
</div>
</div>
</form>

View File

@ -7,8 +7,8 @@
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('deepseek/question/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('deepseek/question/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('deepseek/question/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
<!-- <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('deepseek/question/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>-->
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('deepseek/question/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
@ -18,7 +18,7 @@
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('deepseek/question/edit')}"
data-operate-del="{:$auth->check('deepseek/question/del')}"
data-operate-del="0"
width="100%">
</table>
</div>

View File

@ -1,15 +1,23 @@
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Question_id')}:</label>
<label class="control-label col-xs-12 col-sm-2">{:__('本次应答token')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-question_id" data-rule="required" data-source="deepseek/question/index" class="form-control selectpage" name="row[question_id]" type="text" value="{$row.question_id|htmlentities}">
<!-- <input id="c-content" class="form-control" name="row[content]" type="text" value="{$row.content|htmlentities}">-->
<textarea id="c-content" class="form-control editor" rows="5" name="row[content]">{$row.content|htmlentities}</textarea>
</div>
</div>
<!-- <div class="form-group">-->
<!-- <label class="control-label col-xs-12 col-sm-2">{:__('Question_id')}:</label>-->
<!-- <div class="col-xs-12 col-sm-8">-->
<!-- <input id="c-question_id" data-rule="required" data-source="deepseek/question/index" data-field="key" class="form-control selectpage" name="row[question_id]" type="text" value="{$row.question_id|htmlentities}">-->
<!-- </div>-->
<!-- </div>-->
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Assistant_id')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-assistant_id" data-rule="required" data-source="assistant/index" class="form-control selectpage" name="row[assistant_id]" type="text" value="{$row.assistant_id|htmlentities}">
<input id="c-assistant_id" data-rule="required" class="form-control" name="row[assistant_id]" type="text" value="{$row.assistant_id|htmlentities}">
</div>
</div>
<div class="form-group">
@ -65,7 +73,7 @@
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
<!-- <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>-->
</div>
</div>
</form>

View File

@ -7,8 +7,8 @@
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('deepseek/stream/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('deepseek/stream/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('deepseek/stream/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
<!-- <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('deepseek/stream/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>-->
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('deepseek/stream/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>

View File

@ -0,0 +1,43 @@
<div class="panel panel-default panel-intro">
<div class="panel-heading">
<div class="panel-lead"><em> 用于检测文件是否被恶意修改【本地缓存一天,如有需要可清除缓存再操作】</em>
<br/>
{if $verifi_data}
<a href="{:url('webscan.verifies/bianli')}" class="btn btn-primary btn-refresh" title="立即校验"><i class="fa fa-search"></i> 立即校验</a>
{else/}
<a href="{:url('webscan.verifies/build')}" class="btn btn-primary " title="初始化校验数据"><i class="fa fa-cloud-download"></i> 初始化校验数据</a>
{/if}
</div>
</div>
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class=" btn-refresh" title="刷新"> 检验不一致文件:</a>
<br/>
<br/>
<div class="dropdown btn-group">
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> 批量操作</a>
<ul class="dropdown-menu text-left" role="menu">
<li><a class="btn btn-link btn-multi btn-disabled disabled" data-url="webscan/verifies/trusts" href="javascript:;" data-params="status=normal"><i class="fa fa-exclamation-triangle"></i> 信任</a></li>
</ul>
</div>
</div>
<table id="table" class="table table-striped table-bordered table-hover"
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,257 @@
<style type="text/css">
.sm-st {
background:#fff;
padding:20px;
-webkit-border-radius:3px;
-moz-border-radius:3px;
border-radius:3px;
margin-bottom:20px;
-webkit-box-shadow: 0 1px 0px rgba(0,0,0,0.05);
box-shadow: 0 1px 0px rgba(0,0,0,0.05);
}
.sm-st-icon {
width:60px;
height:60px;
display:inline-block;
line-height:60px;
text-align:center;
font-size:30px;
background:#eee;
-webkit-border-radius:5px;
-moz-border-radius:5px;
border-radius:5px;
float:left;
margin-right:10px;
color:#fff;
}
.sm-st-info {
font-size:12px;
padding-top:2px;
}
.sm-st-info span {
display:block;
font-size:24px;
font-weight:600;
}
.orange {
background:#fa8564 !important;
}
.tar {
background:#45cf95 !important;
}
.sm-st .green {
background:#86ba41 !important;
}
.pink {
background:#AC75F0 !important;
}
.yellow-b {
background: #fdd752 !important;
}
.stat-elem {
background-color: #fff;
padding: 18px;
border-radius: 40px;
}
.stat-info {
text-align: center;
background-color:#fff;
border-radius: 5px;
margin-top: -5px;
padding: 8px;
-webkit-box-shadow: 0 1px 0px rgba(0,0,0,0.05);
box-shadow: 0 1px 0px rgba(0,0,0,0.05);
font-style: italic;
}
.stat-icon {
text-align: center;
margin-bottom: 5px;
}
.st-red {
background-color: #F05050;
}
.st-green {
background-color: #27C24C;
}
.st-violet {
background-color: #7266ba;
}
.st-blue {
background-color: #23b7e5;
}
.stats .stat-icon {
color: #28bb9c;
display: inline-block;
font-size: 26px;
text-align: center;
vertical-align: middle;
width: 50px;
float:left;
}
.stat {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
margin-right: 10px; }
.stat .value {
font-size: 20px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500; }
.stat .name {
overflow: hidden;
text-overflow: ellipsis; }
.stat.lg .value {
font-size: 26px;
line-height: 28px; }
.stat.lg .name {
font-size: 16px; }
.stat-col .progress {height:2px;}
.stat-col .progress-bar {line-height:2px;height:2px;}
.item {
padding:30px 0;
}
</style>
{if $todaytimes>0}
<div class="alert alert-danger-light">
今日有攻击,请注意安全防护,安全并不能完全依赖于本插件。
</div>
{/if}
<div class="panel panel-default panel-intro">
<div class="panel-heading">
{:build_heading(null, false)}
<ul class="nav nav-tabs">
<li class="active"><a href="#one" data-toggle="tab">概括</a></li>
</ul>
</div>
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="row">
<div class="col-sm-3 col-xs-6">
<div class="sm-st clearfix">
<a href="javascript:;" data-title="">
<span class="sm-st-icon st-red"><i class="fa fa-warning"></i></span>
<div class="sm-st-info">
<span>{$todaytimes}</span>
今日攻击(次)
</div>
</a>
</div>
</div>
<div class="col-sm-3 col-xs-6">
<div class="sm-st clearfix">
<a href="javascript:;" data-title="攻击来源(IP)">
<span class="sm-st-icon st-violet"><i class="fa fa-users"></i></span>
<div class="sm-st-info">
<span>{$todayips}</span>
今日IP
</div>
</a>
</div>
</div>
<div class="col-sm-3 col-xs-6">
<div class="sm-st clearfix">
<a href="javascript:;" data-title="">
<span class="sm-st-icon st-violet"><i class="fa fa-warning"></i></span>
<div class="sm-st-info">
<span>{$totaltimes}</span>
总攻击(次)
</div>
</a>
</div>
</div>
<div class="col-sm-3 col-xs-6">
<div class="sm-st clearfix">
<a href="javascript:;" data-title="攻击来源(IP)">
<span class="sm-st-icon st-green"><i class="fa fa-users"></i></span>
<div class="sm-st-info">
<span>{$totalips}</span>
总共(IP)
</div>
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div id="echart" style="height:400px;width:100%;"></div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="box box-info">
<div class="box-header"><h3 class="box-title">攻击排行</h3></div>
<div class="box-body" style="padding-top:0;">
<table class="table table-striped">
<tbody>
{foreach rankingips as $v}
<tr>
<td width="140">{$v['count']}(次)</td>
<td>{$v['ip']}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="box box-info">
<div class="box-header"><h3 class="box-title">今日攻击排行</h3></div>
<div class="box-body" style="padding-top:0;">
<table class="table table-striped">
<tbody>
{foreach $todayranking as $v}
<tr>
<td width="140">{$v['count']}(次)</td>
<td>{$v['ip']}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="two">
<div class="row">
<div class="col-xs-12">
{:__('Custom zone')}
</div>
</div>
</div>
</div>
</div>
<script>
var data = {
column: {:json_encode(array_keys($totallist))},
totallist: {:json_encode(array_values($totallist))},
};
</script>

View File

@ -0,0 +1,24 @@
<div class="panel panel-default panel-intro">
<div class="panel-heading">
<div class="panel-heading"></div>
</div>
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
{:build_toolbar('refresh')}
</div>
<table id="table" class="table table-striped table-bordered table-hover"
data-operate-black="{:$auth->check('webscan/webscanlog/black')}" >
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -40,7 +40,22 @@ class Demo extends Api
*/
public function test()
{
$this->success('返回成功', $this->request->param());
$deepseek_pool = config("site.deepseek_pool");
//得到池里的链接数量
$pool_count = count($deepseek_pool);
// //随机获取一个链接
// $pool_index = array_rand($deepseek_pool);
// $url = $deepseek_pool[$pool_index];
// $url = $url .$deepseek_local_url;
//通过cache计数当前请求数
$pool_index = cache('deepseek_pool_index') ?? 0;
$pool_index = $pool_index % $pool_count;
// var_dump($pool_index);
cache('deepseek_pool_index', $pool_index + 1);
$url = $deepseek_pool[$pool_index] ;
$this->success('返回成功', $url);
}
/**

View File

@ -5,6 +5,8 @@ namespace app\api\controller\deepseek;
use app\common\model\deepseek\Question;
use app\common\model\deepseek\Stream;
use app\common\controller\Api;
use bw\UrlLock;
class Deepseek extends Api{
protected $noNeedLogin = ['*'];
@ -44,7 +46,7 @@ class Deepseek extends Api{
*/
public function question(){
$this->setUrlLock("all","deepseek-question");
// $this->setUrlLock("all","deepseek-question");
$messages = $this->request->post('messages/s',"");
$messages = htmlspecialchars_decode($messages);
@ -62,17 +64,71 @@ class Deepseek extends Api{
// }
try {
// $lock = new UrlLock("all","deepseek-question",600,"您的请求过于频繁请您稍后再试请求最大锁定间隔120秒/一次!");
// $lock->setLockNum(config("site.deepseek_token_number") ?: 1)->lock();
$res = $this->model->question($messages,$user_id,$key,$session_key,false);
} catch (\Exception $e){
// $lock->free();
// Log::log($e->getMessage());
$this->error($e->getMessage(),['errcode'=>$e->getCode()]);
}
// $lock->free();
$this->success('请求成功,请求结果请轮询查询结果接口', ['detail' => $res]);
}
/**
* @ApiTitle(deepseek提问测试)
* @ApiSummary(deepseek提问测试)
* @ApiMethod(POST)
* @ApiParams(name = "messages", type = "string",required=true,description = "提问数组 {'role': 'user', 'content': '中原算力是一个什么样的公司'},{'role': 'assistant', 'content': '</think>'} ")
* @ApiParams(name = "key", type = "string",required=true,description = "单次提问唯一key")
* @ApiParams(name = "session_key", type = "string",required=false,description = "会话唯一key:不传默认取当前日期")
* @ApiReturn({
*
*})
*/
public function questiontest(){
// $this->setUrlLock("all","deepseek-question");
$messages = $this->request->post('messages/s',"");
$messages = htmlspecialchars_decode($messages);
$key = $this->request->post('key/s',"");
$session_key = $this->request->post('session_key/s',"");
//不传默认取当前日期
if(!$session_key) $session_key = date("Ymd");
// $messages = "[{'role': 'user', 'content': '中原算力是一个什么样的公司'},{'role': 'assistant', 'content': '</think>'}]";
// var_dump($messages);
$user_id = 0;
$user = $this->auth->getUser();//登录用户
if($user)$user_id = $user['id'];
// if(empty($id)){
// $this->error(__('缺少必要参数'));
// }
try {
// $lock = new UrlLock("all","deepseek-question",600,"您的请求过于频繁请您稍后再试请求最大锁定间隔120秒/一次!");
// $lock->setLockNum(config("site.deepseek_token_number") ?: 1)->lock();
$res = $this->model->questiontest($messages,$user_id,$key,$session_key,false);
} catch (\Exception $e){
// $lock->free();
// Log::log($e->getMessage());
$this->error($e->getMessage(),['errcode'=>$e->getCode()]);
}
// $lock->free();
$this->success('请求成功,请求结果请轮询查询结果接口', ['detail' => $res]);
}
/**
* @ApiTitle( 输出结果查询接口-list为空数组为在思考中)
* @ApiSummary(输出结果查询接口)

View File

@ -75,11 +75,13 @@ class Question extends BaseModel
$self = self::where('key', $key)->find();
if($self) throw new \Exception('key已被消耗');
$q_json = $messages;
//解析json成属数组
$messages = json_decode($messages, true);
if (!$messages) throw new \Exception('json解析失败');
//messages的json格式为{"role": "user", "content": "中原算力是一个什么样的公司"},{"role": "assistant", "content": "</think>"}
//messages得到最后一个role名为assistant的对象并向上一个role为user的对象
//jiao
//得到官网请求地址
$deepseek_web_url = config("site.deepseek_web_url");
@ -94,6 +96,7 @@ class Question extends BaseModel
$deepseek_pool = config("site.deepseek_pool");
//重置下标
$deepseek_pool = array_values($deepseek_pool);
$deepseek_str_suffix = config("site.deepseek_str_suffix");
$reasoning =true;
$last = end($messages);
@ -116,9 +119,25 @@ class Question extends BaseModel
//得到限制的问题关联数
$deepseek_level_num = config("site.deepseek_level_num");
if($deepseek_level_num>0){
//问题采用 :问 或 问答问 或 问答问答问 即2(n-1)+1
//当$messages的问答数组超出2(n-1)+1则截断前面的问题
$deepseek_level_num = (($deepseek_level_num-1) *2) + 1;
$messages = array_slice($messages, -$deepseek_level_num);
}
//访问deepseek脚本存在超时可能会超时所以需要设置超时时间为10分钟
set_time_limit(6000);
//脚本内存设置成很大
// ini_set('memory_limit', '1024M');
//组转json请求参数准备请求deepseekapi {
//"model": "qwen",
@ -128,13 +147,59 @@ class Question extends BaseModel
//"stream": true,
//"temperature":0.6
//}
$format = $messages;
//将最后一条移出数组$format当中的content拼上$deepseek_str_suffix再放回去
$lastt = array_pop($format);
// if ($lastt['role'] == 'assistant') {
// $lastt = prev($format);
// if ($lastt['role'] != 'user') {
// throw new \Exception('messages参数错误');
// }
// }
$lastt['content'] = $lastt['content'] . $deepseek_str_suffix;
array_push($format, $lastt);
//卸背包问题处理
//允许的$format的总长度
$deepseek_head_max = config("site.deepseek_head_max");//单位b 通常是1024b即1kb
if($deepseek_head_max >0){
// 计算当前$format的总长度
$current_length = 0;
foreach ($format as $message) {
$current_length += mb_strlen($message['content']);
}
// 如果$format的总长度大于允许的总长度则截断$format队列最开始的一问加一答2项
// 每次截取完都重新判断大小,直到$format的总长度小于等于允许的总长度才不再截取
while ($current_length > $deepseek_head_max && count($format) >= 3) {
// 截断最开始的一问加一答
array_splice($format, 0, 2);
// 重新计算当前$format的总长度
$current_length = 0;
foreach ($format as $message) {
$current_length += mb_strlen($message['content']);
}
}
}
$data = [
'model' => $deepseek_switch ? 'deepseek-r1':'deepseek-reasoner',
'messages' => $messages,
'messages' => $format,
'stream' => true,
'temperature' => 0.6,
];
$q_json = json_encode($format, JSON_UNESCAPED_UNICODE);
// var_dump($data);die;
@ -146,17 +211,7 @@ class Question extends BaseModel
$res = true;
try {
//保存本次请求model
$datas = [
'user_id' => $user_id,
'content' => $last["content"],
"key"=>$key,
"session_key" => $session_key,
];
//保存数据库并得自增id
$self = self::create($datas);
// var_dump($self);die;
$question_id = $self->id;
// 判断逻辑
@ -188,6 +243,27 @@ class Question extends BaseModel
// $url = $deepseek_switch ? $deepseek_local_url : $deepseek_web_url;
$headers = $deepseek_switch ? [] : ['Authorization:' . $deepseek_key];
$deepseek_limit_str = config("site.deepseek_limit_str");
//$last["content"] 的unicode字符数不能超过 $deepseek_limit_str
if (mb_strlen($last["content"]) > $deepseek_limit_str) {
throw new \Exception('请求内容超过限制字符数!');
}
//保存本次请求model
$datas = [
'user_id' => $user_id,
'content' => $last["content"],
"key"=>$key,
"session_key" => $session_key,
"ip_addr" =>$url,
"q_json" => $q_json
];
//保存数据库并得自增id
$self = self::create($datas);
// var_dump($self);die;
$question_id = $self->id;
// file_put_contents("test.txt",$messages."\r\n",FILE_APPEND);
// 发送curl post json格式的 stream式请求
$ch = curl_init();
@ -198,9 +274,9 @@ class Question extends BaseModel
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(['Content-Type: application/json'], $headers));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$response,$question_id,$deepseek_switch,&$reasoning,$session_key,$url) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$response,$question_id,$deepseek_switch,&$reasoning,$session_key,$url,$self) {
// var_dump($data);
file_put_contents("test.txt",$url.$data."\r\n",FILE_APPEND);
// file_put_contents("test.txt",$url.$data."\r\n",FILE_APPEND);
// if (!$data) {
// return 0; // 结束流式请求
// }
@ -225,6 +301,9 @@ class Question extends BaseModel
$chunk = json_decode($json, true);
if(isset($chunk['error']) && $chunk['error']){
$self["err_msg"] = $chunk['error'];
$self->save();
throw new \Exception($chunk['error']);
}
@ -239,7 +318,7 @@ class Question extends BaseModel
// 将数据写入数据库
// 假设有一个方法 saveStreamData 用于保存数据
$this->saveStreamData($chunk['choices'][0]['delta']['reasoning_content'],$chunk,$question_id,true,$session_key);
$this->saveStreamData($chunk['choices'][0]['delta']['reasoning_content'],$chunk,$question_id,true,$session_key,$url);
}
if (isset($chunk['choices'][0]['delta']['content'])) {
@ -251,7 +330,7 @@ class Question extends BaseModel
if(strpos($chunk['choices'][0]['delta']['content'],'</think>')!==false){
$reasoning = false;
}elseif(strpos($chunk['choices'][0]['delta']['content'],'<think>')===false){
$this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,$reasoning,$session_key);
$this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,$reasoning,$session_key,$url);
}
}else{
@ -260,7 +339,7 @@ class Question extends BaseModel
// 将数据写入数据库
// 假设有一个方法 saveStreamData 用于保存数据
$this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,false,$session_key);
$this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,false,$session_key,$url);
}
@ -271,6 +350,9 @@ class Question extends BaseModel
$chunk = json_decode($line, true);
if($chunk && isset($chunk['error']) && $chunk['error']){
$self["err_msg"] = $chunk['error'];
$self->save();
throw new \Exception($chunk['error']);
}
@ -295,6 +377,298 @@ class Question extends BaseModel
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
$self["err_msg"] = $curl_error;
$self->save();
throw new \Exception('Curl error: ' . $curl_error);
}
if ($trans) {
self::commitTrans();
}
} catch (\Exception $e) {
if ($trans) {
self::rollbackTrans();
}
throw new \Exception($e->getMessage() . $e->getFile() . $e->getLine());
}
return $res;
}
public function questiontest($messages, $user_id, $key,$session_key,$trans = false)
{
if (!$messages) throw new \Exception('缺少必要参数1');
if (!$key) throw new \Exception('缺少必要参数2');
$self = self::where('key', $key)->find();
if($self) throw new \Exception('key已被消耗');
$q_json = $messages;
//解析json成属数组
$messages = json_decode($messages, true);
if (!$messages) throw new \Exception('json解析失败');
//messages的json格式为{"role": "user", "content": "中原算力是一个什么样的公司"},{"role": "assistant", "content": "</think>"}
//messages得到最后一个role名为assistant的对象并向上一个role为user的对象
//得到官网请求地址
$deepseek_web_url = config("site.deepseek_web_url");
//得到官网请求密钥
$deepseek_key = "Bearer " . config("site.deepseek_key");
//得到本地部署的请求地址
$deepseek_local_url = config("site.deepseek_local_url");
//得到切换开关
$deepseek_switch = config("site.deepseek_switch");
//请求池
$deepseek_pool = config("site.deepseek_pool");
//重置下标
$deepseek_pool = array_values($deepseek_pool);
$deepseek_str_suffix = config("site.deepseek_str_suffix");
$reasoning =true;
$last = end($messages);
if ($last['role'] == 'assistant') {
$last = prev($messages);
if ($last['role'] != 'user') {
throw new \Exception('messages参数错误');
}
}
//默认截断下标为末尾
$cut = count($messages);
//遍历素$messages如果其中有一项assistant的content为空则从这一项assistant在内截断不要了
foreach ($messages as $keys => $message){
if ($message['role'] == 'assistant' && !$message['content']) {
$cut = $keys;
break;
}
}
$messages = array_slice($messages, 0, $cut);
//得到限制的问题关联数
$deepseek_level_num = config("site.deepseek_level_num");
if($deepseek_level_num>0){
//问题采用 :问 或 问答问 或 问答问答问 即2(n-1)+1
//当$messages的问答数组超出2(n-1)+1则截断前面的问题
$deepseek_level_num = (($deepseek_level_num-1) *2) + 1;
$messages = array_slice($messages, -$deepseek_level_num);
}
//访问deepseek脚本存在超时可能会超时所以需要设置超时时间为10分钟
set_time_limit(6000);
//组转json请求参数准备请求deepseekapi {
//"model": "qwen",
//"messages": [
//{"role": "user", "content": "中原算力是一个什么样的公司"},{"role": "assistant", "content": "</think>"}
//],
//"stream": true,
//"temperature":0.6
//}
$format = $messages;
//将最后一条取出来当中的content拼上$deepseek_str_suffix再放回去
$lastt = end($format);
// if ($lastt['role'] == 'assistant') {
// $lastt = prev($format);
// if ($lastt['role'] != 'user') {
// throw new \Exception('messages参数错误');
// }
// }
$lastt['content'] = $lastt['content'] . $deepseek_str_suffix;
array_push($format, $lastt);
$data = [
'model' => $deepseek_switch ? 'deepseek-r1':'deepseek-reasoner',
'messages' => $format,
'stream' => true,
'temperature' => 0.6,
];
$q_json = json_encode($format, JSON_UNESCAPED_UNICODE);
// var_dump($data);die;
//判断逻辑
if ($trans) {
self::beginTrans();
}
$res = true;
try {
// 判断逻辑
if($deepseek_switch) {
//本地逻辑
//得到池里的链接数量
$pool_count = count($deepseek_pool);
if ($pool_count == 0) {
throw new \Exception('链接池为空!');
}
// //随机获取一个链接
// $pool_index = array_rand($deepseek_pool);
// $url = $deepseek_pool[$pool_index];
// $url = $url .$deepseek_local_url;
//通过cache计数当前请求数
$pool_index = cache('deepseek_pool_index') ?? 0;
$pool_index = $pool_index % $pool_count;
// var_dump($pool_index);
cache('deepseek_pool_index', $pool_index + 1);
$url = $deepseek_pool[$pool_index] . $deepseek_local_url;
}else{
//远程逻辑
$url = $deepseek_web_url;
}
// $url = $deepseek_switch ? $deepseek_local_url : $deepseek_web_url;
$headers = $deepseek_switch ? [] : ['Authorization:' . $deepseek_key];
$deepseek_limit_str = config("site.deepseek_limit_str");
//$last["content"] 的unicode字符数不能超过 $deepseek_limit_str
if (mb_strlen($last["content"]) > $deepseek_limit_str) {
throw new \Exception('请求内容超过限制字符数!');
}
//保存本次请求model
$datas = [
'user_id' => $user_id,
'content' => $last["content"],
"key"=>$key,
"session_key" => $session_key,
"ip_addr" =>$url,
"q_json" => $q_json
];
//保存数据库并得自增id
// $self = self::create($datas);
//// var_dump($self);die;
// $question_id = $self->id;
$question_id = 0;
// file_put_contents("test.txt",$messages."\r\n",FILE_APPEND);
// 发送curl post json格式的 stream式请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
//设置等待=时间无限
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(['Content-Type: application/json'], $headers));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$response,$question_id,$deepseek_switch,&$reasoning,$session_key,$url) {
// var_dump($data);
file_put_contents("test2.txt",$url.$data."\r\n",FILE_APPEND);
// var_dump($data);
// if (!$data) {
// return 0; // 结束流式请求
// }
//// var_dump($data);
// $data = htmlspecialchars_decode($data);
// // 处理每一部分数据
//// $lines = explode("\n", trim($data));
// $lines = explode("\n",$data);
//
// if(!$lines){
// throw new \Exception($data);
// }
//
//
// foreach ($lines as $line) {
//
// if (strpos($line, "data: ") === 0) {
//
// $json = substr($line, 6);
// $chunk = json_decode($json, true);
//
// if(isset($chunk['error']) && $chunk['error']){
// throw new \Exception($chunk['error']);
// }
//
//// if(!isset($chunk['choices'][0]['delta']['reasoning_content']) && !isset($chunk['choices'][0]['delta']['content'])){
//// throw new \Exception($data);
//// }
//
//
//
// if (isset($chunk['choices'][0]['delta']['reasoning_content'])) {
// $chunk['choices'][0]['delta']['reasoning_content'] = htmlspecialchars_decode($chunk['choices'][0]['delta']['reasoning_content']);
//
// // 将数据写入数据库
// // 假设有一个方法 saveStreamData 用于保存数据
// $this->saveStreamData($chunk['choices'][0]['delta']['reasoning_content'],$chunk,$question_id,true,$session_key,$url);
// }
//
// if (isset($chunk['choices'][0]['delta']['content'])) {
//
// $chunk['choices'][0]['delta']['content'] = htmlspecialchars_decode($chunk['choices'][0]['delta']['content']);
//
// if($deepseek_switch){
// //本地部署</think>为结尾
// if(strpos($chunk['choices'][0]['delta']['content'],'</think>')!==false){
// $reasoning = false;
// }elseif(strpos($chunk['choices'][0]['delta']['content'],'<think>')===false){
// $this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,$reasoning,$session_key,$url);
// }
//
// }else{
// //官方调用
//
//
// // 将数据写入数据库
// // 假设有一个方法 saveStreamData 用于保存数据
// $this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id,false,$session_key,$url);
//
//
// }
//
// }
// }else{
//// throw new \Exception($data);
// $chunk = json_decode($line, true);
//
// if($chunk && isset($chunk['error']) && $chunk['error']){
// throw new \Exception($chunk['error']);
// }
//
//
//
// }
// }
// 处理每一部分数据
// $json = $data;
// $chunk = json_decode($json, true);
// if (isset($chunk['choices'][0]['delta']['content'])) {
// // 将数据写入数据库
// // 假设有一个方法 saveStreamData 用于保存数据
// $this->saveStreamData($chunk['choices'][0]['delta']['content'],$chunk,$question_id);
// }
return strlen($data);
});
curl_exec($ch);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
throw new \Exception('Curl error: ' . $curl_error);
}
@ -314,7 +688,7 @@ class Question extends BaseModel
}
// 假设有一个方法 saveStreamData 用于保存数据
protected function saveStreamData($content,$chunk,$question_id,$reasoning = false,$session_key)
protected function saveStreamData($content,$chunk,$question_id,$reasoning = false,$session_key="",$url="")
{
// 实现保存数据到数据库的逻辑
$data = [
@ -327,7 +701,8 @@ class Question extends BaseModel
"choices_json" => isset($chunk["choices"])? htmlspecialchars_decode(json_encode($chunk["choices"])): '',
"content" => $content,
"reasoning" => $reasoning,
"session_key" =>$session_key
"session_key" =>$session_key,
// "ip_addr" => $url,
];
Stream::create($data);

View File

@ -13,6 +13,8 @@ class Index extends Frontend
public function index()
{
//重定向到 域名/deepseek/#/
$this->redirect('/deepseek/#/');
return $this->view->fetch();
}

View File

@ -36,20 +36,30 @@ class UrlLock
private $is_lock = false;
//令牌数量
private $lock_num = 1;
public function __construct($lock_key=null,$lock_suffix="",$time_out=null,$err_msg=null)
public function __construct($lock_key=null,$lock_suffix="",$time_out=null,$err_msg=null,$lock_num=1)
{
if($lock_key)$this->lock_key = $lock_key;
if($lock_suffix)$this->lock_suffix = $lock_suffix;
if($lock_num)$this->lock_num = $lock_num;
if($time_out)$this->time_out = $time_out;
if($err_msg)$this->setCacheLockErrorMsg($err_msg);
}
//设置令牌数量
public function setLockNum($num){
$this->lock_num = $num;
return $this;
}
public function lock($bool=false){
try{
$this->getLock($this->lock_key,$this->lock_suffix,$this->time_out);
$this->getLock($this->lock_key,$this->lock_suffix,$this->time_out,$this->lock_num);
}catch (\Exception|\Throwable $e){
$this->is_lock = false;
if($bool){
@ -64,7 +74,7 @@ class UrlLock
public function free($bool=false){
try{
$this->freeLock($this->lock_key,$this->lock_suffix);
$this->freeLock($this->lock_key,$this->lock_suffix,$this->lock_num,$this->time_out);
}catch (\Exception|\Throwable $e){
if($bool){
return false;

View File

@ -72,25 +72,12 @@ trait CacheTrait
public function getLock($key,$suffix = "-lock-suffix", $timeout = 120){
public function getLock($key, $suffix = "-lock-suffix", $timeout = 120, $lock_num = 1) {
$this->hasRedis(true);
$redis = $this->getRedis();
$hashKey = $key . $suffix;
// $redis->del($hashKey);
// var_dump($hashKey);die;
//判断锁是否存在,如果存在,返回错误
// if ($redis->exists($hashKey)){
//// var_dump(111111222);die;
// if($this->cache_lock_thorws_excption){
// throw new \Exception($this->cache_lock_error_msg);
// }else{
// return false;
// }
//
// }
//如果不存在创建锁并返回
// $redis->set($hashKey, 1,$timeout);//注释掉
//替换成原子操作的命令
if ($lock_num == 1) { // 单令牌
$nxLock = $redis->set($hashKey, 1, ['nx', 'ex' => $timeout]);
if ($nxLock == 1) {
return true;
@ -101,8 +88,44 @@ trait CacheTrait
return false;
}
}
} else {
// 多令牌
$lua = 'local hash_key = KEYS[1]
local max_tokens = tonumber(ARGV[3])
local token_value = ARGV[1]
local token_timeout = tonumber(ARGV[2])
-- 获取当前令牌数
local current_value = redis.call("hget", hash_key, "count")
if not current_value then
-- 锁不存在,初始化锁并设置过期时间
redis.call("hset", hash_key, "count", 1)
redis.call("expire", hash_key, token_timeout)
return 1
else
-- 锁存在,增加计数器
local new_value = tonumber(current_value) + 1
if new_value > max_tokens then
return 0
else
redis.call("hset", hash_key, "count", new_value)
redis.call("expire", hash_key, token_timeout)
return new_value
end
end';
$result = $redis->eval($lua, [$hashKey, $timeout, $lock_num], 1);
if ($result >= 1 && $result <= $lock_num) {
return true;
} else {
if ($this->cache_lock_thorws_excption) {
throw new \Exception($this->cache_lock_error_msg);
} else {
return false;
}
}
}
}
@ -117,12 +140,13 @@ trait CacheTrait
}
public function freeLock($key,$suffix = "-lock-suffix"){
public function freeLock($key, $suffix = "-lock-suffix", $lock_num = 1, $timeout = 120) {
$this->hasRedis(true);
$redis = $this->getRedis();
if (!$key || !$suffix) return true;
$hashKey = $key . $suffix;
if ($lock_num == 1) { // 单令牌
// 执行lua脚本确保删除锁是原子操作
$lua = 'if redis.call("get", KEYS[1]) == ARGV[1]
then
@ -133,14 +157,42 @@ trait CacheTrait
$result = $redis->eval($lua, [$hashKey, 1], 1);
if ('1' == $result) {
return true;
}
return false;
} else { // 多令牌
$lua = 'local hash_key = KEYS[1]
local token_value = ARGV[1]
local token_timeout = tonumber(ARGV[2])
// if (!$redis->EXISTS($hashKey)) return true;
// $redis->del($hashKey);
// return true;
-- 获取当前令牌数
local current_value = redis.call("hget", hash_key, "count")
if not current_value then
-- 锁不存在,无需释放
return 0
else
-- 锁存在,减少计数器
local new_value = tonumber(current_value) - 1
if new_value < 1 then
-- 计数器小于1删除锁
local result = redis.call("del", hash_key)
return result
else
-- 计数器大于等于1更新锁
redis.call("hset", hash_key, "count", new_value)
redis.call("expire", hash_key, token_timeout)
return new_value
end
end';
$result = $redis->eval($lua, [$hashKey, $timeout, $lock_num], 1);
if ($result >= 1 && $result <= $lock_num) {
return true;
} else {
return false;
}
}
}
@ -148,5 +200,4 @@ trait CacheTrait
}

View File

@ -0,0 +1,342 @@
define(['jquery', 'bootstrap', 'backend', 'addtabs', 'table', 'echarts', 'echarts-theme', 'template'], function ($, undefined, Backend, Datatable, Table, Echarts, undefined, Template) {
var Controller = {
index: function () {
$(".datetimerange").data('callback', function (start, end) {
loadData(start / 1000, end / 1000);
});
if ($(".datetimerange").size() > 0) {
require(['bootstrap-daterangepicker'], function () {
var ranges = {};
ranges[__('Today')] = [Moment().startOf('day'), Moment().endOf('day')];
ranges[__('Yesterday')] = [Moment().subtract(1, 'days').startOf('day'), Moment().subtract(1, 'days').endOf('day')];
ranges[__('Last 7 Days')] = [Moment().subtract(6, 'days').startOf('day'), Moment().endOf('day')];
ranges[__('Last 30 Days')] = [Moment().subtract(29, 'days').startOf('day'), Moment().endOf('day')];
ranges[__('This Month')] = [Moment().startOf('month'), Moment().endOf('month')];
ranges[__('Last Month')] = [Moment().subtract(1, 'month').startOf('month'), Moment().subtract(1, 'month').endOf('month')];
var options = {
dateLimit: { months: 2 },
timePicker: true,
autoUpdateInput: false,
timePickerSeconds: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY/MM/DD HH:mm:ss',
customRangeLabel: __("Custom Range"),
applyLabel: __("Apply"),
cancelLabel: __("Clear"),
},
ranges: ranges,
};
var origincallback = function (start, end) {
$(this.element).val(start.format(this.locale.format) + " - " + end.format(this.locale.format));
$(this.element).trigger('blur');
};
$(".datetimerange").each(function () {
var callback = typeof $(this).data('callback') == 'function' ? $(this).data('callback') : origincallback;
$(this).on('apply.daterangepicker', function (ev, picker) {
origincallback.call(picker, picker.startDate, picker.endDate);
});
$(this).on('cancel.daterangepicker', function (ev, picker) {
$(this).val('').trigger('blur');
});
$(this).daterangepicker($.extend(true, options, $(this).data()), callback);
});
});
}
//datafilter
$(document).on('click', '.datefilter .btn', function (e) {
var type = $(this).data('type');
var endDate = Moment().unix();
if (type == 1) {
var startDate = Moment().subtract(15, 'minutes').unix();
}
if (type == 2) {
var startDate = Moment().subtract(30, 'minutes').unix();
}
if (type == 3) {
var startDate = Moment().subtract(1, 'hours').unix();
}
if (type == 4) {
var startDate = Moment().subtract(4, 'hours').unix();
}
if (type == 5) {
var startDate = Moment().subtract(12, 'hours').unix();
}
if (type == 6) {
var startDate = Moment().subtract(1, 'days').unix();
}
loadData(startDate, endDate);
});
var codeChart = Echarts.init(document.getElementById('code-chart'), 'dark');
var codeoption = {
title: {
text: '请求状态码',
left: 'left'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: ['a', 'b', 'c'],
},
series: [
{
name: '请求状态码',
type: 'pie',
radius: '55%',
center: ['40%', '50%'],
data: [{ 'name': 'a', value: 1 }, { 'name': 'b', value: 2 }, { 'name': 'c', value: 3 }],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
codeChart.setOption(codeoption);
var timeChart = Echarts.init(document.getElementById('time-chart'), 'dark');
var timeoption = {
title: {
text: '请求处理时间(ms)',
left: 'left'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: ['a', 'b', 'c'],
},
series: [
{
name: '请求处理时间(ms)',
type: 'pie',
radius: '55%',
center: ['40%', '50%'],
data: [{ 'name': 'a', value: 1 }, { 'name': 'b', value: 2 }, { 'name': 'c', value: 3 }],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
timeChart.setOption(timeoption);
var requestChart = Echarts.init(document.getElementById('request-chart'), 'dark');
var requestoption = {
title: {
text: '最多请求 TOP15',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
yAxis: {
inverse: true,
type: 'category',
data: ['巴西', '印尼', '美国', '印度', '中国', '世界人口(万)']
},
series: [
{
name: '次数',
type: 'bar',
data: [18203, 23489, 29034, 104970, 131744, 630230]
}
]
};
requestChart.setOption(requestoption);
var errorChart = Echarts.init(document.getElementById('error-chart'), 'dark');
var erroroption = {
title: {
text: '请求错误 TOP15',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
yAxis: {
inverse: true,
type: 'category',
data: ['巴西', '印尼', '美国', '印度', '中国', '世界人口(万)']
},
series: [
{
name: '次数',
type: 'bar',
data: [18203, 23489, 29034, 104970, 131744, 630230]
}
]
};
errorChart.setOption(erroroption);
var fastChart = Echarts.init(document.getElementById('fast-chart'), 'dark');
var fastoption = {
title: {
text: '平均处理时间最快 TOP15',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
yAxis: {
inverse: true,
type: 'category',
data: ['巴西', '印尼', '美国', '印度', '中国', '世界人口(万)']
},
series: [
{
name: '耗时',
type: 'bar',
data: [18203, 23489, 29034, 104970, 131744, 630230]
}
]
};
fastChart.setOption(fastoption);
var slowChart = Echarts.init(document.getElementById('slow-chart'), 'dark');
var slowoption = {
title: {
text: '平均处理时间最慢 TOP15',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
yAxis: {
inverse: true,
type: 'category',
data: ['巴西', '印尼', '美国', '印度', '中国', '世界人口(万)']
},
series: [
{
name: '耗时',
type: 'bar',
data: [18203, 23489, 29034, 104970, 131744, 630230]
}
]
};
slowChart.setOption(slowoption);
var loadData = function (start, end) {
$('#createtime').val( Moment(start*1000).format('YYYY/MM/DD HH:mm:ss') + ' - ' + Moment(end*1000).format('YYYY/MM/DD HH:mm:ss'));
$.post('apilog/data/index', { start: start, end: end }, function (res) {
$('#bs_request').text(res.base.count_request);
$('#bs_time').text(res.base.avg_time.toFixed(2));
$('#bs_404').text(res.base.count_404);
$('#bs_500').text(res.base.count_500);
$('#bs_error').text((res.base.error_rank * 100).toFixed(2) + '%');
$('#bs_api').text(res.base.count_api);
codeoption.legend.data = res.code.x;
codeoption.series[0].data = res.code.kv;
codeChart.setOption(codeoption);
timeoption.legend.data = res.time.x;
timeoption.series[0].data = res.time.kv;
timeChart.setOption(timeoption);
requestoption.yAxis.data = res.requesttop.x;
requestoption.series[0].data = res.requesttop.y;
requestChart.setOption(requestoption);
erroroption.yAxis.data = res.errortop.x;
erroroption.series[0].data = res.errortop.y;
errorChart.setOption(erroroption);
fastoption.yAxis.data = res.fasttop.x;
fastoption.series[0].data = res.fasttop.y;
fastChart.setOption(fastoption);
slowoption.yAxis.data = res.slowtop.x;
slowoption.series[0].data = res.slowtop.y;
slowChart.setOption(slowoption);
})
};
loadData(Moment().startOf('day').unix(), Moment().endOf('day').unix());
}
};
return Controller;
});

View File

@ -0,0 +1,128 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
// 初始化表格参数配置
Table.api.init({
extend: {
index_url: 'apilog/index' + location.search,
del_url: 'apilog/index/del',
multi_url: 'apilog/index/multi',
table: 'wx_apilog',
}
});
var table = $("#table");
// 初始化表格
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
columns: [
[
{ checkbox: true },
{ field: 'id', title: __('Id') },
{ field: 'username', title: __('UserName'), formatter: Table.api.formatter.search },
{ field: 'url', title: __('Url'), formatter: Table.api.formatter.url },
{
field: 'method', title: __('Method'),
searchList: { "GET": "GET", "POST": "POST", "PUT": "PUT", "DELETE": "DELETE" },
formatter: Table.api.formatter.normal
},
{
field: 'ip', title: __('Ip'), formatter: function (value, row, index) {
var html = '<a class="btn btn-xs btn-ip bg-success" data-toggle="tooltip" data-original-title="点击搜索' + value + '"><i class="fa fa-map-marker"></i> ' + value + '</a>';
if (row.banip == false)
html += '<a class="btn btn-xs btn-dialog btn-banip" data-status=0><i class="fa fa-toggle-on" data-toggle="tooltip" data-original-title="点击禁止该IP访问"></i></a>';
else {
html += '<a class="btn btn-xs btn-dialog btn-banip" data-status=1><i class="fa fa-toggle-off" data-toggle="tooltip" data-original-title="点击允许该IP访问"></i></a>';
}
return html;
},
events: Controller.api.events.ip
},
{
field: 'ua', title: __('Ua'), formatter: function (value, row, index) {
return '<a class="btn btn-xs btn-browser">' + ((!value) ? '' : (value.split(" ")[0])) + '</a>';
},
events: Controller.api.events.browser
},
{ field: 'controller', title: __('Controller') },
{ field: 'action', title: __('Action') },
{ field: 'time', title: __('Time'), sortable: true },
{ field: 'code', title: __('Code'), formatter: Table.api.formatter.search },
{
field: 'createtime', title: __('Createtime'), operate: 'RANGE', sortable: true, addclass: 'datetimerange',
formatter: Table.api.formatter.datetime
},
{
field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate,
buttons: [{
name: 'detail',
text: __('Detail'),
icon: 'fa fa-list',
classname: 'btn btn-info btn-xs btn-detail btn-dialog',
url: 'apilog/index/detail'
}
],
}
]
]
});
// 为表格绑定事件
Table.api.bindevent(table);
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
},
events: {
ip: {
'click .btn-ip': function (e, value, row, index) {
e.stopPropagation();
var container = $("#table").data("bootstrap.table").$container;
$("form.form-commonsearch [name='ip']", container).val(value);
$("form.form-commonsearch", container).trigger('submit');
},
'click .btn-banip': function (e, value, row, index) {
e.stopPropagation();
if (row.banip == false)
layer.prompt({ title: '请输入封禁时长(分钟),0为永久封禁', value: '0' }, function (text, index) {
layer.close(index);
$.post('apilog/index/banip', { status: 0, ip: value, time: text }, function (res) {
if (res.code == 1) {
$('#table').bootstrapTable('refresh');
}
})
});
else {
$.post('apilog/index/banip', { status: 1, ip: value }, function (res) {
if (res.code == 1) {
$('#table').bootstrapTable('refresh');
}
})
}
}
},
browser: {
'click .btn-browser': function (e, value, row, index) {
e.stopPropagation();
var container = $("#table").data("bootstrap.table").$container;
$("form.form-commonsearch [name='ua']", container).val(value);
$("form.form-commonsearch", container).trigger('submit');
}
},
}
}
};
return Controller;
});

View File

@ -0,0 +1,160 @@
define(['jquery', 'bootstrap', 'backend', 'addtabs', 'table', 'echarts', 'echarts-theme', 'template'], function ($, undefined, Backend, Datatable, Table, Echarts, undefined, Template) {
var Controller = {
index: function () {
var countmChart = Echarts.init(document.getElementById('count-m-chart'), 'dark');
var countmoption = {
title: {
text: '每分钟请求次数',
left: 'left'
},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}]
};
countmChart.setOption(countmoption);
var timemChart = Echarts.init(document.getElementById('time-m-chart'), 'dark');
var timemoption = {
title: {
text: '每分钟平均处理时间(ms)',
left: 'left'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}]
};
timemChart.setOption(timemoption);
var counthChart = Echarts.init(document.getElementById('count-h-chart'), 'dark');
var counthoption = {
title: {
text: '每小时请求次数',
},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}]
};
counthChart.setOption(counthoption);
var timehChart = Echarts.init(document.getElementById('time-h-chart'), 'dark');
var timehoption = {
title: {
text: '每小时平均处理时间(ms)',
},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}]
};
timehChart.setOption(timehoption);
var countdChart = Echarts.init(document.getElementById('count-d-chart'), 'dark');
var countdoption = {
title: {
text: '每天请求次数',
},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(220, 220, 220, 0.8)'
}
}]
};
countdChart.setOption(countdoption);
var loadData = function () {
$.post('apilog/data/qushi', null, function (res) {
countmoption.xAxis.data = res.count_m.x;
countmoption.series[0].data = res.count_m.y;
countmChart.setOption(countmoption);
timemoption.xAxis.data = res.time_m.x;
timemoption.series[0].data = res.time_m.y;
timemChart.setOption(timemoption);
counthoption.xAxis.data = res.count_h.x;
counthoption.series[0].data = res.count_h.y;
counthChart.setOption(counthoption);
timehoption.xAxis.data = res.time_h.x;
timehoption.series[0].data = res.time_h.y;
timehChart.setOption(timehoption);
countdoption.xAxis.data = res.count_d.x;
countdoption.series[0].data = res.count_d.y;
countdChart.setOption(countdoption);
})
};
loadData();
}
};
return Controller;
});

View File

@ -1,4 +1,4 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
define(['jquery', 'bootstrap', 'backend', 'csmtable', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
@ -22,18 +22,69 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
// asyndownload: true,
columns: [
[
{checkbox: true},
{field: 'id', title: __('Id')},
{field: 'user_id', title: __('User_id')},
{field: 'content', title: __('Content'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'endtime', title: __('Endtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'user.nickname', title: __('User.nickname'), operate: 'LIKE'},
{field: 'user.mobile', title: __('User.mobile'), operate: 'LIKE'},
{field: 'user.avatar', title: __('User.avatar'), operate: 'LIKE', events: Table.api.events.image, formatter: Table.api.formatter.image},
{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
{field: 'id' ,sortable:true, title: __('Id')},
// {field: 'ip_addr', title: __('请求地址')},
{field: 'key', title: __('提问key'),visible:false},
{field: 'reasoning', title: __('是否思考'),formatter:function (value, row, index) {
//去掉空格,换行
row.reasoning_text = row.reasoning_text.replace(/\s+/g, '');
if(row.reasoning_text){
return '<span class="label label-success">是</span>';
} else{
return '<span class="label label-danger">否</span>';
}
}},
{field: 'answer', operate: false,title: __('是否回答'),formatter:function (value, row, index) {
if(row.answer_text){
return '<span class="label label-success">是</span>';
} else{
return '<span class="label label-danger">否</span>';
}
}},
{field: 'err_msg',operate: 'LIKE', title: __('是否报错'),formatter:function (value, row, index) {
if(!row.err_msg){
return '<span class="label label-success">无</span>';
} else{
return '<span class="label label-danger">是</span>';
}
}},
{field: 'ip_addr', title: __('应答地址'),visible:false},
{field: 'user_id', title: __('User_id'),visible:false},
{field: 'content', title: __('Content') + "(双击查看详情)", operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'reasoning_text', title: __('思考'), operate: false,visible:false},
{field: 'answer_text', title: __('回答'), operate: false,visible:false},
{field: 'createtime',sortable:true, title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
// {field: 'endtime', title: __('Endtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'user.nickname', title: __('User.nickname'), operate: 'LIKE' ,visible:false},
{field: 'user.mobile', title: __('User.mobile'), operate: 'LIKE' ,visible:false},
{field: 'user.avatar', title: __('User.avatar'), operate: 'LIKE' ,visible:false, events: Table.api.events.image, formatter: Table.api.formatter.image},
// {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
{field: 'operate', title: __('Operate'), table: table , buttons: [
{
name: 'order',
text: __('流式应答记录'),
title: __('流式应答记录'),
classname: 'btn btn-xs btn-primary btn-dialog',
icon: 'fa fa-list',
url: order_url,
callback: function (data) {
},
// visible: function (row) {
// return row.status == '2'||row.status == '3';
// }
},
], events: Table.api.events.operate, formatter: Table.api.formatter.operate},
]
]
});
@ -53,5 +104,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
}
};
var order_url = function (row,dom) {
return 'deepseek/stream/index?question_id='+row.id;
}
return Controller;
});

View File

@ -25,15 +25,23 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
columns: [
[
{checkbox: true},
{field: 'id', title: __('Id')},
{field: 'question_id', title: __('Question_id')},
{field: 'id',sortable:true, title: __('Id')},
{field: 'content', title: __('本次应答token'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'session_key', title: __('会话id'),visible:false},
//,formatter:function (value, row, index) {
// switch (row.error_status) {
//
{field: 'reasoning', title: __('是否是思考')},
{field: 'question_id', title: __('Question_id'),visible:false},
{field: 'assistant_id', title: __('Assistant_id'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'object', title: __('Object'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'created_time', title: __('Created_time'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'created_time',sortable:true, title: __('Created_time'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'model', title: __('Model'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'question.user_id', title: __('Question.user_id')},
{field: 'question.content', title: __('Question.content'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'createtime',sortable:true, title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'question.user_id', title: __('Question.user_id'),visible:false},
// {field: 'question.content', title: __('Question.content'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
]
]

View File

@ -0,0 +1,97 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts'], function ($, undefined, Backend, Table, Form,Echarts) {
var Controller = {
index: function () {
// 初始化表格参数配置
Table.api.init({
extend: {
"index_url": "webscan/verifies/index",
"add_url": "",
"edit_url": "",
"del_url": "",
"multi_url": "",
}
});
var table = $("#table");
var method={'local':'本地','official':'官方'};
// 初始化表格
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pagination: false,
commonSearch: false,
search: false,
columns: [
[
{checkbox: true},
{field: 'id', title: __('序号'),formatter:function (value,row,index) {
row.id=index;
return index;
}},
{field: 'method', title: __('校验类型'),formatter:function (value,row,index) {
return method[value];
}},
{field: 'md5', title: __('MD5')},
{field: 'filename', title: __('文件路径')},
{field: 'mktime', title: __('最后更新时间'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true},
{field: 'operate', title: __('Operate'), table: table,buttons: [
{
name: 'cancel',
text: '加入信任',
title: '加入信任',
icon: 'fa fa-exclamation-triangle',
classname: 'btn btn-xs btn-danger btn-ajax',
confirm: '加入信任?',
url: function(row,obj){
return 'webscan/verifies/trust/index/'+row.id;
},
success: function (data, ret) {
table.bootstrapTable('refresh');
}
},{
name: 'edit1',
text: '查看',
title:"查看",
icon: 'fa fa-eye',
classname: 'btn btn-xs btn-success btn-dialog ',
url: function (row) {
return 'webscan/verifies/show/?filename='+row.filename;
}
},], formatter: Table.api.formatter.operate}
]
],
});
// 为表格绑定事件
Table.api.bindevent(table);//当内容渲染完成后
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
},formatter: {//渲染的方法
ip: function (value, row, index) {
return '<a class="btn btn-xs btn-ip bg-success"><i class="fa fa-map-marker"></i> ' + value + '</a>';
},
},
events: {//绑定事件的方法
ip: {
//格式为:方法名+空格+DOM元素
'click .btn-ip': function (e, value, row, index) {
e.stopPropagation();
var container = $("#table").data("bootstrap.table").$container;
var options = $("#table").bootstrapTable('getOptions');
//这里我们手动将数据填充到表单然后提交
$("form.form-commonsearch [name='ip']", container).val(value);
$("form.form-commonsearch", container).trigger('submit');
}
},
}
}
};
return Controller;
});

View File

@ -0,0 +1,116 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts'], function ($, undefined, Backend, Table, Form,Echarts) {
var Controller = {
index: function () {
// 初始化表格参数配置
Table.api.init({
extend: {
"index_url": "webscan/webscanlog/index",
"add_url": "",
"edit_url": "",
"del_url": "",
"multi_url": "",
}
});
var table = $("#table");
// 初始化表格
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
columns: [
[
{field: 'request_url', title:'请求url', operate: 'LIKE'},
{field: 'ip', title: __('IP'), operate: 'LIKE',events: Controller.api.events.ip, formatter: Controller.api.formatter.ip},
{field: 'method', title: __('请求类型')},
{field: 'rkey', title: __('参数')},
{field: 'rdata', title: __('攻击值'), operate: 'LIKE'},
{field: 'user_agent', title: __('user_agent'), operate: 'LIKE'},
{field: 'create_time', title: __('攻击时间'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true},
{field: 'operate', title: __('Operate'), table: table,buttons: [
{
name: 'cancel',
text: '加入黑名单',
title: '加入黑名单',
icon: 'fa fa-exclamation-triangle',
classname: 'btn btn-xs btn-danger btn-ajax',
confirm: '确认加入黑名单?',
url: function(row){
return 'webscan/webscanlog/black/ip/'+row.ip;
},
success: function (data, ret) {
}
}], formatter: Table.api.formatter.operate}
]
],
});
// 为表格绑定事件
Table.api.bindevent(table);//当内容渲染完成后
},
dashboard: function () {
// 基于准备好的dom初始化echarts实例
var myChart = Echarts.init(document.getElementById('echart'), 'walden');
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(setchart(data));
function setchart(data){
// 指定图表的配置项和数据
var option = {
title: {text: '',subtext: ''},
tooltip: {trigger: 'axis'},
legend: {data: ['攻击次数']},
toolbox: {feature: {magicType: {show: true, type: ['stack', 'tiled']},saveAsImage: {show: true}}},
xAxis: {type: 'category',boundaryGap: false,data: data.column},
yAxis: [{type : 'value'}],
grid: [{left: '3%',right: '4%',bottom: '3%',containLabel: true}],
series: [{name: '攻击次数',type: 'line',smooth: true,areaStyle: {normal: {}},lineStyle: { normal: {width: 1.5}},data: data.totallist}]
};
return option;
}
//users_statistics
$(document).on('click', ".statistics", function () {
Backend.api.open($(this).attr("href"),$(this).data("title"),[],'80%','80%');
return false;
});
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
},formatter: {//渲染的方法
ip: function (value, row, index) {
return '<a class="btn btn-xs btn-ip bg-success"><i class="fa fa-map-marker"></i> ' + value + '</a>';
},
},
events: {//绑定事件的方法
ip: {
//格式为:方法名+空格+DOM元素
'click .btn-ip': function (e, value, row, index) {
e.stopPropagation();
var container = $("#table").data("bootstrap.table").$container;
var options = $("#table").bootstrapTable('getOptions');
//这里我们手动将数据填充到表单然后提交
$("form.form-commonsearch [name='ip']", container).val(value);
$("form.form-commonsearch", container).trigger('submit');
}
},
}
}
};
return Controller;
});

225725
public/test.txt Normal file

File diff suppressed because it is too large Load Diff