ExportJob.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. namespace app\jobs\migration;
  3. use yii\base\BaseObject;
  4. use yii\queue\JobInterface;
  5. use app\models\Store;
  6. use app\models\Migration;
  7. class ExportJob extends BaseObject implements JobInterface
  8. {
  9. public $id;
  10. public $store_id;
  11. public function execute($queue)
  12. {
  13. $store = Store::findOne($this->store_id);
  14. if (!$store) {
  15. return;
  16. }
  17. $migration = Migration::findOne($this->id);
  18. if (!$migration) {
  19. return;
  20. }
  21. $migration->status = 1;
  22. $migration->save();
  23. $this->backSql($store->id);
  24. $this->backUpload($store->id);
  25. $path = $this->toZip($store->id);
  26. if (!$path) {
  27. $migration->status = 3;
  28. $migration->save();
  29. return;
  30. }
  31. $migration->status = 2;
  32. $migration->down_url = $path;
  33. $migration->save();
  34. }
  35. public function backSql($store_id)
  36. {
  37. $defaultStoreId = DEFAULT_STORE_ID;
  38. $tables = \Yii::$app->db->schema->getTableNames();
  39. // 黑名单,遇到跳过
  40. $blacklist = [
  41. 'cyy_action_log',
  42. 'cyy_browse_log',
  43. 'cyy_cloud',
  44. 'cyy_color',
  45. 'cyy_common_operation',
  46. 'cyy_country',
  47. 'cyy_district',
  48. 'cyy_express',
  49. 'cyy_notice_err',
  50. 'cyy_old_user_tree_path',
  51. 'cyy_plugins',
  52. 'cyy_purchase',
  53. 'cyy_salesman',
  54. 'cyy_salesman_new_store',
  55. 'cyy_store',
  56. 'cyy_supplier',
  57. 'cyy_supplier_withdraw',
  58. 'cyy_ui_pay_merchant_info',
  59. 'cyy_zone',
  60. 'cyy_user_tree_path',
  61. 'cyy_business_right_info',
  62. ];
  63. $blackMatchList = [
  64. 'cyy_agent_*',
  65. 'cyy_aggregate_*',
  66. 'cyy_ali_*',
  67. 'cyy_alipay_*',
  68. 'cyy_business_*',
  69. 'cyy_cloud_*',
  70. 'cyy_front_*',
  71. 'cyy_purchase_*',
  72. 'cyy_saas_*',
  73. 'cyy_store_*',
  74. ];
  75. // 白名单
  76. $whitelist = [
  77. 'cyy_saas_user',
  78. ];
  79. $path = \Yii::$app->basePath . '/runtime/migration/store_' . $store_id . '/export/sql/';
  80. // 先删除文件夹
  81. if (is_dir($path)) {
  82. $files = scandir($path);
  83. foreach ($files as $file) {
  84. if ($file != '.' && $file != '..') {
  85. unlink($path. $file);
  86. }
  87. }
  88. rmdir($path);
  89. }
  90. if (!is_dir($path)) {
  91. mkdir($path, 0777, true);
  92. }
  93. foreach ($tables as $table) {
  94. // 检查是否在白名单中
  95. if (!in_array($table, $whitelist)) {
  96. // 不在白名单中时,检查黑名单
  97. if (in_array($table, $blacklist)) {
  98. continue;
  99. }
  100. foreach ($blackMatchList as $blackMatch) {
  101. if (fnmatch($blackMatch, $table)) {
  102. continue 2;
  103. }
  104. }
  105. }
  106. $hasStoreId = \has_column($table, 'store_id');
  107. // 检查表是否有数据
  108. if ($hasStoreId) {
  109. $count = \Yii::$app->db->createCommand("SELECT COUNT(*) FROM $table WHERE store_id = :store_id")
  110. ->bindValue(':store_id', $store_id)
  111. ->queryScalar();
  112. } else {
  113. $count = \Yii::$app->db->createCommand("SELECT COUNT(*) FROM $table")->queryScalar();
  114. }
  115. if ($count == 0) {
  116. continue;
  117. }
  118. // 使用游标分批获取数据,减少内存占用
  119. $batchSize = 1000;
  120. $offset = 0;
  121. $fp = fopen($path . $table . '.sql', 'a');
  122. while (true) {
  123. if ($hasStoreId) {
  124. $query = \Yii::$app->db->createCommand("SELECT * FROM $table WHERE store_id = :store_id LIMIT :limit OFFSET :offset")
  125. ->bindValue(':store_id', $store_id)
  126. ->bindValue(':limit', $batchSize)
  127. ->bindValue(':offset', $offset);
  128. } else {
  129. $query = \Yii::$app->db->createCommand("SELECT * FROM $table LIMIT :limit OFFSET :offset")
  130. ->bindValue(':limit', $batchSize)
  131. ->bindValue(':offset', $offset);
  132. }
  133. $rows = $query->queryAll();
  134. if (empty($rows)) {
  135. break;
  136. }
  137. if ($offset === 0) {
  138. // 只在第一批写入INSERT语句头部
  139. $columns = array_keys($rows[0]);
  140. $headerSql = "INSERT INTO $table (`" . implode('`,`', $columns) . "`) VALUES ";
  141. fwrite($fp, $headerSql);
  142. }
  143. // 逐行处理数据并写入文件
  144. foreach ($rows as $i => $row) {
  145. if ($hasStoreId) {
  146. $row['store_id'] = $defaultStoreId; // 替换store_id字段的值为默认store_id
  147. }
  148. $values = array_map(function($val) {
  149. if ($val === null) {
  150. return 'NULL';
  151. }
  152. return is_numeric($val) ? $val : "'" . addslashes($val) . "'";
  153. }, array_values($row));
  154. fwrite($fp, ($offset === 0 && $i === 0 ? '' : ',') . "\n(" . implode(',', $values) . ")");
  155. }
  156. $offset += $batchSize;
  157. // 释放内存
  158. unset($rows);
  159. gc_collect_cycles();
  160. }
  161. // 完成后写入分号
  162. fwrite($fp, ";\n");
  163. fclose($fp);
  164. }
  165. }
  166. public function backUpload($store_id)
  167. {
  168. $from_image_path = \Yii::$app->basePath . '/web/uploads/images/store_' . $store_id . '/';
  169. $from_video_path = \Yii::$app->basePath . '/web/uploads/videos/store_' . $store_id . '/';
  170. $to_path = \Yii::$app->basePath . '/runtime/migration/store_' . $store_id . '/export/uploads/';
  171. // 判断图片路径是否存在,如果存在就整个复制到目标路径,注意,存在二级目录
  172. if (is_dir($from_image_path)) {
  173. $to_image_path = $to_path . 'images/';
  174. if (!is_dir($to_image_path)) {
  175. mkdir($to_image_path, 0777, true);
  176. }
  177. $this->copyDirectory($from_image_path, $to_image_path);
  178. }
  179. // 判断视频路径是否存在,如果存在就整个复制到目标路径,注意,存在二级目录
  180. if (is_dir($from_video_path)) {
  181. $to_video_path = $to_path . 'videos/';
  182. if (!is_dir($to_video_path)) {
  183. mkdir($to_video_path, 0777, true);
  184. }
  185. $this->copyDirectory($from_video_path, $to_video_path);
  186. }
  187. }
  188. public function copyDirectory($source, $destination)
  189. {
  190. if (!is_dir($source)) {
  191. return;
  192. }
  193. if (!is_dir($destination)) {
  194. mkdir($destination, 0777, true);
  195. }
  196. $files = scandir($source);
  197. foreach ($files as $file) {
  198. if ($file != '.' && $file != '..') {
  199. if (is_dir($source . '/' . $file)) {
  200. $this->copyDirectory($source . '/' . $file, $destination . '/' . $file);
  201. } else {
  202. copy($source . '/' . $file, $destination . '/' . $file);
  203. }
  204. }
  205. }
  206. }
  207. // 压缩文件
  208. public function zipFiles($path, $zipFileName)
  209. {
  210. $zip = new \ZipArchive();
  211. if ($zip->open($zipFileName, \ZipArchive::CREATE) !== true) {
  212. return false;
  213. }
  214. $files = new \RecursiveIteratorIterator(
  215. new \RecursiveDirectoryIterator($path),
  216. \RecursiveIteratorIterator::LEAVES_ONLY
  217. );
  218. $pathLength = strlen(rtrim(realpath($path), '/\\') . DIRECTORY_SEPARATOR);
  219. foreach ($files as $file) {
  220. if (!$file->isDir()) {
  221. $filePath = $file->getRealPath();
  222. // Get relative path to maintain directory structure
  223. $relativePath = substr($filePath, $pathLength);
  224. $zip->addFile($filePath, $relativePath);
  225. }
  226. }
  227. $zip->close();
  228. return true;
  229. }
  230. // 压缩
  231. public function toZip($store_id)
  232. {
  233. try {
  234. $path = \Yii::$app->basePath . '/runtime/migration/store_' . $store_id . '/export/';
  235. $fileName = 'export_store_'.$store_id.'_'.date('YmdHis').'.zip';
  236. $zipFileName = \Yii::$app->basePath . '/runtime/migration/store_' . $store_id . '/' . $fileName;
  237. if (file_exists($zipFileName)) {
  238. unlink($zipFileName);
  239. }
  240. $this->zipFiles($path, $zipFileName);
  241. // 删除文件夹
  242. $delPath = \Yii::$app->basePath . '/runtime/migration/store_' . $store_id . '/export/';
  243. $this->deleteDirectory($delPath);
  244. return '/runtime/migration/store_' . $store_id . '/' . $fileName;
  245. } catch (\Exception $e) {
  246. debug_log('ExportJob toZip error: ' . $e->getMessage(), 'syan.log');
  247. return false;
  248. }
  249. }
  250. // 删除文件夹
  251. public function deleteDirectory($dir)
  252. {
  253. if (!is_dir($dir)) {
  254. return;
  255. }
  256. $files = scandir($dir);
  257. foreach ($files as $file) {
  258. if ($file != '.' && $file != '..') {
  259. if (is_dir($dir . '/' . $file)) {
  260. $this->deleteDirectory($dir . '/' . $file);
  261. } else {
  262. unlink($dir . '/' . $file);
  263. }
  264. }
  265. }
  266. rmdir($dir);
  267. }
  268. }