$now_version,'web_api_url' => Yii::getAlias('@apiUrl'),'sign' => '']; //$result = CurlHelper::post($this->service_url,$post); $result = self::sendRequest($this->service_url,$post,'POST'); return $result; } /** * 升级版本 * @param array $data * @throws * @return bool */ public static function upgrade($params) { ini_set('memory_limit',-1); $data = $params['upgrade']; $now_version = $params['now_version']; $log = self::addLog($now_version,$data); // 备份插件文件 //self::backup(); //todo $sqlFiles = []; $addons_arr = []; foreach($data as $key => $value){ $addons_name = $key; $download_url = $value['download_url'] ?? ''; if(!$download_url){ continue; } $tmpFile = self::download($download_url); try { if(strpos($addons_name,'mian') === false){ if(in_array($addons_name, ['h5', 'salary-h5'])){ //h5更新 $dir = self::getH5TmpDir($addons_name); }else{ //插件更新 $dir = Yii::getAlias('@addons').DIRECTORY_SEPARATOR; } }else{ //主包更新 $addons_name = 'mian'; $dir = self::getWorkDir(); } // 解压插件 self::unzip($tmpFile,$dir); if($addons_name == 'h5'){ self::handleH5(); } if($addons_name == 'salary-h5'){ self::handleSalaryH5(); } } catch (Exception $e) { throw new Exception($download_url.','.$e->getMessage()); } finally { // 移除临时文件 @unlink($tmpFile); } if($value['is_install']){ //需要导入sql if($addons_name == 'mian'){ $sqlFiles[] = self::getWorkDir() . 'install.sql'; }else{ $sqlFiles[] = Yii::getAlias('@addons').DIRECTORY_SEPARATOR.$addons_name.DIRECTORY_SEPARATOR.'install.sql'; } } if(!in_array($addons_name,['mian','h5'])){ //插件更新配置 $addons_arr[] = $addons_name; //Addons::install($addons_name); } } //更新文件所有者 // $shell = "chown www:www -R ".\Yii::getAlias('@webpath'); // exec($shell, $result, $status); //判断是否安装mycat,如果按照 则不能执行以下操作 $dbConfig = \Yii::$app->getComponents()['db']; $dsn = $dbConfig['dsn']; preg_match('|port=(.*?);|',$dsn,$match); $port = $match[1] ?? ''; $status = 1; if($port == 3306){ //mysql 端口 if($sqlFiles){ // 导入 foreach($sqlFiles as $sql){ $res = self::importsql($sql); if(!$res) $status = -1; } } $result = self::migrate(); $status = $result === false ? -1 : $status; }else{ Yii::$app->custom->logs('数据库未处理,数据迁移迁移未处理,'.implode(',',$sqlFiles) ); self::$static_error = '请联系技术专员处理数据库'; } //更新完成:释放锁 self::unlockUpgrade(); //更新记录状态 self::updateLogStatus($log,$status); //异步处理更新插件配置 self::asynchAddonsConfig($addons_arr); //重启服务 self::restartService(); return true; } /** * 获取远程服务器 * @return string */ protected static function getServerUrl() { return Yii::getAlias('@serverUrl'); } /** * 获取备份目录 */ public static function getBackupDir() { $dir = Yii::$app->getRuntimePath().DIRECTORY_SEPARATOR.'bak'.DIRECTORY_SEPARATOR; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir; } /** * 获取项目备份地址 * @return string */ public static function getWorkBackupDir(){ //$dir = Yii::getAlias('@webpath').DIRECTORY_SEPARATOR.'../'; $dir = Yii::$app->getRuntimePath().DIRECTORY_SEPARATOR.'bak_up'.DIRECTORY_SEPARATOR; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir; } /** * 获取项目根目录 */ public static function getWorkDir() { $dir = Yii::getAlias('@webpath').DIRECTORY_SEPARATOR; return $dir; } /** * 远程下载更新包 * @param string $url 下载地址 * @return string * @throws */ public static function download($url) { $tempDir = self::getBackupDir(); $tmpFile = $tempDir . date('Y_m_d_H_i_s') . ".zip"; try { $client = self::getClient(); $response = $client->get($url); $statusCode = $response->getStatusCode(); if($statusCode != 200) throw new Exception("query result code error:".$statusCode.',url='.$url); $body = $response->getBody(); $content = $body->getContents(); } catch (TransferException $e) { throw new Exception("package download failed:".$url); } if ($write = fopen($tmpFile, 'w')) { fwrite($write, $content); fclose($write); return $tmpFile; } throw new Exception("No permission to write temporary files"); } /** * 解压压缩包 * * @param string $name 压缩包名称 * @return string * @throws Exception */ public static function unzip($file,$dir) { if (!$file || !file_exists($file)) { throw new Exception('Invalid parameters'); } // 打开插件压缩包 $zip = new ZipFile(); try { $zip->openFile($file); } catch (ZipException $e) { $zip->close(); throw new Exception('Unable to open the zip file'); } // if (!is_dir($dir)) { // @mkdir($dir, 0755); // } // 解压插件压缩包 try { $zip->extractTo($dir); } catch (ZipException $e) { throw new Exception('Unable to extract the file:'.$e->getMessage().' ,dir:'.$dir); } finally { $zip->close(); } return $dir; } /** * 获取请求对象 * @return Client */ public static function getClient() { $options = [ //'base_uri' => self::getServerUrl(), 'timeout' => 30, 'connect_timeout' => 30, 'verify' => false, 'http_errors' => false, 'headers' => [ 'X-REQUESTED-WITH' => 'XMLHttpRequest', //'Referer' => dirname(request()->root(true)), 'User-Agent' => 'qimall', ] ]; static $client; if (empty($client)) { $client = new Client($options); } return $client; } /** * 发送请求 * @return array * @throws Exception * @throws \GuzzleHttp\Exception\GuzzleException */ public static function sendRequest($url, $params = [], $method = 'POST') { $json = []; try { $client = self::getClient(); $options = strtoupper($method) == 'POST' ? ['form_params' => $params] : ['query' => $params]; $response = $client->request($method, $url, $options); $body = $response->getBody(); $content = $body->getContents(); $json = (array)json_decode($content, true); } catch (TransferException $e) { throw new Exception('Network error'); } catch (\Exception $e) { throw new Exception('Unknown data format'); } return $json; } /** * 备份项目 * @return bool * @throws Exception */ public static function backup() { //$workDir = self::getWorkDir(); $backupDir = self::getWorkBackupDir(); $file = $backupDir . 'qimall-backup-' . date("YmdHis") . '.zip'; $zipFile = new ZipFile(); try { $zipFile //->addDirRecursive($workDir) ->addDirRecursive(Yii::getAlias('@common'),'common') ->addDirRecursive(Yii::getAlias('@frontend'),'frontend') ->addDirRecursive(Yii::getAlias('@api'),'api') ->addDirRecursive(Yii::getAlias('@backend'),'backend') ->addDirRecursive(Yii::getAlias('@console'),'console') ->addDirRecursive(Yii::getAlias('@addons'),'addons') ->addDirRecursive(Yii::getAlias('@services'),'services') ->saveAsFile($file) ->close(); } catch (ZipException $e) { } finally { $zipFile->close(); } return true; } /** * 备份H5文件夹 * @return bool */ public static function backupH5() { $backupDir = self::getWorkBackupDir(); $file = $backupDir . 'h5-backup-' . date("YmdHis") . '.zip'; $zipFile = new ZipFile(); try { $zipFile ->addDirRecursive(self::getWorkDir().'h5'.DIRECTORY_SEPARATOR) ->saveAsFile($file) ->close(); } catch (ZipException $e) { } finally { $zipFile->close(); } return true; } /** * 执行SQL * @param $fileName */ public static function importsql($fileName) { //$fileName = is_null($fileName) ? 'qimall.sql' : $fileName; //$sqlFile = self::getWorkDir() . $fileName; $sqlFile = $fileName; $result = true; if (is_file($sqlFile)) { $lines = file($sqlFile); $templine = ''; foreach ($lines as $line) { if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') { continue; } $templine .= $line; if (substr(trim($line), -1, 1) == ';') { //$templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine); $templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine); try { Yii::$app->db->createCommand($templine)->execute(); } catch (Exception $e) { echo $e->getMessage(); $result = false; } $templine = ''; } } } return $result; } /** * 执行数据迁移 */ public static function migrate() { //执行yii migrate //exec('sh /www/wwwroot/qimall/run_migrate.sh', $result, $status); $sh = self::getWorkDir().'run_migrate.sh'; if(file_exists($sh)){ //执行yii migrate exec('sh '.$sh, $result, $status); if( $status ){ echo "yii migrate执行失败"; print_r( $result ); return false; }else{ echo "yii migrate成功执行, 结果如下
"; print_r( $result ); return true; } } } /** * 重启队列服务:重启supervisorctl */ public static function restartService() { //判断是否docker部署 if (is_dir('/www/wwwroot/qimall')) { $is_docker_deploy = \Yii::$app->params['is_docker_deploy'] ?? false; if($is_docker_deploy){ $restart_docker_file = self::getWorkDir().'consume_docker.txt'; if(!file_exists($restart_docker_file)){ fopen($restart_docker_file, "w"); chown($restart_docker_file,'www'); } file_put_contents($restart_docker_file,1); }else{ //需要加上全路径 $shell = "sudo /www/server/panel/pyenv/bin/supervisorctl restart rabbitmq-consume:*"; exec($shell, $result, $status); if( $status ){ echo "shell命令{$shell}执行失败"; } else { echo "shell命令{$shell}成功执行, 结果如下
"; print_r( $result ); } } } else { // 此部署方式的文件夹路径为data/wwwroot/qimall $swoole_consume_num = \Yii::$app->params['swoole_consume_num'] ?? 1; if($swoole_consume_num > 1){ for($i = 1;$i<= $swoole_consume_num;$i++){ $shell = "sudo systemctl restart swoole_consume_run@".$i; exec($shell, $result, $status); if( $status ){ echo "shell命令{$shell}执行失败"; } else { echo "shell命令{$shell}成功执行, 结果如下
"; print_r( $result ); } } }else{ $shell = "sudo systemctl restart swoole_consume_run"; exec($shell, $result, $status); if( $status ){ echo "shell命令{$shell}执行失败"; } else { echo "shell命令{$shell}成功执行, 结果如下
"; print_r( $result ); } } } } /** * 更新加锁 * @return bool * @throws Exception */ public static function lockUpgrade() { $lock_key = RedisKeyEnum::suffix(RedisKeyEnum::LOCK_VERSION_UPGRADE); $identification = 'system'; if (!LockHelper::lock($lock_key, $identification, 1200, 1)){ throw new Exception('系统更新中,请等待...'); } return true; } /** * 版本更新完成释放锁 * @return bool */ public static function unlockUpgrade() { $lock_key = RedisKeyEnum::suffix(RedisKeyEnum::LOCK_VERSION_UPGRADE); $identification = 'system'; LockHelper::uLock($lock_key,$identification); return true; } /** * 添加记录 * @param $now_version * @param $new_version * @return int */ public static function addLog($now_version,$new_version) { preg_match('|mian:(.*?),|is',$now_version,$match); $mian_version = $match[1] ?? ''; $addons_version = str_replace('mian:'.$mian_version.',','',$now_version); $upgrade_addons_version = []; $new_mian_version = ''; foreach($new_version as $key => $value){ if(strpos($key,'mian') === false){ $upgrade_addons_version[] = $key.':'.$value['version']; }else{ $new_mian_version = $value['version']; } } $params = [ 'mian_version' => $mian_version, 'addons_version' => $addons_version, 'upgrade_mian_version' => $new_mian_version, 'upgrade_addons_version' => implode(',',$upgrade_addons_version), 'upgrade_params' => json_encode($new_version), 'status' => 0 ]; $result = SystemVersionLog::setData($params); return $result; } /** * 更新记录状态 * @param $log * @param $status * @return mixed */ public static function updateLogStatus($log,$status) { $log->status = $status; return $log->save(); } /** * 获取h5临时目录 */ public static function getH5TmpDir($dirName = 'h5') { $dirName .= '_tmp'; $dir = self::getWorkDir(). $dirName .DIRECTORY_SEPARATOR; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir; } /** * @param $tmpDirName string 备份目录名 * @param $targetDirName string 目标目录名 * * @return bool */ public static function handleH5($tmpDirName = 'h5', $targetDirName = 'h5') { //1. 压缩原来H5目录 // self::backupH5(); $h5_tmp_dir = self::getH5TmpDir($tmpDirName); $tmp_js_dir = $h5_tmp_dir.'static/js/'; if (!is_dir($tmp_js_dir)) { echo $tmpDirName . '_tmp/static/js/ 目录不存在'; return false; } $apiUrl = Yii::getAlias('@apiUrl') ?? ''; $attachurl = Yii::getAlias('@attachurl') ?? ''; if(!$apiUrl || !$attachurl){ return false; } //apiUrl attachurl 去除前面的https:// , http:// $apiUrl = str_replace('https://','',$apiUrl); $apiUrl = str_replace('http://','',$apiUrl); $attachurl = str_replace('https://','',$attachurl); $attachurl = str_replace('http://','',$attachurl); // 地图密钥 $config = Yii::$app->services->setting->platform->get('map'); $qq_map_key = $config['qq_jsapi_key_h5'] ? $config['qq_jsapi_key_h5'] : 'TSOBZ-PHCWT-MPBX5-LZLSP-4WSW6-UIFVJ'; $mall = Mall::find()->where(['=','status',StatusEnum::ENABLED])->andWhere(['=','is_recycle',StatusEnum::DISABLED])->orderBy('id asc')->asArray()->one(); $search = ['---sign---','---api---','---static---', 'TSOBZ-PHCWT-MPBX5-LZLSP-4WSW6-UIFVJ']; $replace = [ $mall['mall_sign'] ?? '', $apiUrl, $attachurl, $qq_map_key ]; //2.读取h5_tmp 目录:/static/js/ , 替换js文件 $js_list = []; $resource = opendir($tmp_js_dir); while ($file = readdir($resource)) { //排除根目录 if ($file != ".." && $file != ".") { $con = file_get_contents($tmp_js_dir.$file); $con = str_replace($search,$replace,$con); file_put_contents($tmp_js_dir.$file,$con); //根目录下的文件 $js_list[] = $file; } } closedir($resource); $h5_all_files = self::getFileList($h5_tmp_dir,''); foreach($h5_all_files as $key => $file){ $source = $tmpDirName . '_tmp'.'/'.$file; $target = $targetDirName .'/'.$file; self::copyFile(self::getWorkDir(),$source,$target); //删除h5_tmp @unlink(self::getWorkDir() . '/' . $source); } return true; } /** * 处理发薪猫h5包 * * @return void */ public static function handleSalaryH5() { $salaryDir = self::getWorkDir() . 'salary'; // 如果已经存在salary目录,考虑旧目录不是nobody情况,修改目录归属为nobody if (is_dir($salaryDir)) { exec('sudo chown -R nobody:nobody ' . $salaryDir); } self::handleH5('salary-h5', 'salary'); // 生成发薪猫h5 Vhosts文件 try { self::makeSalaryVhosts(); }catch (Exception $e) { echo PHP_EOL . $e->getMessage(); } } /** * 生成发薪猫h5 Vhosts文件 * * @return void */ public static function makeSalaryVhosts() { $deployEnv = self::getDeployEnv(); // 如果是宝塔部署,不生成Vhosts文件 if ($deployEnv === self::DEPLOY_ENV_BT) { return; } $workDir = self::getWorkDir(); if (!is_dir($workDir . 'salary')) { return; } $salaryHost = parse_url(Yii::$app->services->setting->platform->get('system.cat_h5_url') ?: '')['host'] ?? ''; if (!$salaryHost) { throw new RuntimeException('获取发薪猫域名失败'); } // 检测发薪猫域名证书是否上传,不存在则不生成Vhosts文件 if (!self::isExistsSalaryCert($salaryHost, $deployEnv)) { throw new RuntimeException('未检测到发薪猫域名证书'); } $h5Host = parse_url(Yii::$app->services->setting->platform->get('system.h5_url') ?: '')['host'] ?? ''; if (!$h5Host) { throw new RuntimeException('获取h5域名失败'); } // nginx 配置文件目录 $vhostsDir = self::getVhostsPath($deployEnv); if (!$vhostsDir) { throw new RuntimeException('获取nginx 配置文件目录失败'); } $salaryConfFile = $vhostsDir . $salaryHost . '.conf'; // 检查是否存在此文件,已存在则不再生成 if (file_exists($salaryConfFile)) { return; } $h5ConfFile = $vhostsDir . $h5Host . '.conf'; if (!file_exists($h5ConfFile)) { throw new RuntimeException('获取h5 vhosts conf 文件失败'); } // 复制主商城h5Vhosts文件 exec(sprintf('sudo cp %s %s', $h5ConfFile, $salaryConfFile)); // 检查复制情况 if (!file_exists($salaryConfFile)) { throw new RuntimeException('复制h5 vhosts conf 文件失败'); } // 替换域名 exec(sprintf('sudo sed -i "s/%s/%s/g" %s', $h5Host, $salaryHost, $salaryConfFile)); // 替换指向目录 exec(sprintf('sudo sed -i "s|%sh5|%ssalary|g" %s', $workDir, $workDir, $salaryConfFile)); // 热重启nginx self::restartNginx($deployEnv); } /** * 获取部署环境 * * @return string */ public static function getDeployEnv(): string { if (is_dir('/www/wwwroot/qimall')) { if (Yii::$app->params['is_docker_deploy'] ?? false) { return self::DEPLOY_ENV_DOCKER; } return self::DEPLOY_ENV_BT; } return self::DEPLOY_ENV_DEFAULT; } /** * 根据部署环境重启nginx(如果是宝塔则不重启) * * @param string $deployEnv * * @return void */ public static function restartNginx(string $deployEnv) { switch ($deployEnv) { case self::DEPLOY_ENV_DEFAULT: exec('sudo /application/nginx/sbin/nginx -s reload'); break; case self::DEPLOY_ENV_DOCKER: exec('sudo docker restart nginx'); break; default: break; } } /** * 根据部署环境获取nginx vhosts文件路径 * * @param string $deployEnv * * @return string */ public static function getVhostsPath(string $deployEnv): string { switch ($deployEnv) { case self::DEPLOY_ENV_DEFAULT: return '/application/nginx/conf/vhosts/'; case self::DEPLOY_ENV_DOCKER: return '/data/confs/nginx/conf.d/'; default: return ''; } } /** * 检测是否存在发薪猫证书 * * @param string $deployEnv 部署环境 * * @return string */ public static function getSshPath(string $deployEnv): string { switch ($deployEnv) { case self::DEPLOY_ENV_DEFAULT: return '/application/nginx/conf/ssl/'; case self::DEPLOY_ENV_DOCKER: return '/data/confs/nginx/ssl/'; default: return ''; } } /** * 检测是否存在发薪猫证书 * * @param string $host * @param string $deployEnv * * @return bool */ public static function isExistsSalaryCert(string $host, string $deployEnv): bool { $sshBasePath = self::getSshPath($deployEnv) . $host; return file_exists($sshBasePath . '.key') && file_exists($sshBasePath . '.pem'); } private static function getFileList($root, $basePath = '') { $files = []; $handle = opendir($root); while (($path = readdir($handle)) !== false) { if ($path === '.git' || $path === '.svn' || $path === '.' || $path === '..') { continue; } $fullPath = "$root/$path"; $relativePath = $basePath === '' ? $path : "$basePath/$path"; if (is_dir($fullPath)) { $files = array_merge($files, self::getFileList($fullPath, $relativePath)); } else { $files[] = $relativePath; } } closedir($handle); return $files; } private static function copyFile($root, $source, $target) { if (!is_file($root . '/' . $source)) { echo " skip $target ($source not exist)\n"; return true; } if (is_file($root . '/' . $target)) { file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); return true; } //echo " generate $target\n"; @mkdir(dirname($root . '/' . $target), 0777, true); file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); return true; } /** * 异步更新插件配置 * @param $addons_arr * @throws Exception */ private static function asynchAddonsConfig($addons_arr) { if(!$addons_arr) return ; Yii::$app->services->rabbitMq->delay(10, $addons_arr, self::class, 'updateAddonsConfig'); } /** * 更新插件配置 * @param $addons_arr * @return bool */ public static function updateAddonsConfig($addons_arr) { if(!$addons_arr) return true; try{ foreach($addons_arr as $addons_name){ $res = Addons::install($addons_name); if(!$res){ Yii::$app->custom->logs('updateAddonsConfig error:'.Addons::getStaticError()); } } }catch(Exception $e){ Yii::$app->custom->logs('updateAddonsConfig Exception:'.$e->getMessage()); return false; } return true; } /** * 从服务器下载更新代码 * @Author: lun * @DateTime: 2023/5/18 0018 9:45 * @Copyright: copyright (c) 2021 广东七件事集团 * @param $data * @param $slbId * @return bool * @throws Exception */ public static function slbUpgrade($data, $slbId) { foreach($data as $key => $value){ $addons_name = $key; $download_url = $value['download_url'] ?? ''; if(!$download_url){ continue; } $tmpFile = self::download($download_url); try { if(strpos($addons_name,'mian') === false){ if(in_array($addons_name, ['h5', 'salary-h5'])){ //h5更新 $dir = self::getH5TmpDir($addons_name); }else{ //插件更新 $dir = Yii::getAlias('@addons').DIRECTORY_SEPARATOR; } }else{ //主包更新 $addons_name = 'mian'; $dir = self::getWorkDir(); } // 解压插件 self::unzip($tmpFile,$dir); if($addons_name == 'h5'){ self::handleH5(); } if($addons_name == 'salary-h5'){ self::handleSalaryH5(); } } catch (Exception $e) { SystemVersionSlb::setStatus($slbId, -1); throw new Exception('从服务器更新失败:'.$e->getMessage()); } finally { // 移除临时文件 @unlink($tmpFile); } } // 更新状态 SystemVersionSlb::setStatus($slbId, 1); return true; } /** * 强制降级H5版本 * @return bool * @throws Exception */ public static function forceDowngradeH5() { $h5_version_file = self::getWorkDir() . 'h5/version'; if (!file_exists($h5_version_file)) { throw new Exception('h5版本文件不存在'); } file_put_contents($h5_version_file, '1.0.0'); return true; } }