VersionManager.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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 upgrade;
  12. use think\facade\Config;
  13. use think\facade\Db;
  14. use think\facade\Log;
  15. use crmeb\exceptions\AdminException;
  16. /**
  17. * 版本管理器
  18. * 用于跨版本升级管理
  19. * Class VersionManager
  20. * @package upgrade
  21. */
  22. class VersionManager
  23. {
  24. /**
  25. * 配置
  26. * @var array
  27. */
  28. protected $config = [];
  29. /**
  30. * 数据库表前缀
  31. * @var string
  32. */
  33. protected $prefix = '';
  34. /**
  35. * 当前版本信息
  36. * @var array
  37. */
  38. protected $currentVersion = [];
  39. /**
  40. * SQL类型说明
  41. */
  42. const SQL_TYPE_CREATE_TABLE = 1; // 建表
  43. const SQL_TYPE_DROP_TABLE = 2; // 删表
  44. const SQL_TYPE_ADD_COLUMN = 3; // 添加字段
  45. const SQL_TYPE_MODIFY_COLUMN = 4; // 修改字段
  46. const SQL_TYPE_DROP_COLUMN = 5; // 删除字段
  47. const SQL_TYPE_INSERT_DATA = 6; // 添加数据
  48. const SQL_TYPE_UPDATE_DATA = 7; // 修改数据
  49. const SQL_TYPE_DELETE_DATA = 8; // 删除数据
  50. const SQL_TYPE_RAW = -1; // 直接执行SQL
  51. /**
  52. * VersionManager constructor.
  53. */
  54. public function __construct()
  55. {
  56. $this->config = Config::get('upgrade', []);
  57. $this->prefix = config('database.connections.' . config('database.default'))['prefix'];
  58. $this->currentVersion = $this->getCurrentVersion();
  59. }
  60. /**
  61. * 获取当前系统版本信息
  62. * @return array
  63. */
  64. public function getCurrentVersion(): array
  65. {
  66. $file = app()->getRootPath() . '.version';
  67. $arr = [];
  68. if (!file_exists($file)) {
  69. return $arr;
  70. }
  71. $list = @file($file);
  72. if (!$list) {
  73. return $arr;
  74. }
  75. foreach ($list as $val) {
  76. $val = str_replace(["\r", "\n", "\t"], '', $val);
  77. if (strpos($val, '=') !== false) {
  78. list($k, $v) = explode('=', $val, 2);
  79. $arr[trim($k)] = trim($v);
  80. }
  81. }
  82. return $arr;
  83. }
  84. /**
  85. * 获取当前版本代码
  86. * @return int
  87. */
  88. public function getCurrentVersionCode(): int
  89. {
  90. return (int)($this->currentVersion['version_code'] ?? 0);
  91. }
  92. /**
  93. * 获取当前版本名称
  94. * @return string
  95. */
  96. public function getCurrentVersionName(): string
  97. {
  98. return $this->currentVersion['version'] ?? '';
  99. }
  100. /**
  101. * 获取所有版本列表
  102. * @return array
  103. */
  104. public function getAllVersions(): array
  105. {
  106. return $this->config['versions'] ?? [];
  107. }
  108. /**
  109. * 获取最新版本信息
  110. * @return array
  111. */
  112. public function getLatestVersion(): array
  113. {
  114. $versions = $this->getAllVersions();
  115. return end($versions) ?: [];
  116. }
  117. /**
  118. * 获取需要升级的版本列表
  119. * 从当前版本到最新版本之间的所有版本
  120. * @return array
  121. */
  122. public function getPendingVersions(): array
  123. {
  124. $currentCode = $this->getCurrentVersionCode();
  125. $versions = $this->getAllVersions();
  126. $pending = [];
  127. foreach ($versions as $version) {
  128. if ($version['code'] > $currentCode) {
  129. $pending[] = $version;
  130. }
  131. }
  132. // 按版本代码从小到大排序
  133. usort($pending, function ($a, $b) {
  134. return $a['code'] - $b['code'];
  135. });
  136. return $pending;
  137. }
  138. /**
  139. * 获取版本升级差距
  140. * @return int
  141. */
  142. public function getVersionGap(): int
  143. {
  144. return count($this->getPendingVersions());
  145. }
  146. /**
  147. * 是否需要升级
  148. * @return bool
  149. */
  150. public function needUpgrade(): bool
  151. {
  152. return $this->getVersionGap() > 0;
  153. }
  154. /**
  155. * 获取最低版本要求配置
  156. * @return array
  157. */
  158. public function getMinVersionConfig(): array
  159. {
  160. return $this->config['min_version'] ?? [];
  161. }
  162. /**
  163. * 检查当前版本是否满足最低版本要求
  164. * @return bool
  165. */
  166. public function meetsMinVersionRequirement(): bool
  167. {
  168. $minVersion = $this->getMinVersionConfig();
  169. if (empty($minVersion)) {
  170. return true; // 未配置最低版本,默认允许
  171. }
  172. $minCode = $minVersion['code'] ?? 0;
  173. $currentCode = $this->getCurrentVersionCode();
  174. return $currentCode >= $minCode;
  175. }
  176. /**
  177. * 获取最低版本错误提示信息
  178. * @return string
  179. */
  180. public function getMinVersionMessage(): string
  181. {
  182. $minVersion = $this->getMinVersionConfig();
  183. return $minVersion['message'] ?? '当前版本不支持跨版本在线升级功能';
  184. }
  185. /**
  186. * 检查跨版本升级可用性
  187. * @return array ['available' => bool, 'message' => string, 'current_version' => string, 'min_version' => string]
  188. */
  189. public function checkUpgradeAvailability(): array
  190. {
  191. $currentCode = $this->getCurrentVersionCode();
  192. $currentVersion = $this->getCurrentVersionName();
  193. $minVersion = $this->getMinVersionConfig();
  194. if (empty($minVersion)) {
  195. return [
  196. 'available' => true,
  197. 'message' => '可以使用跨版本升级',
  198. 'current_version' => $currentVersion,
  199. 'current_code' => $currentCode,
  200. 'min_version' => '',
  201. 'min_code' => 0
  202. ];
  203. }
  204. $minCode = $minVersion['code'] ?? 0;
  205. $available = $currentCode >= $minCode;
  206. return [
  207. 'available' => $available,
  208. 'message' => $available ? '可以使用跨版本升级' : $this->getMinVersionMessage(),
  209. 'current_version' => $currentVersion,
  210. 'current_code' => $currentCode,
  211. 'min_version' => $minVersion['version'] ?? '',
  212. 'min_code' => $minCode
  213. ];
  214. }
  215. /**
  216. * 获取版本升级脚本
  217. * @param array $version
  218. * @return array
  219. */
  220. public function getVersionUpgradeData(array $version): array
  221. {
  222. $filePath = ($this->config['upgrade_path'] ?? '') . ($version['file'] ?? '');
  223. if (!file_exists($filePath)) {
  224. return [];
  225. }
  226. $data = include $filePath;
  227. return is_array($data) ? $data : [];
  228. }
  229. /**
  230. * 获取所有待执行的升级SQL
  231. * @return array
  232. */
  233. public function getAllPendingUpgradeSql(): array
  234. {
  235. $pendingVersions = $this->getPendingVersions();
  236. $allSql = [];
  237. foreach ($pendingVersions as $version) {
  238. $upgradeData = $this->getVersionUpgradeData($version);
  239. if (!empty($upgradeData['update_sql'])) {
  240. foreach ($upgradeData['update_sql'] as $sql) {
  241. $sql['version'] = $version['version'];
  242. $sql['version_code'] = $version['code'];
  243. $allSql[] = $sql;
  244. }
  245. }
  246. }
  247. return $allSql;
  248. }
  249. /**
  250. * 执行单条升级SQL
  251. * @param array $sqlItem
  252. * @return array ['success' => bool, 'message' => string]
  253. */
  254. public function executeSqlItem(array $sqlItem): array
  255. {
  256. $type = $sqlItem['type'] ?? 0;
  257. $table = $this->prefix . ($sqlItem['table'] ?? '');
  258. $field = $sqlItem['field'] ?? '';
  259. $findSql = $sqlItem['findSql'] ?? '';
  260. $sql = $sqlItem['sql'] ?? '';
  261. $whereSql = $sqlItem['whereSql'] ?? '';
  262. $whereTable = isset($sqlItem['whereTable']) ? $this->prefix . $sqlItem['whereTable'] : '';
  263. $newTable = isset($sqlItem['new_table']) ? $this->prefix . $sqlItem['new_table'] : '';
  264. try {
  265. // 替换表名
  266. if ($findSql) {
  267. $findSql = str_replace('@table', $table, $findSql);
  268. }
  269. // 预检查
  270. if ($findSql) {
  271. $exists = !empty(Db::query($findSql));
  272. switch ($type) {
  273. case self::SQL_TYPE_CREATE_TABLE:
  274. case self::SQL_TYPE_ADD_COLUMN:
  275. case self::SQL_TYPE_INSERT_DATA:
  276. if ($exists) {
  277. return ['success' => true, 'message' => $this->getSkipMessage($type, $table, $field), 'skipped' => true];
  278. }
  279. break;
  280. case self::SQL_TYPE_MODIFY_COLUMN:
  281. case self::SQL_TYPE_DROP_COLUMN:
  282. case self::SQL_TYPE_UPDATE_DATA:
  283. if (!$exists) {
  284. return ['success' => true, 'message' => $this->getSkipMessage($type, $table, $field), 'skipped' => true];
  285. }
  286. break;
  287. case self::SQL_TYPE_DELETE_DATA:
  288. if (!$exists) {
  289. return ['success' => true, 'message' => '数据不存在,跳过删除', 'skipped' => true];
  290. }
  291. break;
  292. }
  293. }
  294. // 替换SQL中的占位符
  295. $execSql = str_replace('@table', $table, $sql);
  296. // 处理关联表查询
  297. if (in_array($type, [self::SQL_TYPE_INSERT_DATA, self::SQL_TYPE_UPDATE_DATA]) && $whereSql && $whereTable) {
  298. $whereSql = str_replace('@whereTable', $whereTable, $whereSql);
  299. $result = Db::query($whereSql);
  300. $tabId = $result[0]['tabId'] ?? 0;
  301. if (!$tabId) {
  302. return ['success' => true, 'message' => '关联数据不存在,跳过', 'skipped' => true];
  303. }
  304. $execSql = str_replace('@tabId', $tabId, $execSql);
  305. }
  306. // 处理新表名
  307. if ($type == self::SQL_TYPE_RAW && $newTable) {
  308. $execSql = str_replace('@new_table', $newTable, $execSql);
  309. }
  310. // 执行SQL
  311. if ($execSql) {
  312. Db::execute($execSql);
  313. Log::write(['type' => 'upgrade_sql', 'sql' => $execSql, 'item' => json_encode($sqlItem)], 'notice');
  314. }
  315. return ['success' => true, 'message' => $this->getSuccessMessage($type, $table, $field)];
  316. } catch (\Throwable $e) {
  317. Log::error(['type' => 'upgrade_sql_error', 'error' => $e->getMessage(), 'item' => json_encode($sqlItem)]);
  318. return ['success' => false, 'message' => $e->getMessage()];
  319. }
  320. }
  321. /**
  322. * 获取跳过消息
  323. */
  324. protected function getSkipMessage(int $type, string $table, string $field): string
  325. {
  326. $messages = [
  327. self::SQL_TYPE_CREATE_TABLE => "{$table} 表已存在",
  328. self::SQL_TYPE_DROP_TABLE => "{$table} 表不存在",
  329. self::SQL_TYPE_ADD_COLUMN => "{$table} 表中 {$field} 字段已存在",
  330. self::SQL_TYPE_MODIFY_COLUMN => "{$table} 表中 {$field} 字段不存在",
  331. self::SQL_TYPE_DROP_COLUMN => "{$table} 表中 {$field} 字段不存在",
  332. self::SQL_TYPE_INSERT_DATA => "{$table} 数据已存在",
  333. self::SQL_TYPE_UPDATE_DATA => "{$table} 数据不存在",
  334. ];
  335. return $messages[$type] ?? '跳过';
  336. }
  337. /**
  338. * 获取成功消息
  339. */
  340. protected function getSuccessMessage(int $type, string $table, string $field): string
  341. {
  342. $messages = [
  343. self::SQL_TYPE_CREATE_TABLE => "{$table} 表创建成功",
  344. self::SQL_TYPE_DROP_TABLE => "{$table} 表删除成功",
  345. self::SQL_TYPE_ADD_COLUMN => "{$table} 表中 {$field} 字段添加成功",
  346. self::SQL_TYPE_MODIFY_COLUMN => "{$table} 表中 {$field} 字段修改成功",
  347. self::SQL_TYPE_DROP_COLUMN => "{$table} 表中 {$field} 字段删除成功",
  348. self::SQL_TYPE_INSERT_DATA => "{$table} 数据添加成功",
  349. self::SQL_TYPE_UPDATE_DATA => "{$table} 数据修改成功",
  350. self::SQL_TYPE_DELETE_DATA => "{$table} 数据删除成功",
  351. self::SQL_TYPE_RAW => "{$table} SQL执行成功",
  352. ];
  353. return $messages[$type] ?? '执行成功';
  354. }
  355. /**
  356. * 更新版本文件
  357. * @param string $version
  358. * @param int $code
  359. * @return bool
  360. */
  361. public function updateVersionFile(string $version, int $code): bool
  362. {
  363. $file = app()->getRootPath() . '.version';
  364. $data = $this->getCurrentVersion();
  365. $data['version'] = $version;
  366. $data['version_code'] = $code;
  367. $data['platform'] = $this->config['platform'] ?? 'CRMEB';
  368. $data['app_id'] = $data['app_id'] ?? ($this->config['app_id'] ?? '');
  369. $data['app_key'] = $data['app_key'] ?? ($this->config['app_key'] ?? '');
  370. $content = '';
  371. foreach ($data as $key => $value) {
  372. $content .= "{$key}={$value}\n";
  373. }
  374. return file_put_contents($file, $content) !== false;
  375. }
  376. /**
  377. * 获取升级概览信息
  378. * @return array
  379. */
  380. public function getUpgradeOverview(): array
  381. {
  382. $currentVersion = $this->getCurrentVersionName();
  383. $currentCode = $this->getCurrentVersionCode();
  384. $latestVersion = $this->getLatestVersion();
  385. $pendingVersions = $this->getPendingVersions();
  386. return [
  387. 'current_version' => $currentVersion,
  388. 'current_code' => $currentCode,
  389. 'latest_version' => $latestVersion['version'] ?? '',
  390. 'latest_code' => $latestVersion['code'] ?? 0,
  391. 'need_upgrade' => $this->needUpgrade(),
  392. 'version_gap' => $this->getVersionGap(),
  393. 'pending_versions' => array_map(function ($v) {
  394. return [
  395. 'version' => $v['version'],
  396. 'code' => $v['code'],
  397. 'description' => $v['description'] ?? ''
  398. ];
  399. }, $pendingVersions)
  400. ];
  401. }
  402. /**
  403. * 获取所有待执行的数据迁移处理器
  404. * @return array
  405. */
  406. public function getAllPendingDataHandlers(): array
  407. {
  408. $pendingVersions = $this->getPendingVersions();
  409. $allHandlers = [];
  410. foreach ($pendingVersions as $version) {
  411. $upgradeData = $this->getVersionUpgradeData($version);
  412. if (!empty($upgradeData['data_handlers'])) {
  413. foreach ($upgradeData['data_handlers'] as $handler) {
  414. $handler['version'] = $version['version'];
  415. $handler['version_code'] = $version['code'];
  416. $allHandlers[] = $handler;
  417. }
  418. }
  419. }
  420. return $allHandlers;
  421. }
  422. /**
  423. * 获取指定版本的数据迁移处理器
  424. * @param array $version
  425. * @return array
  426. */
  427. public function getVersionDataHandlers(array $version): array
  428. {
  429. $upgradeData = $this->getVersionUpgradeData($version);
  430. return $upgradeData['data_handlers'] ?? [];
  431. }
  432. }