Dataexport.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <?php
  2. namespace app\admin\controller\routine;
  3. use app\common\controller\Backend;
  4. use ba\Random;
  5. use PhpZip\Exception\ZipException;
  6. use PhpZip\ZipFile;
  7. use think\db\exception\PDOException;
  8. use think\exception\ValidateException;
  9. use modules\dataexport\library\ExportLib;
  10. use think\facade\Db;
  11. use think\Exception;
  12. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  13. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  14. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  15. /**
  16. * 导出任务管理
  17. *
  18. */
  19. class Dataexport extends Backend
  20. {
  21. /**
  22. * Dataexport模型对象
  23. * @var \app\admin\model\routine\Dataexport
  24. */
  25. protected $model = null;
  26. protected $quickSearchField = ['name'];
  27. protected $defaultSortField = 'id,desc';
  28. protected $withJoinTable = ['admin'];
  29. protected $preExcludeFields = ['createtime'];
  30. protected $noNeedPermission = ['getFieldList', 'test', 'task', 'taskControl', 'taskZip'];
  31. protected $saveDir = null;
  32. protected $exportZipDir = null;
  33. public function initialize()
  34. {
  35. parent::initialize();
  36. $this->model = new \app\admin\model\routine\Dataexport;
  37. $this->saveDir = runtime_path() . 'export' . DIRECTORY_SEPARATOR;// 临时文件保存位置(每次开始任务时清空)
  38. $this->exportZipDir = root_path() . 'public' . DIRECTORY_SEPARATOR . 'export' . DIRECTORY_SEPARATOR;
  39. }
  40. public function test()
  41. {
  42. if (!$this->auth->check('routine/dataexport/start')) {
  43. $this->error(__('You have no permission'), ['routePath' => '/admin'], 302);
  44. }
  45. $id = $this->request->param($this->model->getPk());
  46. $row = $this->model->find($id);
  47. $export = new ExportLib($id);
  48. try {
  49. $exportNumber = $row->export_number ? $row->export_number : $export->getCount();
  50. if (!$exportNumber) {
  51. $this->error('没有数据需要导出~');
  52. }
  53. $subtask = [
  54. [
  55. 'id' => 1,
  56. 'status' => 0,// 状态:0=准备好,1=进行中,2=完成,3=失败
  57. 'min' => 0,
  58. 'max' => 10,
  59. 'sql' => $export->getSql('test')
  60. ]
  61. ];
  62. // 测试sql
  63. Db::query($subtask[0]['sql']);
  64. $row->subtask = $subtask;
  65. $row->save();
  66. } catch (PDOException|Exception $e) {
  67. $this->error($e->getMessage());
  68. }
  69. $this->success('', [
  70. 'taskId' => $id
  71. ]);
  72. }
  73. public function start()
  74. {
  75. $id = $this->request->param($this->model->getPk());
  76. $row = $this->model->find($id);
  77. $export = new ExportLib($id);
  78. try {
  79. $exportNumber = $row->export_number ? $row->export_number : $export->getCount();
  80. $xlsMaxNumber = ($exportNumber >= $row->xls_max_number) ? $row->xls_max_number : $exportNumber;
  81. if (!$exportNumber) {
  82. $this->error('没有数据需要导出~');
  83. }
  84. $subtask = [];
  85. $subtaskCount = ceil($exportNumber / $xlsMaxNumber);// 子任务数量
  86. for ($i = 0; $i < $subtaskCount; $i++) {
  87. $min = $i * $xlsMaxNumber;
  88. $subtask[$i] = [
  89. 'id' => $i,
  90. 'status' => 0,// 状态:0=准备好,1=进行中,2=完成,3=失败
  91. 'min' => $min,
  92. 'max' => $xlsMaxNumber,
  93. 'sql' => $export->getSql('limit', [$min, $xlsMaxNumber])
  94. ];
  95. }
  96. // 测试sql
  97. Db::query($subtask[0]['sql']);
  98. if (!is_dir($this->exportZipDir)) {
  99. mkdir($this->exportZipDir, 0777, true);
  100. }
  101. if ($subtaskCount > 1) {
  102. // 清理任务临时文件目录
  103. if (!is_dir($this->saveDir . $id)) {
  104. mkdir($this->saveDir . $id . DIRECTORY_SEPARATOR, 0777, true);
  105. } else {
  106. deldir($this->saveDir . $id, false);
  107. }
  108. $row->lastprogress = 5;
  109. } else {
  110. $row->lastprogress = 0;
  111. }
  112. // 删除上次任务的zip包
  113. if ($row->lastfile) {
  114. $fileName = explode(DIRECTORY_SEPARATOR, path_transform($row->lastfile));
  115. if (file_exists($this->exportZipDir . end($fileName))) {
  116. unlink($this->exportZipDir . end($fileName));
  117. }
  118. $row->lastfile = '';
  119. }
  120. $row->subtask = $subtask;
  121. $row->lastexporttime = time();
  122. $row->save();
  123. } catch (PDOException|Exception $e) {
  124. $this->error($e->getMessage());
  125. }
  126. $this->success('导出任务初始化成功!', [
  127. 'download' => ($subtaskCount == 1) ? true : false,
  128. 'subtask' => $subtask,
  129. 'id' => $id
  130. ]);
  131. }
  132. public function task(int $id, int $subId, bool $download)
  133. {
  134. if (!$this->auth->check('routine/dataexport/start')) {
  135. $this->error(__('You have no permission'), ['routePath' => '/admin'], 302);
  136. }
  137. $row = $this->model->find($id);
  138. $export = new ExportLib($id);
  139. if (isset($row->subtask[$subId]) && is_array($row->subtask[$subId])) {
  140. $subTask = $row['subtask'][$subId];
  141. $taskName = $row['name'];
  142. } else {
  143. $this->error('导出子任务未找到,请重新开始!', ['subId' => $subId]);
  144. }
  145. set_time_limit(0);// 脚本执行时间限制
  146. ini_set('memory_limit', $row->memory_limit . 'M');// 脚本内存限制
  147. // 检查任务状态
  148. if (!$download) {
  149. if ($subTask['status'] == 1) {
  150. $this->error('此子任务正在执行中~', ['subId' => $subId]);
  151. } else if ($subTask['status'] == 2) {
  152. if (file_exists($this->saveDir . $id . DIRECTORY_SEPARATOR . $subId . '.xlsx')) {
  153. $this->error('此子任务已经处理过啦~', ['subId' => $subId]);
  154. }
  155. }
  156. }
  157. $spreadsheet = new Spreadsheet();
  158. $worksheet = $spreadsheet->getActiveSheet();
  159. $worksheet->setTitle($taskName);
  160. // 设置表头
  161. $head = $export->getXlsTitle();
  162. $headCount = count($head);
  163. for ($i = 0; $i < $headCount; $i++) {
  164. $worksheet->setCellValueByColumnAndRow($i + 1, 1, $head[$i]);
  165. }
  166. // 写入数据
  167. try {
  168. $data = Db::query($subTask['sql']);
  169. } catch (PDOException $e) {
  170. $this->error('任务失败!', ['subId' => $subId, 'msg' => $e->getMessage()]);
  171. }
  172. $y = 2;
  173. $fields = $export->getFields();
  174. foreach ($data as $rowKey => $row) {
  175. $i = 1;
  176. foreach ($fields as $key => $value) {
  177. $field = $value['field'];
  178. if ($value['discern'] == 'text') {
  179. // 文本
  180. $worksheet->setCellValueExplicitByColumnAndRow($i, $y, $row[$field], DataType::TYPE_STRING);
  181. } else if ($value['discern'] == 'int') {
  182. // 数字
  183. $worksheet->setCellValueExplicitByColumnAndRow($i, $y, (int)$row[$field], DataType::TYPE_NUMERIC);
  184. } else if ($value['discern'] == 'time') {
  185. // 日期时间
  186. if ($row[$field]) {
  187. if (is_numeric($row[$field])) {
  188. $excelDateValue = date('Y-m-d H:i:s', $row[$field]);
  189. } else {
  190. $excelDateValue = $row[$field];
  191. }
  192. $worksheet->setCellValueByColumnAndRow($i, $y, $excelDateValue);
  193. } else {
  194. $worksheet->setCellValueByColumnAndRow($i, $y, '-');
  195. }
  196. } else if ($value['discern'] == 'valuation') {
  197. // 赋值
  198. $fieldValue = $export->assignment($row[$field], $value['comment']);
  199. $worksheet->setCellValueByColumnAndRow($i, $y, $fieldValue);
  200. }
  201. $i++;
  202. }
  203. $y++;
  204. unset($data[$rowKey]); // 能节约一点内存
  205. }
  206. // xls文件处理
  207. if ($download) {
  208. // 直接下载
  209. ob_end_clean();
  210. header("Pragma: public");
  211. header("Expires: 0");
  212. header("Cache-Control:must-revalidate, post-check=0, pre-check=0");
  213. header("Content-Type:application/force-download");
  214. header("Content-Type:application/vnd.ms-execl");
  215. header("Content-Type:application/octet-stream");
  216. header("Content-Type:application/download");
  217. $taskName = $id . '.' . $taskName . '.xlsx';
  218. $encodedFilename = urlencode($taskName);
  219. $ua = $_SERVER["HTTP_USER_AGENT"];
  220. if (preg_match("/MSIE/", $ua)) {
  221. header('Content-Disposition: attachment; filename="' . $encodedFilename . '"');
  222. } else if (preg_match("/Firefox/", $ua)) {
  223. header('Content-Disposition: attachment; filename*="utf8\'\'' . $taskName . '"');
  224. } else {
  225. header('Content-Disposition: attachment; filename="' . $taskName . '"');
  226. }
  227. header("Content-Transfer-Encoding:binary");
  228. header('Cache-Control: max-age=0');
  229. $writer = new Xlsx($spreadsheet);
  230. $writer->save('php://output');
  231. $spreadsheet->disconnectWorksheets();
  232. unset($spreadsheet);
  233. } else {
  234. // 保存
  235. $writer = new Xlsx($spreadsheet);
  236. $writer->save($this->saveDir . $id . DIRECTORY_SEPARATOR . $subId . '.xlsx');
  237. $result = false;
  238. Db::startTrans();
  239. try {
  240. // 获取最新状态
  241. $row = $this->model->find($id);
  242. $lastprogress = $row->lastprogress + round(92 / count($row->subtask), 2);
  243. $lastprogress = ($lastprogress > 100) ? 100 : $lastprogress;
  244. if (isset($row->subtask[$subId]) && is_array($row->subtask[$subId])) {
  245. $subTaskTemp = $row->subtask;
  246. $subTaskTemp[$subId]['status'] = 2;
  247. $row->subtask = $subTaskTemp;
  248. $row->lastprogress = $lastprogress;
  249. $row->save();
  250. $result = true;
  251. }
  252. Db::commit();
  253. } catch (PDOException|Exception $e) {
  254. Db::rollback();
  255. $this->error('任务失败!', ['subId' => $subId, 'msg' => $e->getMessage()]);
  256. }
  257. $spreadsheet->disconnectWorksheets();
  258. unset($spreadsheet);
  259. if ($result) {
  260. $this->success('', [
  261. 'subId' => $subId,
  262. ]);
  263. } else {
  264. $this->error('', [
  265. 'subId' => $subId,
  266. ]);
  267. }
  268. }
  269. }
  270. public function taskControl()
  271. {
  272. if (!$this->auth->check('routine/dataexport/start')) {
  273. $this->error(__('You have no permission'), ['routePath' => '/admin'], 302);
  274. }
  275. $id = $this->request->param($this->model->getPk());
  276. $row = $this->model->find($id);
  277. if (!$row) {
  278. $this->error('任务找不到啦~');
  279. }
  280. if (!isset($row->subtask) || !is_array($row->subtask)) {
  281. $this->error('子任务找不到啦,请重新开始任务~');
  282. }
  283. $row = $row->toArray();
  284. $currentPage = 0;
  285. $subtaskPage = [];
  286. foreach ($row['subtask'] as $value) {
  287. $value['status'] = 0;// 用户可能会刷新任务控制页面,js将重新确定状态
  288. $subtaskPage[$currentPage][] = $value;
  289. if (count($subtaskPage[$currentPage]) >= $row['concurrent_create_xls']) {
  290. $currentPage++;
  291. }
  292. }
  293. $subtaskCount = is_array($row['subtask']) ? count($row['subtask']) : 0;
  294. $row['subtask_progress'] = round(92 / $subtaskCount, 2);
  295. $row['lastprogress'] = 5;
  296. $this->success('', [
  297. 'task' => $row,
  298. 'subtaskPage' => $subtaskPage,
  299. ]);
  300. }
  301. public function taskZip()
  302. {
  303. if (!$this->auth->check('routine/dataexport/start')) {
  304. $this->error(__('You have no permission'), ['routePath' => '/admin'], 302);
  305. }
  306. $id = $this->request->param($this->model->getPk());
  307. $row = $this->model->find($id);
  308. if (!$row) {
  309. $this->error(__('Record not found'));
  310. }
  311. if (!isset($row->subtask) || !is_array($row->subtask)) {
  312. $this->error('打包失败,任务找不到啦~');
  313. }
  314. if ($row->lastfile) {
  315. $this->success('', ['file' => $row->lastfile]);
  316. }
  317. foreach ($row->subtask as $subtask) {
  318. if (!file_exists($this->saveDir . $id . DIRECTORY_SEPARATOR . $subtask['id'] . '.xlsx')) {
  319. $this->error('子任务未处理完毕!');
  320. }
  321. }
  322. $taskDir = $this->saveDir . $id . DIRECTORY_SEPARATOR;// 导出任务的临时文件目录
  323. $zipName = $id . '.export_' . Random::build() . '.zip';
  324. $zipUrl = full_url(path_transform(DIRECTORY_SEPARATOR . 'export' . DIRECTORY_SEPARATOR . $zipName));// 绝对地址,以便各处直接点击下载
  325. $zipName = $this->exportZipDir . $zipName;
  326. $zip = new ZipFile();
  327. try {
  328. $dh = opendir($taskDir);
  329. while ($file = readdir($dh)) {
  330. if ($file != "." && $file != "..") {
  331. $fullPath = $taskDir . $file;
  332. if (!is_dir($fullPath)) {
  333. $zip->addFile($fullPath, $file);
  334. }
  335. }
  336. }
  337. closedir($dh);
  338. $zip->saveAsFile($zipName);
  339. deldir($taskDir);
  340. } catch (ZipException $e) {
  341. $this->error('打包失败:' . $e->getMessage());
  342. } finally {
  343. $zip->close();
  344. }
  345. $row->lastfile = $zipUrl;
  346. $row->lastprogress = 100;
  347. $row->save();
  348. $this->success('', ['url' => $zipUrl]);
  349. }
  350. public function add()
  351. {
  352. if ($this->request->isPost()) {
  353. $this->request->filter(['trim']);
  354. $data = $this->request->post();
  355. if (!$data) {
  356. $this->error(__('Parameter %s can not be empty', ['']));
  357. }
  358. $data = $this->excludeFields($data);
  359. $data['admin_id'] = $this->auth->id;
  360. $result = false;
  361. Db::startTrans();
  362. try {
  363. // 模型验证
  364. if ($this->modelValidate) {
  365. $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  366. if (class_exists($validate)) {
  367. $validate = new $validate;
  368. if ($this->modelSceneValidate) $validate->scene('add');
  369. $validate->check($data);
  370. }
  371. }
  372. $result = $this->model->save($data);
  373. Db::commit();
  374. } catch (ValidateException|PDOException|Exception $e) {
  375. Db::rollback();
  376. $this->error($e->getMessage());
  377. }
  378. if ($result !== false) {
  379. $this->success(__('Added successfully'));
  380. } else {
  381. $this->error(__('No rows were added'));
  382. }
  383. }
  384. $this->success('', [
  385. 'tables' => $this->getTableList(),
  386. ]);
  387. }
  388. /**
  389. * 编辑
  390. */
  391. public function edit()
  392. {
  393. $id = $this->request->param($this->model->getPk());
  394. $row = $this->model->find($id);
  395. if (!$row) {
  396. $this->error(__('Record not found'));
  397. }
  398. $dataLimitAdminIds = $this->getDataLimitAdminIds();
  399. if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
  400. $this->error(__('You have no permission'));
  401. }
  402. if ($this->request->isPost()) {
  403. $this->request->filter(['trim']);
  404. $data = $this->request->post();
  405. if (!$data) {
  406. $this->error(__('Parameter %s can not be empty', ['']));
  407. }
  408. $data = $this->excludeFields($data);
  409. $result = false;
  410. Db::startTrans();
  411. try {
  412. // 模型验证
  413. if ($this->modelValidate) {
  414. $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  415. if (class_exists($validate)) {
  416. $validate = new $validate;
  417. if ($this->modelSceneValidate) $validate->scene('edit');
  418. $validate->check($data);
  419. }
  420. }
  421. $result = $row->save($data);
  422. Db::commit();
  423. } catch (ValidateException|PDOException|Exception $e) {
  424. Db::rollback();
  425. $this->error($e->getMessage());
  426. }
  427. if ($result !== false) {
  428. $this->success(__('Update successful'));
  429. } else {
  430. $this->error(__('No rows updated'));
  431. }
  432. }
  433. $this->success('', [
  434. 'row' => $row,
  435. 'tables' => $this->getTableList(),
  436. ]);
  437. }
  438. /**
  439. * 查看
  440. */
  441. public function index()
  442. {
  443. // 设置过滤方法
  444. $this->request->filter(['strip_tags', 'trim']);
  445. // 如果是select则转发到select方法,若select未重写,其实还是继续执行index
  446. if ($this->request->param('select')) {
  447. $this->select();
  448. }
  449. list($where, $alias, $limit, $order) = $this->queryBuilder();
  450. $res = $this->model
  451. ->withJoin($this->withJoinTable, $this->withJoinType)
  452. ->alias($alias)
  453. ->where($where)
  454. ->order($order)
  455. ->paginate($limit);
  456. $res->visible(['admin' => ['nickname']]);
  457. $this->success('', [
  458. 'list' => $res->items(),
  459. 'total' => $res->total(),
  460. 'remark' => get_route_remark(),
  461. ]);
  462. }
  463. protected function getTableList()
  464. {
  465. $tablePrefix = config('database.connections.mysql.prefix');
  466. $outExcludeTable = [
  467. // 功能表
  468. 'area',
  469. 'config',
  470. 'token',
  471. 'captcha',
  472. 'admin_group_access',
  473. ];
  474. $outTables = [];
  475. $tables = get_table_list();
  476. $pattern = '/^' . $tablePrefix . '/i';
  477. foreach ($tables as $table => $tableComment) {
  478. $table = preg_replace($pattern, '', $table);
  479. if (!in_array($table, $outExcludeTable)) {
  480. $outTables[$table] = $tableComment;
  481. }
  482. }
  483. return $outTables;
  484. }
  485. public function getFieldList($table = null)
  486. {
  487. if (!$table) {
  488. $this->error(__('Parameter error'));
  489. }
  490. // 字段类型识别
  491. $dataTypeInt = [
  492. 'tinyint',
  493. 'int',
  494. 'smallint',
  495. 'mediumint',
  496. 'integer',
  497. 'bigint'
  498. ];
  499. $fieldList = get_table_fields($table);
  500. $fields = [];
  501. foreach ($fieldList as $key => $item) {
  502. $discern = 'text'; // 文本
  503. if (in_array($item['DATA_TYPE'], $dataTypeInt)) {
  504. $discern = 'int'; // 数字
  505. }
  506. if (preg_match("/time$|datetime$/i", $key)) {
  507. $discern = 'time'; // 日期时间
  508. }
  509. if (preg_match("/switch$|toggle$/i", $key)) {
  510. $discern = 'valuation';// 赋值
  511. $commentVal = '0=关闭,1=开启';
  512. }
  513. $comment = str_replace(',', ',', $item['COLUMN_COMMENT']);
  514. $comment = str_replace(['(多选)', '(单选)', '(多选)', '(单选)'], '', $comment);
  515. if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
  516. list($fieldName, $commentVal) = explode(':', $comment);
  517. $discern = 'valuation';// 赋值
  518. }
  519. $fields[$key] = [
  520. 'name' => $key,
  521. 'discern' => $discern,
  522. 'title' => $fieldName ?? ($item['COLUMN_COMMENT'] ? $item['COLUMN_COMMENT'] : $key),
  523. 'comment' => $commentVal ?? '',
  524. ];
  525. unset($commentVal, $fieldName);
  526. }
  527. $this->success('', [
  528. 'fields' => $fields,
  529. ]);
  530. }
  531. }