Database.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 海豚PHP框架 [ DolphinPHP ]
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2016~2019 广东卓锐软件有限公司 [ http://www.zrthink.com ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://dolphinphp.com
  8. // +----------------------------------------------------------------------
  9. namespace app\admin\controller;
  10. use app\common\builder\ZBuilder;
  11. use think\Db;
  12. use util\Database as DatabaseModel;
  13. /**
  14. * 数据库管理
  15. * @package app\admin\controller
  16. */
  17. class Database extends Admin
  18. {
  19. /**
  20. * 数据库管理
  21. * @param string $group 分组
  22. * @author 蔡伟明 <314013107@qq.com>
  23. * @return mixed
  24. * @throws \think\Exception
  25. */
  26. public function index($group = 'export')
  27. {
  28. // 配置分组信息
  29. $list_group = ['export' =>'备份数据库', 'import' => '还原数据库'];
  30. $tab_list = [];
  31. foreach ($list_group as $key => $value) {
  32. $tab_list[$key]['title'] = $value;
  33. $tab_list[$key]['url'] = url('index', ['group' => $key]);
  34. }
  35. switch ($group) {
  36. case 'export':
  37. $data_list = Db::query("SHOW TABLE STATUS");
  38. $data_list = array_map('array_change_key_case', $data_list);
  39. // 自定义按钮
  40. $btn_export = [
  41. 'title' => '立即备份',
  42. 'icon' => 'fa fa-fw fa-copy',
  43. 'class' => 'btn btn-primary ajax-post confirm',
  44. 'href' => url('export')
  45. ];
  46. $btn_optimize_all = [
  47. 'title' => '优化表',
  48. 'icon' => 'fa fa-fw fa-cogs',
  49. 'class' => 'btn btn-success ajax-post',
  50. 'href' => url('optimize')
  51. ];
  52. $btn_repair_all = [
  53. 'title' => '修复表',
  54. 'icon' => 'fa fa-fw fa-wrench',
  55. 'class' => 'btn btn-success ajax-post',
  56. 'href' => url('repair')
  57. ];
  58. $btn_optimize = [
  59. 'title' => '优化表',
  60. 'icon' => 'fa fa-fw fa-cogs',
  61. 'class' => 'btn btn-xs btn-default ajax-get',
  62. 'href' => url('optimize', ['ids' => '__id__'])
  63. ];
  64. $btn_repair = [
  65. 'title' => '修复表',
  66. 'icon' => 'fa fa-fw fa-wrench',
  67. 'class' => 'btn btn-xs btn-default ajax-get',
  68. 'href' => url('repair', ['ids' => '__id__'])
  69. ];
  70. // 使用ZBuilder快速创建数据表格
  71. return ZBuilder::make('table')
  72. ->setPageTitle('数据库管理') // 设置页面标题
  73. ->setPrimaryKey('name')
  74. ->setTabNav($tab_list, $group) // 设置tab分页
  75. ->addColumns([ // 批量添加数据列
  76. ['name', '表名'],
  77. ['rows', '行数'],
  78. ['data_length', '大小', 'byte'],
  79. ['data_free', '冗余', 'byte'],
  80. ['comment', '备注'],
  81. ['right_button', '操作', 'btn']
  82. ])
  83. ->addTopButton('custom', $btn_export) // 添加单个顶部按钮
  84. ->addTopButton('custom', $btn_optimize_all) // 添加单个顶部按钮
  85. ->addTopButton('custom', $btn_repair_all) // 添加单个顶部按钮
  86. ->addRightButton('custom', $btn_optimize) // 添加右侧按钮
  87. ->addRightButton('custom', $btn_repair) // 添加右侧按钮
  88. ->setRowList($data_list) // 设置表格数据
  89. ->fetch(); // 渲染模板
  90. break;
  91. case 'import':
  92. // 列出备份文件列表
  93. $path = config('data_backup_path');
  94. if(!is_dir($path)){
  95. mkdir($path, 0755, true);
  96. }
  97. $path = realpath($path);
  98. $flag = \FilesystemIterator::KEY_AS_FILENAME;
  99. $glob = new \FilesystemIterator($path, $flag);
  100. $data_list = [];
  101. foreach ($glob as $name => $file) {
  102. if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){
  103. $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
  104. $date = "{$name[0]}-{$name[1]}-{$name[2]}";
  105. $time = "{$name[3]}:{$name[4]}:{$name[5]}";
  106. $part = $name[6];
  107. if(isset($data_list["{$date} {$time}"])){
  108. $info = $data_list["{$date} {$time}"];
  109. $info['part'] = max($info['part'], $part);
  110. $info['size'] = $info['size'] + $file->getSize();
  111. } else {
  112. $info['part'] = $part;
  113. $info['size'] = $file->getSize();
  114. }
  115. $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
  116. $info['compress'] = ($extension === 'SQL') ? '-' : $extension;
  117. $info['time'] = strtotime("{$date} {$time}");
  118. $info['name'] = $info['time'];
  119. $data_list["{$date} {$time}"] = $info;
  120. }
  121. }
  122. $data_list = !empty($data_list) ? array_values($data_list) : $data_list;
  123. // 自定义按钮
  124. $btn_import = [
  125. 'title' => '还原',
  126. 'icon' => 'fa fa-fw fa-reply',
  127. 'class' => 'btn btn-xs btn-default ajax-get confirm',
  128. 'href' => url('import', ['time' => '__id__'])
  129. ];
  130. // 使用ZBuilder快速创建数据表格
  131. return ZBuilder::make('table')
  132. ->setPageTitle('数据库管理') // 设置页面标题
  133. ->setPrimaryKey('time')
  134. ->hideCheckbox()
  135. ->setTabNav($tab_list, $group) // 设置tab分页
  136. ->addColumns([ // 批量添加数据列
  137. ['name', '备份名称', 'datetime', '', 'Ymd-His'],
  138. ['part', '卷数'],
  139. ['compress', '压缩'],
  140. ['size', '数据大小', 'byte'],
  141. ['time', '备份时间', 'datetime', '', 'Y-m-d H:i:s'],
  142. ['right_button', '操作', 'btn']
  143. ])
  144. ->addRightButton('custom', $btn_import) // 添加右侧按钮
  145. ->addRightButton('delete') // 添加右侧按钮
  146. ->setRowList($data_list) // 设置表格数据
  147. ->fetch(); // 渲染模板
  148. break;
  149. }
  150. }
  151. /**
  152. * 备份数据库(参考onthink 麦当苗儿 <zuojiazi@vip.qq.com>)
  153. * @param null|array $ids 表名
  154. * @param integer $start 起始行数
  155. * @author 蔡伟明 <314013107@qq.com>
  156. */
  157. public function export($ids = null, $start = 0)
  158. {
  159. $tables = $ids;
  160. if ($this->request->isPost() && !empty($tables) && is_array($tables)) {
  161. // 初始化
  162. $path = config('data_backup_path');
  163. if(!is_dir($path)){
  164. mkdir($path, 0755, true);
  165. }
  166. // 读取备份配置
  167. $config = array(
  168. 'path' => realpath($path) . DIRECTORY_SEPARATOR,
  169. 'part' => config('data_backup_part_size'),
  170. 'compress' => config('data_backup_compress'),
  171. 'level' => config('data_backup_compress_level'),
  172. );
  173. // 检查是否有正在执行的任务
  174. $lock = "{$config['path']}backup.lock";
  175. if(is_file($lock)){
  176. $this->error('检测到有一个备份任务正在执行,请稍后再试!');
  177. } else {
  178. // 创建锁文件
  179. file_put_contents($lock, $this->request->time());
  180. }
  181. // 检查备份目录是否可写
  182. is_writeable($config['path']) || $this->error('备份目录不存在或不可写,请检查后重试!');
  183. // 生成备份文件信息
  184. $file = array(
  185. 'name' => date('Ymd-His', $this->request->time()),
  186. 'part' => 1,
  187. );
  188. // 创建备份文件
  189. $Database = new DatabaseModel($file, $config);
  190. if(false !== $Database->create()){
  191. // 备份指定表
  192. foreach ($tables as $table) {
  193. $start = $Database->backup($table, $start);
  194. while (0 !== $start) {
  195. if (false === $start) { // 出错
  196. $this->error('备份出错!');
  197. }
  198. $start = $Database->backup($table, $start[0]);
  199. }
  200. }
  201. // 备份完成,删除锁定文件
  202. unlink($lock);
  203. // 记录行为
  204. action_log('database_export', 'database', 0, UID, implode(',', $tables));
  205. $this->success('备份完成!');
  206. } else {
  207. $this->error('初始化失败,备份文件创建失败!');
  208. }
  209. } else {
  210. $this->error('参数错误!');
  211. }
  212. }
  213. /**
  214. * 还原数据库(参考onthink 麦当苗儿 <zuojiazi@vip.qq.com>)
  215. * @param int $time 文件时间戳
  216. * @author 蔡伟明 <314013107@qq.com>
  217. */
  218. public function import($time = 0)
  219. {
  220. if ($time === 0) $this->error('参数错误!');
  221. // 初始化
  222. $name = date('Ymd-His', $time) . '-*.sql*';
  223. $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
  224. $files = glob($path);
  225. $list = array();
  226. foreach($files as $name){
  227. $basename = basename($name);
  228. $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
  229. $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename);
  230. $list[$match[6]] = array($match[6], $name, $gz);
  231. }
  232. ksort($list);
  233. // 检测文件正确性
  234. $last = end($list);
  235. if(count($list) === $last[0]){
  236. foreach ($list as $item) {
  237. $config = [
  238. 'path' => realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR,
  239. 'compress' => $item[2]
  240. ];
  241. $Database = new DatabaseModel($item, $config);
  242. $start = $Database->import(0);
  243. // 循环导入数据
  244. while (0 !== $start) {
  245. if (false === $start) { // 出错
  246. $this->error('还原数据出错!');
  247. }
  248. $start = $Database->import($start[0]);
  249. }
  250. }
  251. // 记录行为
  252. action_log('database_import', 'database', 0, UID, date('Ymd-His', $time));
  253. $this->success('还原完成!');
  254. } else {
  255. $this->error('备份文件可能已经损坏,请检查!');
  256. }
  257. }
  258. /**
  259. * 优化表
  260. * @param null|string|array $ids 表名
  261. * @author 蔡伟明 <314013107@qq.com>
  262. */
  263. public function optimize($ids = null)
  264. {
  265. $tables = $ids;
  266. if($tables) {
  267. if(is_array($tables)){
  268. $tables = implode('`,`', $tables);
  269. $list = Db::query("OPTIMIZE TABLE `{$tables}`");
  270. if($list){
  271. // 记录行为
  272. action_log('database_optimize', 'database', 0, UID, "`{$tables}`");
  273. $this->success("数据表优化完成!");
  274. } else {
  275. $this->error("数据表优化出错请重试!");
  276. }
  277. } else {
  278. $list = Db::query("OPTIMIZE TABLE `{$tables}`");
  279. if($list){
  280. // 记录行为
  281. action_log('database_optimize', 'database', 0, UID, $tables);
  282. $this->success("数据表'{$tables}'优化完成!");
  283. } else {
  284. $this->error("数据表'{$tables}'优化出错请重试!");
  285. }
  286. }
  287. } else {
  288. $this->error("请选择要优化的表!");
  289. }
  290. }
  291. /**
  292. * 修复表
  293. * @param null|string|array $ids 表名
  294. * @author 蔡伟明 <314013107@qq.com>
  295. */
  296. public function repair($ids = null)
  297. {
  298. $tables = $ids;
  299. if($tables) {
  300. if(is_array($tables)){
  301. $tables = implode('`,`', $tables);
  302. $list = Db::query("REPAIR TABLE `{$tables}`");
  303. if($list){
  304. // 记录行为
  305. action_log('database_repair', 'database', 0, UID, "`{$tables}`");
  306. $this->success("数据表修复完成!");
  307. } else {
  308. $this->error("数据表修复出错请重试!");
  309. }
  310. } else {
  311. $list = Db::query("REPAIR TABLE `{$tables}`");
  312. if($list){
  313. // 记录行为
  314. action_log('database_repair', 'database', 0, UID, $tables);
  315. $this->success("数据表'{$tables}'修复完成!");
  316. } else {
  317. $this->error("数据表'{$tables}'修复出错请重试!");
  318. }
  319. }
  320. } else {
  321. $this->error("请指定要修复的表!");
  322. }
  323. }
  324. /**
  325. * 删除备份文件
  326. * @param int $ids 备份时间
  327. * @author 蔡伟明 <314013107@qq.com>
  328. * @return mixed
  329. */
  330. public function delete($ids = 0)
  331. {
  332. if ($ids == 0) $this->error('参数错误!');
  333. $name = date('Ymd-His', $ids) . '-*.sql*';
  334. $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
  335. array_map("unlink", glob($path));
  336. if(count(glob($path))){
  337. $this->error('备份文件删除失败,请检查权限!');
  338. } else {
  339. // 记录行为
  340. action_log('database_backup_delete', 'database', 0, UID, date('Ymd-His', $ids));
  341. $this->success('备份文件删除成功!');
  342. }
  343. }
  344. }