NodeEnvironmentServices.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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\BaseServices;
  13. use crmeb\exceptions\AdminException;
  14. use think\facade\Log;
  15. /**
  16. * Node.js 环境管理服务类
  17. *
  18. * 功能概述:
  19. * 本服务类负责检测和管理小程序 CI 上传所需的运行环境,
  20. * 包括 Node.js、npm 和 miniprogram-ci 工具的检测与安装指导。
  21. *
  22. * 主要功能:
  23. * 1. 环境检测 - 检测 Node.js、npm、miniprogram-ci 的安装状态和版本
  24. * 2. 系统识别 - 识别操作系统类型 (CentOS/Ubuntu/macOS/Windows)
  25. * 3. 安装指南 - 根据操作系统提供对应的安装命令
  26. * 4. exec 函数检测 - 检查 PHP 的 exec() 函数是否可用
  27. *
  28. * 环境要求:
  29. * - Node.js >= 14.0.0 (推荐 18.x LTS)
  30. * - npm (随 Node.js 一起安装)
  31. * - miniprogram-ci (通过 npm install -g miniprogram-ci 全局安装)
  32. * - PHP exec() 函数未被禁用
  33. *
  34. * @package app\services\system
  35. */
  36. class NodeEnvironmentServices extends BaseServices
  37. {
  38. /**
  39. * Node.js 最低版本要求
  40. *
  41. * miniprogram-ci 工具需要 Node.js 14.0.0 或更高版本。
  42. */
  43. const MIN_NODE_VERSION = '14.0.0';
  44. /**
  45. * 推荐的 Node.js 版本
  46. *
  47. * 推荐使用 Node.js 18 LTS 版本,提供更好的性能和安全性。
  48. */
  49. const RECOMMENDED_NODE_VERSION = '18';
  50. /**
  51. * 获取完整的环境状态信息
  52. *
  53. * 检测并返回小程序 CI 上传所需的所有环境信息,
  54. * 前端根据这些信息展示环境就绪状态或引导用户完成环境配置。
  55. *
  56. * @return array 完整的环境状态信息,包含:
  57. * - os: 操作系统信息 {family, type, version}
  58. * - node: Node.js 状态 {installed, version, path, meets_requirement}
  59. * - npm: npm 状态 {installed, version}
  60. * - miniprogram_ci: CI工具状态 {installed, version}
  61. * - ready: 环境是否完全就绪
  62. * - can_install: 是否支持自动安装
  63. * - exec_enabled: exec 函数是否可用
  64. * - message: 提示信息
  65. */
  66. public function getEnvironmentStatus(): array
  67. {
  68. // 检测各项环境
  69. $nodeInfo = $this->checkNodeInstalled(); // Node.js 状态
  70. $npmInfo = $this->checkNpmInstalled(); // npm 状态
  71. $ciInfo = $this->checkMiniprogramCIInstalled(); // miniprogram-ci 状态
  72. $osInfo = $this->getOsInfo(); // 操作系统信息
  73. $execEnabled = $this->isExecEnabled(); // exec 函数可用性
  74. return [
  75. 'os' => $osInfo, // 操作系统信息
  76. 'node' => $nodeInfo, // Node.js 状态
  77. 'npm' => $npmInfo, // npm 状态
  78. 'miniprogram_ci' => $ciInfo, // miniprogram-ci 状态
  79. // 环境就绪条件: Node.js + npm + miniprogram-ci 均已安装且 exec 可用
  80. 'ready' => $nodeInfo['installed'] && $npmInfo['installed'] && $ciInfo['installed'] && $execEnabled,
  81. 'can_install' => $this->canAutoInstall(), // 是否支持自动安装
  82. 'exec_enabled' => $execEnabled, // exec 函数是否可用
  83. 'message' => $execEnabled ? '' : '服务器禁用了 exec 函数,无法使用小程序上传功能。请联系服务器管理员启用 exec 函数。',
  84. ];
  85. }
  86. /**
  87. * 检查 PHP exec() 函数是否可用
  88. *
  89. * 小程序 CI 上传功能依赖 PHP 的 exec() 函数来执行命令行工具。
  90. * 很多服务器出于安全考虑会禁用该函数,需要检测其可用性。
  91. *
  92. * 检测步骤:
  93. * 1. 检查 exec 函数是否存在
  94. * 2. 检查 disable_functions 配置中是否包含 exec
  95. * 3. 尝试执行简单命令验证实际可用性
  96. *
  97. * @return bool exec 函数可用返回 true,否则返回 false
  98. */
  99. public function isExecEnabled(): bool
  100. {
  101. // 检查 exec 函数是否存在
  102. if (!function_exists('exec')) {
  103. return false;
  104. }
  105. // 检查 disable_functions 配置中是否禁用了 exec
  106. $disabled = explode(',', ini_get('disable_functions'));
  107. $disabled = array_map('trim', $disabled);
  108. if (in_array('exec', $disabled)) {
  109. return false;
  110. }
  111. // 尝试执行一个简单命令验证实际可用性
  112. $output = [];
  113. $code = 0;
  114. @exec('echo test 2>&1', $output, $code);
  115. return $code === 0 && !empty($output);
  116. }
  117. /**
  118. * 检查 Node.js 是否已安装
  119. *
  120. * 通过执行 node -v 命令获取 Node.js 的安装状态和版本信息。
  121. *
  122. * @return array Node.js 状态信息,包含:
  123. * - installed: 是否已安装
  124. * - version: 版本号 (如 18.17.0)
  125. * - path: 可执行文件路径
  126. * - meets_requirement: 是否满足最低版本要求
  127. */
  128. public function checkNodeInstalled(): array
  129. {
  130. $result = [
  131. 'installed' => false,
  132. 'version' => '',
  133. 'path' => '',
  134. 'meets_requirement' => false,
  135. ];
  136. // 执行 node -v 获取版本信息
  137. $output = $this->execCommand('node -v 2>&1');
  138. if ($output && preg_match('/v?(\d+\.\d+\.\d+)/', $output, $matches)) {
  139. $result['installed'] = true;
  140. $result['version'] = $matches[1];
  141. // 检查版本是否满足最低要求 (>= 14.0.0)
  142. $result['meets_requirement'] = version_compare($matches[1], self::MIN_NODE_VERSION, '>=');
  143. // 获取 node 可执行文件的完整路径
  144. $path = $this->execCommand('which node 2>&1');
  145. $result['path'] = trim($path);
  146. }
  147. return $result;
  148. }
  149. /**
  150. * 检查 npm 是否已安装
  151. *
  152. * npm 是 Node.js 的包管理器,通常随 Node.js 一起安装。
  153. * 用于安装 miniprogram-ci 等 npm 包。
  154. *
  155. * @return array npm 状态信息,包含:
  156. * - installed: 是否已安装
  157. * - version: 版本号
  158. */
  159. public function checkNpmInstalled(): array
  160. {
  161. $result = [
  162. 'installed' => false,
  163. 'version' => '',
  164. ];
  165. // 执行 npm -v 获取版本信息
  166. $output = $this->execCommand('npm -v 2>&1');
  167. if ($output && preg_match('/(\d+\.\d+\.\d+)/', $output, $matches)) {
  168. $result['installed'] = true;
  169. $result['version'] = $matches[1];
  170. }
  171. return $result;
  172. }
  173. /**
  174. * 检查 miniprogram-ci 是否已全局安装
  175. *
  176. * miniprogram-ci 是微信官方提供的小程序代码上传工具。
  177. * 需要通过 npm install -g miniprogram-ci 全局安装。
  178. *
  179. * @return array miniprogram-ci 状态信息,包含:
  180. * - installed: 是否已安装
  181. * - version: 版本号
  182. */
  183. public function checkMiniprogramCIInstalled(): array
  184. {
  185. $result = [
  186. 'installed' => false,
  187. 'version' => '',
  188. ];
  189. // 通过 npm list -g 检查全局安装的包
  190. $output = $this->execCommand('npm list -g miniprogram-ci --depth=0 2>&1');
  191. if ($output && preg_match('/miniprogram-ci@(\d+\.\d+\.\d+)/', $output, $matches)) {
  192. $result['installed'] = true;
  193. $result['version'] = $matches[1];
  194. }
  195. return $result;
  196. }
  197. /**
  198. * 获取操作系统信息
  199. *
  200. * 识别服务器的操作系统类型,用于提供对应的安装指南。
  201. * 使用命令行方式检测,避免受 open_basedir 限制影响。
  202. *
  203. * 支持识别的系统:
  204. * - Linux: CentOS/RHEL/Rocky/Ubuntu/Debian/Alpine 等
  205. * - macOS: 通过 sw_vers 获取版本
  206. * - Windows: 通过 PHP_OS_FAMILY 识别
  207. *
  208. * @return array 操作系统信息,包含:
  209. * - family: 系统家族 (Linux/Darwin/Windows)
  210. * - type: 具体类型 (centos/ubuntu/debian/macos/windows)
  211. * - version: 系统版本号
  212. */
  213. public function getOsInfo(): array
  214. {
  215. $os = PHP_OS_FAMILY; // 获取 PHP 识别的操作系统家族
  216. $type = 'unknown';
  217. $version = '';
  218. if ($os === 'Linux') {
  219. // Linux 系统: 通过命令行读取 /etc/os-release 获取发行版信息
  220. $osRelease = $this->execCommand('cat /etc/os-release 2>/dev/null');
  221. if ($osRelease) {
  222. // 解析 os-release 文件内容
  223. $lines = explode("\n", $osRelease);
  224. $osInfo = [];
  225. foreach ($lines as $line) {
  226. if (strpos($line, '=') !== false) {
  227. list($key, $value) = explode('=', $line, 2);
  228. $osInfo[trim($key)] = trim($value, '"\' ');
  229. }
  230. }
  231. $id = strtolower($osInfo['ID'] ?? '');
  232. $version = $osInfo['VERSION_ID'] ?? '';
  233. // 映射操作系统类型
  234. if (in_array($id, ['centos', 'rhel', 'rocky', 'almalinux', 'fedora'])) {
  235. $type = 'centos'; // Red Hat 系列
  236. } elseif ($id === 'ubuntu') {
  237. $type = 'ubuntu';
  238. } elseif (in_array($id, ['debian', 'raspbian'])) {
  239. $type = 'debian';
  240. } elseif ($id === 'alpine') {
  241. $type = 'alpine';
  242. } else {
  243. $type = $id ?: 'linux';
  244. }
  245. } else {
  246. // 备用方案: 使用 uname 命令
  247. $uname = $this->execCommand('uname -a 2>/dev/null');
  248. $type = 'linux';
  249. $version = $uname ?: '';
  250. }
  251. } elseif ($os === 'Darwin') {
  252. // macOS 系统
  253. $type = 'macos';
  254. $version = $this->execCommand('sw_vers -productVersion 2>&1') ?: '';
  255. } elseif ($os === 'Windows') {
  256. // Windows 系统
  257. $type = 'windows';
  258. }
  259. return [
  260. 'family' => $os, // 系统家族
  261. 'type' => $type, // 具体类型
  262. 'version' => trim($version), // 版本号
  263. ];
  264. }
  265. /**
  266. * 检查是否支持自动安装
  267. *
  268. * 检查服务器环境是否支持自动安装 Node.js 和相关工具。
  269. * 自动安装功能依赖于操作系统类型和 PHP 函数可用性。
  270. *
  271. * 支持的操作系统: CentOS/RHEL、Ubuntu、Debian、Alpine、macOS
  272. *
  273. * @return bool 支持自动安装返回 true
  274. */
  275. public function canAutoInstall(): bool
  276. {
  277. $os = $this->getOsInfo();
  278. // 支持自动安装的操作系统列表
  279. $supportedOs = ['centos', 'rhel', 'ubuntu', 'debian', 'alpine', 'macos'];
  280. if (!in_array($os['type'], $supportedOs)) {
  281. return false;
  282. }
  283. // 检查是否有执行命令的权限 (exec 或 shell_exec)
  284. if (!function_exists('exec') && !function_exists('shell_exec')) {
  285. return false;
  286. }
  287. return true;
  288. }
  289. /**
  290. * 执行命令并返回输出
  291. *
  292. * 封装的命令执行方法,优先使用 exec,如果不可用则尝试 shell_exec。
  293. *
  294. * @param string $command 要执行的命令
  295. * @return string|null 命令输出,执行失败返回 null
  296. */
  297. protected function execCommand(string $command): ?string
  298. {
  299. // 优先使用 exec 函数
  300. if (function_exists('exec')) {
  301. $output = [];
  302. exec($command, $output);
  303. return implode("\n", $output);
  304. }
  305. // 备用: 使用 shell_exec 函数
  306. elseif (function_exists('shell_exec')) {
  307. return shell_exec($command);
  308. }
  309. return null;
  310. }
  311. /**
  312. * 获取一键安装脚本 URL
  313. *
  314. * 返回用于自动安装 Node.js 环境的 Shell 脚本地址。
  315. *
  316. * @return string 安装脚本的 URL 地址
  317. */
  318. public function getInstallScriptUrl(): string
  319. {
  320. // return sys_config('site_url', '') . '/statics/scripts/install_node_env.sh';
  321. return 'https://www.crmeb.com/static/upgrade/install_node_env.sh';
  322. }
  323. /**
  324. * 获取安装指南 (手动安装说明)
  325. *
  326. * 根据服务器操作系统类型返回对应的 Node.js 和 miniprogram-ci 安装步骤。
  327. * 每个操作系统都有专门优化的安装命令。
  328. *
  329. * 支持的操作系统:
  330. * - CentOS/RHEL: 使用 NodeSource 的 rpm 仓库
  331. * - Ubuntu/Debian: 使用 NodeSource 的 deb 仓库
  332. * - macOS: 使用 Homebrew 包管理器
  333. * - Windows: 从 Node.js 官网下载安装包
  334. *
  335. * @return array 安装指南信息,包含:
  336. * - title: 指南标题 (如 "CentOS/RHEL 安装指南")
  337. * - steps: 安装步骤数组,包含具体的命令行指令
  338. * - script_url: 一键安装脚本的 URL 地址
  339. */
  340. public function getInstallGuide(): array
  341. {
  342. // 获取当前操作系统信息
  343. $os = $this->getOsInfo();
  344. // 各操作系统的安装指南
  345. $guides = [
  346. // CentOS/RHEL 系列 - 使用 yum 包管理器
  347. 'centos' => [
  348. 'title' => 'CentOS/RHEL 安装指南',
  349. 'steps' => [
  350. '1. 添加 NodeSource 仓库:',
  351. ' curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -',
  352. '2. 安装 Node.js:',
  353. ' sudo yum install -y nodejs',
  354. '3. 验证安装:',
  355. ' node -v && npm -v',
  356. '4. 安装 miniprogram-ci:',
  357. ' sudo npm install miniprogram-ci -g',
  358. ],
  359. ],
  360. // Ubuntu - 使用 apt 包管理器
  361. 'ubuntu' => [
  362. 'title' => 'Ubuntu/Debian 安装指南',
  363. 'steps' => [
  364. '1. 添加 NodeSource 仓库:',
  365. ' curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -',
  366. '2. 安装 Node.js:',
  367. ' sudo apt-get install -y nodejs',
  368. '3. 验证安装:',
  369. ' node -v && npm -v',
  370. '4. 安装 miniprogram-ci:',
  371. ' sudo npm install miniprogram-ci -g',
  372. ],
  373. ],
  374. // Debian - 与 Ubuntu 相同
  375. 'debian' => [
  376. 'title' => 'Ubuntu/Debian 安装指南',
  377. 'steps' => [
  378. '1. 添加 NodeSource 仓库:',
  379. ' curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -',
  380. '2. 安装 Node.js:',
  381. ' sudo apt-get install -y nodejs',
  382. '3. 验证安装:',
  383. ' node -v && npm -v',
  384. '4. 安装 miniprogram-ci:',
  385. ' sudo npm install miniprogram-ci -g',
  386. ],
  387. ],
  388. // macOS - 使用 Homebrew
  389. 'macos' => [
  390. 'title' => 'macOS 安装指南',
  391. 'steps' => [
  392. '1. 安装 Homebrew (如果未安装):',
  393. ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
  394. '2. 安装 Node.js:',
  395. ' brew install node@18',
  396. '3. 验证安装:',
  397. ' node -v && npm -v',
  398. '4. 安装 miniprogram-ci:',
  399. ' npm install miniprogram-ci -g',
  400. ],
  401. ],
  402. // Windows - 从官网下载安装
  403. 'windows' => [
  404. 'title' => 'Windows 安装指南',
  405. 'steps' => [
  406. '1. 下载 Node.js 安装包:',
  407. ' 访问 https://nodejs.org/zh-cn/download/',
  408. '2. 运行安装程序,按提示完成安装',
  409. '3. 打开命令提示符,验证安装:',
  410. ' node -v && npm -v',
  411. '4. 安装 miniprogram-ci:',
  412. ' npm install miniprogram-ci -g',
  413. ],
  414. ],
  415. ];
  416. // 根据操作系统类型获取对应指南,未知系统使用通用指南
  417. $type = $os['type'] ?: 'unknown';
  418. $guide = isset($guides[$type]) ? $guides[$type] : [
  419. 'title' => '通用安装指南',
  420. 'steps' => [
  421. '1. 访问 Node.js 官网下载安装包:',
  422. ' https://nodejs.org/zh-cn/download/',
  423. '2. 按照官方文档完成安装',
  424. '3. 验证安装:',
  425. ' node -v && npm -v',
  426. '4. 安装 miniprogram-ci:',
  427. ' npm install miniprogram-ci -g',
  428. ],
  429. ];
  430. // 添加一键安装脚本 URL
  431. $guide['script_url'] = $this->getInstallScriptUrl();
  432. return $guide;
  433. }
  434. }