feat(admin): XHProf 函数监控规则与命中记录

This commit is contained in:
augushong
2026-08-15 07:02:37 +08:00
parent e57b186126
commit c980ebdff0
23 changed files with 901 additions and 1 deletions

View File

@@ -0,0 +1,84 @@
<?php
namespace app\admin\controller\xhprof;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
/**
* XHProf 监控规则命中记录(只读)
* 数据由导入定时任务物化写入,后台仅查看,不支持增删改。
* 页面零计算:耗时换算与阈值比对均在前端展示层完成。
*
* @ControllerAnnotation(title="XHProf 命中记录")
*/
class RunWatch extends AdminController
{
use \app\admin\traits\Curd;
protected $relationSearch = true;
public function __construct(App $app)
{
parent::__construct($app);
$this->model = new \app\admin\model\XhprofRunWatch();
// 命中记录的规则下拉(含禁用规则,便于回溯历史命中)
$this->assign('select_list_watch', \app\admin\model\XhprofWatch::column('name', 'id'), true);
}
/**
* @NodeAnotation(title="列表")
*/
public function index()
{
if ($this->request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames();
$count = $this->model
->withJoin(['run', 'watch'], 'LEFT')
->where($where)
->group($group)
->count();
$list = $this->model
->withJoin(['run', 'watch'], 'LEFT')
->where($where)
->page($page, $limit)
->order($this->sort)
->group($group)
->select();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
/**
* @NodeAnotation(title="详情")
*/
public function read($id)
{
$row = $this->model->withJoin(['run', 'watch'], 'LEFT')->find($id);
empty($row) && $this->error('数据不存在');
// 获取模型的标题(表注释)
$title = $row->title;
$this->assign('row', $row);
$this->assign('title', $title);
return $this->fetch();
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace app\admin\controller\xhprof;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
/**
* XHProf 函数监控规则管理
* regex 为 PCRE对被调用函数名匹配保存前校验编译合法性
* 防止非法正则打爆导入任务的规则编译。
*
* @ControllerAnnotation(title="XHProf 函数监控规则")
*/
class Watch extends AdminController
{
use \app\admin\traits\Curd {
\app\admin\traits\Curd::add as curdAdd;
\app\admin\traits\Curd::edit as curdEdit;
}
public function __construct(App $app)
{
parent::__construct($app);
$this->model = new \app\admin\model\XhprofWatch();
$this->assign('select_list_status', $this->model::SELECT_LIST_STATUS, true);
}
/**
* @NodeAnotation(title="添加")
*/
public function add()
{
if ($this->request->isPost()) {
$this->validateWatchRegex((string) $this->request->post('regex', ''));
}
return $this->curdAdd();
}
/**
* @NodeAnotation(title="编辑")
*/
public function edit($id)
{
if ($this->request->isPost()) {
$this->validateWatchRegex((string) $this->request->post('regex', ''));
}
return $this->curdEdit($id);
}
/**
* 校验 PCRE 正则合法性(与导入任务 compileWatchRegexes 同一判据).
*/
protected function validateWatchRegex(string $regex): void
{
if ($regex === '') {
$this->error('正则不能为空');
}
error_clear_last();
if (@preg_match($regex, '') === false) {
$error = error_get_last();
$message = $error !== null ? $error['message'] : preg_last_error_msg();
$this->error('正则非法(必须带定界符,如 ~think\\\\db\\\\.*~' . $message);
}
}
}

View File

@@ -5,6 +5,8 @@ namespace app\admin\model;
use app\common\model\TimeModel;
/**
* XHProf 采样与监控规则命中聚合(导入时物化)
*
* @property int $id 主键ID
* @property int $run_id 采样记录IDul_xhprof_run.id
* @property int $watch_id 监控规则IDul_xhprof_watch.id
@@ -20,4 +22,13 @@ class XhprofRunWatch extends TimeModel
protected $deleteTime = false;
public function run()
{
return $this->belongsTo('app\admin\model\XhprofRun', 'run_id', 'id');
}
public function watch()
{
return $this->belongsTo('app\admin\model\XhprofWatch', 'watch_id', 'id');
}
}

View File

@@ -5,9 +5,11 @@ namespace app\admin\model;
use app\common\model\TimeModel;
/**
* XHProf 函数监控规则
*
* @property int $id 主键ID
* @property string $name 规则名称(人读)
* @property string $regex PCRE 正则(对被调用函数名匹配)
* @property string $regex PCRE 正则(对被调用函数名匹配,带定界符
* @property string $note 备注
* @property int $threshold_ms 比对阈值毫秒0=不比对
* @property int $status 状态:1=启用,0=禁用
@@ -21,4 +23,5 @@ class XhprofWatch extends TimeModel
protected $deleteTime = false;
public const SELECT_LIST_STATUS = ['0' => '禁用', '1' => '启用'];
}

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'xhprof.run_watch/index',
addUrl: '',
editUrl: '',
readUrl: 'xhprof.run_watch/read',
deleteUrl: '',
exportUrl: '',
modifyUrl: '',
};

View File

@@ -0,0 +1,44 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">采样记录IDul_xhprof_run.id</label>
<div class="layui-input-block">
<input type="text" name="run_id" class="layui-input" placeholder="请输入采样记录IDul_xhprof_run.id" value="{$Request.param.run_id|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">监控规则IDul_xhprof_watch.id</label>
<div class="layui-input-block">
<input type="text" name="watch_id" class="layui-input" placeholder="请输入监控规则IDul_xhprof_watch.id" value="{$Request.param.watch_id|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中边次数和</label>
<div class="layui-input-block">
<input type="text" name="ct" class="layui-input" placeholder="请输入命中边次数和" value="{$Request.param.ct|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中耗时合计(微秒)</label>
<div class="layui-input-block">
<input type="text" name="wt_sum" class="layui-input" placeholder="请输入命中耗时合计(微秒)" value="{$Request.param.wt_sum|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中单次最大耗时(微秒)</label>
<div class="layui-input-block">
<input type="text" name="wt_max" class="layui-input" placeholder="请输入命中单次最大耗时(微秒)" value="{$Request.param.wt_max|default='0'}">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,44 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">采样记录IDul_xhprof_run.id</label>
<div class="layui-input-block">
<input type="text" name="run_id" class="layui-input" placeholder="请输入采样记录IDul_xhprof_run.id" value="{$row.run_id|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">监控规则IDul_xhprof_watch.id</label>
<div class="layui-input-block">
<input type="text" name="watch_id" class="layui-input" placeholder="请输入监控规则IDul_xhprof_watch.id" value="{$row.watch_id|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中边次数和</label>
<div class="layui-input-block">
<input type="text" name="ct" class="layui-input" placeholder="请输入命中边次数和" value="{$row.ct|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中耗时合计(微秒)</label>
<div class="layui-input-block">
<input type="text" name="wt_sum" class="layui-input" placeholder="请输入命中耗时合计(微秒)" value="{$row.wt_sum|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">命中单次最大耗时(微秒)</label>
<div class="layui-input-block">
<input type="text" name="wt_max" class="layui-input" placeholder="请输入命中单次最大耗时(微秒)" value="{$row.wt_max|default=''}">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,9 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('xhprof.run_watch/index')}"
data-auth-read="{:auth('xhprof.run_watch/read')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,62 @@
$(function(){
ua.table.render({
init: init,
toolbar: [],
defaultToolbar: ['filter'],
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'watch_id', title: '监控规则', width: 160, search: 'select', searchOp: '=', selectList: ua.getDataBrage('select_list_watch'), templet: function(d) {
return d.watch && d.watch.name ? d.watch.name : ('#' + d.watch_id);
}},
{field: 'run.url', title: '请求 URL', minWidth: 220, search: false, templet: function(d) {
if (!d.run || !d.run.url) {
return '<span class="layui-text-em">#' + d.run_id + '</span>';
}
var esc = String(d.run.url).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
return '<span title="' + esc + '">' + esc + '</span>';
}},
{field: 'run.request_time', title: '请求时间', width: 170, search: 'range', templet: function(d) {
if (!d.run || !d.run.request_time) {
return '-';
}
var ts = parseFloat(d.run.request_time);
if (isNaN(ts) || ts <= 0) {
return '-';
}
return window.UA_CORE.util.toDateString(ts * 1000, 'yyyy-MM-dd HH:mm:ss');
}},
{field: 'ct', title: '命中次数', width: 100, search: 'number_limit'},
{field: 'wt_sum', title: '耗时合计(ms)', width: 140, search: 'number_limit', templet: function(d) {
var ms = parseFloat(d.wt_sum) / 1000;
var threshold = d.watch ? parseInt(d.watch.threshold_ms, 10) : 0;
var text = (isNaN(ms) ? 0 : ms).toFixed(3) + ' ms';
if (threshold > 0 && ms > threshold) {
return '<span class="layui-badge layui-bg-red">' + text + '(超阈值 ' + threshold + 'ms</span>';
}
return text;
}},
{field: 'wt_max', title: '单次最大(ms)', width: 130, search: 'number_limit', templet: function(d) {
var ms = parseFloat(d.wt_max) / 1000;
return (isNaN(ms) ? 0 : ms).toFixed(3) + ' ms';
}},
{field: 'create_time', title: '入库时间', width: 170, search: false, templet: ua.table.date},
{
width: 100, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
[{
class: 'layui-btn layui-btn-primary layui-btn-xs',
method: 'tab',
field: 'id',
text: '详情',
title: '查看详情',
auth: 'read',
url: init.readUrl,
icon: ''
}]
]
},
]],
});
ua.listen();
})

View File

@@ -0,0 +1,97 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">监控规则</div>
<div class="detail-field-value">
{notempty name="row.watch.name"}
{$row.watch.name}#{$row.watch_id}
{else/}
<span class="layui-text-em">#{$row.watch_id}</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">命中次数(边数和)</div>
<div class="detail-field-value">
{$row.ct}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">耗时合计</div>
<div class="detail-field-value">
{$row.wt_sum} μs = {:number_format($row['wt_sum'] / 1000, 3)} ms
{if condition="$row['watch']['threshold_ms'] > 0 && ($row['wt_sum'] / 1000) > $row['watch']['threshold_ms']"}
<span class="layui-badge layui-bg-red">超阈值 {$row.watch.threshold_ms}ms</span>
{/if}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">单次最大耗时</div>
<div class="detail-field-value">
{$row.wt_max} μs = {:number_format($row['wt_max'] / 1000, 3)} ms
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">关联采样</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">采样记录ID</div>
<div class="detail-field-value">#{$row.run_id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">请求 URL</div>
<div class="detail-field-value">
{notempty name="row.run.url"}
{$row.run.url}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">请求时间</div>
<div class="detail-field-value">
{notempty name="row.run.request_time"}
{:date('Y-m-d H:i:s', intval($row['run']['request_time']))}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">入库时间</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
$(function(){
// 删除数据
window.deleteData = function(id) {
layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, function(index) {
$.post('{{:url("delete")}}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功', {icon: 1}, function() {
location.href = '{{:url("index")}}';
});
} else {
layer.msg(res.msg, {icon: 2});
}
}, 'json');
layer.close(index);
});
};
ua.listen();
})

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'xhprof.watch/index',
addUrl: 'xhprof.watch/add' + location.search,
editUrl: 'xhprof.watch/edit',
readUrl: 'xhprof.watch/read',
deleteUrl: 'xhprof.watch/delete',
exportUrl: '',
modifyUrl: 'xhprof.watch/modify',
};

View File

@@ -0,0 +1,48 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">规则名称</label>
<div class="layui-input-block">
<input type="text" name="name" class="layui-input" lay-verify="required" placeholder="请输入规则名称人读SQL 执行层" value="{$Request.param.name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">正则表达式</label>
<div class="layui-input-block">
<input type="text" name="regex" class="layui-input" lay-verify="required|watchRegex" placeholder="必须带 PCRE 定界符,如 ~think\\db\\.*~(对被调用函数名匹配)" value="{$Request.param.regex|default=''}">
<div class="layui-form-mid layui-word-aux">对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<input type="text" name="note" class="layui-input" placeholder="备注(可选)" value="{$Request.param.note|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">阈值(毫秒)</label>
<div class="layui-input-block">
<input type="number" name="threshold_ms" class="layui-input" lay-verify="number" placeholder="单位毫秒0=不比对(命中记录页超阈值标红)" value="{$Request.param.threshold_ms|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$Request.param.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,40 @@
$(function(){
// PCRE 正则前端预校验:必须带定界符(权威校验在后端 @preg_match
var form = window.UA_CORE.form;
if (form) {
form.verify({
watchRegex: function (value) {
value = $.trim(value);
if (value === '') {
return '正则不能为空';
}
var delim = value.charAt(0);
if (/[a-zA-Z0-9\s\\]/.test(delim)) {
return '必须带 PCRE 定界符,如 ~think\\db\\.*~';
}
var body = value.substring(1);
var closeIdx = -1;
for (var i = 0; i < body.length; i++) {
var c = body.charAt(i);
if (c === '\\') {
i++;
continue;
}
if (c === delim) {
closeIdx = i;
break;
}
}
if (closeIdx === -1) {
return '找不到闭合定界符 ' + delim;
}
var modifiers = body.substring(closeIdx + 1);
if (!/^[imsxADSUXJu]*$/.test(modifiers)) {
return '非法修饰符:' + modifiers;
}
},
});
}
ua.listen();
})

View File

@@ -0,0 +1,48 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">规则名称</label>
<div class="layui-input-block">
<input type="text" name="name" class="layui-input" lay-verify="required" placeholder="请输入规则名称(人读)" value="{$row.name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">正则表达式</label>
<div class="layui-input-block">
<input type="text" name="regex" class="layui-input" lay-verify="required|watchRegex" placeholder="必须带 PCRE 定界符,如 ~think\\db\\.*~(对被调用函数名匹配)" value="{$row.regex|default=''}">
<div class="layui-form-mid layui-word-aux">对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注</label>
<div class="layui-input-block">
<input type="text" name="note" class="layui-input" placeholder="备注(可选)" value="{$row.note|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">阈值(毫秒)</label>
<div class="layui-input-block">
<input type="number" name="threshold_ms" class="layui-input" lay-verify="number" placeholder="单位毫秒0=不比对(命中记录页超阈值标红)" value="{$row.threshold_ms|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$row.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,40 @@
$(function(){
// PCRE 正则前端预校验:必须带定界符(权威校验在后端 @preg_match
var form = window.UA_CORE.form;
if (form) {
form.verify({
watchRegex: function (value) {
value = $.trim(value);
if (value === '') {
return '正则不能为空';
}
var delim = value.charAt(0);
if (/[a-zA-Z0-9\s\\]/.test(delim)) {
return '必须带 PCRE 定界符,如 ~think\\db\\.*~';
}
var body = value.substring(1);
var closeIdx = -1;
for (var i = 0; i < body.length; i++) {
var c = body.charAt(i);
if (c === '\\') {
i++;
continue;
}
if (c === delim) {
closeIdx = i;
break;
}
}
if (closeIdx === -1) {
return '找不到闭合定界符 ' + delim;
}
var modifiers = body.substring(closeIdx + 1);
if (!/^[imsxADSUXJu]*$/.test(modifiers)) {
return '非法修饰符:' + modifiers;
}
},
});
}
ua.listen();
})

View File

@@ -0,0 +1,12 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('xhprof.watch/index')}"
data-auth-add="{:auth('xhprof.watch/add')}"
data-auth-edit="{:auth('xhprof.watch/edit')}"
data-auth-delete="{:auth('xhprof.watch/delete')}"
data-auth-modify="{:auth('xhprof.watch/modify')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,38 @@
$(function(){
ua.table.render({
init: init,
toolbar: ['add', 'delete'],
cols: [[
{type: 'checkbox'},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'name', title: '规则名称', minWidth: 140},
{field: 'regex', title: '正则(匹配被调用函数名)', minWidth: 260, templet: function(d) {
var esc = String(d.regex == null ? '' : d.regex).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
return '<code style="font-size:12px;">' + esc + '</code>';
}},
{field: 'note', title: '备注', minWidth: 140},
{field: 'threshold_ms', title: '阈值(ms)', width: 100, search: 'number_limit'},
{field: 'status', title: '状态', width: 100, search: 'select', selectList: ua.getDataBrage('select_list_status'), templet: ua.table.switch},
{field: 'create_time', title: '创建时间', width: 170, search: false, templet: ua.table.date},
{
width: 200, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
[{
class: 'layui-btn layui-btn-primary layui-btn-xs',
method: 'tab',
field: 'id',
text: '详情',
title: '查看详情',
auth: 'read',
url: init.readUrl,
icon: ''
}],
'edit',
'delete'
]
},
]],
});
ua.listen();
})

View File

@@ -0,0 +1,95 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
<button class="layui-btn" onclick="location.href='{:url("edit", ["id" => $row.id])}'">编辑</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">规则名称(人读)</div>
<div class="detail-field-value">
{notempty name="row.name"}
{$row.name}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">PCRE 正则(对被调用函数名匹配)</div>
<div class="detail-field-value">
{notempty name="row.regex"}
{$row.regex}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">备注</div>
<div class="detail-field-value">
{notempty name="row.note"}
{$row.note}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">比对阈值毫秒0=不比对</div>
<div class="detail-field-value">
{notempty name="row.threshold_ms"}
{$row.threshold_ms}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">状态:1=启用,0=禁用</div>
<div class="detail-field-value">
<span class="layui-badge">{$select_list_status[$row.status]|default=''}</span>
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">创建时间戳</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
$(function(){
// 删除数据
window.deleteData = function(id) {
layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, function(index) {
$.post('{{:url("delete")}}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功', {icon: 1}, function() {
location.href = '{{:url("index")}}';
});
} else {
layer.msg(res.msg, {icon: 2});
}
}, 'json');
layer.close(index);
});
};
ua.listen();
})

View File

@@ -0,0 +1,81 @@
<?php
use think\migration\Migrator;
/**
* XHProf 函数监控规则 - 预置三条规则
*
* regex 为 PCRE带 ~...~ 定界符导入任务物化时对被调用callee函数名匹配。
* 反斜杠形态说明DB 存储值形如 ~think\\db\\PDOConnection::.*~(双反斜杠字符),
* PCRE 解析 \\ 为一个字面 \,与采样键 think\db\PDOConnection::xxx命名空间单反斜杠对齐。
* 已对运行环境真实采样验证PDOConnection::(静态形态)、::save如 think\Cookie::save均有命中
* app\admin\controller\.* 在后台页面流量下命中(控制器中心主义)。
*
* MySQL 字符串字面量会把 \\ 解析为单个 \,故写入前用 addcslashes 将每个字面 \ 加倍,
* 保证落库值为双反斜杠字符。
*
* 幂等regex 有 UNIQUE 键ON DUPLICATE KEY UPDATE id=id 使重复执行不产生新行、不覆盖用户改动。
*/
class XhprofWatchSeed extends Migrator
{
public function up()
{
$now = time();
$rows = [
[
'name' => 'SQL 执行层',
// PHP 值:~think\\db\\PDOConnection::.*~(每个分隔位置两个反斜杠字符)
'regex' => '~think\\\\db\\\\PDOConnection::.*~',
'note' => 'PDO 连接层查询执行(真实形态实测)',
'threshold_ms' => 500,
],
[
'name' => '业务层控制器',
'regex' => '~app\\\\admin\\\\controller\\\\.*~',
'note' => '控制器中心主义,业务逻辑所在',
'threshold_ms' => 1000,
],
[
'name' => '数据保存操作',
'regex' => '~.*::save$~',
'note' => 'Model/log/session 的 save 调用',
'threshold_ms' => 0,
],
];
foreach ($rows as $row) {
$sql = sprintf(
"INSERT INTO `%sxhprof_watch` (`name`, `regex`, `note`, `threshold_ms`, `status`, `create_time`, `update_time`) VALUES (%s, %s, %s, %d, 1, %d, %d) ON DUPLICATE KEY UPDATE `id` = `id`",
$this->getAdapter()->getOption('table_prefix'),
$this->quoteLiteral($row['name']),
$this->quoteLiteral($row['regex']),
$this->quoteLiteral($row['note']),
$row['threshold_ms'],
$now,
$now
);
$this->execute($sql);
}
}
public function down()
{
$prefix = $this->getAdapter()->getOption('table_prefix');
$regexes = implode(', ', array_map(function ($regex) {
return $this->quoteLiteral($regex);
}, [
'~think\\\\db\\\\PDOConnection::.*~',
'~app\\\\admin\\\\controller\\\\.*~',
'~.*::save$~',
]));
$this->execute("DELETE FROM `{$prefix}xhprof_watch` WHERE `regex` IN ({$regexes})");
}
/**
* MySQL 字面量包装:单引号包裹 + 反斜杠转义(保证落库值与 PHP 值逐字符一致).
*/
private function quoteLiteral(string $value): string
{
return "'" . addcslashes($value, "\\") . "'";
}
}