UpgradeServices.php 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2026 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 app\services\system\log\SystemFileMd5Services;
  13. use SplFileInfo;
  14. use think\facade\Config;
  15. use think\facade\Db;
  16. use think\facade\Log;
  17. use app\jobs\UpgradeJob;
  18. use app\services\BaseServices;
  19. use crmeb\services\FileService;
  20. use crmeb\services\HttpService;
  21. use crmeb\services\CacheService;
  22. use crmeb\utils\fileVerification;
  23. use crmeb\exceptions\AdminException;
  24. use app\dao\system\upgrade\UpgradeLogDao;
  25. /**
  26. * 在线升级
  27. * Class UpgradeServices
  28. * @package app\services\system
  29. */
  30. class UpgradeServices extends BaseServices
  31. {
  32. const LOGIN_URL = 'http://upgrade.crmeb.net/api/login';
  33. const UPGRADE_URL = 'http://upgrade.crmeb.net/api/upgrade/list';
  34. const UPGRADE_CURRENT_URL = 'http://upgrade.crmeb.net/api/upgrade/current_list';
  35. const AGREEMENT_URL = 'http://upgrade.crmeb.net/api/upgrade/agreement';
  36. const PACKAGE_DOWNLOAD_URL = 'http://upgrade.crmeb.net/api/upgrade/download';
  37. const UPGRADE_STATUS_URL = 'http://upgrade.crmeb.net/api/upgrade/status';
  38. const UPGRADE_LOG_URL = 'http://upgrade.crmeb.net/api/upgrade/log';
  39. /**
  40. * @var array $requestData
  41. */
  42. private $requestData = [];
  43. /**
  44. * @var int $timeStamp
  45. */
  46. private $timeStamp = 0;
  47. /**
  48. * UpgradeServices constructor.
  49. * @param UpgradeLogDao $dao
  50. */
  51. public function __construct(UpgradeLogDao $dao)
  52. {
  53. $this->dao = $dao;
  54. $versionData = $this->getVersion();
  55. // if ($versionData['version_code'] < 450) return true;
  56. if (empty($versionData)) {
  57. throw new AdminException('授权信息丢失');
  58. }
  59. $this->timeStamp = time();
  60. $recVersion = $this->recombinationVersion($versionData['version'] ?? '');
  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. if (!CacheService::get('upgrade_auth_token')) {
  70. $this->getAuth();
  71. }
  72. }
  73. /**
  74. * 获取版本信息
  75. * @return void
  76. */
  77. /**
  78. * 获取文件配置信息
  79. * @param string $name
  80. * @param string $path
  81. * @return array|string
  82. */
  83. public function getVersion(string $name = '', string $path = '')
  84. {
  85. $file = '.version';
  86. $arr = [];
  87. $list = @file($path ?: app()->getRootPath() . $file);
  88. foreach ($list as $val) {
  89. list($k, $v) = explode('=', str_replace(PHP_EOL, '', $val));
  90. $arr[$k] = $v;
  91. }
  92. return !empty($name) ? $arr[$name] ?? '' : $arr;
  93. }
  94. /**
  95. * 获取版本号
  96. * @param $input
  97. * @return array
  98. */
  99. public function recombinationVersion($input): array
  100. {
  101. $version = substr($input, strpos($input, ' v') + 1);
  102. return array_map(function ($item) {
  103. if (preg_match('/\d+/', $item, $arr)) {
  104. $item = $arr[0];
  105. }
  106. return (int)$item;
  107. }, explode('.', $version));
  108. }
  109. /**
  110. * 获取Token
  111. * @return void
  112. */
  113. public function getAuth()
  114. {
  115. $this->getSign($this->timeStamp);
  116. $result = HttpService::postRequest(self::LOGIN_URL, $this->requestData);
  117. if (!$result) {
  118. throw new AdminException('授权失败');
  119. }
  120. $authData = json_decode($result, true);
  121. if (!isset($authData['status']) || $authData['status'] != 200) {
  122. Log::error(['msg' => $authData['msg'] ?? '', 'error' => $authData['data'] ?? []]);
  123. throw new AdminException($authData['msg'] ?? '授权失败');
  124. }
  125. CacheService::set('upgrade_auth_token', $authData['data']['access_token'], 7200);
  126. }
  127. /**
  128. * 获取签名
  129. * @param int $timeStamp
  130. * @return void
  131. */
  132. public function getSign(int $timeStamp)
  133. {
  134. $data = $this->requestData;
  135. if ((!isset($data['host']) || !$data['host']) ||
  136. (!isset($data['nonce']) || !$data['nonce']) ||
  137. (!isset($data['app_id']) || !$data['app_id']) ||
  138. (!isset($data['version']) || !$data['version']) ||
  139. (!isset($data['app_key']) || !$data['app_key'])
  140. ) {
  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. if ($data['data']) {
  199. $routineData = [
  200. 'version' => $data['data'][0]['first_version'] . '.' . $data['data'][0]['second_version'] . '.' . $data['data'][0]['third_version'],
  201. 'desc' => $data['data'][0]['content'],
  202. 'is_live' => 0
  203. ];
  204. CacheService::set('routine_upload_data', $routineData, 86400);
  205. }
  206. return $data['data'] ?? [];
  207. }
  208. /**
  209. * 升级协议
  210. * @return mixed
  211. */
  212. public function getAgreement()
  213. {
  214. $this->getSign($this->timeStamp);
  215. $result = HttpService::getRequest(self::AGREEMENT_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  216. if (!$result) {
  217. throw new AdminException('升级协议获取失败');
  218. }
  219. $data = json_decode($result, true);
  220. if (!$this->checkAuth($data)) {
  221. throw new AdminException($data['msg'] ?? '升级协议获取失败');
  222. }
  223. return $data['data'] ?? [];
  224. }
  225. /**
  226. * 下载
  227. * @param string $packageKey
  228. * @return bool
  229. */
  230. public function packageDownload(string $packageKey): bool
  231. {
  232. $token = md5(time());
  233. //检查数据库大小
  234. $this->checkDatabaseSize();
  235. $this->requestData['package_key'] = $packageKey;
  236. $this->getSign($this->timeStamp);
  237. $result = HttpService::getRequest(self::PACKAGE_DOWNLOAD_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  238. if (!$result) {
  239. throw new AdminException('升级包获取失败');
  240. }
  241. $data = json_decode($result, true);
  242. if (!$this->checkAuth($data)) {
  243. throw new AdminException($data['msg'] ?? '授权失败');
  244. }
  245. if (empty($data['data']['server_package_link']) && empty($data['data']['client_package_link']) && empty($data['data']['pc_package_link'])) {
  246. CacheService::set($token . 'upgrade_status', 2, 86400);
  247. return true;
  248. }
  249. if (!empty($data['data']['server_package_link'])) {
  250. $this->downloadFile($data['data']['server_package_link'], $token . '_server_package');
  251. } else {
  252. CacheService::set($token . '_server_package', 2, 86400);
  253. }
  254. if (!empty($data['data']['client_package_link'])) {
  255. $this->downloadFile($data['data']['client_package_link'], $token . '_client_package');
  256. } else {
  257. CacheService::set($token . '_client_package', 2, 86400);
  258. }
  259. if (!empty($data['data']['pc_package_link'])) {
  260. $this->downloadFile($data['data']['pc_package_link'], $token . '_pc_package');
  261. } else {
  262. CacheService::set($token . '_pc_package', 2, 86400);
  263. }
  264. CacheService::set('upgrade_token', $token, 86400);
  265. CacheService::set($token . '_upgrade_data', $data, 86400);
  266. return true;
  267. }
  268. /**
  269. * 执行下载
  270. * @param string $seq
  271. * @param string $url
  272. * @param string $downloadPath
  273. * @param string $fileName
  274. * @param int $timeout
  275. * @return void
  276. */
  277. public function download(string $seq, string $url, string $downloadPath, string $fileName, int $timeout = 300)
  278. {
  279. ini_set('memory_limit', '-1');
  280. $filePath = $downloadPath . DS . $fileName;
  281. $fp_output = fopen($filePath, 'w');
  282. if (!$fp_output) {
  283. throw new AdminException('无法创建下载文件');
  284. }
  285. $ch = curl_init();
  286. curl_setopt($ch, CURLOPT_URL, $url);
  287. curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
  288. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); // 连接超时
  289. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // 总超时时间
  290. curl_setopt($ch, CURLOPT_FILE, $fp_output);
  291. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  292. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟随重定向
  293. curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
  294. curl_setopt($ch, CURLOPT_REFERER, 'https://www.crmeb.com');
  295. if (stripos($url, "https://") !== false) {
  296. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  297. }
  298. $result = curl_exec($ch);
  299. $error = curl_error($ch);
  300. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  301. curl_close($ch);
  302. fclose($fp_output); // 关闭文件句柄
  303. // 检查下载结果
  304. if ($result === false || !empty($error)) {
  305. @unlink($filePath);
  306. throw new AdminException('下载失败: ' . $error);
  307. }
  308. if ($httpCode !== 200) {
  309. @unlink($filePath);
  310. throw new AdminException('下载失败,HTTP状态码: ' . $httpCode);
  311. }
  312. // 检查文件是否存在且有内容
  313. if (!is_file($filePath) || filesize($filePath) < 100) {
  314. @unlink($filePath);
  315. throw new AdminException('下载的文件无效');
  316. }
  317. if (pathinfo($fileName, PATHINFO_EXTENSION) !== 'zip') {
  318. throw new AdminException('安装包格式错误');
  319. }
  320. /** @var FileService $fileService */
  321. $fileService = app()->make(FileService::class);
  322. $downloadFilePath = $downloadPath . DS . pathinfo($fileName, PATHINFO_FILENAME);
  323. if (!$fileService->extractFile($filePath, $downloadFilePath)) {
  324. throw new AdminException('升级包解压失败');
  325. }
  326. CacheService::set($seq . '_path', $downloadFilePath, 86400);
  327. CacheService::set($seq . '_name', $filePath, 86400);
  328. CacheService::set($seq, 2, 86400);
  329. }
  330. /**
  331. * 开始下载
  332. * @param string $packageLink
  333. * @param string $seq
  334. * @return void
  335. */
  336. private function downloadFile(string $packageLink, string $seq)
  337. {
  338. $fileName = substr($packageLink, strrpos($packageLink, '/') + 1);
  339. $filePath = app()->getRootPath() . 'upgrade' . DS . date('Y-m-d');
  340. if (!is_dir($filePath)) mkdir($filePath, 0755, true);
  341. UpgradeJob::dispatch('download', [$seq, $packageLink, $filePath, $fileName, 300]);
  342. CacheService::set($seq, 1, 86400);
  343. }
  344. /**
  345. * 升级进度
  346. * @return array
  347. */
  348. public function getProgress(): array
  349. {
  350. $token = CacheService::get('upgrade_token');
  351. if (empty($token)) {
  352. throw new AdminException('请重新升级');
  353. }
  354. $serverProgress = CacheService::get($token . '_server_package'); // 服务端包下载进度
  355. $clientProgress = CacheService::get($token . '_client_package'); // 客户端包下载进度
  356. $pcProgress = CacheService::get($token . '_pc_package'); // PC端包下载进度
  357. $databaseBackupProgress = CacheService::get($token . '_database_backup'); // 数据库备份进度
  358. $projectBackupProgress = CacheService::get($token . '_project_backup'); // 项目备份备份进度
  359. $databaseUpgradeProgress = CacheService::get($token . '_database_upgrade'); // 数据库升级进度
  360. $coverageProjectProgress = CacheService::get($token . '_coverage_project'); // 项目覆盖进度
  361. $stepNum = 1;
  362. $tip = '开始升级';
  363. if ($serverProgress == $clientProgress && $clientProgress == $pcProgress) {
  364. $tip = $serverProgress == 1 ? '开始下载安装包' : '安装包下载完成';
  365. if ($serverProgress == 2) {
  366. $stepNum += 1;
  367. }
  368. } else {
  369. $tip = '正在下载安装包';
  370. }
  371. if ($databaseBackupProgress == 2) {
  372. $tip = '数据库备份完成';
  373. $stepNum += 1;
  374. }
  375. if ($projectBackupProgress == 2) {
  376. $tip = '项目备份完成';
  377. $stepNum += 1;
  378. }
  379. if ((int)$databaseUpgradeProgress == 2) {
  380. $tip = '数据库升级完成';
  381. $stepNum += 1;
  382. }
  383. if ((int)$coverageProjectProgress == 2) {
  384. $tip = '项目升级完成';
  385. $stepNum += 1;
  386. }
  387. $upgradeStatus = (int)CacheService::get($token . 'upgrade_status');
  388. if ($upgradeStatus == 2) {
  389. $stepNum = 6;
  390. $tip = '升级完成';
  391. } elseif ($upgradeStatus < 0) {
  392. $this->saveLog($token);
  393. throw new AdminException(CacheService::get($token . 'upgrade_status_tip', '升级失败'));
  394. } elseif ($serverProgress == 2 && $clientProgress == 2 && $pcProgress == 2 && $databaseBackupProgress == 2 && $projectBackupProgress == 2) {
  395. try {
  396. $this->overwriteProject();
  397. } catch (\Exception $e) {
  398. $this->sendUpgradeLog($token);
  399. }
  400. }
  401. $speed = sprintf("%.1f", $stepNum / 6 * 100);
  402. return compact('speed', 'tip');
  403. }
  404. /**
  405. * 数据库备份
  406. * @param $token
  407. * @return bool
  408. * @throws \think\db\exception\BindParamException
  409. */
  410. public function databaseBackup($token): bool
  411. {
  412. try {
  413. //备份表数据
  414. /** @var SystemDatabackupServices $backServices */
  415. $backServices = app()->make(SystemDatabackupServices::class);
  416. $tables = $backServices->getDataList();
  417. if (count($tables['list']) < 1) {
  418. throw new AdminException('数据表获取失败');
  419. }
  420. // 从.version文件获取版本号
  421. $versionData = $this->getVersion();
  422. $backServices->getDbBackup()->setFile(['name' => $versionData['version_code'], 'part' => 1]);
  423. $tables = implode(',', array_column($tables['list'], 'name'));
  424. $result = $backServices->backup($tables);
  425. if (!empty($result)) {
  426. throw new AdminException('数据库备份失败 ' . $result);
  427. }
  428. $fileData = $backServices->getDbBackup()->getFile();
  429. $fileName = $fileData['filename'] . '.gz';
  430. if (!is_file($fileData['filepath'] . $fileName)) {
  431. throw new AdminException('数据库备份失败');
  432. }
  433. CacheService::set($token . '_database_backup', 2, 86400);
  434. CacheService::set($token . '_database_backup_name', $fileName, 86400);
  435. return true;
  436. } catch (\Exception $e) {
  437. Log::error('升级失败,失败原因:' . $e->getMessage());
  438. CacheService::set($token . 'upgrade_status', -1, 86400);
  439. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  440. }
  441. return false;
  442. }
  443. /**
  444. * 项目备份
  445. * @param string $token
  446. * @return bool
  447. */
  448. public function projectBackup(string $token): bool
  449. {
  450. try {
  451. ini_set('memory_limit', '-1');
  452. $appPath = app()->getRootPath();
  453. /** @var FileService $fileService */
  454. $fileService = app()->make(FileService::class);
  455. $dir = 'backup' . DS . date('Ymd') . DS . $token;
  456. $backupDir = $appPath . $dir;
  457. $fileService->handleDir($appPath . 'app', $backupDir . DS . 'app');
  458. $fileService->handleDir($appPath . 'config', $backupDir . DS . 'config');
  459. $fileService->handleDir($appPath . 'crmeb', $backupDir . DS . 'crmeb');
  460. // 从.version文件获取版本号
  461. $versionData = $this->getVersion();
  462. $fileName = $versionData['version_code'] . '-1.project.zip';
  463. $filePath = $appPath . 'backup' . DS . $fileName;
  464. /** @var FileService $fileService */
  465. $fileService = app()->make(FileService::class);
  466. $result = $fileService->addZip($backupDir, $filePath, $backupDir);
  467. if (!$result) {
  468. throw new AdminException('项目备份失败');
  469. }
  470. CacheService::set($token . '_project_backup', 2, 86400);
  471. CacheService::set($token . '_project_backup_name', $fileName, 86400);
  472. //检测项目备份
  473. if (!is_file($filePath)) {
  474. throw new AdminException('项目备份检测失败');
  475. }
  476. // 压缩完成,删除移动的文件
  477. $iterator = new \RecursiveIteratorIterator(
  478. new \RecursiveDirectoryIterator($appPath . 'backup' . DS . date('Ymd'), \FilesystemIterator::SKIP_DOTS),
  479. \RecursiveIteratorIterator::CHILD_FIRST
  480. );
  481. /** @var SplFileInfo $fileInfo */
  482. foreach ($iterator as $fileInfo) {
  483. if ($fileInfo->isDir()) {
  484. @rmdir($fileInfo->getRealPath());
  485. } else {
  486. @unlink($fileInfo->getRealPath());
  487. }
  488. }
  489. return true;
  490. } catch (\Exception $e) {
  491. Log::error('升级失败,失败原因:' . $e->getMessage());
  492. CacheService::set($token . 'upgrade_status', -1, 86400);
  493. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  494. }
  495. return false;
  496. }
  497. /**
  498. * 升级
  499. * @return bool
  500. * @throws \Exception
  501. */
  502. public function overwriteProject(): bool
  503. {
  504. try {
  505. if (!$token = CacheService::get('upgrade_token')) {
  506. throw new AdminException('请重新下载升级包');
  507. }
  508. if (CacheService::get($token . 'is_execute') == 2) {
  509. return true;
  510. }
  511. CacheService::set($token . 'is_execute', 2, 86400);
  512. $dataBackupName = CacheService::get($token . '_database_backup_name');
  513. if (!$dataBackupName || !is_file(app()->getRootPath() . 'backup' . DS . $dataBackupName)) {
  514. throw new AdminException('数据库备份失败');
  515. }
  516. $serverPackageFilePath = CacheService::get($token . '_server_package_path');
  517. if (!is_dir($serverPackageFilePath)) {
  518. throw new AdminException('项目文件获取异常');
  519. }
  520. // 执行sql文件
  521. if (!$this->databaseUpgrade($token, $serverPackageFilePath)) {
  522. throw new AdminException('数据库升级失败');
  523. }
  524. // 替换文件目录
  525. $this->coverageProject($token);
  526. // 发送升级日志
  527. $this->sendUpgradeLog($token);
  528. $this->saveLog($token);
  529. CacheService::set($token . 'upgrade_status', 2, 86400);
  530. return true;
  531. } catch (\Exception $e) {
  532. Log::error('升级失败,失败原因:' . $e->getMessage());
  533. CacheService::set($token . 'upgrade_status', -1, 86400);
  534. CacheService::set($token . 'upgrade_status_tip', '升级失败,失败原因:' . $e->getMessage(), 86400);
  535. }
  536. return false;
  537. }
  538. /**
  539. * 写入日志
  540. * @param $token
  541. * @return void
  542. */
  543. public function saveLog($token)
  544. {
  545. if (CacheService::get($token . 'is_save') == 2) {
  546. return true;
  547. }
  548. CacheService::set($token . 'is_save', 2, 86400);
  549. $upgradeData = CacheService::get($token . '_upgrade_data');
  550. $this->dao->save([
  551. 'title' => $upgradeData['data']['title'] ?? '',
  552. 'content' => $upgradeData['data']['content'] ?? '',
  553. 'first_version' => $upgradeData['data']['first_version'] ?? '',
  554. 'second_version' => $upgradeData['data']['second_version'] ?? '',
  555. 'third_version' => $upgradeData['data']['third_version'] ?? '',
  556. 'fourth_version' => $upgradeData['data']['fourth_version'] ?? '',
  557. 'upgrade_time' => time(),
  558. 'error_data' => CacheService::get($token . 'upgrade_status_tip', ''),
  559. 'package_link' => CacheService::get($token . '_project_backup_name', ''),
  560. 'data_link' => CacheService::get($token . '_database_backup_name', '')
  561. ]);
  562. }
  563. /**
  564. * 发送日志
  565. * @param string $token
  566. * @return bool
  567. */
  568. public function sendUpgradeLog(string $token): bool
  569. {
  570. try {
  571. $versionBefore = CacheService::get('version_before', '');
  572. $versionData = $this->getVersion();
  573. if (empty($versionData)) {
  574. throw new AdminException('授权信息丢失');
  575. }
  576. $versionAfter = $this->recombinationVersion($versionData['version'] ?? '');
  577. $this->requestData['version_before'] = implode('.', $versionBefore);
  578. $this->requestData['version_after'] = implode('.', $versionAfter);
  579. $this->requestData['error_data'] = CacheService::get($token . 'upgrade_status_tip', '');
  580. $this->getSign($this->timeStamp);
  581. $result = HttpService::postRequest(self::UPGRADE_LOG_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  582. if (!$result) {
  583. throw new AdminException('升级日志推送失败');
  584. }
  585. $data = json_decode($result, true);
  586. $this->checkAuth($data);
  587. } catch (\Exception $e) {
  588. Log::error(['msg' => '升级日志发送失败:,失败原因' . ($data['msg'] ?? '') . $e->getMessage(), 'data' => $data]);
  589. }
  590. return true;
  591. }
  592. /**
  593. * 数据库升级
  594. * @param string $token
  595. * @param string $serverPackageFilePath
  596. * @return bool
  597. */
  598. public function databaseUpgrade(string $token, string $serverPackageFilePath): bool
  599. {
  600. $databaseFilePath = $serverPackageFilePath . DS . "upgrade" . DS . "database.php";
  601. if (!is_file($databaseFilePath)) {
  602. CacheService::set($token . '_database_upgrade', 2, 86400);
  603. return true;
  604. }
  605. CacheService::set($token . '_database_upgrade', 1, 86400);
  606. $sqlData = include $databaseFilePath;
  607. $nowCode = $this->getVersion('version_code');
  608. if ($sqlData['new_code'] <= $nowCode) {
  609. CacheService::set($token . '_database_upgrade', 2, 86400);
  610. return true;
  611. }
  612. $updateSql = $upgradeSql = [];
  613. foreach ($sqlData['update_sql'] as $items) {
  614. if ($items['code'] > $nowCode) {
  615. $upgradeSql[] = $items;
  616. }
  617. }
  618. if (empty($upgradeSql)) {
  619. CacheService::set($token . '_database_upgrade', 2, 86400);
  620. return true;
  621. }
  622. $prefix = config('database.connections.' . config('database.default'))['prefix'];
  623. Db::startTrans();
  624. try {
  625. foreach ($upgradeSql as $item) {
  626. $tip = [
  627. '1' => '表已存在',
  628. '2' => '表不存在',
  629. '3' => '表中' . ($item['field'] ?? '') . '字段已存在',
  630. '4' => '表中' . ($item['field'] ?? '') . '字段不存在',
  631. '5' => '表中删除字段' . ($item['field'] ?? '') . '不存在',
  632. '6' => '表中数据已存在',
  633. '6_2' => '表中查询父类ID不存在',
  634. '7' => '表中数据已存在',
  635. '8' => '表中数据不存在',
  636. ];
  637. if (!isset($item['table']) || !$item['table']) {
  638. throw new AdminException('请核对升级数据结构:table');
  639. }
  640. if (!isset($item['sql']) || !$item['sql']) {
  641. throw new AdminException('请核对升级数据结构:sql');
  642. }
  643. $whereTable = '';
  644. $table = $prefix . $item['table'];
  645. if (isset($item['whereTable']) && $item['whereTable']) {
  646. $whereTable = $prefix . $item['whereTable'];
  647. }
  648. if (isset($item['findSql']) && $item['findSql']) {
  649. $findSql = str_replace('@table', $table, $item['findSql']);
  650. if (!empty(Db::query($findSql))) {
  651. // 1建表 2删表 3添加字段 4修改字段 5删除字段 6添加数据 7修改数据 8删数据 -1直接执行
  652. // 表/字段/数据已存在时跳过,不中断升级
  653. if (in_array($item['type'], [1, 3, 6])) {
  654. Log::notice(['type' => 'database_upgrade_skip', 'reason' => $table . ($tip[$item['type']] ?? ''), 'item' => json_encode($item)]);
  655. continue;
  656. }
  657. } else {
  658. // 表/字段/数据不存在时跳过修改和删除操作
  659. if (in_array($item['type'], [4, 5, 7])) {
  660. Log::notice(['type' => 'database_upgrade_skip', 'reason' => $table . ($tip[$item['type']] ?? ''), 'item' => json_encode($item)]);
  661. continue;
  662. }
  663. if ($item['type'] == 8) {
  664. continue;
  665. }
  666. }
  667. }
  668. if ($item['type'] == 4) {
  669. if (!isset($item['rollback_sql']) || !$item['rollback_sql']) {
  670. throw new AdminException('请核对升级数据结构:rollback_sql');
  671. }
  672. $updateSql[] = $item;
  673. }
  674. $upSql = str_replace('@table', $table, $item['sql']);
  675. if ($item['type'] == 6 || $item['type'] == 7) {
  676. if (isset($item['whereSql']) && $item['whereSql']) {
  677. $whereSql = str_replace('@whereTable', $whereTable, $item['whereSql']);
  678. $tabId = Db::query($whereSql)[0]['tabId'] ?? 0;
  679. if (!$tabId) {
  680. // 关联数据不存在时跳过,不中断升级
  681. Log::notice(['type' => 'database_upgrade_skip', 'reason' => $table . ' 关联数据不存在', 'item' => json_encode($item)]);
  682. continue;
  683. }
  684. $upSql = str_replace('@tabId', $tabId, $upSql);
  685. }
  686. } elseif ($item['type'] == 8) {
  687. $upSql = str_replace(['@table', '@field', '@value'], [$table, $item['field'], $item['value']], $item['sql']);
  688. } elseif ($item['type'] == -1) {
  689. if (isset($item['new_table']) && $item['new_table']) {
  690. $new_table = $prefix . $item['new_table'];
  691. $upSql = str_replace('@new_table', $new_table, $upSql);
  692. }
  693. }
  694. if ($upSql) {
  695. Db::execute($upSql);
  696. }
  697. Log::write(['type' => 'database_upgrade', '`item' => json_encode($item), 'upSql' => $upSql], 'notice');
  698. }
  699. Db::commit();
  700. CacheService::set($token . '_database_upgrade', 2, 86400);
  701. } catch (\Throwable $e) {
  702. Db::rollback();
  703. Log::error(['msg' => '数据库升级失败,失败原因:' . $e->getMessage(), 'data' => json_encode($upgradeSql)]);
  704. CacheService::set($token . 'upgrade_status', -1, 86400);
  705. CacheService::set($token . 'upgrade_status_tip', '数据库升级失败,失败原因:' . $e->getMessage(), 86400);
  706. if (!empty($updateSql)) {
  707. $this->rollbackStructure($prefix, $updateSql);
  708. }
  709. return false;
  710. }
  711. return true;
  712. }
  713. /**
  714. * 覆盖项目
  715. * @param string $token
  716. * @return bool
  717. */
  718. public function coverageProject(string $token): bool
  719. {
  720. $versionData = $this->getVersion();
  721. if (empty($versionData)) {
  722. throw new AdminException('授权信息异常');
  723. }
  724. CacheService::set('version_before', $this->recombinationVersion($versionData['version'] ?? ''), 86400);
  725. /** @var FileService $fileService */
  726. $fileService = app()->make(FileService::class);
  727. // 服务端项目
  728. $serverPackageName = CacheService::get($token . '_server_package_name');
  729. // 客户端项目
  730. $clientPackageName = CacheService::get($token . '_client_package_name');
  731. // PC端项目
  732. $pcPackageName = CacheService::get($token . '_pc_package_name');
  733. if (!is_file($serverPackageName) && !is_file($clientPackageName) && !is_file($pcPackageName)) {
  734. throw new AdminException('升级文件异常,请重新下载');
  735. }
  736. // 服务端解压前,清空前端相关目录,避免旧文件残留
  737. if (is_file($serverPackageName)) {
  738. $clearDirs = [
  739. app()->getRootPath() . 'public' . DS . 'admin',
  740. app()->getRootPath() . 'public' . DS . 'statics' . DS . 'mp_view',
  741. app()->getRootPath() . 'public' . DS . 'statics' . DS . 'download',
  742. ];
  743. foreach ($clearDirs as $clearDir) {
  744. if (is_dir($clearDir)) {
  745. $this->clearDirectory($clearDir);
  746. }
  747. }
  748. if (!$fileService->extractFile($serverPackageName, app()->getRootPath())) {
  749. throw new AdminException('服务端解压失败');
  750. }
  751. }
  752. if (is_file($clientPackageName) && !$fileService->extractFile($clientPackageName, app()->getRootPath())) {
  753. throw new AdminException('客户端解压失败');
  754. }
  755. if (is_file($pcPackageName) && !$fileService->extractFile($pcPackageName, app()->getRootPath())) {
  756. throw new AdminException('PC端解压失败');
  757. }
  758. CacheService::set($token . '_coverage_project', 2, 86400);
  759. return true;
  760. }
  761. /**
  762. * 清空目录下所有文件和子目录(保留目录本身)
  763. * @param string $dir 目录路径
  764. * @return void
  765. */
  766. public function clearDirectory(string $dir): void
  767. {
  768. $iterator = new \RecursiveIteratorIterator(
  769. new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
  770. \RecursiveIteratorIterator::CHILD_FIRST
  771. );
  772. foreach ($iterator as $file) {
  773. if ($file->isDir()) {
  774. @rmdir($file->getRealPath());
  775. } else {
  776. @unlink($file->getRealPath());
  777. }
  778. }
  779. }
  780. /**
  781. * 回滚表结构
  782. * @param string $prefix
  783. * @param array $updateSql
  784. * @return void
  785. */
  786. public function rollbackStructure(string $prefix, array $updateSql): void
  787. {
  788. try {
  789. foreach ($updateSql as $item) {
  790. Db::execute(str_replace('@table', $prefix . $item['table'], $item['rollback_sql']));
  791. }
  792. } catch (\Exception $e) {
  793. Log::error(['msg' => '数据库结构回滚失败', 'error' => $e->getFile() . '__' . $e->getLine() . '__' . $e->getMessage(), 'data' => $updateSql]);
  794. }
  795. }
  796. /**
  797. * 恢复数据库备份
  798. * @param string $backupFileName 备份文件名
  799. * @return bool
  800. */
  801. public function restoreDatabase(string $backupFileName): bool
  802. {
  803. try {
  804. $backupPath = app()->getRootPath() . 'backup' . DS . $backupFileName;
  805. if (!is_file($backupPath)) {
  806. throw new AdminException('数据库备份文件不存在');
  807. }
  808. // 检测是否是 gz 压缩文件
  809. $isGz = (pathinfo($backupFileName, PATHINFO_EXTENSION) === 'gz');
  810. // 直接读取并执行 SQL 文件
  811. $db = \think\facade\Db::connect();
  812. if ($isGz) {
  813. $gz = gzopen($backupPath, 'r');
  814. if (!$gz) {
  815. throw new AdminException('无法打开备份文件');
  816. }
  817. $sql = '';
  818. while (!gzeof($gz)) {
  819. $line = gzgets($gz);
  820. // 跳过注释和空行
  821. if (empty(trim($line)) || strpos(trim($line), '--') === 0) {
  822. continue;
  823. }
  824. $sql .= $line;
  825. // 检测是否是完整的 SQL 语句
  826. if (preg_match('/;\s*$/', trim($sql))) {
  827. try {
  828. $db->execute($sql);
  829. } catch (\Exception $e) {
  830. // 跳过 SET 和某些特殊语句的错误
  831. if (strpos($sql, 'SET FOREIGN_KEY_CHECKS') === false) {
  832. Log::warning('执行SQL失败: ' . $e->getMessage());
  833. }
  834. }
  835. $sql = '';
  836. }
  837. }
  838. gzclose($gz);
  839. } else {
  840. $handle = fopen($backupPath, 'r');
  841. if (!$handle) {
  842. throw new AdminException('无法打开备份文件');
  843. }
  844. $sql = '';
  845. while (!feof($handle)) {
  846. $line = fgets($handle);
  847. // 跳过注释和空行
  848. if (empty(trim($line)) || strpos(trim($line), '--') === 0) {
  849. continue;
  850. }
  851. $sql .= $line;
  852. // 检测是否是完整的 SQL 语句
  853. if (preg_match('/;\s*$/', trim($sql))) {
  854. try {
  855. $db->execute($sql);
  856. } catch (\Exception $e) {
  857. if (strpos($sql, 'SET FOREIGN_KEY_CHECKS') === false) {
  858. Log::warning('执行SQL失败: ' . $e->getMessage());
  859. }
  860. }
  861. $sql = '';
  862. }
  863. }
  864. fclose($handle);
  865. }
  866. Log::notice(['type' => 'database_restore', 'file' => $backupFileName]);
  867. return true;
  868. } catch (\Exception $e) {
  869. Log::error('数据库恢复失败: ' . $e->getMessage());
  870. throw new AdminException('数据库恢复失败: ' . $e->getMessage());
  871. }
  872. }
  873. /**
  874. * 恢复项目文件备份
  875. * @param string $backupFileName 备份文件名
  876. * @return bool
  877. */
  878. public function restoreProject(string $backupFileName): bool
  879. {
  880. try {
  881. $backupPath = app()->getRootPath() . 'backup' . DS . $backupFileName;
  882. if (!is_file($backupPath)) {
  883. throw new AdminException('项目备份文件不存在');
  884. }
  885. /** @var FileService $fileService */
  886. $fileService = app()->make(FileService::class);
  887. // 解压zip文件到项目根目录
  888. $result = $fileService->extractFile($backupPath, app()->getRootPath());
  889. if (!$result) {
  890. throw new AdminException('项目文件恢复失败');
  891. }
  892. Log::notice(['type' => 'project_restore', 'file' => $backupFileName]);
  893. return true;
  894. } catch (\Exception $e) {
  895. Log::error('项目文件恢复失败: ' . $e->getMessage());
  896. throw new AdminException('项目文件恢复失败: ' . $e->getMessage());
  897. }
  898. }
  899. /**
  900. * 完整回退到指定版本
  901. * @param int $logId 升级日志ID
  902. * @return array
  903. */
  904. public function rollbackToVersion(int $logId): array
  905. {
  906. try {
  907. // 获取升级记录
  908. $logData = $this->dao->getOne(['id' => $logId]);
  909. if (!$logData) {
  910. throw new AdminException('升级记录不存在');
  911. }
  912. $result = [
  913. 'database_restored' => false,
  914. 'project_restored' => false,
  915. 'version_restored' => false,
  916. 'message' => ''
  917. ];
  918. // 1. 恢复数据库
  919. if (!empty($logData['data_link'])) {
  920. $this->restoreDatabase($logData['data_link']);
  921. $result['database_restored'] = true;
  922. }
  923. // 2. 恢复项目文件
  924. if (!empty($logData['package_link'])) {
  925. $this->restoreProject($logData['package_link']);
  926. $result['project_restored'] = true;
  927. }
  928. // 3. 恢复版本号
  929. $versionStr = sprintf(
  930. '%s.%s.%s.%s',
  931. $logData['first_version'] ?? '5',
  932. $logData['second_version'] ?? '5',
  933. $logData['third_version'] ?? '0',
  934. $logData['fourth_version'] ?? '0'
  935. );
  936. $versionCode = (int)($logData['first_version'] . $logData['second_version'] . $logData['third_version']);
  937. $versionManager = $this->getVersionManager();
  938. $versionManager->updateVersionFile('CRMEB-BZ v' . $versionStr, $versionCode);
  939. $result['version_restored'] = true;
  940. $result['message'] = '回退成功';
  941. Log::notice(['type' => 'version_rollback', 'log_id' => $logId, 'version' => $versionStr]);
  942. return $result;
  943. } catch (\Exception $e) {
  944. Log::error('版本回退失败: ' . $e->getMessage());
  945. throw new AdminException('版本回退失败: ' . $e->getMessage());
  946. }
  947. }
  948. /**
  949. * 获取可回退的版本列表
  950. * @return array
  951. */
  952. public function getRollbackVersions(): array
  953. {
  954. $list = $this->dao->getList(['id', 'title', 'first_version', 'second_version', 'third_version', 'fourth_version', 'upgrade_time', 'package_link', 'data_link'], 1, 20);
  955. $rootPath = app()->getRootPath();
  956. $rollbackList = [];
  957. foreach ($list as $item) {
  958. $canRollback = true;
  959. $reason = [];
  960. // 检查备份文件是否存在
  961. if (empty($item['package_link']) || !is_file($rootPath . 'backup' . DS . $item['package_link'])) {
  962. $canRollback = false;
  963. $reason[] = '项目备份文件不存在';
  964. }
  965. if (empty($item['data_link']) || !is_file($rootPath . 'backup' . DS . $item['data_link'])) {
  966. $canRollback = false;
  967. $reason[] = '数据库备份文件不存在';
  968. }
  969. $rollbackList[] = [
  970. 'id' => $item['id'],
  971. 'title' => $item['title'],
  972. 'version' => sprintf('v%s.%s.%s', $item['first_version'], $item['second_version'], $item['third_version']),
  973. 'upgrade_time' => date('Y-m-d H:i:s', $item['upgrade_time']),
  974. 'can_rollback' => $canRollback,
  975. 'reason' => implode(', ', $reason)
  976. ];
  977. }
  978. return $rollbackList;
  979. }
  980. /**
  981. * 检查访问权限
  982. * @param array $data
  983. * @return bool
  984. */
  985. public function checkAuth(array $data): bool
  986. {
  987. if (!isset($data['status']) || $data['status'] != 200) {
  988. if ($data['status'] == '请输入账号和密码') {
  989. $this->getAuth();
  990. }
  991. Log::error(['msg' => $data['msg'] ?? '', 'error' => $data]);
  992. return false;
  993. }
  994. return true;
  995. }
  996. /**
  997. * 升级状态
  998. * @return array
  999. */
  1000. public function getUpgradeStatus(): array
  1001. {
  1002. $this->getSign($this->timeStamp);
  1003. $result = HttpService::getRequest(self::UPGRADE_STATUS_URL, $this->requestData, ['Access-Token: Bearer ' . CacheService::get('upgrade_auth_token')]);
  1004. if (!$result) {
  1005. throw new AdminException('升级状态获取失败');
  1006. }
  1007. $data = json_decode($result, true);
  1008. $this->checkAuth($data);
  1009. if (!isset($data['data']['auth']) || !$data['data']['auth']) {
  1010. throw new AdminException('您的域名未授权,请先授权');
  1011. }
  1012. $upgradeData['status'] = $data['data']['status'] ?? 0;
  1013. $upgradeData['force_reminder'] = $data['data']['force_reminder'] ?? 0;
  1014. $upgradeData['title'] = $upgradeData['status'] < 1 ? "您已升级至最新版本,无需更新" : "系统有新版本可更新";
  1015. return $upgradeData;
  1016. }
  1017. /**
  1018. * 重新执行升级
  1019. * @param $type
  1020. * @return bool
  1021. * @author wuhaotian
  1022. * @email 442384644@qq.com
  1023. * @date 2026/2/27
  1024. */
  1025. public function reExecute($type)
  1026. {
  1027. $token = CacheService::get('upgrade_token');
  1028. switch ($type) {
  1029. case 0:
  1030. CacheService::set($token . '_check_md5_status', 0, 86400);
  1031. break;
  1032. case 1:
  1033. CacheService::set($token . '_database_backup', 0, 86400);
  1034. CacheService::set($token . '_project_backup', 0, 86400);
  1035. break;
  1036. case 2:
  1037. CacheService::set($token . '_server_package', 0, 86400);
  1038. CacheService::set($token . '_client_package', 0, 86400);
  1039. CacheService::set($token . '_pc_package', 0, 86400);
  1040. break;
  1041. case 3:
  1042. CacheService::set($token . '_coverage_project', 0, 86400);
  1043. break;
  1044. default:
  1045. throw new AdminException('参数错误');
  1046. }
  1047. return true;
  1048. }
  1049. /**
  1050. * 获取下载进度
  1051. * 返回下载包、备份、解压的进度状态
  1052. * @param $type
  1053. */
  1054. public function getDownloadProgress($type)
  1055. {
  1056. // 如果type不在0,1,2,3,4中,则返回错误
  1057. if (!in_array($type, [0, 1, 2, 3, 4])) {
  1058. throw new AdminException('参数错误');
  1059. }
  1060. // 获取升级token
  1061. $token = CacheService::get('upgrade_token');
  1062. if (empty($token)) {
  1063. return [
  1064. 'stage' => 'idle',
  1065. 'progress' => 0,
  1066. 'message' => '未开始下载',
  1067. 'completed' => false,
  1068. 'can_upgrade' => false
  1069. ];
  1070. }
  1071. // 阶段1:检测文件是否有改动
  1072. if ($type == 0) {
  1073. // 检查MD5状态,0未检查,1检查中,2检查成功,3忽略
  1074. $checkMd5Status = (int)CacheService::get($token . '_check_md5_status', 0);
  1075. // 检查MD5文件差异数据,空为无差异,否则为差异文件
  1076. $checkMd5File = (array)CacheService::get($token . '_check_md5_file', []);
  1077. // 检测文件是否有改动
  1078. if ($checkMd5Status < 2) {
  1079. // 检测文件状态为0时,开始检测
  1080. if ($checkMd5Status == 0) {
  1081. // 设置检测状态为1,开始检测
  1082. CacheService::set($token . '_check_md5_status', 1, 86400);
  1083. // 触发检测任务
  1084. UpgradeJob::dispatch('checkFileMd5', [$token]);
  1085. }
  1086. // 检测文件状态为1时,等待检测完成
  1087. if (empty($checkMd5File)) {
  1088. return [
  1089. 'stage' => 'loading',
  1090. 'type' => 0,
  1091. 'progress' => 0,
  1092. 'message' => '正在检测文件是否有改动...',
  1093. 'completed' => false,
  1094. 'can_upgrade' => false
  1095. ];
  1096. } else {
  1097. return [
  1098. 'stage' => 'error',
  1099. 'type' => 0,
  1100. 'progress' => 0,
  1101. 'message' => '文件有改动,请确认是否继续升级...',
  1102. 'completed' => false,
  1103. 'can_upgrade' => false,
  1104. 'data' => $checkMd5File
  1105. ];
  1106. }
  1107. } else {
  1108. return [
  1109. 'stage' => 'success',
  1110. 'type' => 0,
  1111. 'progress' => 0,
  1112. 'message' => '文件检测完成',
  1113. 'completed' => false,
  1114. 'can_upgrade' => false
  1115. ];
  1116. }
  1117. }
  1118. // 阶段2: 备份
  1119. if ($type == 1) {
  1120. // 检测数据库备份状态,0未备份,1备份中,2备份成功,3忽略
  1121. $databaseBackup = (int)CacheService::get($token . '_database_backup', 0);
  1122. // 检测项目备份状态,0未备份,1备份中,2备份成功,3忽略
  1123. $projectBackup = (int)CacheService::get($token . '_project_backup', 0);
  1124. // 检测备份
  1125. if ($databaseBackup < 2 || $projectBackup < 2) {
  1126. // 检测数据库备份状态为0时,开始备份
  1127. if ($databaseBackup == 0) {
  1128. // 设置数据库备份状态为1,开始备份
  1129. CacheService::set($token . '_database_backup', 1, 86400);
  1130. // 触发备份任务
  1131. UpgradeJob::dispatch('databaseBackup', [$token]);
  1132. }
  1133. // 检测项目备份状态为0时,开始备份
  1134. if ($projectBackup == 0) {
  1135. // 设置项目备份状态为1,开始备份
  1136. CacheService::set($token . '_project_backup', 1, 86400);
  1137. // 触发备份任务
  1138. UpgradeJob::dispatch('projectBackup', [$token]);
  1139. }
  1140. return [
  1141. 'stage' => 'loading',
  1142. 'type' => 1,
  1143. 'progress' => 33,
  1144. 'message' => '正在备份文件和数据库...',
  1145. 'completed' => false,
  1146. 'can_upgrade' => false
  1147. ];
  1148. } else {
  1149. // 检测文件状态为2时,检查文件是否备份成功,备份路径为 backup/600-1.project.zip和 backup/600-1.sql.gz
  1150. $backupPath = app()->getRootPath() . 'backup' . DS;
  1151. $versionData = $this->getVersion();
  1152. $projectBackupFile = $backupPath . $versionData['version_code'] . '-1.project.zip';
  1153. $databaseBackupFile = $backupPath . $versionData['version_code'] . '-1.sql.gz';
  1154. if (!is_file($projectBackupFile) || !is_file($databaseBackupFile)) {
  1155. return [
  1156. 'stage' => 'error',
  1157. 'type' => 1,
  1158. 'progress' => 33,
  1159. 'message' => '系统备份失败!',
  1160. 'completed' => false,
  1161. 'can_upgrade' => false
  1162. ];
  1163. } else {
  1164. return [
  1165. 'stage' => 'success',
  1166. 'type' => 1,
  1167. 'progress' => 33,
  1168. 'message' => '系统备份成功!',
  1169. 'completed' => false,
  1170. 'can_upgrade' => false
  1171. ];
  1172. }
  1173. }
  1174. }
  1175. // 阶段3: 下载更新包
  1176. if ($type == 2) {
  1177. // 检测服务端升级包状态,0未下载,1下载中,2下载成功,3忽略
  1178. $serverPackage = (int)CacheService::get($token . '_server_package', 0);
  1179. // 检测客户端升级包状态,0未下载,1下载中,2下载成功,3忽略
  1180. $clientPackage = (int)CacheService::get($token . '_client_package', 0);
  1181. // 检测PC升级包状态,0未下载,1下载中,2下载成功,3忽略
  1182. $pcPackage = (int)CacheService::get($token . '_pc_package', 0);
  1183. // 检测升级包下载
  1184. if ($serverPackage < 2 || $clientPackage < 2 || $pcPackage < 2) {
  1185. return [
  1186. 'stage' => 'loading',
  1187. 'type' => 2,
  1188. 'progress' => 66,
  1189. 'message' => '正在下载升级包...',
  1190. 'completed' => false,
  1191. 'can_upgrade' => false,
  1192. 'detail' => [
  1193. 'server' => $serverPackage,
  1194. 'client' => $clientPackage,
  1195. 'pc' => $pcPackage
  1196. ]
  1197. ];
  1198. } else {
  1199. return [
  1200. 'stage' => 'success',
  1201. 'type' => 2,
  1202. 'progress' => 66,
  1203. 'message' => '升级包下载完成!',
  1204. 'completed' => false,
  1205. 'can_upgrade' => false
  1206. ];
  1207. }
  1208. }
  1209. // 阶段4: 覆盖数据库升级文件,进行升级
  1210. if ($type == 3) {
  1211. $coverageProject = (int)CacheService::get($token . '_coverage_project', 0);
  1212. if ($coverageProject < 2) {
  1213. // 解压覆盖数据库更新文件
  1214. if ($coverageProject == 0) {
  1215. // 设置解压覆盖项目状态为1,开始解压覆盖
  1216. CacheService::set($token . '_coverage_project', 1, 86400);
  1217. // 获取下载解压的文件目录
  1218. $serverPackagePath = CacheService::get($token . '_server_package_path');
  1219. // 判断目录$downloadFilePath目录下面是否存在config/和upgrade/versions/目录,如果存在,将这两个文件夹覆盖到项目根目录
  1220. $configPath = $serverPackagePath . DS . 'config';
  1221. $versionsPath = $serverPackagePath . DS . 'upgrade' . DS . 'versions';
  1222. if (is_dir($configPath) && is_dir($versionsPath)) {
  1223. /** @var FileService $fileService */
  1224. $fileService = app()->make(FileService::class);
  1225. // 复制config目录
  1226. $res = $fileService->copyDir($configPath, app()->getRootPath() . 'config');
  1227. // 复制upgrade/versions目录
  1228. $res = $res && $fileService->copyDir($versionsPath, app()->getRootPath() . 'upgrade' . DS . 'versions');
  1229. // 覆盖成功
  1230. if ($res) {
  1231. // 设置解压覆盖项目状态为2,覆盖成功
  1232. CacheService::set($token . '_coverage_project', 2, 86400);
  1233. return [
  1234. 'stage' => 'loading',
  1235. 'type' => 3,
  1236. 'progress' => 100,
  1237. 'message' => '覆盖数据库升级文件完成,开始执行升级...',
  1238. 'completed' => true,
  1239. 'can_upgrade' => true,
  1240. ];
  1241. }
  1242. }
  1243. }
  1244. } else {
  1245. // 执行所有跨版本升级
  1246. $data = $this->executeAllCrossVersionUpgrade();
  1247. // 执行成功
  1248. return [
  1249. 'stage' => 'success',
  1250. 'type' => 3,
  1251. 'progress' => 100,
  1252. 'message' => '数据库更新完成',
  1253. 'completed' => true,
  1254. 'can_upgrade' => true,
  1255. 'data' => $data
  1256. ];
  1257. }
  1258. }
  1259. // 阶段5: 覆盖项目文件
  1260. if ($type == 4) {
  1261. // 覆盖项目文件
  1262. $this->coverageProject($token);
  1263. return [
  1264. 'stage' => 'complete',
  1265. 'type' => 4,
  1266. 'progress' => 100,
  1267. 'message' => '全部更新完成',
  1268. 'completed' => true,
  1269. 'can_upgrade' => true,
  1270. 'routine_upload_data' => CacheService::get('routine_upload_data', [])
  1271. ];
  1272. }
  1273. }
  1274. /**
  1275. * 升级日志
  1276. * @return array
  1277. * @throws \think\db\exception\DataNotFoundException
  1278. * @throws \think\db\exception\DbException
  1279. * @throws \think\db\exception\ModelNotFoundException
  1280. */
  1281. public function getUpgradeLogList(): array
  1282. {
  1283. [$page, $limit] = $this->getPageValue();
  1284. $count = $this->dao->count();
  1285. $list = $this->dao->getList(['id', 'title', 'content', 'first_version', 'second_version', 'third_version', 'fourth_version', 'upgrade_time', 'package_link', 'data_link'], $page, $limit);
  1286. $rootPath = app()->getRootPath();
  1287. foreach ($list as &$item) {
  1288. $item['file_status'] = 0;
  1289. $item['data_status'] = 0;
  1290. if ($item['package_link'] && is_file($rootPath . 'backup' . DS . $item['package_link'])) {
  1291. $item['package_link'] = 'backup/' . $item['package_link'];
  1292. $item['file_status'] = 1;
  1293. }
  1294. if ($item['data_link'] && is_file($rootPath . 'backup' . DS . $item['data_link'])) {
  1295. $item['data_link'] = 'backup/' . $item['data_link'];
  1296. $item['data_status'] = 1;
  1297. }
  1298. $item['upgrade_time'] = date('Y-m-d H:i:s', $item['upgrade_time']);
  1299. }
  1300. return compact('list', 'count');
  1301. }
  1302. /**
  1303. * 导出
  1304. * @param int $id
  1305. * @param string $type
  1306. * @return void
  1307. * @throws \think\db\exception\DataNotFoundException
  1308. * @throws \think\db\exception\DbException
  1309. * @throws \think\db\exception\ModelNotFoundException
  1310. */
  1311. public function export(int $id, string $type)
  1312. {
  1313. $data = $this->dao->getOne(['id' => $id], 'package_link, data_link');
  1314. if (!$data || !$data['package_link']) {
  1315. throw new AdminException('备份文件不存在');
  1316. }
  1317. $fileName = $type == 'file' ? $data['package_link'] : $data['data_link'];
  1318. $filePath = app()->getRootPath() . 'backup' . DS . $fileName;
  1319. if (!is_file($filePath)) {
  1320. throw new AdminException('备份文件不存在');
  1321. }
  1322. //下载文件
  1323. header('Content-Description: File Transfer');
  1324. header('Content-Type: application/octet-stream');
  1325. header('Content-Disposition: attachment; filename=' . $fileName);
  1326. header('Content-Transfer-Encoding: binary');
  1327. header('Expires: 0');
  1328. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  1329. header('Pragma: public');
  1330. header('Content-Length: ' . filesize($filePath));
  1331. ob_clean();
  1332. flush();
  1333. readfile($filePath); //输出文件
  1334. }
  1335. /**
  1336. * 检查数据库大小
  1337. * @return bool
  1338. */
  1339. public function checkDatabaseSize(): bool
  1340. {
  1341. if (!$database = Config::get('database.connections.' . Config::get('database.default') . '.database')) {
  1342. throw new AdminException('数据库信息获取失败');
  1343. }
  1344. $result = Db::query("select concat(round(sum(data_length/1024/1024))) as size from information_schema.tables where table_schema='{$database}';");
  1345. if ((int)($result[0]['size'] ?? '') > 500) {
  1346. throw new AdminException('数据库文件过大, 不能升级');
  1347. }
  1348. return true;
  1349. }
  1350. // ==================== 跨版本升级相关方法 ====================
  1351. /**
  1352. * 获取版本管理器实例
  1353. * @return \upgrade\VersionManager
  1354. */
  1355. protected function getVersionManager()
  1356. {
  1357. // 手动加载 VersionManager 类(避免修改 composer.json)
  1358. $file = app()->getRootPath() . 'upgrade' . DIRECTORY_SEPARATOR . 'VersionManager.php';
  1359. if (!class_exists('\\upgrade\\VersionManager') && file_exists($file)) {
  1360. require_once $file;
  1361. }
  1362. return new \upgrade\VersionManager();
  1363. }
  1364. /**
  1365. * 获取跨版本升级概览
  1366. * @return array
  1367. */
  1368. public function getCrossVersionUpgradeOverview(): array
  1369. {
  1370. $versionManager = $this->getVersionManager();
  1371. return $versionManager->getUpgradeOverview();
  1372. }
  1373. /**
  1374. * 获取待升级版本列表
  1375. * @return array
  1376. */
  1377. public function getPendingVersions(): array
  1378. {
  1379. $versionManager = $this->getVersionManager();
  1380. return $versionManager->getPendingVersions();
  1381. }
  1382. /**
  1383. * 获取所有待执行的升级SQL
  1384. * @return array
  1385. */
  1386. public function getAllPendingUpgradeSql(): array
  1387. {
  1388. $versionManager = $this->getVersionManager();
  1389. return $versionManager->getAllPendingUpgradeSql();
  1390. }
  1391. /**
  1392. * 执行跨版本升级
  1393. * @param int $step 当前执行到第几步
  1394. * @return array ['success' => bool, 'step' => int, 'total' => int, 'message' => string, 'completed' => bool]
  1395. */
  1396. public function executeCrossVersionUpgrade(int $step = 0): array
  1397. {
  1398. $versionManager = $this->getVersionManager();
  1399. $allSql = $versionManager->getAllPendingUpgradeSql();
  1400. $total = count($allSql);
  1401. // 检查是否已完成所有升级
  1402. if ($step >= $total) {
  1403. // 获取升级前的版本信息
  1404. $beforeVersion = CacheService::get('cross_version_before_version', []);
  1405. $pendingVersions = $versionManager->getPendingVersions();
  1406. // 更新版本文件为最新版本
  1407. $latestVersion = $versionManager->getLatestVersion();
  1408. if (!empty($latestVersion)) {
  1409. $versionManager->updateVersionFile($latestVersion['version'], $latestVersion['code']);
  1410. }
  1411. // 保存升级日志
  1412. $token = CacheService::get('cross_version_upgrade_token', '');
  1413. if ($token) {
  1414. $this->saveCrossVersionUpgradeLog($token, $beforeVersion, $latestVersion, $pendingVersions);
  1415. }
  1416. return [
  1417. 'success' => true,
  1418. 'step' => $step,
  1419. 'total' => $total,
  1420. 'message' => '升级完成',
  1421. 'completed' => true,
  1422. 'current_version' => $latestVersion['version'] ?? ''
  1423. ];
  1424. }
  1425. // 对于第一步,执行备份
  1426. if ($step == 0) {
  1427. // 记录升级前的版本信息
  1428. $beforeVersion = $versionManager->getCurrentVersion();
  1429. CacheService::set('cross_version_before_version', $beforeVersion, 86400);
  1430. $token = md5(time() . uniqid());
  1431. CacheService::set('cross_version_upgrade_token', $token, 86400);
  1432. $backupResult = $this->performBackup($token);
  1433. if (!$backupResult) {
  1434. return [
  1435. 'success' => false,
  1436. 'step' => $step,
  1437. 'total' => $total,
  1438. 'message' => '备份失败,无法继续升级',
  1439. 'completed' => false
  1440. ];
  1441. }
  1442. }
  1443. // 执行当前步骤的SQL
  1444. $sqlItem = $allSql[$step];
  1445. $result = $versionManager->executeSqlItem($sqlItem);
  1446. $message = $result['message'] ?? '';
  1447. if (isset($sqlItem['version'])) {
  1448. $message = "[{$sqlItem['version']}] " . $message;
  1449. }
  1450. return [
  1451. 'success' => $result['success'],
  1452. 'step' => $step + 1,
  1453. 'total' => $total,
  1454. 'message' => $message,
  1455. 'completed' => false,
  1456. 'skipped' => $result['skipped'] ?? false,
  1457. 'sql_info' => [
  1458. 'table' => $sqlItem['table'] ?? '',
  1459. 'field' => $sqlItem['field'] ?? '',
  1460. 'type' => $sqlItem['type'] ?? 0,
  1461. 'version' => $sqlItem['version'] ?? ''
  1462. ]
  1463. ];
  1464. }
  1465. /**
  1466. * 一键执行全部跨版本升级
  1467. * @return array
  1468. */
  1469. public function executeAllCrossVersionUpgrade(): array
  1470. {
  1471. $versionManager = $this->getVersionManager();
  1472. $pendingVersions = $versionManager->getPendingVersions();
  1473. if (empty($pendingVersions)) {
  1474. return [
  1475. 'success' => true,
  1476. 'message' => '当前已是最新版本,无需升级',
  1477. 'executed' => 0,
  1478. 'skipped' => 0,
  1479. 'failed' => 0,
  1480. 'sql_logs' => []
  1481. ];
  1482. }
  1483. // 记录升级前的版本信息
  1484. $beforeVersion = $versionManager->getCurrentVersion();
  1485. // 在执行跨版本升级前进行备份
  1486. $token = CacheService::get('upgrade_token');
  1487. CacheService::set('cross_version_upgrade_token', $token, 86400);
  1488. // 初始化进度状态
  1489. CacheService::set($token . '_sql_progress', ['current' => 0, 'total' => 0], 86400);
  1490. CacheService::set($token . '_sql_logs', [], 86400);
  1491. CacheService::set($token . '_upgrade_complete', 0, 86400);
  1492. $executed = 0;
  1493. $skipped = 0;
  1494. $failed = 0;
  1495. $failedMessages = [];
  1496. $migrationResults = [];
  1497. $sqlLogs = [];
  1498. // 统计总SQL数量
  1499. $totalSql = 0;
  1500. foreach ($pendingVersions as $version) {
  1501. $upgradeData = $versionManager->getVersionUpgradeData($version);
  1502. if (!empty($upgradeData['update_sql'])) {
  1503. $totalSql += count($upgradeData['update_sql']);
  1504. }
  1505. }
  1506. $this->updateSqlProgress($token, 0, $totalSql);
  1507. $currentSql = 0;
  1508. // 遍历每个版本
  1509. foreach ($pendingVersions as $version) {
  1510. $upgradeData = $versionManager->getVersionUpgradeData($version);
  1511. // 执行SQL升级
  1512. if (!empty($upgradeData['update_sql'])) {
  1513. foreach ($upgradeData['update_sql'] as $sqlItem) {
  1514. $currentSql++;
  1515. $result = $versionManager->executeSqlItem($sqlItem);
  1516. // 记录SQL执行日志
  1517. $logEntry = [
  1518. 'version' => $version['version'],
  1519. 'table' => $sqlItem['table'] ?? '-',
  1520. 'field' => $sqlItem['field'] ?? '',
  1521. 'type' => $sqlItem['type'] ?? 0,
  1522. 'status' => $result['success'] ? (isset($result['skipped']) && $result['skipped'] ? 'skipped' : 'success') : 'failed',
  1523. 'message' => $result['message'] ?? ''
  1524. ];
  1525. $sqlLogs[] = $logEntry;
  1526. $this->addSqlLog($token, $logEntry);
  1527. if ($result['success']) {
  1528. if (isset($result['skipped']) && $result['skipped']) {
  1529. $skipped++;
  1530. } else {
  1531. $executed++;
  1532. }
  1533. } else {
  1534. $failed++;
  1535. $failedMessages[] = "[{$version['version']}] " . $result['message'];
  1536. }
  1537. // 更新进度
  1538. $this->updateSqlProgress($token, $currentSql, $totalSql);
  1539. }
  1540. }
  1541. // 执行数据迁移处理器
  1542. if (!empty($upgradeData['data_handlers']) && $failed == 0) {
  1543. /** @var DataMigrationServices $migrationServices */
  1544. $migrationServices = app()->make(DataMigrationServices::class);
  1545. $migrationResult = $migrationServices->executeAllHandlers($upgradeData['data_handlers']);
  1546. $migrationResults[$version['version']] = $migrationResult;
  1547. if (!$migrationResult['success']) {
  1548. $failed++;
  1549. $failedMessages[] = "[{$version['version']}] 数据迁移失败";
  1550. }
  1551. }
  1552. // 每个版本升级完成后更新版本文件
  1553. $versionManager->updateVersionFile($version['version'], $version['code']);
  1554. Log::notice(['type' => 'cross_version_upgrade', 'version' => $version['version'], 'code' => $version['code']]);
  1555. }
  1556. // 标记升级完成
  1557. $this->markUpgradeComplete($token);
  1558. // 最终获取最新版本信息
  1559. $latestVersion = $versionManager->getLatestVersion();
  1560. // 升级成功后保存升级日志
  1561. if ($failed == 0) {
  1562. $this->saveCrossVersionUpgradeLog($token, $beforeVersion, $latestVersion, $pendingVersions);
  1563. }
  1564. return [
  1565. 'success' => $failed == 0,
  1566. 'message' => $failed == 0 ? '升级成功' : '部分SQL执行失败',
  1567. 'executed' => $executed,
  1568. 'skipped' => $skipped,
  1569. 'failed' => $failed,
  1570. 'total' => $totalSql,
  1571. 'failed_messages' => $failedMessages,
  1572. 'migration_results' => $migrationResults,
  1573. 'current_version' => $latestVersion['version'] ?? '',
  1574. 'sql_logs' => $sqlLogs
  1575. ];
  1576. }
  1577. /**
  1578. * 执行备份
  1579. * @param string $token
  1580. * @return bool
  1581. */
  1582. protected function performBackup(string $token): bool
  1583. {
  1584. try {
  1585. // 执行数据库备份
  1586. CacheService::set($token . '_database_backup', 1, 86400);
  1587. $this->databaseBackup($token);
  1588. // 等待数据库备份完成
  1589. $maxWait = 30; // 最大等待30秒
  1590. $waited = 0;
  1591. while ($waited < $maxWait) {
  1592. if (CacheService::get($token . '_database_backup') == 2) {
  1593. break;
  1594. }
  1595. sleep(1);
  1596. $waited++;
  1597. }
  1598. if (CacheService::get($token . '_database_backup') != 2) {
  1599. throw new AdminException('数据库备份超时');
  1600. }
  1601. // 执行项目备份
  1602. CacheService::set($token . '_project_backup', 1, 86400);
  1603. $this->projectBackup($token);
  1604. // 等待项目备份完成
  1605. $waited = 0;
  1606. while ($waited < $maxWait) {
  1607. if (CacheService::get($token . '_project_backup') == 2) {
  1608. break;
  1609. }
  1610. sleep(1);
  1611. $waited++;
  1612. }
  1613. if (CacheService::get($token . '_project_backup') != 2) {
  1614. throw new AdminException('项目备份超时');
  1615. }
  1616. return true;
  1617. } catch (\Exception $e) {
  1618. Log::error('执行备份失败: ' . $e->getMessage());
  1619. return false;
  1620. }
  1621. }
  1622. /**
  1623. * 获取备份状态
  1624. * @return array
  1625. */
  1626. public function getBackupStatus(): array
  1627. {
  1628. $token = CacheService::get('upgrade_token');
  1629. if (empty($token)) {
  1630. return [
  1631. 'database_backup' => 0,
  1632. 'project_backup' => 0,
  1633. 'message' => '未开始备份'
  1634. ];
  1635. }
  1636. $databaseBackup = CacheService::get($token . '_database_backup');
  1637. $projectBackup = CacheService::get($token . '_project_backup');
  1638. return [
  1639. 'database_backup' => $databaseBackup ?? 0,
  1640. 'project_backup' => $projectBackup ?? 0,
  1641. 'message' => $this->getBackupMessage($databaseBackup, $projectBackup)
  1642. ];
  1643. }
  1644. /**
  1645. * 获取备份状态信息
  1646. * @param int $databaseBackup
  1647. * @param int $projectBackup
  1648. * @return string
  1649. */
  1650. private function getBackupMessage(int $databaseBackup, int $projectBackup): string
  1651. {
  1652. if ($databaseBackup == 2 && $projectBackup == 2) {
  1653. return '备份完成';
  1654. } elseif ($databaseBackup == 1 || $projectBackup == 1) {
  1655. return '正在备份';
  1656. } elseif ($databaseBackup == 0 && $projectBackup == 0) {
  1657. return '未开始备份';
  1658. } else {
  1659. return '备份异常';
  1660. }
  1661. }
  1662. /**
  1663. * 获取升级进度详情
  1664. * @return array
  1665. */
  1666. public function getUpgradeProgressDetail(): array
  1667. {
  1668. $token = CacheService::get('cross_version_upgrade_token') ?: CacheService::get('upgrade_token');
  1669. if (empty($token)) {
  1670. return [
  1671. 'step' => 0,
  1672. 'progress' => 0,
  1673. 'step_details' => [
  1674. 'database' => '未开始',
  1675. 'project' => '未开始',
  1676. 'sql' => '未开始',
  1677. 'complete' => '未开始',
  1678. ],
  1679. 'sql_logs' => [],
  1680. ];
  1681. }
  1682. // 获取各步骤状态
  1683. $databaseBackup = CacheService::get($token . '_database_backup', 0);
  1684. $projectBackup = CacheService::get($token . '_project_backup', 0);
  1685. $sqlProgress = CacheService::get($token . '_sql_progress', ['current' => 0, 'total' => 0]);
  1686. $sqlLogs = CacheService::get($token . '_sql_logs', []);
  1687. $upgradeComplete = CacheService::get($token . '_upgrade_complete', 0);
  1688. // 计算当前步骤和进度
  1689. $step = 0;
  1690. $progress = 0;
  1691. $stepDetails = [
  1692. 'database' => '等待中...',
  1693. 'project' => '等待中...',
  1694. 'sql' => '等待中...',
  1695. 'complete' => '等待中...',
  1696. ];
  1697. // 步骤1: 数据库备份
  1698. if ($databaseBackup == 1) {
  1699. $step = 0;
  1700. $progress = 10;
  1701. $stepDetails['database'] = '正在备份数据库...';
  1702. } elseif ($databaseBackup == 2) {
  1703. $step = 1;
  1704. $progress = 25;
  1705. $stepDetails['database'] = '数据库备份成功 ✓';
  1706. }
  1707. // 步骤2: 项目文件备份
  1708. if ($databaseBackup == 2) {
  1709. if ($projectBackup == 1) {
  1710. $step = 1;
  1711. $progress = 35;
  1712. $stepDetails['project'] = '正在备份项目文件...';
  1713. } elseif ($projectBackup == 2) {
  1714. $step = 2;
  1715. $progress = 50;
  1716. $stepDetails['project'] = '项目文件备份成功 ✓';
  1717. }
  1718. }
  1719. // 步骤3: SQL执行
  1720. if ($databaseBackup == 2 && $projectBackup == 2) {
  1721. $current = $sqlProgress['current'] ?? 0;
  1722. $total = $sqlProgress['total'] ?? 0;
  1723. if ($total > 0) {
  1724. $sqlPercent = ($current / $total) * 100;
  1725. $progress = 50 + ($sqlPercent * 0.4); // 50-90
  1726. $stepDetails['sql'] = "执行中: {$current}/{$total}";
  1727. if ($current >= $total) {
  1728. $step = 3;
  1729. $progress = 90;
  1730. $failedCount = count(array_filter($sqlLogs, function ($log) { return $log['status'] === 'failed'; }));
  1731. $stepDetails['sql'] = $failedCount > 0
  1732. ? "SQL执行完成({$failedCount}项失败)"
  1733. : 'SQL执行完成 ✓';
  1734. }
  1735. } else {
  1736. $stepDetails['sql'] = '等待执行...';
  1737. }
  1738. }
  1739. // 步骤4: 完成
  1740. if ($upgradeComplete == 2) {
  1741. $step = 4;
  1742. $progress = 100;
  1743. $stepDetails['complete'] = '升级完成 ✓';
  1744. }
  1745. return [
  1746. 'step' => $step,
  1747. 'progress' => (int)$progress,
  1748. 'step_details' => $stepDetails,
  1749. 'sql_logs' => array_slice($sqlLogs, -50), // 最多返回50条
  1750. ];
  1751. }
  1752. /**
  1753. * 更新SQL执行进度
  1754. * @param string $token
  1755. * @param int $current
  1756. * @param int $total
  1757. * @return void
  1758. */
  1759. protected function updateSqlProgress(string $token, int $current, int $total): void
  1760. {
  1761. CacheService::set($token . '_sql_progress', ['current' => $current, 'total' => $total], 86400);
  1762. }
  1763. /**
  1764. * 添加SQL执行日志
  1765. * @param string $token
  1766. * @param array $log
  1767. * @return void
  1768. */
  1769. protected function addSqlLog(string $token, array $log): void
  1770. {
  1771. $logs = CacheService::get($token . '_sql_logs', []);
  1772. $logs[] = $log;
  1773. CacheService::set($token . '_sql_logs', $logs, 86400);
  1774. }
  1775. /**
  1776. * 标记升级完成
  1777. * @param string $token
  1778. * @return void
  1779. */
  1780. protected function markUpgradeComplete(string $token): void
  1781. {
  1782. CacheService::set($token . '_upgrade_complete', 2, 86400);
  1783. }
  1784. /**
  1785. * 判断是否需要跨版本升级
  1786. * @return bool
  1787. */
  1788. public function needCrossVersionUpgrade(): bool
  1789. {
  1790. $versionManager = $this->getVersionManager();
  1791. return $versionManager->needUpgrade();
  1792. }
  1793. /**
  1794. * 获取版本差距
  1795. * @return int
  1796. */
  1797. public function getVersionGap(): int
  1798. {
  1799. $versionManager = $this->getVersionManager();
  1800. return $versionManager->getVersionGap();
  1801. }
  1802. /**
  1803. * 检查跨版本升级可用性
  1804. * 检查当前版本是否满足最低版本要求
  1805. * @return array
  1806. */
  1807. public function checkCrossVersionUpgradeAvailability(): array
  1808. {
  1809. $versionManager = $this->getVersionManager();
  1810. return $versionManager->checkUpgradeAvailability();
  1811. }
  1812. /**
  1813. * 当前版本是否满足最低版本要求
  1814. * @return bool
  1815. */
  1816. public function meetsMinVersionRequirement(): bool
  1817. {
  1818. $versionManager = $this->getVersionManager();
  1819. return $versionManager->meetsMinVersionRequirement();
  1820. }
  1821. /**
  1822. * 保存跨版本升级日志
  1823. * @param string $token 升级token
  1824. * @param array $beforeVersion 升级前版本信息
  1825. * @param array $latestVersion 升级后版本信息
  1826. * @param array $pendingVersions 升级的版本列表
  1827. * @return bool
  1828. */
  1829. protected function saveCrossVersionUpgradeLog(string $token, array $beforeVersion, array $latestVersion, array $pendingVersions): bool
  1830. {
  1831. try {
  1832. // 解析升级前的版本号
  1833. $beforeVersionStr = $beforeVersion['version'] ?? '';
  1834. // 解析升级后的版本号
  1835. $afterVersionStr = $latestVersion['version'] ?? '';
  1836. $afterVersionParts = $this->parseVersionString($afterVersionStr);
  1837. // 获取备份文件名
  1838. $packageLink = CacheService::get($token . '_project_backup_name', '');
  1839. $dataLink = CacheService::get($token . '_database_backup_name', '');
  1840. $updateContent = CacheService::get('routine_upload_data', [])['desc'] ?? '暂无';
  1841. // 保存到数据库
  1842. $this->dao->save([
  1843. 'title' => '升级 ' . $afterVersionStr . ' 完成',
  1844. 'content' => '版本升级: ' . $beforeVersionStr . ' -> ' . $afterVersionStr . ';更新内容:' . $updateContent . ';',
  1845. 'first_version' => $afterVersionParts['first'] ?? '6',
  1846. 'second_version' => $afterVersionParts['second'] ?? '0',
  1847. 'third_version' => $afterVersionParts['third'] ?? '0',
  1848. 'fourth_version' => $afterVersionParts['fourth'] ?? '0',
  1849. 'upgrade_time' => time(),
  1850. 'error_data' => '',
  1851. 'package_link' => $packageLink,
  1852. 'data_link' => $dataLink
  1853. ]);
  1854. Log::notice(['type' => 'cross_version_upgrade_log', 'before' => $beforeVersionStr, 'after' => $afterVersionStr, 'token' => $token]);
  1855. return true;
  1856. } catch (\Exception $e) {
  1857. Log::error('保存跨版本升级日志失败: ' . $e->getMessage());
  1858. return false;
  1859. }
  1860. }
  1861. /**
  1862. * 解析版本字符串
  1863. * @param string $versionStr 例如 "CRMEB-BZ v5.6.4"
  1864. * @return array
  1865. */
  1866. protected function parseVersionString(string $versionStr): array
  1867. {
  1868. $result = ['first' => '5', 'second' => '5', 'third' => '0', 'fourth' => '0'];
  1869. // 匹配版本号 如 v5.6.4 或 5.6.4
  1870. if (preg_match('/v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?/i', $versionStr, $matches)) {
  1871. $result['first'] = $matches[1] ?? '5';
  1872. $result['second'] = $matches[2] ?? '5';
  1873. $result['third'] = $matches[3] ?? '0';
  1874. $result['fourth'] = $matches[4] ?? '0';
  1875. }
  1876. return $result;
  1877. }
  1878. }