mirror of https://github.com/1099438829/apeblog
完善网站备份
This commit is contained in:
parent
1934a08c44
commit
09eddda97f
|
|
@ -0,0 +1,330 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | HulaCWMS 呼啦企业网站管理系统
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2021 https://www.kaifashu.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: 开发树
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\admin\controller;
|
||||
use app\Request;
|
||||
use think\facade\Db;
|
||||
use app\common\service\Database;
|
||||
use app\admin\service\UtilService as Util;
|
||||
|
||||
/*
|
||||
* 数据库备份还原控制器
|
||||
*/
|
||||
class Databases extends AuthController
|
||||
{
|
||||
/**
|
||||
* 数据库备份/还原列表
|
||||
* @param null $type
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-30 12:45
|
||||
*/
|
||||
public function index($type = null){
|
||||
if (!$type){
|
||||
$type = 'export';
|
||||
}
|
||||
switch ($type) {
|
||||
/* 数据还原 */
|
||||
case 'import':
|
||||
$title = '数据还原';
|
||||
break;
|
||||
/* 数据备份 */
|
||||
case 'export':
|
||||
$title = '数据备份';
|
||||
break;
|
||||
default:
|
||||
$this->error('参数错误!');
|
||||
}
|
||||
$this->assign('mate_title',$title);
|
||||
return $this->fetch($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-31 0:12
|
||||
*/
|
||||
public function lst(Request $request)
|
||||
{
|
||||
$data = Util::postMore([
|
||||
['type','export'],
|
||||
]);
|
||||
|
||||
switch ($data['type']) {
|
||||
/* 数据还原 */
|
||||
case 'import':
|
||||
//列出备份文件列表
|
||||
$path = system_config('data_backup_path');
|
||||
if(!is_dir($path)){
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
$path = realpath($path);
|
||||
$flag = \FilesystemIterator::KEY_AS_FILENAME;
|
||||
$glob = new \FilesystemIterator($path, $flag);
|
||||
|
||||
$list = array();
|
||||
foreach ($glob as $name => $file) {
|
||||
if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){
|
||||
$name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
|
||||
|
||||
$date = "{$name[0]}-{$name[1]}-{$name[2]}";
|
||||
$time = "{$name[3]}:{$name[4]}:{$name[5]}";
|
||||
$part = $name[6];
|
||||
|
||||
if(isset($list["{$date} {$time}"])){
|
||||
$info = $list["{$date} {$time}"];
|
||||
$info['part'] = max($info['part'], $part);
|
||||
$info['size'] = $info['size'] + $file->getSize();
|
||||
} else {
|
||||
$info['part'] = $part;
|
||||
$info['size'] = $file->getSize();
|
||||
}
|
||||
$extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
|
||||
$info['compress'] = ($extension === 'SQL') ? '-' : $extension;
|
||||
$info['time'] = strtotime("{$date} {$time}");
|
||||
|
||||
$list["{$date} {$time}"] = $info;
|
||||
}
|
||||
}
|
||||
$title = '数据还原';
|
||||
break;
|
||||
|
||||
/* 数据备份 */
|
||||
case 'export':
|
||||
$list = Db::query('SHOW TABLE STATUS');
|
||||
$list = array_map('array_change_key_case', $list);
|
||||
$title = '数据备份';
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->error('参数错误!');
|
||||
}
|
||||
return app("json")->layui($list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 优化表
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-30 12:46
|
||||
*/
|
||||
public function optimize()
|
||||
{
|
||||
$data = Util::postMore([
|
||||
['ids',''],
|
||||
]);
|
||||
if ($data['ids'] == "") return app("json")->fail("请指定要优化的表");
|
||||
$ids = $data['ids'];
|
||||
if(is_array($ids)){
|
||||
$ids = implode('`,`', $ids);
|
||||
$list = Db::query("OPTIMIZE TABLE `{$ids}`");
|
||||
return $list ? app("json")->success("数据表优化完成",'code') : app("json")->fail("数据表优化出错请重试");
|
||||
} else {
|
||||
$list = Db::query("OPTIMIZE TABLE `{$ids}`");
|
||||
return $list ? app("json")->success("数据表'{$ids}'优化完成",'code') : app("json")->fail("数据表'{$ids}'优化出错请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复表
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-31 0:12
|
||||
*/
|
||||
public function repair()
|
||||
{
|
||||
$data = Util::postMore([
|
||||
['ids',''],
|
||||
]);
|
||||
if ($data['ids'] == "") return app("json")->fail("请指定要优化的表");
|
||||
$ids = $data['ids'];
|
||||
if(is_array($ids)){
|
||||
$ids = implode('`,`', $ids);
|
||||
$list = Db::query("REPAIR TABLE `{$ids}`");
|
||||
return $list ? app("json")->success("数据表修复完成",'code') : app("json")->fail("数据表修复出错请重试");
|
||||
} else {
|
||||
$list = Db::query("REPAIR TABLE `{$ids}`");
|
||||
return $list ? app("json")->success("数据表'{$ids}'修复完成",'code') : app("json")->fail("数据表'{$ids}'修复出错请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除备份文件
|
||||
* @return mixed|void
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-30 13:31
|
||||
*/
|
||||
public function delOne(){
|
||||
$data = Util::postMore([
|
||||
['time',''],
|
||||
]);
|
||||
if ($data['time'] == "") return app("json")->fail("参数错误");
|
||||
$name = date('Ymd-His', $data['time']) . '-*.sql*';
|
||||
$path = realpath(system_config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
|
||||
array_map("unlink", glob($path));
|
||||
return !count(glob($path)) ? app("json")->success("备份文件删除成功",'code') : app("json")->fail("备份文件删除失败,请检查权限");
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份数据库
|
||||
* @return mixed
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-31 0:35
|
||||
*/
|
||||
public function export(){
|
||||
$data = Util::postMore([
|
||||
['ids',''],
|
||||
['id',''],
|
||||
['start',''],
|
||||
]);
|
||||
if(request()->isPost() && !empty($data['ids'])){ //初始化
|
||||
if (!is_array($data['ids'])){
|
||||
$data['ids'] = explode(',',$data['ids']);
|
||||
}
|
||||
$path = system_config('data_backup_path');
|
||||
if(!is_dir($path)){
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
//读取备份配置
|
||||
$config = array(
|
||||
'path' => realpath($path) . DIRECTORY_SEPARATOR,
|
||||
'part' => system_config('data_backup_part_size'),
|
||||
'compress' => system_config('data_backup_compress'),
|
||||
'level' => system_config('data_backup_compress_level'),
|
||||
);
|
||||
//检查备份目录是否可写
|
||||
if(!is_writeable($config['path'])) return app("json")->fail('备份目录不存在或不可写,请检查后重试');
|
||||
//检查是否有正在执行的任务
|
||||
$lock = "{$config['path']}backup.lock";
|
||||
if(is_file($lock)){
|
||||
return app("json")->fail('检测到有一个备份任务正在执行,请稍后再试');
|
||||
} else {
|
||||
//创建锁文件
|
||||
file_put_contents($lock, time());
|
||||
}
|
||||
session('backup_config', $config);
|
||||
//生成备份文件信息
|
||||
$file = array(
|
||||
'name' => date('Ymd-His', time()),
|
||||
'part' => 1,
|
||||
);
|
||||
session('backup_file', $file);
|
||||
|
||||
//缓存要备份的表
|
||||
session('backup_tables', $data['ids']);
|
||||
|
||||
//创建备份文件
|
||||
$Database = new Database($file, $config);
|
||||
$res = $Database->create();
|
||||
return $res ? app("json")->success("初始化成功",'code') : app("json")->fail("初始化失败,备份文件创建失败!");
|
||||
} elseif (request()->isGet() && is_numeric($data['id']) && is_numeric($data['start'])) { //备份数据
|
||||
$data['ids'] = session('backup_tables');
|
||||
//备份指定表
|
||||
$Database = new Database(session('backup_file'), session('backup_config'));
|
||||
$data['start'] = $Database->backup($data['ids'][$data['id']], $data['start']);
|
||||
if(false === $data['start']){ //出错
|
||||
$this->error('备份出错!');
|
||||
} elseif (0 === $data['start']) { //下一表
|
||||
if(isset($data['ids'][++$data['id']])){
|
||||
return app("json")->success("备份完成",'code');
|
||||
} else { //备份完成,清空缓存
|
||||
unlink(session('backup_config.path') . 'backup.lock');
|
||||
session('backup_tables', null);
|
||||
session('backup_file', null);
|
||||
session('backup_config', null);
|
||||
return app("json")->success("备份完成",'code');
|
||||
}
|
||||
} else {
|
||||
$rate = floor(100 * ($data['start'][0] / $data['start'][1]));
|
||||
return app("json")->success("正在备份...({$rate}%)",'code');
|
||||
}
|
||||
|
||||
} else { //出错
|
||||
return app("json")->fail("参数错误");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原数据库
|
||||
* @author 李玉坤
|
||||
* @date 2021-10-30 12:46
|
||||
*/
|
||||
public function import(){
|
||||
$data = Util::postMore([
|
||||
['time',''],
|
||||
['part',''],
|
||||
['start',''],
|
||||
]);
|
||||
if(is_numeric($data['time']) && is_null($data['part']) && is_null($data['start'])){ //初始化
|
||||
//获取备份文件信息
|
||||
$name = date('Ymd-His', $data['time']) . '-*.sql*';
|
||||
$path = realpath(system_config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
|
||||
$files = glob($path);
|
||||
$list = array();
|
||||
foreach($files as $name){
|
||||
$basename = basename($name);
|
||||
$match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
|
||||
$gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename);
|
||||
$list[$match[6]] = array($match[6], $name, $gz);
|
||||
}
|
||||
ksort($list);
|
||||
|
||||
//检测文件正确性
|
||||
$last = end($list);
|
||||
if(count($list) === $last[0]){
|
||||
session('backup_list', $list); //缓存备份列表
|
||||
return app("json")->success("初始化完成",'code');
|
||||
} else {
|
||||
return app("json")->fail("备份文件可能已经损坏,请检查",'code');
|
||||
}
|
||||
} elseif(is_numeric($data['part']) && is_numeric($data['start'])) {
|
||||
$list = session('backup_list');
|
||||
$db = new Database($list[$data['part']], array(
|
||||
'path' => realpath(system_config('data_backup_path')) . DIRECTORY_SEPARATOR,
|
||||
'compress' => $list[$data['part']][2]));
|
||||
|
||||
$data['start'] = $db->import($data['start']);
|
||||
|
||||
if(false === $data['start']){
|
||||
$this->error('还原数据出错!');
|
||||
} elseif(0 === $data['start']) { //下一卷
|
||||
if(isset($list[++$data['part']])){
|
||||
return app("json")->success("正在还原...#{$data['part']}",'code');
|
||||
} else {
|
||||
session('backup_list', null);
|
||||
return app("json")->success("还原完成",'code');
|
||||
}
|
||||
} else {
|
||||
$data = array('part' => $data['part'], 'start' => $data['start'][0]);
|
||||
if($data['start'][1]){
|
||||
$rate = floor(100 * ($data['start'][0] / $data['start'][1]));
|
||||
return app("json")->success("正在还原...#{$data['part']} ({$rate}%)");
|
||||
} else {
|
||||
$data['gz'] = 1;
|
||||
return app("json")->success("正在还原...#{$data['part']}");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return app("json")->fail('参数错误!');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -202,24 +202,6 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
function test() {
|
||||
var selRows = $treeTable.bootstrapTable("getSelections");
|
||||
if(selRows.length == 0){
|
||||
alert("请至少选择一行");
|
||||
return;
|
||||
}
|
||||
console.log(selRows);
|
||||
|
||||
var postData = "";
|
||||
$.each(selRows,function(i) {
|
||||
postData += this.id;
|
||||
if (i < selRows.length - 1) {
|
||||
postData += ", ";
|
||||
}
|
||||
});
|
||||
alert("你选中行的 id 为:"+postData);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>{$mate_title} - {:system_config("title")}</title>
|
||||
{include file="public/header" /}
|
||||
<link rel="stylesheet" href="__ADMIN_PATH__css/more.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid p-t-15">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<ul class="nav nav-tabs page-tabs">
|
||||
<li class="active"><a href="#!">备份数据库</a> </li>
|
||||
<li><a href="/admin/databases/index?type=import">还原数据库</a> </li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div id="toolbar" class="toolbar-btn-action">
|
||||
<button type="button" class="btn btn-success btn-primary m-r-5" id="btn-export">
|
||||
<span aria-hidden="true">立即备份</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-success m-r-5" id="btn-optimize">
|
||||
<span aria-hidden="true">优化表</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-success m-r-5" id="btn-repair">
|
||||
<span aria-hidden="true">修复表</span>
|
||||
</button>
|
||||
</div>
|
||||
<table id="tb_departments"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{include file="public/footer"/}
|
||||
<script type="text/javascript">
|
||||
$('#tb_departments').bootstrapTable({
|
||||
classes: 'table table-bordered table-hover table-striped',
|
||||
url: '/admin/databases/Lst',
|
||||
method: 'post',
|
||||
dataType : 'json', // 因为本示例中是跨域的调用,所以涉及到ajax都采用jsonp,
|
||||
uniqueId: 'id',
|
||||
idField: 'id', // 每行的唯一标识字段
|
||||
toolbar: '#toolbar', // 工具按钮容器
|
||||
showColumns: false, // 是否显示所有的列
|
||||
showRefresh: false, // 是否显示刷新按钮
|
||||
responseHandler: function (res) {
|
||||
return {
|
||||
"total": res.count,
|
||||
"rows": res.data,
|
||||
};
|
||||
},
|
||||
pagination: true,
|
||||
queryParams: function(params) {
|
||||
let temp = toArrayList($(".searchForm").serializeArray());
|
||||
temp['limit'] = params.limit;
|
||||
temp['page'] = (params.offset / params.limit) + 1;
|
||||
return temp;
|
||||
},
|
||||
sidePagination: "server",
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
pageList: [10, 20, 50, 100],
|
||||
columns: [{
|
||||
checkbox: true, // 是否显示复选框
|
||||
},{
|
||||
field: 'name',
|
||||
title: '表名'
|
||||
}, {
|
||||
field: 'rows',
|
||||
title: '数据量',
|
||||
}, {
|
||||
field: 'data_length',
|
||||
title: '数据大小',
|
||||
formatter:function (value,row,index) {
|
||||
return renderSize(value);
|
||||
}
|
||||
},{
|
||||
field: 'create_time',
|
||||
title: '创建时间',
|
||||
}, {
|
||||
field: 'operate',
|
||||
title: '操作',
|
||||
formatter:function (value,row,index) {
|
||||
let html = '<a href="#!" class="btn btn-xs btn-default m-r-5 btn-optimize" title="优化表" data-toggle="tooltip" data-trigger="hover"><i class="mdi mdi-rotate-3d"></i></a> \n' +
|
||||
'<a class="btn btn-xs btn-default btn-repair" href="#!" title="修复表" data-toggle="tooltip" data-trigger="hover"><i class="mdi mdi-rotate-right-variant"></i></a>';
|
||||
return html;
|
||||
},
|
||||
events: {
|
||||
'click .btn-optimize': function (event, value, row, index) {
|
||||
$.ajax({
|
||||
type: "post",
|
||||
url: "/admin/databases/optimize",
|
||||
data: {ids:row.name},
|
||||
success: function (res) {
|
||||
if (res.code == 200 || res.status == 200) {
|
||||
parent.lightyear.notify(res.msg, 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');
|
||||
} else{
|
||||
parent.lightyear.notify(res.msg, 'danger', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .btn-repair': function (event, value, row, index) {
|
||||
$.ajax({
|
||||
type: "post",
|
||||
url: "/admin/databases/repair",
|
||||
data: {ids:row.name},
|
||||
success: function (res) {
|
||||
if (res.code == 200 || res.status == 200) {
|
||||
parent.lightyear.notify(res.msg, 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');
|
||||
} else{
|
||||
parent.lightyear.notify(res.msg, 'danger', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}],
|
||||
onLoadSuccess: function(data){
|
||||
$("[data-toggle='tooltip']").tooltip();
|
||||
}
|
||||
});
|
||||
|
||||
//格式化文件大小
|
||||
function renderSize(value){
|
||||
if(null==value||value==''){
|
||||
return "0 Bytes";
|
||||
}
|
||||
var unitArr = new Array("Bytes","KB","MB","GB","TB","PB","EB","ZB","YB");
|
||||
var index=0;
|
||||
var srcsize = parseFloat(value);
|
||||
index=Math.floor(Math.log(srcsize)/Math.log(1024));
|
||||
var size =srcsize/Math.pow(1024,index);
|
||||
size=size.toFixed(2);//保留的小数位数
|
||||
return size+unitArr[index];
|
||||
}
|
||||
|
||||
$("#btn-export").click(function () {
|
||||
var checkID = "";
|
||||
var selectedItem = $('#tb_departments').bootstrapTable('getAllSelections');
|
||||
if (selectedItem=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
for (var i = 0 ; i< selectedItem.length; i++)
|
||||
{
|
||||
checkID += selectedItem[i]['name']+",";
|
||||
}
|
||||
if (checkID=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
$.confirm({
|
||||
title: '重要提醒!',
|
||||
content: '您确认要执行该操作吗?',
|
||||
backgroundDismiss: true,
|
||||
buttons: {
|
||||
ok: {
|
||||
text: '确认',
|
||||
btnClass: 'btn-danger',
|
||||
action: function () {
|
||||
$.post("/admin/databases/export",data={ids:checkID},function (res) {
|
||||
if (res.status == 200) {lightyear.notify(res.msg, 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');location.reload();}
|
||||
else lightyear.notify(res.msg, 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
})
|
||||
loadIndex = parent.layer.open({
|
||||
type: 1,
|
||||
title:'',
|
||||
area:'440px',
|
||||
skin: 'layui-layer-demo', //样式类名
|
||||
closeBtn: 0, //不显示关闭按钮
|
||||
anim: 2,
|
||||
content: '<div id="zz-div-backdata" class="zz-div-backdata"><i class="layui-icon layui-icon-loading layui-icon layui-anim layui-anim-rotate layui-anim-loop"></i><div class="content">正在备份数据库,请稍等...</div></div>'
|
||||
});
|
||||
window.onbeforeunload = function(){ return "正在备份数据库,请不要关闭!" }
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: '取消',
|
||||
btnClass: 'btn-primary'
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
$("#btn-optimize").click(function () {
|
||||
var checkID = "";
|
||||
var selectedItem = $('#tb_departments').bootstrapTable('getAllSelections');
|
||||
if (selectedItem=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
for (var i = 0 ; i< selectedItem.length; i++)
|
||||
{
|
||||
checkID += selectedItem[i]['name']+",";
|
||||
}
|
||||
if (checkID=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
$.confirm({
|
||||
title: '重要提醒!',
|
||||
content: '您确认要执行该操作吗?',
|
||||
backgroundDismiss: true,
|
||||
buttons: {
|
||||
ok: {
|
||||
text: '确认',
|
||||
btnClass: 'btn-danger',
|
||||
action: function () {
|
||||
$.post("/admin/databases/optimize",data={ids:checkID},function (res) {
|
||||
if (res.status == 200) {lightyear.notify(res.msg, 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');location.reload();}
|
||||
else lightyear.notify(res.msg, 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: '取消',
|
||||
btnClass: 'btn-primary'
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
$("#btn-repair").click(function () {
|
||||
var checkID = "";
|
||||
var selectedItem = $('#tb_departments').bootstrapTable('getAllSelections');
|
||||
if (selectedItem=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
for (var i = 0 ; i< selectedItem.length; i++)
|
||||
{
|
||||
checkID += selectedItem[i]['name']+",";
|
||||
}
|
||||
if (checkID=="") return lightyear.notify("没有选中项", 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
$.confirm({
|
||||
title: '重要提醒!',
|
||||
content: '您确认要执行该操作吗?',
|
||||
backgroundDismiss: true,
|
||||
buttons: {
|
||||
ok: {
|
||||
text: '确认',
|
||||
btnClass: 'btn-danger',
|
||||
action: function () {
|
||||
$.post("/admin/databases/repair",data={ids:checkID},function (res) {
|
||||
if (res.status == 200) {lightyear.notify(res.msg, 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');location.reload();}
|
||||
else lightyear.notify(res.msg, 'danger', 3000, 'mdi mdi-emoticon-neutral', 'top', 'center');
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: '取消',
|
||||
btnClass: 'btn-primary'
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>{$mate_title} - {:system_config("title")}</title>
|
||||
{include file="public/header" /}
|
||||
<link rel="stylesheet" href="__ADMIN_PATH__css/more.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid p-t-15">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<ul class="nav nav-tabs page-tabs">
|
||||
<li><a href="/admin/databases/index?type=export">备份数据库</a></li>
|
||||
<li class="active"><a href="#!">还原数据库</a> </li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<table id="tb_departments"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{include file="public/footer"/}
|
||||
<script type="text/javascript">
|
||||
// tree-grid使用
|
||||
var $treeTable = $('#tree-table');
|
||||
$treeTable.bootstrapTable({
|
||||
url: '/admin/category/lst',
|
||||
method: 'post',
|
||||
responseHandler: function (res) {
|
||||
return {
|
||||
"rows": res.data,
|
||||
};
|
||||
},
|
||||
queryParams: function(params) {
|
||||
var temp = toArrayList($(".searchForm").serializeArray());
|
||||
return temp;
|
||||
},
|
||||
idField: 'id',
|
||||
uniqueId: 'id',
|
||||
dataType: 'json',
|
||||
toolbar: '#toolbar2',
|
||||
showColumns: true, // 是否显示所有的列
|
||||
showRefresh: true, // 是否显示刷新按钮
|
||||
columns: [
|
||||
// {
|
||||
// field: 'check',
|
||||
// checkbox: true
|
||||
// },
|
||||
{
|
||||
field: 'title',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'display',
|
||||
title: '可见性',
|
||||
formatter: function (value, row, index) {
|
||||
if (value == 0) {
|
||||
is_checked = '';
|
||||
} else if (value == 1){
|
||||
is_checked = 'checked="checked"';
|
||||
}
|
||||
var field = "display";
|
||||
result = '<label class="lyear-switch switch-primary switch-solid lyear-status"><input type="checkbox" '+ is_checked +'><span onClick="updateStatus('+ row.id +', '+ value +', \''+ field +'\')"></span></label>';
|
||||
return result;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'operate',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
events : {
|
||||
'click .category-add': function (e, value, row, index) {
|
||||
location.href = '/admin/category/add?pid='+row.id;
|
||||
},
|
||||
'click .article-add': function (e, value, row, index) {
|
||||
location.href = '/admin/article/add?category_id='+row.id;
|
||||
},
|
||||
'click .category-delete': function (e, value, row, index) {
|
||||
$.alert({
|
||||
title: '系统提示',
|
||||
content: '删除提醒',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: '确认',
|
||||
btnClass: 'btn-primary',
|
||||
action: function(){
|
||||
$.post(url="/admin/category/del",data={"id":row.id},function (res) {
|
||||
if (res.code == 200) {parent.lightyear.notify('删除成功', 'success', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');$("#tree-table").bootstrapTable('refresh');}
|
||||
else parent.lightyear.notify('删除失败', 'danger', 3000, 'mdi mdi-emoticon-happy', 'top', 'center');
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: '取消'
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .category-edit': function (e, value, row, index) {
|
||||
iframe.createIframe('修改分类','/admin/category/edit?id='+row.id)
|
||||
}
|
||||
},
|
||||
formatter: operateFormatter
|
||||
}
|
||||
],
|
||||
treeShowField: 'title',
|
||||
parentIdField: 'pid',
|
||||
|
||||
onResetView: function(data) {
|
||||
$treeTable.treegrid({
|
||||
initialState: 'collapsed', // 所有节点都折叠
|
||||
treeColumn: 1,
|
||||
//expanderExpandedClass: 'mdi mdi-folder-open', // 可自定义图标样式
|
||||
//expanderCollapsedClass: 'mdi mdi-folder',
|
||||
onChange: function() {
|
||||
$treeTable.bootstrapTable('resetWidth');
|
||||
}
|
||||
});
|
||||
|
||||
// 只展开树形的第一集节点
|
||||
$treeTable.treegrid('getRootNodes').treegrid('expand');
|
||||
},
|
||||
onCheck: function(row) {
|
||||
var datas = $treeTable.bootstrapTable('getData');
|
||||
|
||||
selectChilds(datas, row, 'id', 'pid', true); // 选择子类
|
||||
|
||||
selectParentChecked(datas, row, 'id', 'pid'); // 选择父类
|
||||
|
||||
$treeTable.bootstrapTable('load', datas);
|
||||
},
|
||||
|
||||
onUncheck: function(row) {
|
||||
var datas = $treeTable.bootstrapTable('getData');
|
||||
selectChilds(datas, row, 'id', 'pid', false);
|
||||
$treeTable.bootstrapTable('load', datas);
|
||||
},
|
||||
});
|
||||
|
||||
// 操作按钮
|
||||
function operateFormatter(value, row, index) {
|
||||
return [
|
||||
'<a type="button" class="category-add btn btn-xs btn-default m-r-5" title="编辑" data-toggle="tooltip"><i class="mdi mdi-plus"></i></a>',
|
||||
'<a type="button" class="article-add btn btn-xs btn-default m-r-5" title="添加文章" data-toggle="tooltip"><i class="mdi mdi-pencil-box"></i></a>',
|
||||
'<a type="button" class="category-edit btn btn-xs btn-default m-r-5" title="修改" data-toggle="tooltip"><i class="mdi mdi-pencil"></i></a>',
|
||||
'<a type="button" class="category-delete btn btn-xs btn-default" title="删除" data-toggle="tooltip"><i class="mdi mdi-delete"></i></a>'
|
||||
].join('');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
use think\db;
|
||||
|
||||
//数据导出模型
|
||||
class Database{
|
||||
/**
|
||||
* 文件指针
|
||||
* @var resource
|
||||
*/
|
||||
private $fp;
|
||||
|
||||
/**
|
||||
* 备份文件信息 part - 卷号,name - 文件名
|
||||
* @var array
|
||||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* 当前打开文件大小
|
||||
* @var integer
|
||||
*/
|
||||
private $size = 0;
|
||||
|
||||
/**
|
||||
* 备份配置
|
||||
* @var integer
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 数据库备份构造方法
|
||||
* @param array $file 备份或还原的文件信息
|
||||
* @param array $config 备份配置信息
|
||||
* @param string $type 执行类型,export - 备份数据, import - 还原数据
|
||||
*/
|
||||
public function __construct($file, $config, $type = 'export'){
|
||||
$this->file = $file;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个卷,用于写入数据
|
||||
* @param integer $size 写入数据的大小
|
||||
*/
|
||||
private function open($size){
|
||||
if($this->fp){
|
||||
$this->size += $size;
|
||||
if($this->size > $this->config['part']){
|
||||
$this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
|
||||
$this->fp = null;
|
||||
$this->file['part']++;
|
||||
session('backup_file', $this->file);
|
||||
$this->create();
|
||||
}
|
||||
} else {
|
||||
$backuppath = $this->config['path'];
|
||||
$filename = "{$backuppath}{$this->file['name']}-{$this->file['part']}.sql";
|
||||
if($this->config['compress']){
|
||||
$filename = "{$filename}.gz";
|
||||
$this->fp = @gzopen($filename, "a{$this->config['level']}");
|
||||
} else {
|
||||
$this->fp = @fopen($filename, 'a');
|
||||
}
|
||||
$this->size = filesize($filename) + $size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入初始数据
|
||||
* @return boolean true - 写入成功,false - 写入失败
|
||||
*/
|
||||
public function create(){
|
||||
$type = config('database.default');
|
||||
$sql = "-- -----------------------------\n";
|
||||
$sql .= "-- Think MySQL Data Transfer \n";
|
||||
$sql .= "-- \n";
|
||||
$sql .= "-- Host : " . config('database.connections.'.$type.'.hostname') . "\n";
|
||||
$sql .= "-- Port : " . config('database.connections.'.$type.'.hostport'). "\n";
|
||||
$sql .= "-- Database : " . config('database.connections.'.$type.'.database'). "\n";
|
||||
$sql .= "-- \n";
|
||||
$sql .= "-- Part : #{$this->file['part']}\n";
|
||||
$sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n";
|
||||
$sql .= "-- -----------------------------\n\n";
|
||||
$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
|
||||
return $this->write($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入SQL语句
|
||||
* @param string $sql 要写入的SQL语句
|
||||
* @return boolean true - 写入成功,false - 写入失败!
|
||||
*/
|
||||
private function write($sql){
|
||||
$size = strlen($sql);
|
||||
|
||||
//由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%,
|
||||
//一般情况压缩率都会高于50%;
|
||||
$size = $this->config['compress'] ? $size / 2 : $size;
|
||||
|
||||
$this->open($size);
|
||||
return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份表结构
|
||||
* @param string $table 表名
|
||||
* @param integer $start 起始行数
|
||||
* @return boolean false - 备份失败
|
||||
*/
|
||||
public function backup($table, $start){
|
||||
//创建DB对象
|
||||
//备份表结构
|
||||
if(0 == $start){
|
||||
$result = Db::query("SHOW CREATE TABLE `{$table}`");
|
||||
$sql = "\n";
|
||||
$sql .= "-- -----------------------------\n";
|
||||
$sql .= "-- Table structure for `{$table}`\n";
|
||||
$sql .= "-- -----------------------------\n";
|
||||
$sql .= "DROP TABLE IF EXISTS `{$table}`;\n";
|
||||
$sql .= trim($result[0]['Create Table']) . ";\n\n";
|
||||
if(false === $this->write($sql)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//数据总数
|
||||
$result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`");
|
||||
$count = $result['0']['count'];
|
||||
|
||||
//备份表数据
|
||||
if($count){
|
||||
//写入数据注释
|
||||
if(0 == $start){
|
||||
$sql = "-- -----------------------------\n";
|
||||
$sql .= "-- Records of `{$table}`\n";
|
||||
$sql .= "-- -----------------------------\n";
|
||||
$this->write($sql);
|
||||
}
|
||||
|
||||
//备份数据记录
|
||||
$result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000",[]);
|
||||
foreach ($result as $row) {
|
||||
$sql = "INSERT INTO `{$table}` VALUES (";
|
||||
foreach ($row as $item){
|
||||
if($item===null){
|
||||
$sql=$sql.'null,';
|
||||
}
|
||||
else{
|
||||
$item=addslashes($item);
|
||||
$sql=$sql."'".str_replace(array("\r","\n"),array('\r','\n'),$item)."',";
|
||||
}
|
||||
}
|
||||
$sql=substr($sql,0,-1);
|
||||
$sql=$sql. ");\n";
|
||||
if(false === $this->write($sql)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//还有更多数据
|
||||
if($count > $start + 1000){
|
||||
return array($start + 1000, $count);
|
||||
}
|
||||
}
|
||||
|
||||
//备份下一表
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function import($start){
|
||||
//还原数据
|
||||
|
||||
if($this->config['compress']){
|
||||
$gz = gzopen($this->file[1], 'r');
|
||||
$size = 0;
|
||||
} else {
|
||||
$size = filesize($this->file[1]);
|
||||
$gz = fopen($this->file[1], 'r');
|
||||
}
|
||||
|
||||
$sql = '';
|
||||
if($start){
|
||||
$this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start);
|
||||
}
|
||||
|
||||
for($i = 0; $i < 1000; $i++){
|
||||
$sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz);
|
||||
if(preg_match('/.*;$/', trim($sql))){
|
||||
if(false !== Db::query($sql)){
|
||||
$start += strlen($sql);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
$sql = '';
|
||||
} elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return array($start, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 析构方法,用于关闭文件资源
|
||||
*/
|
||||
public function __destruct(){
|
||||
$this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"/>
|
||||
<title>跳转提示</title>
|
||||
<style type="text/css">
|
||||
*{ padding: 0; margin: 0; }
|
||||
body{ background: #FBFBFB; font-family: "Microsoft Yahei","Helvetica Neue",Helvetica,Arial,sans-serif; color: #333; font-size: 16px; }
|
||||
.system-message{ width:70%;padding: 24px 0; position: absolute;
|
||||
top:50%;
|
||||
left:15%;
|
||||
margin-top: -180px;
|
||||
}
|
||||
.system-message h1{ font-size: 100px; font-weight: normal; line-height: 120px; margin-bottom: 12px; color: #333;}
|
||||
.system-message .msg{
|
||||
padding: 10px 0;
|
||||
text-align: center; line-height: 1.8em; font-size: 28px;
|
||||
}
|
||||
.system-message .jump{ font-size: 14px; text-align: center; color: #666;}
|
||||
.system-message .jump a{ color: #666; }
|
||||
|
||||
.system-message .detail{ font-size: 12px; line-height: 20px; margin-top: 12px; display: none; }
|
||||
.icon{
|
||||
height:120px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.icon-error{
|
||||
background-image: url('/theme/ico_stop.png');
|
||||
}
|
||||
.icon-success{
|
||||
background-image: url('/theme/ico_success.png');
|
||||
}
|
||||
.copyright{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left:0;
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
font-size:12px;
|
||||
color: #bbb;
|
||||
}
|
||||
.copyright a{
|
||||
color: #bbb;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="system-message">
|
||||
<?php switch ($code) {?>
|
||||
<?php case 1:?>
|
||||
<h1 class="icon icon-success"></h1>
|
||||
<p class="msg success"><?php echo(strip_tags($msg));?></p>
|
||||
<?php break;?>
|
||||
<?php case 0:?>
|
||||
<h1 class="icon icon-error"></h1>
|
||||
<p class="msg error"><?php echo(strip_tags($msg));?></p>
|
||||
<?php break;?>
|
||||
<?php } ?>
|
||||
<p class="detail"></p>
|
||||
<p class="jump">
|
||||
页面自动 <a id="href" href="<?php echo($url);?>">跳转</a> 等待时间: <b id="wait"><?php echo($wait);?></b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
Power by HulaCWMS © <a href="http://www.zhuopro.com" target="_blank">灼灼文化</a>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function(){
|
||||
var wait = document.getElementById('wait'),
|
||||
href = document.getElementById('href').href;
|
||||
var interval = setInterval(function(){
|
||||
var time = --wait.innerHTML;
|
||||
if(time <= 0) {
|
||||
location.href = href;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, 1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -34,8 +34,8 @@ return [
|
|||
// 异常页面的模板文件
|
||||
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
|
||||
// 默认跳转页面对应的模板文件
|
||||
'dispatch_success_tmpl' => root_path(). 'extend/tpl/dispatch_jump.tpl',
|
||||
'dispatch_error_tmpl' => root_path(). 'extend/tpl/dispatch_jump.tpl',
|
||||
'dispatch_success_tmpl' => root_path(). 'app/common/tpl/dispatch_jump.tpl',
|
||||
'dispatch_error_tmpl' => root_path(). 'app/common/tpl/dispatch_jump.tpl',
|
||||
|
||||
// 错误显示信息,非调试模式有效
|
||||
'error_message' => '页面错误!请稍后再试~',
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1 @@
|
|||
1635613086
|
||||
Loading…
Reference in New Issue