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