UpgradeServices.php 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\services\system;
  12. use think\facade\Config;
  13. use think\facade\Db;
  14. use think\facade\Log;
  15. use app\jobs\UpgradeJob;
  16. use app\services\BaseServices;
  17. use crmeb\services\FileService;
  18. use crmeb\services\HttpService;
  19. use crmeb\services\CacheService;
  20. use crmeb\utils\fileVerification;
  21. use crmeb\exceptions\AdminException;
  22. use app\dao\system\upgrade\UpgradeLogDao;
  23. /**
  24. * 在线升级
  25. * Class UpgradeServices
  26. * @package app\services\system
  27. */
  28. class UpgradeServices extends BaseServices
  29. {
  30. const LOGIN_URL = 'http://upgrade.crmeb.net/api/login';
  31. const UPGRADE_URL = 'http://upgrade.crmeb.net/api/upgrade/list';
  32. const UPGRADE_CURRENT_URL = 'http://upgrade.crmeb.net/api/upgrade/current_list';
  33. const AGREEMENT_URL = 'http://upgrade.crmeb.net/api/upgrade/agreement';
  34. const PACKAGE_DOWNLOAD_URL = 'http://upgrade.crmeb.net/api/upgrade/download';
  35. const UPGRADE_STATUS_URL = 'http://upgrade.crmeb.net/api/upgrade/status';
  36. const UPGRADE_LOG_URL = 'http://upgrade.crmeb.net/api/upgrade/log';
  37. /**
  38. * @var array $requestData
  39. */
  40. private $requestData = [];
  41. /**
  42. * @var int $timeStamp
  43. */
  44. private $timeStamp = 0;
  45. /**
  46. * UpgradeServices constructor.
  47. * @param UpgradeLogDao $dao
  48. */
  49. public function __construct(UpgradeLogDao $dao)
  50. {
  51. // $versionData = $this->getVersion();
  52. // if ($versionData['version_code'] < 450) return true;
  53. // if (empty($versionData)) {
  54. // throw new AdminException('授权信息丢失');
  55. // }
  56. //
  57. // $this->timeStamp = time();
  58. // $recVersion = $this->recombinationVersion($versionData['version'] ?? '');
  59. // $this->dao = $dao;
  60. //
  61. // $this->requestData = [
  62. // 'nonce' => mt_rand(111, 999),
  63. // 'host' => app()->request->host(),
  64. // 'timestamp' => $this->timeStamp,
  65. // 'app_id' => trim($versionData['app_id'] ?? ''),
  66. // 'app_key' => trim($versionData['app_key'] ?? ''),
  67. // 'version' => implode('.', $recVersion)
  68. // ];
  69. //
  70. // if (!CacheService::get('upgrade_auth_token')) {
  71. // $this->getAuth();
  72. // }
  73. }
  74. /**
  75. * 获取版本信息
  76. * @return void
  77. */
  78. /**
  79. * 获取文件配置信息
  80. * @param string $name
  81. * @param string $path
  82. * @return array|string
  83. */
  84. public function getVersion(string $name = '', string $path = '')
  85. {
  86. $file = '.version';
  87. $arr = [];
  88. $list = @file($path ?: app()->getRootPath() . $file);
  89. foreach ($list as $val) {
  90. list($k, $v) = explode('=', str_replace(PHP_EOL, '', $val));
  91. $arr[$k] = $v;
  92. }
  93. return !empty($name) ? $arr[$name] ?? '' : $arr;
  94. }
  95. /**
  96. * 获取版本号
  97. * @param $input
  98. * @return array
  99. */
  100. public function recombinationVersion($input): array
  101. {
  102. $version = substr($input, strpos($input, ' v') + 1);
  103. return array_map(function ($item) {
  104. if (preg_match('/\d+/', $item, $arr)) {
  105. $item = $arr[0];
  106. }
  107. return (int)$item;
  108. }, explode('.', $version));
  109. }
  110. /**
  111. * 获取Token
  112. * @return void
  113. */
  114. public function getAuth()
  115. {
  116. $this->getSign($this->timeStamp);
  117. $result = HttpService::postRequest(self::LOGIN_URL, $this->requestData);
  118. if (!$result) {
  119. throw new AdminException('授权失败');
  120. }
  121. $authData = json_decode($result, true);
  122. if (!isset($authData['status']) || $authData['status'] != 200) {
  123. Log::error(['msg' => $authData['msg'] ?? '', 'error' => $authData['data'] ?? []]);
  124. throw new AdminException($authData['msg'] ?? '授权失败');
  125. }
  126. CacheService::set('upgrade_auth_token', $authData['data']['access_token'], 7200);
  127. }
  128. /**
  129. * 获取签名
  130. * @param int $timeStamp
  131. * @return void
  132. */
  133. public function getSign(int $timeStamp)
  134. {
  135. $data = $this->requestData;
  136. if ((!isset($data['host']) || !$data['host']) ||
  137. (!isset($data['nonce']) || !$data['nonce']) ||
  138. (!isset($data['app_id']) || !$data['app_id']) ||
  139. (!isset($data['version']) || !$data['version']) ||
  140. (!isset($data['app_key']) || !$data['app_key'])) {
  141. throw new AdminException('验证失效,请重新请求');
  142. }
  143. $host = $data['host'];
  144. $nonce = $data['nonce'];
  145. $appId = $data['app_id'];
  146. $appKey = $data['app_key'];
  147. $version = $data['version'];
  148. unset($data['sign'], $data['nonce'], $data['host'], $data['version'], $data['app_id'], $data['app_key']);
  149. $params = json_encode($data);
  150. $shaiAtt = [
  151. 'host' => $host,
  152. 'nonce' => $nonce,
  153. 'app_id' => $appId,
  154. 'params' => $params,
  155. 'app_key' => $appKey,
  156. 'version' => $version,
  157. 'time_stamp' => $timeStamp
  158. ];
  159. sort($shaiAtt, SORT_STRING);
  160. $shaiStr = implode(',', $shaiAtt);
  161. $this->requestData['sign'] = hash("SHA256", $shaiStr);
  162. }
  163. /**
  164. * 升级列表
  165. * @return mixed
  166. */
  167. public function getUpgradeList()
  168. {
  169. [$page, $limit] = $this->getPageValue();
  170. $this->requestData['page'] = (string)($page ?: 1);
  171. $this->requestData['limit'] = (string)($limit ?: 10);
  172. $this->getSign($this->timeStamp);
  173. $result = HttpService::getRequest(self::UPGRADE_URL, $this->requestData);
  174. if (!$result) {
  175. throw new AdminException('升级列表获取失败');
  176. }
  177. $data = json_decode($result, true);
  178. if (!$this->checkAuth($data)) {
  179. throw new AdminException($data['msg'] ?? '升级列表获取失败');
  180. }
  181. return $data['data'] ?? [];
  182. }
  183. /**
  184. * 可升级列表
  185. * @return mixed
  186. */
  187. public function getUpgradeableList()
  188. {
  189. $this->getSign($this->timeStamp);
  190. $result = HttpService::getRequest(self::UPGRADE_CURRENT_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  191. if (!$result) {
  192. throw new AdminException('可升级列表获取失败');
  193. }
  194. $data = json_decode($result, true);
  195. if (!$this->checkAuth($data)) {
  196. throw new AdminException($data['msg'] ?? '升级列表获取失败');
  197. }
  198. return $data['data'] ?? [];
  199. }
  200. /**
  201. * 升级协议
  202. * @return mixed
  203. */
  204. public function getAgreement()
  205. {
  206. $this->getSign($this->timeStamp);
  207. $result = HttpService::getRequest(self::AGREEMENT_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  208. if (!$result) {
  209. throw new AdminException('升级协议获取失败');
  210. }
  211. $data = json_decode($result, true);
  212. if (!$this->checkAuth($data)) {
  213. throw new AdminException($data['msg'] ?? '升级协议获取失败');
  214. }
  215. return $data['data'] ?? [];
  216. }
  217. /**
  218. * 下载
  219. * @param string $packageKey
  220. * @return bool
  221. */
  222. public function packageDownload(string $packageKey): bool
  223. {
  224. $token = md5(time());
  225. //检查数据库大小
  226. $this->checkDatabaseSize();
  227. //核对项目签名
  228. $this->checkSignature();
  229. $this->requestData['package_key'] = $packageKey;
  230. $this->getSign($this->timeStamp);
  231. $result = HttpService::getRequest(self::PACKAGE_DOWNLOAD_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  232. if (!$result) {
  233. throw new AdminException('升级包获取失败');
  234. }
  235. $data = json_decode($result, true);
  236. if (!$this->checkAuth($data)) {
  237. throw new AdminException($data['msg'] ?? '授权失败');
  238. }
  239. if (empty($data['data']['server_package_link']) && empty($data['data']['client_package_link']) && empty($data['data']['pc_package_link'])) {
  240. CacheService::set($token . 'upgrade_status', 2, 86400);
  241. return true;
  242. }
  243. if (!empty($data['data']['server_package_link'])) {
  244. $this->downloadFile($data['data']['server_package_link'], $token . '_server_package');
  245. } else {
  246. CacheService::set($token . '_server_package', 2, 86400);
  247. }
  248. if (!empty($data['data']['client_package_link'])) {
  249. $this->downloadFile($data['data']['client_package_link'], $token . '_client_package');
  250. } else {
  251. CacheService::set($token . '_client_package', 2, 86400);
  252. }
  253. if (!empty($data['data']['pc_package_link'])) {
  254. $this->downloadFile($data['data']['pc_package_link'], $token . '_pc_package');
  255. } else {
  256. CacheService::set($token . '_pc_package', 2, 86400);
  257. }
  258. CacheService::set($token . '_database_backup', 1, 86400);
  259. UpgradeJob::dispatch('databaseBackup', [$token]);
  260. CacheService::set($token . '_project_backup', 1, 86400);
  261. UpgradeJob::dispatch('projectBackup', [$token]);
  262. CacheService::set('upgrade_token', $token, 86400);
  263. CacheService::set($token . '_upgrade_data', $data, 86400);
  264. return true;
  265. }
  266. /**
  267. * 执行下载
  268. * @param string $seq
  269. * @param string $url
  270. * @param string $downloadPath
  271. * @param string $fileName
  272. * @param int $timeout
  273. * @return void
  274. */
  275. public function download(string $seq, string $url, string $downloadPath, string $fileName, int $timeout = 300)
  276. {
  277. ini_set('memory_limit', '-1');
  278. $fp_output = fopen($downloadPath . DS . $fileName, 'w');
  279. $ch = curl_init();
  280. curl_setopt($ch, CURLOPT_URL, $url);
  281. curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
  282. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
  283. curl_setopt($ch, CURLOPT_FILE, $fp_output);
  284. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  285. if (stripos($url, "https://") !== FALSE) curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  286. curl_exec($ch);
  287. curl_close($ch);
  288. if (strpos($fileName, 'zip') === false) {
  289. throw new AdminException('安装包格式错误');
  290. }
  291. /** @var FileService $fileService */
  292. $fileService = app()->make(FileService::class);
  293. $downloadFilePath = $downloadPath . DS . substr($fileName, 0, strpos($fileName, 'zip') - 1);
  294. if (!$fileService->extractFile($downloadPath . DS . $fileName, $downloadFilePath)) {
  295. throw new AdminException('升级包解压失败');
  296. }
  297. CacheService::set($seq . '_path', $downloadFilePath, 86400);
  298. CacheService::set($seq . '_name', $downloadPath . DS . $fileName, 86400);
  299. CacheService::set($seq, 2, 86400);
  300. }
  301. /**
  302. * 开始下载
  303. * @param string $packageLink
  304. * @param string $seq
  305. * @return void
  306. */
  307. private function downloadFile(string $packageLink, string $seq)
  308. {
  309. $fileName = substr($packageLink, strrpos($packageLink, '/') + 1);
  310. $filePath = app()->getRootPath() . 'upgrade' . DS . date('Y-m-d');;
  311. if (!is_dir($filePath)) mkdir($filePath, 0755, true);
  312. UpgradeJob::dispatch('download', [$seq, $packageLink, $filePath, $fileName, 300]);
  313. CacheService::set($seq, 1, 86400);
  314. }
  315. /**
  316. * 升级进度
  317. * @return array
  318. */
  319. public function getProgress(): array
  320. {
  321. $token = CacheService::get('upgrade_token');
  322. if (empty($token)) {
  323. throw new AdminException('请重新升级');
  324. }
  325. $serverProgress = CacheService::get($token . '_server_package'); // 服务端包下载进度
  326. $clientProgress = CacheService::get($token . '_client_package'); // 客户端包下载进度
  327. $pcProgress = CacheService::get($token . '_pc_package'); // PC端包下载进度
  328. $databaseBackupProgress = CacheService::get($token . '_database_backup'); // 数据库备份进度
  329. $projectBackupProgress = CacheService::get($token . '_project_backup'); // 项目备份备份进度
  330. $databaseUpgradeProgress = CacheService::get($token . '_database_upgrade'); // 数据库升级进度
  331. $coverageProjectProgress = CacheService::get($token . '_coverage_project'); // 项目覆盖进度
  332. $stepNum = 1;
  333. $tip = '开始升级';
  334. if ($serverProgress == $clientProgress && $clientProgress == $pcProgress) {
  335. $tip = $serverProgress == 1 ? '开始下载安装包' : '安装包下载完成';
  336. if ($serverProgress == 2) {
  337. $stepNum += 1;
  338. }
  339. } else {
  340. $tip = '正在下载安装包';
  341. }
  342. if ($databaseBackupProgress == 2) {
  343. $tip = '数据库备份完成';
  344. $stepNum += 1;
  345. }
  346. if ($projectBackupProgress == 2) {
  347. $tip = '项目备份完成';
  348. $stepNum += 1;
  349. }
  350. if ((int)$databaseUpgradeProgress == 2) {
  351. $tip = '数据库升级完成';
  352. $stepNum += 1;
  353. }
  354. if ((int)$coverageProjectProgress == 2) {
  355. $tip = '项目升级完成';
  356. $stepNum += 1;
  357. }
  358. $upgradeStatus = (int)CacheService::get($token . 'upgrade_status');
  359. if ($upgradeStatus == 2) {
  360. $stepNum = 6;
  361. $tip = '升级完成';
  362. } elseif ($upgradeStatus < 0) {
  363. $this->saveLog($token);
  364. throw new AdminException(CacheService::get($token . 'upgrade_status_tip', '升级失败'));
  365. } elseif ($serverProgress == 2 && $clientProgress == 2 && $pcProgress == 2 && $databaseBackupProgress == 2 && $projectBackupProgress == 2) {
  366. try {
  367. $this->overwriteProject();
  368. } catch (\Exception $e) {
  369. $this->sendUpgradeLog($token);
  370. }
  371. }
  372. $speed = sprintf("%.1f", $stepNum / 6 * 100);
  373. return compact('speed', 'tip');
  374. }
  375. /**
  376. * 数据库备份
  377. * @param $token
  378. * @return bool
  379. * @throws \think\db\exception\BindParamException
  380. */
  381. public function databaseBackup($token): bool
  382. {
  383. try {
  384. //备份表数据
  385. /** @var SystemDatabackupServices $backServices */
  386. $backServices = app()->make(SystemDatabackupServices::class);
  387. $tables = $backServices->getDataList();
  388. if (count($tables['list']) < 1) {
  389. throw new AdminException('数据表获取失败');
  390. }
  391. $version = str_replace('.', '', $this->requestData['version']);
  392. $backServices->getDbBackup()->setFile(['name' => date("YmdHis") . '_' . $version, 'part' => 1]);
  393. $tables = implode(',', array_column($tables['list'], 'name'));
  394. $result = $backServices->backup($tables);
  395. if (!empty($result)) {
  396. throw new AdminException('数据库备份失败 ' . $result);
  397. }
  398. $fileData = $backServices->getDbBackup()->getFile();
  399. $fileName = $fileData['filename'] . '.gz';
  400. if (!is_file($fileData['filepath'] . $fileName)) {
  401. throw new AdminException('数据库备份失败');
  402. }
  403. CacheService::set($token . '_database_backup', 2, 86400);
  404. CacheService::set($token . '_database_backup_name', $fileName, 86400);
  405. return true;
  406. } catch (\Exception $e) {
  407. Log::error('升级失败,失败原因:' . $e->getMessage());
  408. CacheService::set($token . 'upgrade_status', -1, 86400);
  409. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  410. }
  411. return false;
  412. }
  413. /**
  414. * 项目备份
  415. * @param string $token
  416. * @return bool
  417. */
  418. public function projectBackup(string $token): bool
  419. {
  420. try {
  421. ini_set('memory_limit', '-1');
  422. $appPath = app()->getRootPath();
  423. /** @var FileService $fileService */
  424. $fileService = app()->make(FileService::class);
  425. $dir = 'backup' . DS . date('Ymd') . DS . $token;
  426. $backupDir = $appPath . $dir;
  427. $projectPath = $this->getProjectDir($appPath);
  428. if (empty($projectPath)) {
  429. throw new AdminException('项目目录获取异常');
  430. }
  431. foreach ($projectPath as $key => $path) {
  432. foreach ($path as $item) {
  433. if ($key == 'file') {
  434. $fileService->handleFile($appPath . $item, $backupDir . DS . $item, 'copy', false, ['zip']);
  435. } else {
  436. $fileService->handleDir($appPath . $item, $backupDir . DS . $item, 'copy', false, ['uploads']);
  437. }
  438. }
  439. }
  440. $version = str_replace('.', '', $this->requestData['version']);
  441. $fileName = date("YmdHis") . '_' . $version . '_project' . '.zip';
  442. $filePath = $appPath . 'backup' . DS . $fileName;
  443. /** @var FileService $fileService */
  444. $fileService = app()->make(FileService::class);
  445. $result = $fileService->addZip($backupDir, $filePath, $backupDir);
  446. if (!$result) {
  447. throw new AdminException('项目备份失败');
  448. }
  449. CacheService::set($token . '_project_backup', 2, 86400);
  450. CacheService::set($token . '_project_backup_name', $fileName, 86400);
  451. //检测项目备份
  452. if (!is_file($filePath)) {
  453. throw new AdminException('项目备份检测失败');
  454. }
  455. return true;
  456. } catch (\Exception $e) {
  457. Log::error('升级失败,失败原因:' . $e->getMessage());
  458. CacheService::set($token . 'upgrade_status', -1, 86400);
  459. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  460. }
  461. return false;
  462. }
  463. /**
  464. * 获取项目目录
  465. * @param $path
  466. * @return array
  467. */
  468. public function getProjectDir($path): array
  469. {
  470. /** @var FileService $fileService */
  471. $fileService = app()->make(FileService::class);
  472. $list = $fileService->getDirs($path);
  473. $ignore = ['.', '..', '.git', '.idea', 'runtime', 'backup', 'upgrade'];
  474. foreach ($list as $key => $path) {
  475. if (empty($key)) {
  476. unset($list[$key]);
  477. continue;
  478. }
  479. if (is_array($path)) {
  480. foreach ($path as $key2 => $item) {
  481. if (in_array($item, $ignore) && $item) {
  482. unset($list[$key][$key2]);
  483. }
  484. }
  485. }
  486. }
  487. return $list;
  488. }
  489. /**
  490. * 升级
  491. * @return bool
  492. * @throws \Exception
  493. */
  494. public function overwriteProject(): bool
  495. {
  496. try {
  497. if (!$token = CacheService::get('upgrade_token')) {
  498. throw new AdminException('请重新下载升级包');
  499. }
  500. if (CacheService::get($token . 'is_execute') == 2) {
  501. return true;
  502. }
  503. CacheService::set($token . 'is_execute', 2, 86400);
  504. $dataBackupName = CacheService::get($token . '_database_backup_name');
  505. if (!$dataBackupName || !is_file(app()->getRootPath() . 'backup' . DS . $dataBackupName)) {
  506. throw new AdminException('数据库备份失败');
  507. }
  508. $serverPackageFilePath = CacheService::get($token . '_server_package_path');
  509. if (!is_dir($serverPackageFilePath)) {
  510. throw new AdminException('项目文件获取异常');
  511. }
  512. // 执行sql文件
  513. if (!$this->databaseUpgrade($token, $serverPackageFilePath)) {
  514. throw new AdminException('数据库升级失败');
  515. }
  516. // 替换文件目录
  517. $this->coverageProject($token);
  518. // 发送升级日志
  519. $this->sendUpgradeLog($token);
  520. $this->saveLog($token);
  521. CacheService::set($token . 'upgrade_status', 2, 86400);
  522. return true;
  523. } catch (\Exception $e) {
  524. Log::error('升级失败,失败原因:' . $e->getMessage());
  525. CacheService::set($token . 'upgrade_status', -1, 86400);
  526. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  527. }
  528. return false;
  529. }
  530. /**
  531. * 写入日志
  532. * @param $token
  533. * @return void
  534. */
  535. public function saveLog($token)
  536. {
  537. if (CacheService::get($token . 'is_save') == 2) {
  538. return true;
  539. }
  540. CacheService::set($token . 'is_save', 2, 86400);
  541. $upgradeData = CacheService::get($token . '_upgrade_data');
  542. $this->dao->save([
  543. 'title' => $upgradeData['data']['title'] ?? '',
  544. 'content' => $upgradeData['data']['content'] ?? '',
  545. 'first_version' => $upgradeData['data']['first_version'] ?? '',
  546. 'second_version' => $upgradeData['data']['second_version'] ?? '',
  547. 'third_version' => $upgradeData['data']['third_version'] ?? '',
  548. 'fourth_version' => $upgradeData['data']['fourth_version'] ?? '',
  549. 'upgrade_time' => time(),
  550. 'error_data' => CacheService::get($token . 'upgrade_status_tip', ''),
  551. 'package_link' => CacheService::get($token . '_project_backup_name', ''),
  552. 'data_link' => CacheService::get($token . '_database_backup_name', '')
  553. ]);
  554. }
  555. /**
  556. * 发送日志
  557. * @param string $token
  558. * @return bool
  559. */
  560. public function sendUpgradeLog(string $token): bool
  561. {
  562. try {
  563. $versionBefore = CacheService::get('version_before', '');
  564. $versionData = $this->getVersion();
  565. if (empty($versionData)) {
  566. throw new AdminException('授权信息丢失');
  567. }
  568. $versionAfter = $this->recombinationVersion($versionData['version'] ?? '');
  569. $this->requestData['version_before'] = implode('.', $versionBefore);
  570. $this->requestData['version_after'] = implode('.', $versionAfter);
  571. $this->requestData['error_data'] = CacheService::get($token . 'upgrade_status_tip', '');
  572. $this->getSign($this->timeStamp);
  573. $result = HttpService::postRequest(self::UPGRADE_LOG_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  574. if (!$result) {
  575. throw new AdminException('升级日志推送失败');
  576. }
  577. $data = json_decode($result, true);
  578. $this->checkAuth($data);
  579. } catch (\Exception $e) {
  580. Log::error(['msg' => '升级日志发送失败:,失败原因' . ($data['msg'] ?? '') . $e->getMessage(), 'data' => $data]);
  581. }
  582. return true;
  583. }
  584. /**
  585. * 核对签名
  586. * @return void
  587. * @throws \Exception
  588. */
  589. public function checkSignature()
  590. {
  591. $projectSignature = rtrim($this->getVersion('project_signature'));
  592. if (!$projectSignature) {
  593. throw new AdminException('项目签名获取异常');
  594. }
  595. /** @var fileVerification $verification */
  596. $verification = app()->make(fileVerification::class);
  597. $newSignature = $verification->getSignature(app()->getRootPath());
  598. if ($projectSignature != $newSignature) {
  599. throw new AdminException('项目签名核对异常');
  600. }
  601. }
  602. /**
  603. * 生成签名
  604. * @return void
  605. * @throws \Exception
  606. */
  607. public function generateSignature()
  608. {
  609. $file = app()->getRootPath() . '.version';
  610. if (!$data = @file($file)) {
  611. throw new AdminException('.version读取失败');
  612. }
  613. $list = [];
  614. if (!empty($data)) {
  615. foreach ($data as $datum) {
  616. list($name, $value) = explode('=', $datum);
  617. $list[$name] = rtrim($value);
  618. }
  619. }
  620. if (!isset($list['project_signature'])) {
  621. $list['project_signature'] = '';
  622. }
  623. /** @var fileVerification $verification */
  624. $verification = app()->make(fileVerification::class);
  625. $list['project_signature'] = $verification->getSignature(app()->getRootPath());
  626. $str = "";
  627. foreach ($list as $key => $item) {
  628. $str .= "{$key}={$item}\n";
  629. }
  630. file_put_contents($file, $str);
  631. }
  632. /**
  633. * 数据库升级
  634. * @param string $token
  635. * @param string $serverPackageFilePath
  636. * @return bool
  637. */
  638. public function databaseUpgrade(string $token, string $serverPackageFilePath): bool
  639. {
  640. $databaseFilePath = $serverPackageFilePath . DS . "upgrade" . DS . "database.php";
  641. if (!is_file($databaseFilePath)) {
  642. CacheService::set($token . '_database_upgrade', 2, 86400);
  643. return true;
  644. }
  645. CacheService::set($token . '_database_upgrade', 1, 86400);
  646. $sqlData = include $databaseFilePath;
  647. $nowCode = $this->getVersion('version_code');
  648. if ($sqlData['new_code'] <= $nowCode) {
  649. CacheService::set($token . '_database_upgrade', 2, 86400);
  650. return true;
  651. }
  652. $updateSql = $upgradeSql = [];
  653. foreach ($sqlData['update_sql'] as $items) {
  654. if ($items['code'] > $nowCode) {
  655. $upgradeSql[] = $items;
  656. }
  657. }
  658. if (empty($upgradeSql)) {
  659. CacheService::set($token . '_database_upgrade', 2, 86400);
  660. return true;
  661. }
  662. $prefix = config('database.connections.' . config('database.default'))['prefix'];
  663. Db::startTrans();
  664. try {
  665. foreach ($upgradeSql as $item) {
  666. $tip = [
  667. '1' => '表已存在',
  668. '2' => '表不存在',
  669. '3' => '表中' . ($item['field'] ?? '') . '字段已存在',
  670. '4' => '表中' . ($item['field'] ?? '') . '字段不存在',
  671. '5' => '表中删除字段' . ($item['field'] ?? '') . '不存在',
  672. '6' => '表中数据已存在',
  673. '6_2' => '表中查询父类ID不存在',
  674. '7' => '表中数据已存在',
  675. '8' => '表中数据不存在',
  676. ];
  677. if (!isset($item['table']) || !$item['table']) {
  678. throw new AdminException('请核对升级数据结构:table');
  679. }
  680. if (!isset($item['sql']) || !$item['sql']) {
  681. throw new AdminException('请核对升级数据结构:sql');
  682. }
  683. $whereTable = '';
  684. $table = $prefix . $item['table'];
  685. if (isset($item['whereTable']) && $item['whereTable']) {
  686. $whereTable = $prefix . $item['whereTable'];
  687. }
  688. if (isset($item['findSql']) && $item['findSql']) {
  689. $findSql = str_replace('@table', $table, $item['findSql']);
  690. if (!empty(Db::query($findSql))) {
  691. // 1建表 2删表 3添加字段 4修改字段 5删除字段 6添加数据 7修改数据 8删数据 -1直接执行
  692. if (in_array($item['type'], [1, 3, 6])) {
  693. throw new AdminException($table . $tip[$item['type']] ?? '未知异常');
  694. }
  695. } else {
  696. if (in_array($item['type'], [4, 5, 7])) {
  697. throw new AdminException($table . $tip[$item['type']] ?? '未知异常');
  698. }
  699. if ($item['type'] == 8) {
  700. continue;
  701. }
  702. }
  703. }
  704. if ($item['type'] == 4) {
  705. if (!isset($item['rollback_sql']) || !$item['rollback_sql']) {
  706. throw new AdminException('请核对升级数据结构:rollback_sql');
  707. }
  708. $updateSql[] = $item;
  709. }
  710. $upSql = str_replace('@table', $table, $item['sql']);
  711. if ($item['type'] == 6 || $item['type'] == 7) {
  712. if (isset($item['whereSql']) && $item['whereSql']) {
  713. $whereSql = str_replace('@whereTable', $whereTable, $item['whereSql']);
  714. $tabId = Db::query($whereSql)[0]['tabId'] ?? 0;
  715. if (!$tabId) {
  716. throw new AdminException($table . $tip[$item['type']] ?? '未知异常');
  717. }
  718. $upSql = str_replace('@tabId', $tabId, $upSql);
  719. }
  720. } elseif ($item['type'] == 8) {
  721. $upSql = str_replace(['@table', '@field', '@value'], [$table, $item['field'], $item['value']], $item['sql']);
  722. } elseif ($item['type'] == -1) {
  723. if (isset($item['new_table']) && $item['new_table']) {
  724. $new_table = $prefix . $item['new_table'];
  725. $upSql = str_replace('@new_table', $new_table, $upSql);
  726. }
  727. }
  728. if ($upSql) {
  729. Db::execute($upSql);
  730. }
  731. Log::write(['type' => 'database_upgrade', '`item' => json_encode($item), 'upSql' => $upSql], 'notice');
  732. }
  733. Db::commit();
  734. CacheService::set($token . '_database_upgrade', 2, 86400);
  735. } catch (\Throwable $e) {
  736. Db::rollback();
  737. Log::error(['msg' => '数据库升级失败,失败原因:' . $e->getMessage(), 'data' => json_encode($upgradeSql)]);
  738. CacheService::set($token . 'upgrade_status', -1, 86400);
  739. CacheService::set($token . 'upgrade_status_tip', '数据库升级失败,失败原因:' . $e->getMessage(), 86400);
  740. if (!empty($updateSql)) {
  741. $this->rollbackStructure($prefix, $updateSql);
  742. }
  743. return false;
  744. }
  745. return true;
  746. }
  747. /**
  748. * 覆盖项目
  749. * @param string $token
  750. * @return bool
  751. */
  752. public function coverageProject(string $token): bool
  753. {
  754. $versionData = $this->getVersion();
  755. if (empty($versionData)) {
  756. throw new AdminException('授权信息异常');
  757. }
  758. CacheService::set('version_before', $this->recombinationVersion($versionData['version'] ?? ''), 86400);
  759. /** @var FileService $fileService */
  760. $fileService = app()->make(FileService::class);
  761. // 服务端项目
  762. $serverPackageName = CacheService::get($token . '_server_package_name');
  763. // 客户端项目
  764. $clientPackageName = CacheService::get($token . '_client_package_name');
  765. // PC端项目
  766. $pcPackageName = CacheService::get($token . '_pc_package_name');
  767. if (!is_file($serverPackageName) && !is_file($clientPackageName) && !is_file($pcPackageName)) {
  768. throw new AdminException('升级文件异常,请重新下载');
  769. }
  770. if (is_file($serverPackageName) && !$fileService->extractFile($serverPackageName, app()->getRootPath())) {
  771. throw new AdminException('服务端解压失败');
  772. }
  773. if (is_file($clientPackageName) && !$fileService->extractFile($clientPackageName, app()->getRootPath())) {
  774. throw new AdminException('客户端解压失败');
  775. }
  776. if (is_file($pcPackageName) && !$fileService->extractFile($pcPackageName, app()->getRootPath())) {
  777. throw new AdminException('PC端解压失败');
  778. }
  779. //生成项目签名
  780. $this->generateSignature();
  781. CacheService::set($token . '_coverage_project', 2, 86400);
  782. return true;
  783. }
  784. /**
  785. * 回滚表结构
  786. * @param string $prefix
  787. * @param array $updateSql
  788. * @return void
  789. */
  790. public function rollbackStructure(string $prefix, array $updateSql): void
  791. {
  792. try {
  793. foreach ($updateSql as $item) {
  794. Db::execute(str_replace('@table', $prefix . $item['table'], $item['rollback_sql']));
  795. }
  796. } catch (\Exception $e) {
  797. Log::error(['msg' => '数据库结构回滚失败', 'error' => $e->getFile() . '__' . $e->getLine() . '__' . $e->getMessage(), 'data' => $updateSql]);
  798. }
  799. }
  800. /**
  801. * 检查访问权限
  802. * @param array $data
  803. * @return bool
  804. */
  805. public function checkAuth(array $data): bool
  806. {
  807. if (!isset($data['status']) || $data['status'] != 200) {
  808. if ($data['status'] == 410000) {
  809. $this->getAuth();
  810. }
  811. Log::error(['msg' => $data['msg'] ?? '', 'error' => $data]);
  812. return false;
  813. }
  814. return true;
  815. }
  816. /**
  817. * 升级状态
  818. * @return array
  819. */
  820. public function getUpgradeStatus(): array
  821. {
  822. $this->getSign($this->timeStamp);
  823. $result = HttpService::getRequest(self::UPGRADE_STATUS_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  824. if (!$result) {
  825. throw new AdminException('升级状态获取失败');
  826. }
  827. $data = json_decode($result, true);
  828. $this->checkAuth($data);
  829. $upgradeData['status'] = $data['data']['status'] ?? 0;
  830. $upgradeData['force_reminder'] = $data['data']['force_reminder'] ?? 0;
  831. $upgradeData['title'] = $upgradeData['status'] < 1 ? "您已升级至最新版本,无需更新" : "系统有新版本可更新";
  832. return $upgradeData;
  833. }
  834. /**
  835. * 升级日志
  836. * @return array
  837. * @throws \think\db\exception\DataNotFoundException
  838. * @throws \think\db\exception\DbException
  839. * @throws \think\db\exception\ModelNotFoundException
  840. */
  841. public function getUpgradeLogList(): array
  842. {
  843. [$page, $limit] = $this->getPageValue();
  844. $count = $this->dao->count();
  845. $list = $this->dao->getList(['id', 'title', 'content', 'first_version', 'second_version', 'third_version', 'fourth_version', 'upgrade_time', 'package_link', 'data_link'], $page, $limit);
  846. $rootPath = app()->getRootPath();
  847. foreach ($list as &$item) {
  848. $item['file_status'] = 1;
  849. $item['data_status'] = 1;
  850. if (!$item['package_link'] || !is_file($rootPath . 'backup' . DS . $item['package_link'])) {
  851. $item['file_status'] = 0;
  852. }
  853. if (!$item['data_link'] || !is_file($rootPath . 'backup' . DS . $item['data_link'])) {
  854. $item['data_status'] = 0;
  855. }
  856. unset($item['package_link'], $item['data_link']);
  857. $item['upgrade_time'] = date('Y-m-d H:i:s', $item['upgrade_time']);
  858. }
  859. return compact('list', 'count');
  860. }
  861. /**
  862. * 导出
  863. * @param int $id
  864. * @param string $type
  865. * @return void
  866. * @throws \think\db\exception\DataNotFoundException
  867. * @throws \think\db\exception\DbException
  868. * @throws \think\db\exception\ModelNotFoundException
  869. */
  870. public function export(int $id, string $type)
  871. {
  872. $data = $this->dao->getOne(['id' => $id], 'package_link, data_link');
  873. if (!$data || !$data['package_link']) {
  874. throw new AdminException('备份文件不存在');
  875. }
  876. $fileName = $type == 'file' ? $data['package_link'] : $data['data_link'];
  877. $filePath = app()->getRootPath() . 'backup' . DS . $fileName;
  878. if (!is_file($filePath)) {
  879. throw new AdminException('备份文件不存在');
  880. }
  881. //下载文件
  882. header('Content-Description: File Transfer');
  883. header('Content-Type: application/octet-stream');
  884. header('Content-Disposition: attachment; filename=' . $fileName);
  885. header('Content-Transfer-Encoding: binary');
  886. header('Expires: 0');
  887. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  888. header('Pragma: public');
  889. header('Content-Length: ' . filesize($filePath));
  890. ob_clean();
  891. flush();
  892. readfile($filePath); //输出文件
  893. }
  894. /**
  895. * 检查数据库大小
  896. * @return bool
  897. */
  898. public function checkDatabaseSize(): bool
  899. {
  900. if (!$database = Config::get('database.connections.' . Config::get('database.default') . '.database')) {
  901. throw new AdminException('数据库信息获取失败');
  902. }
  903. $result = Db::query("select concat(round(sum(data_length/1024/1024))) as size from information_schema.tables where table_schema='{$database}';");
  904. if ((int)($result[0]['size'] ?? '') > 500) {
  905. throw new AdminException('数据库文件过大, 不能升级');
  906. }
  907. return true;
  908. }
  909. }