Parcourir la source

20250215 数据库管理

贾小兵 il y a 1 semaine
Parent
commit
e84cd5e35c

+ 315 - 0
application/admin/controller/general/Database.php

@@ -0,0 +1,315 @@
+<?php
+
+namespace app\admin\controller\general;
+
+use addons\database\library\Backup;
+use app\common\controller\Backend;
+use think\Db;
+use think\Debug;
+use think\Exception;
+use think\exception\PDOException;
+use ZipArchive;
+
+/**
+ * 数据库管理
+ *
+ * @icon   fa fa-database
+ * @remark 可在线进行一些简单的数据库表优化或修复,查看表结构和数据。也可以进行SQL语句的操作
+ */
+class Database extends Backend
+{
+    protected $noNeedRight = ['backuplist'];
+
+    public function _initialize()
+    {
+        if (!config("app_debug")) {
+            $this->error("数据库管理插件只允许在开发环境下使用");
+        }
+        return parent::_initialize();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $tables_data_length = $tables_index_length = $tables_free_length = $tables_data_count = 0;
+        $tables = $list = [];
+        $list = Db::query("SHOW TABLES");
+        foreach ($list as $key => $row) {
+            $tables[] = ['name' => reset($row), 'rows' => 0];
+        }
+        $data['tables'] = $tables;
+        $data['saved_sql'] = [];
+        $this->view->assign($data);
+        return $this->view->fetch();
+    }
+
+    /**
+     * SQL查询
+     */
+    public function query()
+    {
+        $do_action = $this->request->post('do_action');
+
+        echo '<style type="text/css">
+            xmp,body{margin:0;padding:0;line-height:18px;font-size:13px;font-family:"Helvetica Neue", Helvetica, Microsoft Yahei, Hiragino Sans GB, WenQuanYi Micro Hei, sans-serif;}
+            hr{height:1px;margin:5px 1px;background:#e3e3e3;border:none;}
+            </style>';
+        if ($do_action == '') {
+            exit(__('Invalid parameters'));
+        }
+
+        $tablename = $this->request->post("tablename/a");
+        if(empty($tablename)){
+
+        }
+        if (in_array($do_action, array('doquery', 'optimizeall', 'repairall'))) {
+            $this->$do_action();
+        } elseif (count($tablename) == 0) {
+            exit(__('Invalid parameters'));
+        } else {
+            foreach ($tablename as $table) {
+                $this->$do_action($table);
+                echo "<br />";
+            }
+        }
+    }
+
+    /**
+     * 备份列表
+     * @internal
+     */
+    public function backuplist()
+    {
+        $config = get_addon_config('database');
+        $backupDir = ROOT_PATH . 'public' . DS . $config['backupDir'];
+        $backuplist = [];
+        foreach (glob($backupDir . "*.zip") as $filename) {
+            $time = filemtime($filename);
+            $backuplist[$time] =
+                [
+                    'file' => str_replace($backupDir, '', $filename),
+                    'date' => date("Y-m-d H:i:s", $time),
+                    'size' => format_bytes(filesize($filename))
+                ];
+        }
+        krsort($backuplist);
+
+        $this->success("", null, ['backuplist' => array_values($backuplist)]);
+    }
+
+    /**
+     * 还原
+     */
+    public function restore($ids = '')
+    {
+        $config = get_addon_config('database');
+        $backupDir = ROOT_PATH . 'public' . DS . $config['backupDir'];
+        if ($this->request->isPost()) {
+            $action = $this->request->request('action');
+            $file = $this->request->request('file');
+            if (!preg_match("/^backup\-([a-z0-9\-_\.]+)\.zip$/i", $file)) {
+                $this->error(__("Invalid parameters"));
+            }
+            $file = $backupDir . $file;
+            if ($action == 'restore') {
+                if (!class_exists('ZipArchive')) {
+                    $this->error("服务器缺少php-zip组件,无法进行还原操作");
+                }
+                try {
+                    $dir = RUNTIME_PATH . 'database' . DS;
+                    if (!is_dir($dir)) {
+                        mkdir($dir, 0755);
+                    }
+
+                    $zip = new ZipArchive;
+                    if ($zip->open($file) !== true) {
+                        throw new Exception(__('Can not open zip file'));
+                    }
+                    if (!$zip->extractTo($dir)) {
+                        $zip->close();
+                        throw new Exception(__('Can not unzip file'));
+                    }
+                    $zip->close();
+                    $filename = basename($file);
+                    $sqlFile = $dir . str_replace('.zip', '.sql', $filename);
+                    if (!is_file($sqlFile)) {
+                        throw new Exception(__('Sql file not found'));
+                    }
+                    $filesize = filesize($sqlFile);
+                    $list = Db::query('SELECT @@global.max_allowed_packet');
+                    if (isset($list[0]['@@global.max_allowed_packet']) && $filesize >= $list[0]['@@global.max_allowed_packet']) {
+                        Db::execute('SET @@global.max_allowed_packet = ' . ($filesize + 1024));
+                        //throw new Exception('备份文件超过配置max_allowed_packet大小,请修改Mysql服务器配置');
+                    }
+                    $sql = file_get_contents($sqlFile);
+
+                    Db::clear();
+                    //必须重连一次
+                    Db::connect([], true)->query("select 1");
+                    Db::getPdo()->exec($sql);
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                } catch (PDOException $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success(__('Restore successful'));
+            } elseif ($action == 'delete') {
+                unlink($file);
+                $this->success(__('Delete successful'));
+            }
+        }
+    }
+
+    /**
+     * 备份
+     */
+    public function backup()
+    {
+        $config = get_addon_config('database');
+        $backupDir = ROOT_PATH . 'public' . DS . $config['backupDir'];
+        if ($this->request->isPost()) {
+            if (!class_exists('ZipArchive')) {
+                $this->error("服务器缺少php-zip组件,无法进行备份操作");
+            }
+            $database = config('database');
+            try {
+                $backup = new Backup($database['hostname'], $database['username'], $database['database'], $database['password'], $database['hostport']);
+                $backup->setIgnoreTable($config['backupIgnoreTables'])->backup($backupDir);
+            } catch (Exception $e) {
+                $this->error($e->getMessage());
+            }
+            $this->success(__('Backup successful'));
+        }
+        return;
+    }
+
+    private function viewinfo($name)
+    {
+        $row = Db::query("SHOW CREATE TABLE `{$name}`");
+        $row = array_values($row[0]);
+        $info = $row[1];
+        echo "<xmp>{$info};</xmp>";
+    }
+
+    private function viewdata($name = '')
+    {
+        $sqlquery = "SELECT * FROM `{$name}`";
+        $this->doquery($sqlquery);
+    }
+
+    private function optimize($name = '')
+    {
+        if (Db::execute("OPTIMIZE TABLE `{$name}`")) {
+            echo __('Optimize table %s done', $name);
+        } else {
+            echo __('Optimize table %s fail', $name);
+        }
+    }
+
+    private function optimizeall($name = '')
+    {
+        $list = Db::query("SHOW TABLES");
+        foreach ($list as $key => $row) {
+            $name = reset($row);
+            if (Db::execute("OPTIMIZE TABLE {$name}")) {
+                echo __('Optimize table %s done', $name);
+            } else {
+                echo __('Optimize table %s fail', $name);
+            }
+            echo "<br />";
+        }
+    }
+
+    private function repair($name = '')
+    {
+        if (Db::execute("REPAIR TABLE `{$name}`")) {
+            echo __('Repair table %s done', $name);
+        } else {
+            echo __('Repair table %s fail', $name);
+        }
+    }
+
+    private function repairall($name = '')
+    {
+        $list = Db::query("SHOW TABLES");
+        foreach ($list as $key => $row) {
+            $name = reset($row);
+            if (Db::execute("REPAIR TABLE {$name}")) {
+                echo __('Repair table %s done', $name);
+            } else {
+                echo __('Repair table %s fail', $name);
+            }
+            echo "<br />";
+        }
+    }
+
+    private function doquery($sql = null)
+    {
+        $sqlquery = $sql ? $sql : $this->request->post('sqlquery');
+        if ($sqlquery == '') {
+            exit(__('SQL can not be empty'));
+        }
+        $sqlquery = str_replace('__PREFIX__', config('database.prefix'), $sqlquery);
+        $sqlquery = str_replace("\r", "", $sqlquery);
+        $sqls = preg_split("/;[ \t]{0,}\n/i", $sqlquery);
+        $maxreturn = 100;
+        $r = '';
+        foreach ($sqls as $key => $val) {
+            if (trim($val) == '') {
+                continue;
+            }
+            $val = rtrim($val, ';');
+            $r .= "SQL:<span style='color:green;'>{$val}</span> ";
+            if (preg_match("/^(select|explain)(.*)/i ", $val)) {
+                Debug::remark("begin");
+                $limit = stripos(strtolower($val), "limit") !== false ? true : false;
+                try {
+                    $count = Db::execute($val);
+                    if ($count > 0) {
+                        $resultlist = Db::query($val . (!$limit && $count > $maxreturn ? ' LIMIT ' . $maxreturn : ''));
+                    } else {
+                        $resultlist = [];
+                    }
+                } catch (\PDOException $e) {
+                    continue;
+                }
+                Debug::remark("end");
+                $time = Debug::getRangeTime('begin', 'end', 4);
+
+                $usedseconds = __('Query took %s seconds', $time) . "<br />";
+                if ($count <= 0) {
+                    $r .= __('Query returned an empty result');
+                } else {
+                    $r .= (__('Total:%s', $count) . (!$limit && $count > $maxreturn ? ',' . __('Max output:%s', $maxreturn) : ""));
+                }
+                $r = $r . ',' . $usedseconds;
+                $j = 0;
+                foreach ($resultlist as $m => $n) {
+                    $j++;
+                    if (!$limit && $j > $maxreturn) {
+                        break;
+                    }
+                    $r .= "<hr/>";
+                    $r .= "<font color='red'>" . __('Row:%s', $j) . "</font><br />";
+                    foreach ($n as $k => $v) {
+                        $r .= "<font color='blue'>{$k}:</font>{$v}<br/>\r\n";
+                    }
+                }
+            } else {
+                try {
+                    Debug::remark("begin");
+                    $count = Db::getPdo()->exec($val);
+                    Debug::remark("end");
+
+                } catch (\PDOException $e) {
+                    continue;
+                }
+                $time = Debug::getRangeTime('begin', 'end', 4);
+                $r .= __('Query affected %s rows and took %s seconds', $count, $time) . "<br />";
+            }
+        }
+        echo $r;
+    }
+}

+ 43 - 0
application/admin/lang/zh-cn/general/database.php

@@ -0,0 +1,43 @@
+<?php
+
+return [
+    'SQL Result'                                                             => '查询结果',
+    'Basic query'                                                            => '基础查询',
+    'View structure'                                                         => '查看表结构',
+    'View data'                                                              => '查看表数据',
+    'Backup and Restore'                                                     => '备份与还原',
+    'Backup now'                                                             => '立即备份',
+    'File'                                                                   => '文件',
+    'Size'                                                                   => '大小',
+    'Date'                                                                   => '备份日期',
+    'Restore'                                                                => '还原',
+    'Delete'                                                                 => '删除',
+    'Optimize'                                                               => '优化表',
+    'Repair'                                                                 => '修复表',
+    'Optimize all'                                                           => '优化全部表',
+    'Repair all'                                                             => '修复全部表',
+    'Backup successful'                                                      => '备份成功',
+    'Restore successful'                                                     => '还原成功',
+    'Delete successful'                                                      => '删除成功',
+    'Can not open zip file'                                                  => '无法打开备份文件',
+    'Can not unzip file'                                                     => '无法解压备份文件',
+    'Sql file not found'                                                     => '未找到SQL文件',
+    'Table:%s'                                                               => '总计:%s个表',
+    'Record:%s'                                                              => '记录:%s条',
+    'Data:%s'                                                                => '占用:%s',
+    'Index:%s'                                                               => '索引:%s',
+    'SQL Result:'                                                            => '查询结果:',
+    'SQL can not be empty'                                                   => 'SQL语句不能为空',
+    'Max output:%s'                                                          => '最大返回%s条',
+    'Total:%s'                                                               => '共有%s条记录! ',
+    'Row:%s'                                                                 => '记录:%s',
+    'Executes one or multiple queries which are concatenated by a semicolon' => '请输入SQL语句,支持批量查询,多条SQL以分号(;)分格',
+    'Query affected %s rows and took %s seconds'                             => '共影响%s条记录! 耗时:%s秒!',
+    'Query returned an empty result'                                         => '返回结果为空!',
+    'Query took %s seconds'                                                  => '耗时%s秒!',
+    'Optimize table %s done'                                                 => '优化表[%s]成功',
+    'Repair table %s done'                                                   => '修复表[%s]成功',
+    'Optimize table %s fail'                                                 => '优化表[%s]失败',
+    'Repair table %s fail'                                                   => '修复表[%s]失败'
+];
+

+ 135 - 0
application/admin/view/general/database/index.html

@@ -0,0 +1,135 @@
+<style type="text/css">
+    #searchfloat {position:absolute;top:40px;right:20px;background:#F7F0A0;padding:10px;}
+    #saved {position: relative;}
+    #saved_sql {position:absolute;bottom:0;height:300px;background:#F7F0A0;width:100%;overflow:auto;display:none;}
+    #saved_sql li {display:block;clear:both;width:100%;float:left;line-height:18px;padding:1px 0}
+    #saved_sql li a{float:left;text-decoration: none;display:block;padding:0 5px;}
+    #saved_sql li i{display:none;float:left;color:#06f;font-size: 14px;font-style: normal;margin-left:2px;line-height:18px;}
+    #saved_sql li:hover{background:#fff;}
+    #saved_sql li:hover i{display:block;cursor:pointer;}
+    #database #tablename {height:205px;width:100%;padding:5px;}
+    #database #tablename option{height:18px;}
+    #database #subaction {height:210px;width:100%;}
+    #database .select-striped > option:nth-of-type(odd) {background-color: #f9f9f9;}
+    #database .dropdown-menu ul {margin:-3px 0;}
+    #database .dropdown-menu ul li{margin:3px 0;}
+    #database .dropdown-menu.row .col-xs-6{padding:0 5px;}
+    #sqlquery {color:#444;}
+    #resultparent {padding:5px;}
+</style>
+<style data-render="darktheme">
+    body.darktheme #database .select-striped > option:nth-of-type(odd) {
+        background-color: #262626;
+    }
+    body.darktheme #tablename::-webkit-scrollbar {
+        width: 9px;
+        height: 9px;
+    }
+    body.darktheme #tablename::-webkit-scrollbar-thumb {
+        background: #999;
+    }
+    body.darktheme #tablename::-webkit-scrollbar-track {
+        background: #333;
+    }
+    body.darktheme #sqlquery {
+        color: #ccc;
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="database" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    {if $auth->check('general/database/query')}
+                    <div class="row">
+                        <!-- <div class="col-xs-4">
+                            <h4>{:__('SQL Result')}:</h4>
+                        </div> -->
+                        <div class="col-xs-8 text-right">
+                            <form action="{:url('general.database/query')}" method="post" name="infoform" target="resultframe">
+                                <input type="hidden" name="do_action" id="topaction" />
+                                <a href="javascript:;" class="btn btn-success btn-compress"><i class="fa fa-compress"></i> {:__('Backup and Restore')}</a>
+                                <!-- <div class="btn-group">
+                                    <button data-toggle="dropdown" class="btn btn-primary btn-embossed dropdown-toggle" type="button">{:__('Basic query')} <span class="caret"></span></button>
+                                    <div class="row dropdown-menu pull-right" style="width:300px;">
+                                        <div class="col-xs-6">
+                                            <select class="form-control select-striped" id="tablename" name="tablename[]" multiple="multiple">
+                                                {foreach $tables as $table}
+                                                <option value="{$table.name}" title="">{$table.name}</option>
+                                                {/foreach}
+                                            </select>
+                                        </div>
+                                        <div class="col-xs-6">
+                                            <ul id="subaction" class="list-unstyled">
+                                                <li><input type="submit" name="submit1" value="{:__('View structure')}" rel="viewinfo" class="btn btn-primary btn-embossed btn-sm btn-block"/></li>
+                                                <li><input type="submit" name="submit2" value="{:__('View data')}" rel="viewdata" class="btn btn-primary btn-embossed btn-sm btn-block"/></li>
+                                                <li><input type="submit" name="submit3" value="{:__('Optimize')}" rel="optimize" class="btn btn-primary btn-embossed btn-sm btn-block" /></li>
+                                                <li><input type="submit" name="submit4" value="{:__('Repair')}" rel="repair" class="btn btn-primary btn-embossed btn-sm btn-block"/></li>
+                                                <li><input type="submit" name="submit5" value="{:__('Optimize all')}" rel="optimizeall" class="btn btn-primary btn-embossed btn-sm btn-block" /></li>
+                                                <li><input type="submit" name="submit6" value="{:__('Repair all')}" rel="repairall" class="btn btn-primary btn-embossed btn-sm btn-block" /></li>
+                                            </ul>
+                                        </div>
+                                        <div class="clear"></div>
+                                    </div>
+
+                                </div> -->
+                            </form>
+                        </div>
+
+                    </div>
+                    <!-- <div class="well" id="resultparent">
+                        <iframe name="resultframe" frameborder="0" id="resultframe" style="height:100%;" width="100%" height="100%"></iframe>
+                    </div>
+                    <form action="{:url('general.database/query')}" method="post" id="sqlexecute" name="form1" target="resultframe">
+                        <input type="hidden" name="do_action" value="doquery" />
+                        <div class="form-group">
+                            <textarea name="sqlquery" placeholder="{:__('Executes one or multiple queries which are concatenated by a semicolon')}" cols="60" rows="5" class="form-control" id="sqlquery"></textarea>
+                        </div>
+
+                        <button type="submit" class="btn btn-primary btn-embossed"><i class="fa fa-check"></i> {:__('Execute')}</button>
+                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                    </form> -->
+                    {else /}
+                    <div id="backuplist"></div>
+                    {/if}
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>
+
+<script id="backuptpl" type="text/html">
+    <p>
+        <a href="javascript:;" class="btn btn-success btn-backup"><i class="fa fa-compress"></i> {:__('Backup now')}</a>
+        <span class="text-danger">如果你的数据过大,不建议采用此方式进行备份,会导致内存溢出的错误。</span>
+    </p>
+
+    <table id="dt_basic" class="table table-striped table-bordered table-hover" width="100%" style="min-width:600px;">
+        <thead>
+        <tr>
+            <th>ID</th>
+            <th>{:__('File')}</th>
+            <th>{:__('Size')}</th>
+            <th>{:__('Date')}</th>
+            <th>{:__('Operate')}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <%for (var i=0;i<backuplist.length;i++){%>
+        <tr>
+            <td><%=i+1%></td>
+            <td><%=backuplist[i].file%></td>
+            <td><%=backuplist[i].size%></td>
+            <td><%=backuplist[i].date%></td>
+            <td>
+                <a href="javascript:;" class="btn btn-primary btn-restore btn-xs" data-file="<%=backuplist[i].file%>"><i class="fa fa-reply"></i> {:__('Restore')}</a>
+                <a href="javascript:;" class="btn btn-danger btn-delete btn-xs" data-file="<%=backuplist[i].file%>"><i class="fa fa-times"></i> {:__('Delete')}</a>
+            </td>
+        </tr>
+        <%}%>
+        </tbody>
+    </table>
+</script>

