You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Install.php 14KB

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