Install.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\command;
  12. use app\services\system\admin\SystemAdminServices;
  13. use app\services\system\config\SystemConfigServices;
  14. use crmeb\services\MysqlBackupService;
  15. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  16. use think\console\Command;
  17. use think\console\Input;
  18. use think\console\input\Argument;
  19. use think\console\input\Option;
  20. use think\console\Output;
  21. use think\console\Table;
  22. class Install extends Command
  23. {
  24. protected $host;
  25. protected function configure()
  26. {
  27. $this->setName('install')
  28. ->addArgument('status', Argument::OPTIONAL, 'start/remove')
  29. ->addOption('h', null, Option::VALUE_REQUIRED, '网站域名')
  30. ->addOption('db', null, Option::VALUE_NONE, '是否卸载数据库')
  31. ->setDescription('命令行一键安装/卸载');
  32. }
  33. protected function execute(Input $input, Output $output)
  34. {
  35. $status = $input->getArgument('status') ?: 'start';
  36. $database = env('database.database');
  37. if (in_array($status, ['start', 'remove'])) {
  38. if (!env('database.hostname') ||
  39. !$database ||
  40. !env('database.username') ||
  41. !env('database.password') ||
  42. !env('database.hostport')
  43. ) {
  44. $output->error('请先配置数据库,确保数据库能正常连接!');
  45. return;
  46. }
  47. }
  48. if ($input->hasOption('h')) {
  49. $this->host = $input->getOption('h');
  50. }
  51. if (!$status || !in_array($status, ['start', 'remove'])) {
  52. $question = $output->choice($input, ' 请选择执行指令数字 ', ['start' => '1', 'remove' => '-1', 'exit' => 0]);
  53. if ($question === 'exit') {
  54. $output->info('您已退出命令');
  55. return;
  56. } else {
  57. $status = $question;
  58. }
  59. }
  60. if ($status === 'remove') {
  61. $this->remove($input, $output);
  62. } elseif ($status === 'start') {
  63. $this->start($input, $output);
  64. }
  65. return;
  66. }
  67. /**
  68. * 开始安装
  69. * @param Input $input
  70. * @param Output $output
  71. * @throws \think\db\exception\BindParamException
  72. */
  73. protected function start(Input $input, Output $output)
  74. {
  75. $installLockDir = root_path('public') . 'install/install.lock';
  76. if (file_exists($installLockDir)) {
  77. $crmeb = get_crmeb_version();
  78. $question = $output->confirm($input, '您已经安装' . $crmeb . '版本是否重新安装,重新安装会清除掉之前的数据请谨慎操作?', false);
  79. if ($question) {
  80. $res = $this->authBackups();
  81. if (!$res) {
  82. $output->info('已退出安装程序');
  83. }
  84. $database = env('database.database');
  85. $this->dropTable($database);
  86. unlink($installLockDir);
  87. } else {
  88. $output->info('已退出安装程序');
  89. return;
  90. }
  91. }
  92. $installSql = file_get_contents(root_path('public' . DIRECTORY_SEPARATOR . 'install') . 'crmeb.sql');
  93. if (!$installSql) {
  94. $output->error('读取安装sql失败,请检查安装sql文件权限,再次尝试安装!');
  95. return;
  96. }
  97. $this->query($installSql);
  98. $this->cleanTable();
  99. $this->output->writeln('+---------------------------- [创建管理员] ---------------------------------+');
  100. $this->output->newLine();
  101. [$account, $password] = $this->createAdmin();
  102. file_put_contents($installLockDir, time());
  103. $output->info('安装完成!!请妥善保管您的账号密码!');
  104. $output->info('账号:' . $account);
  105. $output->info('密码:' . $password);
  106. return;
  107. }
  108. /**
  109. * 创建账号
  110. */
  111. protected function createAdmin()
  112. {
  113. $account = $this->adminAccount();
  114. $password = $this->adminPassword();
  115. /** @var SystemAdminServices $service */
  116. $service = app()->make(SystemAdminServices::class);
  117. $tablepre = env('database.prefix');
  118. $this->app->db->query('truncate table ' . $tablepre . 'system_admin');
  119. try {
  120. $service->create(['conf_pwd' => $password, 'roles' => [1], 'pwd' => $password, 'account' => $account, 'level' => 0, 'status' => 1]);
  121. } catch (\Throwable $e) {
  122. $this->output->writeln($e->getMessage());
  123. $this->output->newLine();
  124. [$account, $password] = $this->createAdmin();
  125. }
  126. if ($this->host) {
  127. /** @var SystemConfigServices $configService */
  128. $configService = app()->make(SystemConfigServices::class);
  129. $configService->update('site_url', ['value' => json_encode($this->host)], 'menu_name');
  130. }
  131. return [$account, $password];
  132. }
  133. /**
  134. * 卸载程序
  135. * @param Input $input
  136. * @param Output $output
  137. * @throws \think\db\exception\BindParamException
  138. */
  139. protected function remove(Input $input, Output $output)
  140. {
  141. $installLockDir = root_path('public') . 'install/install.lock';
  142. if (!file_exists($installLockDir)) {
  143. $this->output->info('你尚未安装本程序');
  144. return;
  145. }
  146. $database = env('database.database');
  147. $output->info(' 正在进行卸载中...');
  148. $this->authBackups();
  149. if ($input->hasOption('db')) {
  150. $question = $output->confirm($input, '您确定要清除掉[' . $database . ']数据吗?', false);
  151. if ($question) {
  152. $this->dropTable($database);
  153. }
  154. }
  155. unlink($installLockDir);
  156. $this->output->info('卸载完成');
  157. return;
  158. }
  159. /**
  160. * 清除所有表数据
  161. * @param string $database
  162. */
  163. protected function dropTable(string $database)
  164. {
  165. $this->output->writeln('+---------------------------- [清理表数据] ---------------------------------+');
  166. $this->output->newLine();
  167. $this->output->write("\r 正在清理表数据");
  168. /** @var MysqlBackupService $service */
  169. $service = app()->make(MysqlBackupService::class, [[
  170. //数据库备份卷大小
  171. 'compress' => 1,
  172. //数据库备份文件是否启用压缩 0不压缩 1 压缩
  173. 'level' => 5,
  174. ]]);
  175. $dataList = $service->dataList();
  176. $tableName = array_column($dataList, 'name');
  177. $count = count($tableName);
  178. if ($count) {
  179. $res = $this->app->db->transaction(function () use ($database, $tableName) {
  180. foreach ($tableName as $name) {
  181. $this->app->db->query('DROP TABLE ' . $name);
  182. }
  183. });
  184. }
  185. $this->output->write("\r 已清理完毕");
  186. $this->output->newLine(2);
  187. return $res;
  188. }
  189. /**
  190. * 执行安装sql
  191. * @param string $installSql
  192. */
  193. protected function query(string $installSql)
  194. {
  195. $tablepre = env('database.prefix');
  196. $sqlArray = $this->sqlSplit($installSql, $tablepre);
  197. $table = new Table();
  198. $this->output->writeln('+----------------------------- [SQL安装] -----------------------------------+');
  199. $this->output->newLine();
  200. $header = ['表名', '执行结果', '错误原因', '时间'];
  201. $table->setHeader($header);
  202. foreach ($sqlArray as $sql) {
  203. $sql = trim($sql);
  204. if (strstr($sql, 'CREATE TABLE')) {
  205. preg_match('/CREATE TABLE (IF NOT EXISTS)? `eb_([^ ]*)`/is', $sql, $matches);
  206. $tableName = $tablepre . ($matches[2] ?? '');
  207. } else {
  208. $tableName = '';
  209. }
  210. try {
  211. $this->app->db->transaction(function () use ($tablepre, $sql, $tableName) {
  212. $sql = str_replace('`eb_', '`' . $tablepre, $sql);//替换表前缀
  213. $this->app->db->query($sql);
  214. });
  215. $tableName && $table->addRow([$tableName, 'ok', '无错误', date('Y-m-d H:i:s')]);
  216. } catch (\Throwable $e) {
  217. $tableName && $table->addRow([$tableName, 'x', $e->getMessage(), date('Y-m-d H:i:s')]);
  218. }
  219. }
  220. $this->output->writeln($table->render());
  221. $this->output->newLine(2);
  222. }
  223. /**
  224. * 账号
  225. * @return bool|mixed|string
  226. */
  227. protected function adminAccount()
  228. {
  229. $account = $this->output->ask($this->input, '请输入后台登陆账号,最少4个字符', null, function ($value) {
  230. if (strlen($value) < 4) {
  231. return false;
  232. }
  233. return $value;
  234. });
  235. if (!$account) {
  236. $this->output->error('账号至少4个字符');
  237. $this->output->newLine();
  238. $account = $this->adminAccount();
  239. }
  240. return $account;
  241. }
  242. /**
  243. * 密码
  244. * @return bool|mixed|string|null
  245. */
  246. protected function adminPassword()
  247. {
  248. $password = $this->output->ask($this->input, '请输入登陆密码,密码为数字加字母/字母加符号/数字加字符的组合不能少于6位');
  249. if (!preg_match('/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^<>&*]+$)[a-zA-Z\d!@#$%^&<>*]{6,16}$/', $password)) {
  250. $this->output->error('请输入符合要求的密码');
  251. $this->output->newLine();
  252. $password = $this->adminPassword();
  253. }
  254. return $password;
  255. }
  256. /**
  257. * 清楚多余数据
  258. */
  259. protected function cleanTable()
  260. {
  261. $tablepre = env('database.prefix');
  262. $database = env('database.database');
  263. $blTable = ['eb_system_admin', 'eb_system_role', 'eb_system_config', 'eb_system_config_tab',
  264. 'eb_system_menus', 'eb_system_file', 'eb_express', 'eb_system_group', 'eb_system_group_data',
  265. 'eb_template_message', 'eb_shipping_templates', "eb_shipping_templates_region",
  266. "eb_shipping_templates_free", 'eb_system_city', 'eb_diy', 'eb_member_ship', 'eb_member_right',
  267. 'eb_agreement', 'eb_store_service_speechcraft', 'eb_system_user_level', 'eb_cache'];
  268. if ($tablepre !== 'eb_') {
  269. $blTable = array_map(function ($name) use ($tablepre) {
  270. return str_replace('eb_', $tablepre, $name);
  271. }, $blTable);
  272. }
  273. $tableList = $this->app->db->query("select table_name from information_schema.tables where table_schema='$database'");
  274. $tableList = array_column($tableList, 'table_name');
  275. foreach ($tableList as $table) {
  276. if (!in_array($table, $blTable)) {
  277. $this->app->db->query('truncate table ' . $table);
  278. }
  279. }
  280. }
  281. /**
  282. * 切割sql
  283. * @param $sql
  284. * @return array
  285. */
  286. protected function sqlSplit(string $sql, string $tablepre)
  287. {
  288. if ($tablepre != "tp_")
  289. $sql = str_replace("tp_", $tablepre, $sql);
  290. $sql = preg_replace("/TYPE=(InnoDB|MyISAM|MEMORY)( DEFAULT CHARSET=[^; ]+)?/", "ENGINE=\\1 DEFAULT CHARSET=utf8", $sql);
  291. $sql = str_replace("\r", "\n", $sql);
  292. $ret = [];
  293. $num = 0;
  294. $queriesarray = explode(";\n", trim($sql));
  295. unset($sql);
  296. foreach ($queriesarray as $query) {
  297. $ret[$num] = '';
  298. $queries = explode("\n", trim($query));
  299. $queries = array_filter($queries);
  300. foreach ($queries as $query) {
  301. $str1 = substr($query, 0, 1);
  302. if ($str1 != '#' && $str1 != '-')
  303. $ret[$num] .= $query;
  304. }
  305. $num++;
  306. }
  307. return $ret;
  308. }
  309. /**
  310. * 自动备份表
  311. * @return bool|mixed|string|null
  312. * @throws \think\db\exception\BindParamException
  313. */
  314. protected function authBackups(bool $g = false)
  315. {
  316. /** @var MysqlBackupService $service */
  317. $service = app()->make(MysqlBackupService::class, [[
  318. //数据库备份卷大小
  319. 'compress' => 1,
  320. //数据库备份文件是否启用压缩 0不压缩 1 压缩
  321. 'level' => 5,
  322. ]]);
  323. $dataList = $service->dataList();
  324. $tableName = array_column($dataList, 'name');
  325. $count = count($tableName);
  326. if ($count) {
  327. $this->output->writeln('+----------------------------- [自动备份] ----------------------------------+');
  328. $this->output->newLine();
  329. $this->output->newLine();
  330. $this->output->writeln(' 正在自动备份[start]');
  331. $data = [];
  332. foreach ($tableName as $i => $t) {
  333. // $equalStr = str_repeat("=", $i);
  334. // $space = str_repeat(" ", $count - $i);
  335. // $this->output->write("\r [$equalStr>$space]($i/$count%)");
  336. $this->output->writeln(' 已备份:' . $t . ' 完成:(' . ($i + 1) . '/' . $count . ')');
  337. $res = $service->backup($t, 0);
  338. if ($res == false && $res != 0) {
  339. $data [] = $t;
  340. }
  341. }
  342. $this->output->writeln("\r\n 备份结束[end]");
  343. if ($data && $g) {
  344. return $this->output->confirm($this->input, '自动备份表失败,失败数据库表:' . implode('|', $data) . ';是否继续执行?');
  345. }
  346. $this->output->newLine();
  347. }
  348. return true;
  349. }
  350. /**
  351. * 创建进度条
  352. * @param $percent
  353. * @return string
  354. */
  355. protected function buildLine($percent)
  356. {
  357. $repeatTimes = 100;
  358. if ($percent > 0) {
  359. $hasColor = str_repeat('■', $percent);
  360. } else {
  361. $hasColor = '';
  362. }
  363. if ($repeatTimes - $percent > 0) {
  364. $noColor = str_repeat(' ', $repeatTimes - $percent);
  365. } else {
  366. $noColor = '';
  367. }
  368. $buffer = sprintf("[{$hasColor}{$noColor}]");
  369. if ($percent !== 100) {
  370. $percentString = sprintf("[ %-6s]", $percent . '%');
  371. } else {
  372. $percentString = sprintf("[ %-5s]", 'OK');;
  373. }
  374. return $percentString . $buffer . "\r";
  375. }
  376. }