+ 115 - 0
public/assets/js/backend/general/database.js

@@ -0,0 +1,115 @@
+define(['jquery', 'bootstrap', 'backend', 'template'], function ($, undefined, Backend, Template) {
+
+    var Controller = {
+        index: function () {
+
+            //如果有备份权限
+            if ($("#backuplist").length > 0) {
+                Fast.api.ajax({
+                    url: "general/database/backuplist",
+                    type: 'get'
+                }, function (data) {
+                    $("#backuplist").html(Template("backuptpl", {backuplist: data.backuplist}));
+                    return false;
+                });
+                return false;
+            }
+
+            //禁止在操作select元素时关闭dropdown的关闭事件
+            $("#database").on('click', '.dropdown-menu input, .dropdown-menu label, .dropdown-menu select', function (e) {
+                e.stopPropagation();
+            });
+
+            //提交时检查是否有删除或清空操作
+            $("#database").on("submit", "#sqlexecute", function () {
+                var v = $("#sqlquery").val().toLowerCase();
+                if ((v.indexOf("delete ") >= 0 || v.indexOf("truncate ") >= 0) && !confirm(__('Are you sure you want to delete or turncate?'))) {
+                    return false;
+                }
+            });
+
+            //事件按钮操作
+            $("#database").on("click", "ul#subaction li input", function () {
+                $("#topaction").val($(this).attr("rel"));
+                return true;
+            });
+
+            //窗口变更的时候重设结果栏高度
+            $(window).on("resize", function () {
+                $("#database .well").height($(window).height() - $("#database #sqlexecute").height() - $("#ribbon").outerHeight() - $(".panel-heading").outerHeight() - 130);
+            });
+
+            //修复iOS下iframe无法滚动的BUG
+            if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
+                $("#resultparent").css({"-webkit-overflow-scrolling": "touch", "overflow": "auto"});
+            }
+
+            $(document).on("click", ".btn-compress", function () {
+                Fast.api.ajax({
+                    url: "general/database/backuplist",
+                    type: 'get'
+                }, function (data) {
+                    Layer.open({
+                        area: ["780px", "500px"],
+                        btn: [],
+                        title: "备份与还原",
+                        content: Template("backuptpl", {backuplist: data.backuplist})
+                    });
+                    return false;
+                });
+                return false;
+            });
+
+            $(document).on("click", ".btn-backup", function () {
+                Fast.api.ajax({
+                    url: "general/database/backup",
+                    data: {action: 'backup'}
+                }, function (data) {
+                    Layer.closeAll();
+                    $(".btn-compress").trigger("click");
+                });
+            });
+
+            $(document).on("click", ".btn-restore", function () {
+                var that = this;
+                Layer.confirm("确定恢复备份?<br><font color='red'>建议先备份当前数据后再进行恢复操作!!!</font><br><font color='red'>当前数据库将被清空覆盖,请谨慎操作!!!</font>", {
+                    type: 5,
+                    skin: 'layui-layer-dialog layui-layer-fast'
+                }, function (index) {
+                    Fast.api.ajax({
+                        url: "general/database/restore",
+                        data: {action: 'restore', file: $(that).data('file')}
+                    }, function (data) {
+                        Layer.closeAll();
+                        Fast.api.ajax({
+                            url: 'ajax/wipecache',
+                            data: {type: 'all'},
+                        }, function () {
+                            Layer.alert("备份恢复成功,点击确定将刷新页面", function () {
+                                top.location.reload();
+                            });
+                            return false;
+                        });
+
+                    });
+                });
+            });
+
+            $(document).on("click", ".btn-delete", function () {
+                var that = this;
+                Layer.confirm("确定删除备份?", {type: 5, skin: 'layui-layer-dialog layui-layer-fast', title: '温馨提示'}, function (index) {
+                    Fast.api.ajax({
+                        url: "general/database/restore",
+                        data: {action: 'delete', file: $(that).data('file')}
+                    }, function (data) {
+                        $(that).closest("tr").remove();
+                        Layer.close(index);
+                    });
+                });
+            });
+
+            $(window).resize();
+        }
+    };
+    return Controller;
+});