UpgradeServices.php 37 KB

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