From c980ebdff0e4740a306ca8907d37ed4a84f2e852 Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 15 Aug 2026 07:02:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20XHProf=20=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E8=A7=84=E5=88=99=E4=B8=8E=E5=91=BD=E4=B8=AD?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/xhprof/RunWatch.php | 84 ++++++++++++++++ app/admin/controller/xhprof/Watch.php | 72 ++++++++++++++ app/admin/model/XhprofRunWatch.php | 11 +++ app/admin/model/XhprofWatch.php | 5 +- app/admin/view/xhprof/run_watch/_common.js | 11 +++ app/admin/view/xhprof/run_watch/add.html | 44 +++++++++ app/admin/view/xhprof/run_watch/add.js | 3 + app/admin/view/xhprof/run_watch/edit.html | 44 +++++++++ app/admin/view/xhprof/run_watch/edit.js | 3 + app/admin/view/xhprof/run_watch/index.html | 9 ++ app/admin/view/xhprof/run_watch/index.js | 62 ++++++++++++ app/admin/view/xhprof/run_watch/read.html | 97 +++++++++++++++++++ app/admin/view/xhprof/run_watch/read.js | 22 +++++ app/admin/view/xhprof/watch/_common.js | 11 +++ app/admin/view/xhprof/watch/add.html | 48 +++++++++ app/admin/view/xhprof/watch/add.js | 40 ++++++++ app/admin/view/xhprof/watch/edit.html | 48 +++++++++ app/admin/view/xhprof/watch/edit.js | 40 ++++++++ app/admin/view/xhprof/watch/index.html | 12 +++ app/admin/view/xhprof/watch/index.js | 38 ++++++++ app/admin/view/xhprof/watch/read.html | 95 ++++++++++++++++++ app/admin/view/xhprof/watch/read.js | 22 +++++ .../20260814110001_xhprof_watch_seed.php | 81 ++++++++++++++++ 23 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 app/admin/controller/xhprof/RunWatch.php create mode 100644 app/admin/controller/xhprof/Watch.php create mode 100644 app/admin/view/xhprof/run_watch/_common.js create mode 100644 app/admin/view/xhprof/run_watch/add.html create mode 100644 app/admin/view/xhprof/run_watch/add.js create mode 100644 app/admin/view/xhprof/run_watch/edit.html create mode 100644 app/admin/view/xhprof/run_watch/edit.js create mode 100644 app/admin/view/xhprof/run_watch/index.html create mode 100644 app/admin/view/xhprof/run_watch/index.js create mode 100644 app/admin/view/xhprof/run_watch/read.html create mode 100644 app/admin/view/xhprof/run_watch/read.js create mode 100644 app/admin/view/xhprof/watch/_common.js create mode 100644 app/admin/view/xhprof/watch/add.html create mode 100644 app/admin/view/xhprof/watch/add.js create mode 100644 app/admin/view/xhprof/watch/edit.html create mode 100644 app/admin/view/xhprof/watch/edit.js create mode 100644 app/admin/view/xhprof/watch/index.html create mode 100644 app/admin/view/xhprof/watch/index.js create mode 100644 app/admin/view/xhprof/watch/read.html create mode 100644 app/admin/view/xhprof/watch/read.js create mode 100644 database/migrations/20260814110001_xhprof_watch_seed.php diff --git a/app/admin/controller/xhprof/RunWatch.php b/app/admin/controller/xhprof/RunWatch.php new file mode 100644 index 0000000..261f7e0 --- /dev/null +++ b/app/admin/controller/xhprof/RunWatch.php @@ -0,0 +1,84 @@ +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(); + } +} diff --git a/app/admin/controller/xhprof/Watch.php b/app/admin/controller/xhprof/Watch.php new file mode 100644 index 0000000..5988f67 --- /dev/null +++ b/app/admin/controller/xhprof/Watch.php @@ -0,0 +1,72 @@ +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); + } + } +} diff --git a/app/admin/model/XhprofRunWatch.php b/app/admin/model/XhprofRunWatch.php index 3729b07..199d35d 100644 --- a/app/admin/model/XhprofRunWatch.php +++ b/app/admin/model/XhprofRunWatch.php @@ -5,6 +5,8 @@ namespace app\admin\model; use app\common\model\TimeModel; /** + * XHProf 采样与监控规则命中聚合(导入时物化) + * * @property int $id 主键ID * @property int $run_id 采样记录ID(ul_xhprof_run.id) * @property int $watch_id 监控规则ID(ul_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'); + } } diff --git a/app/admin/model/XhprofWatch.php b/app/admin/model/XhprofWatch.php index c4507c6..668d149 100644 --- a/app/admin/model/XhprofWatch.php +++ b/app/admin/model/XhprofWatch.php @@ -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' => '启用']; } diff --git a/app/admin/view/xhprof/run_watch/_common.js b/app/admin/view/xhprof/run_watch/_common.js new file mode 100644 index 0000000..796e547 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/_common.js @@ -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: '', +}; diff --git a/app/admin/view/xhprof/run_watch/add.html b/app/admin/view/xhprof/run_watch/add.html new file mode 100644 index 0000000..e9f206d --- /dev/null +++ b/app/admin/view/xhprof/run_watch/add.html @@ -0,0 +1,44 @@ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ {notempty name='$Request.param.backTagId'} +
返回
+ {/notempty} + + +
+ +
+
\ No newline at end of file diff --git a/app/admin/view/xhprof/run_watch/add.js b/app/admin/view/xhprof/run_watch/add.js new file mode 100644 index 0000000..4a445e0 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/add.js @@ -0,0 +1,3 @@ +$(function(){ + ua.listen(); +}) \ No newline at end of file diff --git a/app/admin/view/xhprof/run_watch/edit.html b/app/admin/view/xhprof/run_watch/edit.html new file mode 100644 index 0000000..ff5d652 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/edit.html @@ -0,0 +1,44 @@ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ {notempty name='$Request.param.backTagId'} +
返回
+ {/notempty} + + +
+ +
+
\ No newline at end of file diff --git a/app/admin/view/xhprof/run_watch/edit.js b/app/admin/view/xhprof/run_watch/edit.js new file mode 100644 index 0000000..4a445e0 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/edit.js @@ -0,0 +1,3 @@ +$(function(){ + ua.listen(); +}) \ No newline at end of file diff --git a/app/admin/view/xhprof/run_watch/index.html b/app/admin/view/xhprof/run_watch/index.html new file mode 100644 index 0000000..1f43095 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/index.html @@ -0,0 +1,9 @@ +
+
+ +
+
+
diff --git a/app/admin/view/xhprof/run_watch/index.js b/app/admin/view/xhprof/run_watch/index.js new file mode 100644 index 0000000..e54fdd3 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/index.js @@ -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 '#' + d.run_id + ''; + } + var esc = String(d.run.url).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + return '' + esc + ''; + }}, + {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 '' + text + '(超阈值 ' + threshold + 'ms)'; + } + 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(); +}) diff --git a/app/admin/view/xhprof/run_watch/read.html b/app/admin/view/xhprof/run_watch/read.html new file mode 100644 index 0000000..6c74f8d --- /dev/null +++ b/app/admin/view/xhprof/run_watch/read.html @@ -0,0 +1,97 @@ +
+
+
+
+
+
+

#{$row.id} {$title}

+
ID: {$row.id}
+
+
+ +
+
+
+
+
+ +
+
+
+
监控规则
+
+ {notempty name="row.watch.name"} + {$row.watch.name}(#{$row.watch_id}) + {else/} + #{$row.watch_id} + {/notempty} +
+
+
+
命中次数(边数和)
+
+ {$row.ct} +
+
+
+
耗时合计
+
+ {$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']"} + 超阈值 {$row.watch.threshold_ms}ms + {/if} +
+
+
+
单次最大耗时
+
+ {$row.wt_max} μs = {:number_format($row['wt_max'] / 1000, 3)} ms +
+
+
+
+ +
+

关联采样

+
+
+
采样记录ID
+
#{$row.run_id}
+
+
+
请求 URL
+
+ {notempty name="row.run.url"} + {$row.run.url} + {else/} + 暂无数据 + {/notempty} +
+
+
+
请求时间
+
+ {notempty name="row.run.request_time"} + {:date('Y-m-d H:i:s', intval($row['run']['request_time']))} + {else/} + 暂无数据 + {/notempty} +
+
+
+
入库时间
+
+ {notempty name="row.create_time"} + {$row.create_time|date="Y-m-d H:i:s"} + {else/} + 暂无数据 + {/notempty} +
+
+
+
+
+
+
+
+
diff --git a/app/admin/view/xhprof/run_watch/read.js b/app/admin/view/xhprof/run_watch/read.js new file mode 100644 index 0000000..f5671a7 --- /dev/null +++ b/app/admin/view/xhprof/run_watch/read.js @@ -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(); +}) \ No newline at end of file diff --git a/app/admin/view/xhprof/watch/_common.js b/app/admin/view/xhprof/watch/_common.js new file mode 100644 index 0000000..4f9f9c5 --- /dev/null +++ b/app/admin/view/xhprof/watch/_common.js @@ -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', +}; diff --git a/app/admin/view/xhprof/watch/add.html b/app/admin/view/xhprof/watch/add.html new file mode 100644 index 0000000..c278175 --- /dev/null +++ b/app/admin/view/xhprof/watch/add.html @@ -0,0 +1,48 @@ +
+
+ +
+ +
+ +
+
+
+ +
+ +
对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ {foreach $select_list_status as $k=>$v} + + {/foreach} +
+
+ +
+
+ {notempty name='$Request.param.backTagId'} +
返回
+ {/notempty} + + +
+ +
+
diff --git a/app/admin/view/xhprof/watch/add.js b/app/admin/view/xhprof/watch/add.js new file mode 100644 index 0000000..75497d6 --- /dev/null +++ b/app/admin/view/xhprof/watch/add.js @@ -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(); +}) diff --git a/app/admin/view/xhprof/watch/edit.html b/app/admin/view/xhprof/watch/edit.html new file mode 100644 index 0000000..3538728 --- /dev/null +++ b/app/admin/view/xhprof/watch/edit.html @@ -0,0 +1,48 @@ +
+
+ +
+ +
+ +
+
+
+ +
+ +
对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ {foreach $select_list_status as $k=>$v} + + {/foreach} +
+
+ +
+
+ {notempty name='$Request.param.backTagId'} +
返回
+ {/notempty} + + +
+ +
+
diff --git a/app/admin/view/xhprof/watch/edit.js b/app/admin/view/xhprof/watch/edit.js new file mode 100644 index 0000000..75497d6 --- /dev/null +++ b/app/admin/view/xhprof/watch/edit.js @@ -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(); +}) diff --git a/app/admin/view/xhprof/watch/index.html b/app/admin/view/xhprof/watch/index.html new file mode 100644 index 0000000..cba547b --- /dev/null +++ b/app/admin/view/xhprof/watch/index.html @@ -0,0 +1,12 @@ +
+
+ +
+
+
diff --git a/app/admin/view/xhprof/watch/index.js b/app/admin/view/xhprof/watch/index.js new file mode 100644 index 0000000..1cc281f --- /dev/null +++ b/app/admin/view/xhprof/watch/index.js @@ -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, '&').replace(//g, '>').replace(/"/g, '"'); + return '' + esc + ''; + }}, + {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(); +}) diff --git a/app/admin/view/xhprof/watch/read.html b/app/admin/view/xhprof/watch/read.html new file mode 100644 index 0000000..42d4f76 --- /dev/null +++ b/app/admin/view/xhprof/watch/read.html @@ -0,0 +1,95 @@ +
+
+
+
+
+
+

#{$row.id} {$title}

+
ID: {$row.id}
+
+
+ + +
+
+
+
+
+ +
+
+
+
规则名称(人读)
+
+ {notempty name="row.name"} + {$row.name} + {else/} + 暂无数据 + {/notempty} +
+
+
+
PCRE 正则(对被调用函数名匹配)
+
+ {notempty name="row.regex"} + {$row.regex} + {else/} + 暂无数据 + {/notempty} +
+
+
+
备注
+
+ {notempty name="row.note"} + {$row.note} + {else/} + 暂无数据 + {/notempty} +
+
+
+
比对阈值(毫秒),0=不比对
+
+ {notempty name="row.threshold_ms"} + {$row.threshold_ms} + {else/} + 暂无数据 + {/notempty} +
+
+ +
+
+ +
+

基础信息

+
+
+
ID
+
{$row.id}
+
+
+
状态:1=启用,0=禁用
+
+ {$select_list_status[$row.status]|default=''} +
+
+
+
创建时间戳
+
+ {notempty name="row.create_time"} + {$row.create_time|date="Y-m-d H:i:s"} + {else/} + 暂无数据 + {/notempty} +
+
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/app/admin/view/xhprof/watch/read.js b/app/admin/view/xhprof/watch/read.js new file mode 100644 index 0000000..f5671a7 --- /dev/null +++ b/app/admin/view/xhprof/watch/read.js @@ -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(); +}) \ No newline at end of file diff --git a/database/migrations/20260814110001_xhprof_watch_seed.php b/database/migrations/20260814110001_xhprof_watch_seed.php new file mode 100644 index 0000000..84f2a86 --- /dev/null +++ b/database/migrations/20260814110001_xhprof_watch_seed.php @@ -0,0 +1,81 @@ + '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, "\\") . "'"; + } +}