ImportService.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. <?php
  2. namespace App\Service;
  3. use App\Exports\TableHeadExport;
  4. use App\Import\ImportAll;
  5. use App\Model\Device;
  6. use App\Model\Employee;
  7. use App\Model\Item;
  8. use App\Model\ItemDetails;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. use Maatwebsite\Excel\Facades\Excel;
  12. use PhpOffice\PhpSpreadsheet\IOFactory;
  13. use PhpOffice\PhpSpreadsheet\Shared\Date;
  14. class ImportService extends Service
  15. {
  16. public static $type = [
  17. 'device', // 设备
  18. 'item', // 项目
  19. ];
  20. public function getTableTitleXls($data,$user){
  21. if(empty($data['type'])) return [false,'缺少类型'];
  22. if(! in_array($data['type'],self::$type)) return [false,'类型不存在'];
  23. //获取配置文件
  24. $fuc = $data['type'];
  25. list($status,$return) = $this->$fuc($data,$user);
  26. list($msg,$filename) = $return;
  27. if(! $status) return [false, $msg];
  28. $headers = array_column($msg,'value');
  29. $comments = $enums = [];
  30. foreach ($msg as $value){
  31. if(! empty($value['comments'])) $comments[$value['value']] = $value['comments'];
  32. if(! empty($value['enums'])) $enums[$value['value']] = $value['enums'];
  33. }
  34. Excel::store(new TableHeadExport([], $headers, $comments, $enums),"/public/export/{$filename}", null, 'Xlsx', []);
  35. return [true, ['file' => $filename]];
  36. }
  37. private function getTableConfig($type = ""){
  38. if(empty($type)) return [];
  39. //获取配置文件
  40. $config = "excel." . $type;
  41. return config($config) ?? [];
  42. }
  43. private function device($data,$user){
  44. $config = $this->getTableConfig($data['type']);
  45. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  46. $config_array = $config['array'] ?? [];
  47. //生成下载文件
  48. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  49. return [true, [$config_array, $filename]];
  50. }
  51. private function item($data,$user){
  52. $config = $this->getTableConfig($data['type']);
  53. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  54. $config_array = $config['array'] ?? [];
  55. //生成下载文件
  56. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  57. return [true, [$config_array, $filename]];
  58. }
  59. //导入入口
  60. public function importAll($data,$user){
  61. // //不超时
  62. // ini_set('max_execution_time', 0);
  63. // //内存设置
  64. // ini_set('memory_limit', -1);
  65. // $reader = IOFactory::createReader('Xlsx');
  66. // $reader->setReadDataOnly(true); // 只读取有数据的单元格
  67. // $spreadsheet = $reader->load($data['file']);
  68. // dd($spreadsheet);
  69. // // 创建一个Reader对象
  70. // $reader = IOFactory::createReader('Xlsx'); // 根据你的文件格式选择合适的reader
  71. //
  72. //// 加载Excel文件
  73. // $spreadsheet = $reader->load($data['file']);
  74. //
  75. //// 获取第一个工作表
  76. // $worksheet = $spreadsheet->getActiveSheet();
  77. //
  78. //// 获取总行数
  79. // $totalRows = $worksheet->getHighestRow();dd($totalRows);
  80. if(empty($data['type'])) return [false,'缺少导入类型,导入失败'];
  81. if(! in_array($data['type'],self::$type)) return [false,'导入类型不存在,导入失败'];
  82. if(empty($data['file'])) return [false,'导入文件不能为空'];
  83. try {
  84. $import = new ImportAll();
  85. //设置导入人id
  86. $import->setCrt($user['id']);
  87. $import->setUser($user);
  88. $import->setType($data['type']);
  89. $other = $data;
  90. unset($other['file']);
  91. $import->setOtherParam($other);
  92. //导入
  93. \Maatwebsite\Excel\Facades\Excel::import($import,$data['file']);
  94. if($import->getMsg()) {
  95. $bool = $import->getIsLongText();
  96. if($bool) {
  97. return [0, $import->getMsg()];
  98. }else{
  99. return [false, $import->getMsg()];
  100. }
  101. }
  102. }catch (\Throwable $exception) {
  103. return [false, $exception->getMessage() . ' (Code: ' . $exception->getCode() . ', Line: ' . $exception->getLine() . ')'];
  104. }
  105. return [true, ''];
  106. }
  107. // 设备 ------------------------------------
  108. public function deviceImport($array, $user, $other_param){
  109. $upload = $array[0];
  110. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  111. if(!$status) return [false, $msg];
  112. $table_config = $msg;
  113. unset($array[0]);
  114. if(empty($array)) return [false, '导入数据不能为空'];
  115. // 公共校验
  116. list($array, $error) = $this->checkCommon($array, $table_config);
  117. if(!empty($error)) return [0, $error];
  118. // 详细校验 (这里 $array 传引用,内部会转换日期)
  119. list($error, $update_map) = $this->deviceCheck($array, $user, $table_config);
  120. if(!empty($error)) return [0, $error];
  121. $time = time();
  122. $insert = [];
  123. $update = [];
  124. foreach ($array as $key => $value){
  125. $main_tmp = [];
  126. foreach ($value as $k => $val){
  127. if(!empty($table_config[$k]['is_main'])){
  128. $main_tmp[$table_config[$k]['key']] = $val;
  129. }
  130. }
  131. if(isset($update_map[$key])){
  132. // 存入待更新数组
  133. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  134. } else {
  135. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  136. $main_tmp['crt_id'] = $user['id'];
  137. $main_tmp['crt_time'] = $time;
  138. $insert[] = $main_tmp;
  139. }
  140. }
  141. DB::beginTransaction();
  142. try {
  143. // 1. 批量新增
  144. if(!empty($insert)){
  145. foreach(array_chunk($insert, 500) as $chunkInsert){
  146. Device::insert($chunkInsert);
  147. }
  148. }
  149. // 批量更新
  150. foreach (array_chunk($update, 100) as $chunk) {
  151. foreach ($chunk as $item) {
  152. $id = $item['id'];
  153. unset($item['id']);
  154. Device::where('id', $id)->update($item);
  155. }
  156. }
  157. DB::commit();
  158. } catch (\Exception $e) {
  159. DB::rollBack();
  160. return [false, "错误:" . $e->getMessage() . " 行:" . $e->getLine()];
  161. }
  162. return [true, ''];
  163. }
  164. private function deviceCheck(&$array, $user, $table_config)
  165. {
  166. // 动态获取关键列的索引
  167. $codeIdx = array_search('code', array_column($table_config, 'key'));
  168. $dateIdx = array_search('in_time', array_column($table_config, 'key'));
  169. $typeIdx = array_search('type', array_column($table_config, 'key'));
  170. $type2Idx = array_search('is_use', array_column($table_config, 'key'));
  171. $code_map = $this->getDeviceList($array, $user, $codeIdx);
  172. $errors = [];
  173. $update = [];
  174. $map_type = array_flip(Device::$type);
  175. $map_type_2 = array_flip(Device::Use);
  176. foreach ($array as $rowIndex => $value) {
  177. $valCode = $value[$codeIdx] ?? '';
  178. // 记录更新 ID
  179. if(isset($code_map[$valCode])){
  180. $update[$rowIndex] = $code_map[$valCode];
  181. }
  182. if(empty($map_type[$value[$typeIdx]])){
  183. $errors[] = "第{$rowIndex}行固定资产类型错误";
  184. }else{
  185. $array[$rowIndex][$typeIdx] = $map_type[$value[$typeIdx]];
  186. }
  187. if(empty($map_type_2[$value[$type2Idx]])){
  188. $errors[] = "第{$rowIndex}行是否启用错误";
  189. }else{
  190. $array[$rowIndex][$type2Idx] = $map_type_2[$value[$type2Idx]];
  191. }
  192. // 日期处理
  193. if($dateIdx !== false && !empty($value[$dateIdx])){
  194. list($status, $msg) = $this->convertExcelCellToDate($value[$dateIdx]);
  195. if(!$status) {
  196. $errors[] = "第{$rowIndex}行日期格式错误";
  197. } else {
  198. $array[$rowIndex][$dateIdx] = $msg;
  199. }
  200. }
  201. }
  202. $error_string = "";
  203. if(! empty($errors)) $error_string = implode('|', $errors);
  204. return [$error_string, $update];
  205. }
  206. private function getDeviceList($array, $user, $index){
  207. //查找设备
  208. $codes = array_unique(array_filter(array_column($array,$index)));
  209. return Device::where('del_time', 0)
  210. ->where('top_depart_id', $user['top_depart_id'])
  211. ->whereIn('code', $codes)
  212. ->pluck('id','code')
  213. ->toArray();
  214. }
  215. // 项目 -----------------------------------
  216. public function itemImport($array, $user, $other_param)
  217. {
  218. $upload = $array[0];
  219. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  220. if (!$status) return [false, $msg];
  221. $table_config = $msg;
  222. unset($array[0]);
  223. if (empty($array)) return [false, '导入数据不能为空'];
  224. // 1. 公共校验 (必填、唯一性等)
  225. list($array, $error) = $this->checkCommon($array, $table_config);
  226. if (!empty($error)) return [0, $error];
  227. // 2. 业务详细校验 (获取更新映射及明细数据)
  228. list($error, $update_map, $detail_data_map) = $this->itemCheck($array, $user, $table_config);
  229. if (!empty($error)) return [0, $error];
  230. $time = time();
  231. $insert_data = [];
  232. $update_data = [];
  233. $all_detail_insert = [];
  234. $update_main_ids = [];
  235. // 3. 数据分拣
  236. foreach ($array as $key => $value) {
  237. $main_tmp = [];
  238. foreach ($value as $k => $val) {
  239. if (!empty($table_config[$k]['is_main'])) {
  240. $main_tmp[$table_config[$k]['key']] = $val;
  241. }
  242. }
  243. if (isset($update_map[$key])) {
  244. // 更新逻辑
  245. $itemId = $update_map[$key];
  246. $update_data[] = array_merge($main_tmp, ['id' => $itemId]);
  247. $update_main_ids[] = $itemId;
  248. // 收集明细 (后续统一插入)
  249. if (isset($detail_data_map[$key])) {
  250. foreach ($detail_data_map[$key] as $d) {
  251. $all_detail_insert[] = array_merge($d, ['item_id' => $itemId, 'crt_time' => $time]);
  252. }
  253. }
  254. } else {
  255. // 新增逻辑
  256. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  257. $main_tmp['crt_id'] = $user['id'];
  258. $main_tmp['crt_time'] = $time;
  259. // 以 code 为键,方便后续回填 ID
  260. $insert_data[$main_tmp['code']] = $main_tmp;
  261. if (isset($detail_data_map[$key])) {
  262. foreach ($detail_data_map[$key] as $d) {
  263. $all_detail_insert[] = array_merge($d, ['_code' => $main_tmp['code'], 'crt_time' => $time]);
  264. }
  265. }
  266. }
  267. }
  268. DB::beginTransaction();
  269. try {
  270. // 4. 执行新增主表
  271. if (!empty($insert_data)) {
  272. foreach (array_chunk($insert_data, 500) as $chunk) {
  273. Item::insert($chunk);
  274. }
  275. // 获取新插入数据的 ID 映射
  276. $new_item_maps = Item::whereIn('code', array_keys($insert_data))
  277. ->where('del_time', 0)
  278. ->where('top_depart_id', $user['top_depart_id'])
  279. ->pluck('id', 'code')->toArray();
  280. }
  281. // 5. 执行更新主表 (分批更新)
  282. if (!empty($update_data)) {
  283. foreach (array_chunk($update_data, 100) as $chunk) {
  284. foreach ($chunk as $uItem) {
  285. $id = $uItem['id'];
  286. unset($uItem['id']);
  287. Item::where('id', $id)->update($uItem);
  288. }
  289. }
  290. }
  291. // 6. 处理明细表 (先删后插策略)
  292. // 删除旧明细 (逻辑删除)
  293. if (!empty($update_main_ids)) {
  294. ItemDetails::whereIn('item_id', $update_main_ids)
  295. ->where('del_time', 0)
  296. ->update(['del_time' => $time]);
  297. }
  298. // 回填新增主表的 ID 到明细数组
  299. foreach ($all_detail_insert as &$di) {
  300. if (isset($di['_code'])) {
  301. $di['item_id'] = $new_item_maps[$di['_code']] ?? 0;
  302. unset($di['_code']);
  303. }
  304. }
  305. unset($di);
  306. // 批量插入所有明细
  307. if (!empty($all_detail_insert)) {
  308. foreach (array_chunk($all_detail_insert, 500) as $chunk) {
  309. ItemDetails::insert($chunk);
  310. }
  311. }
  312. DB::commit();
  313. } catch (\Exception $e) {
  314. DB::rollBack();
  315. return [false, "入库失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  316. }
  317. return [true, '导入成功'];
  318. }
  319. private function getItemList($array, $user, $index){
  320. //查找设备
  321. $codes = array_unique(array_filter(array_column($array,$index)));
  322. return Item::where('del_time', 0)
  323. ->where('top_depart_id', $user['top_depart_id'])
  324. ->whereIn('code', $codes)
  325. ->pluck('id','code')
  326. ->toArray();
  327. }
  328. private function itemCheck(&$array, $user, $table_config)
  329. {
  330. $keys = array_column($table_config, 'key');
  331. $codeIdx = array_search('code', $keys);
  332. $stateIdx = array_search('state', $keys);
  333. $manIdx = array_search('man_list', $keys);
  334. $deviceIdx = array_search('device_list', $keys);
  335. // 日期索引
  336. $dateIdx = array_search('start_time', $keys);
  337. $date2Idx = array_search('end_time', $keys);
  338. $code_map = $this->getItemList($array, $user, $codeIdx);
  339. list($man_map, $device_map) = $this->getDataList($array, $user, $manIdx, $deviceIdx);
  340. $errors = [];
  341. $update_mapping = [];
  342. $detail_storage = [];
  343. $state_type_map = array_flip(Item::State_Type);
  344. foreach ($array as $rowIndex => $rowValue) {
  345. $valCode = $rowValue[$codeIdx] ?? '';
  346. // 1. 判定更新还是新增
  347. if (isset($code_map[$valCode])) {
  348. $update_mapping[$rowIndex] = $code_map[$valCode];
  349. }
  350. // 2. 状态校验
  351. $state_text = $rowValue[$stateIdx] ?? '';
  352. if (!isset($state_type_map[$state_text])) {
  353. $errors[] = "第{$rowIndex}行:状态[{$state_text}]无效";
  354. } else {
  355. $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
  356. }
  357. // 3. 日期转换
  358. foreach ([$dateIdx, $date2Idx] as $dIdx) {
  359. if ($dIdx !== false && !empty($rowValue[$dIdx])) {
  360. list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
  361. if (!$s) $errors[] = "第{$rowIndex}行:日期格式非法";
  362. else $array[$rowIndex][$dIdx] = $m;
  363. }
  364. }
  365. // 4. 解析人员 (明细类型1)
  366. if ($manIdx !== false && !empty($rowValue[$manIdx])) {
  367. foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
  368. $mNum = trim($mNum);
  369. if (!isset($man_map[$mNum])) {
  370. $errors[] = "第{$rowIndex}行:人员编码[{$mNum}]不存在";
  371. } else {
  372. $detail_storage[$rowIndex][] = [
  373. 'type' => ItemDetails::type_one,
  374. 'data_id' => $man_map[$mNum],
  375. ];
  376. }
  377. }
  378. }
  379. // 5. 解析设备 (明细类型2)
  380. if ($deviceIdx !== false && !empty($rowValue[$deviceIdx])) {
  381. foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
  382. $dCode = trim($dCode);
  383. if (!isset($device_map[$dCode])) {
  384. $errors[] = "第{$rowIndex}行:资产编码[{$dCode}]不存在";
  385. } else {
  386. $detail_storage[$rowIndex][] = [
  387. 'type' => ItemDetails::type_two,
  388. 'data_id' => $device_map[$dCode],
  389. ];
  390. }
  391. }
  392. }
  393. }
  394. $error_str = !empty($errors) ? implode('|', $errors) : "";
  395. return [$error_str, $update_mapping, $detail_storage];
  396. }
  397. private function getDataList($array, $user, $index1, $index2)
  398. {
  399. $manNums = [];
  400. $devCodes = [];
  401. // 去重收集
  402. foreach ($array as $row) {
  403. if (!empty($row[$index1])) {
  404. foreach (explode(',', $row[$index1]) as $v) $manNums[trim($v)] = true;
  405. }
  406. if (!empty($row[$index2])) {
  407. foreach (explode(',', $row[$index2]) as $v) $devCodes[trim($v)] = true;
  408. }
  409. }
  410. $manMap = Employee::where('del_time', 0)
  411. ->where('top_depart_id', $user['top_depart_id'])
  412. ->whereIn('number', array_keys($manNums))
  413. ->pluck('id', 'number')->toArray();
  414. $devMap = Device::where('del_time', 0)
  415. ->where('top_depart_id', $user['top_depart_id'])
  416. ->whereIn('code', array_keys($devCodes))
  417. ->pluck('id', 'code')->toArray();
  418. return [$manMap, $devMap];
  419. }
  420. //公共校验 -----------------------------------------
  421. private function checkCommon($array, $table_config) {
  422. $error = [];
  423. $uniqueCheck = []; // 格式:[$column_index => [$value => $first_line]]
  424. foreach ($array as $line => $row) {
  425. $rowData = array_filter($row);
  426. if (empty($rowData)) {
  427. unset($array[$line]);
  428. continue;
  429. }
  430. foreach ($row as $colIndex => $value) {
  431. $value = trim($value);
  432. $config = $table_config[$colIndex] ?? null;
  433. // 1. 基础存在性检查
  434. if (!$config) {
  435. $error[] = "第{$line}行第{$colIndex}列配置不存在";
  436. continue;
  437. }
  438. $fieldName = $config['value'];
  439. // 2. 必填校验
  440. if (!empty($config['required']) && ($value === '' || !isset($value))) {
  441. $error[] = "第{$line}行:[{$fieldName}] 必填";
  442. }
  443. // 3. 默认值填充
  444. if ($value === '' && isset($config['default'])) {
  445. $value = $config['default'];
  446. }
  447. // 4. 唯一性校验(重点:一次遍历解决)
  448. if (!empty($config['unique']) && $value !== '') {
  449. if (isset($uniqueCheck[$colIndex][$value])) {
  450. $prevLine = $uniqueCheck[$colIndex][$value];
  451. $error[] = "第{$line}行:[{$fieldName}] 与第{$prevLine}行重复,重复值:{$value}";
  452. } else {
  453. // 记录该值第一次出现的位置
  454. $uniqueCheck[$colIndex][$value] = $line;
  455. }
  456. }
  457. $row[$colIndex] = $value;
  458. }
  459. $array[$line] = $row;
  460. }
  461. $error_string = !empty($error) ? implode('|', $error) : "";
  462. return [$array, $error_string];
  463. }
  464. //模板校验 -----------------------------------------
  465. private function compareTableAndReturn($upload, $param){
  466. if(empty($upload)) return [false, '表头不能为空'];
  467. $config_array = $this->getTableConfig($param['type']);
  468. if(empty($config_array)) return [false, '导入配置表头文件不存在'];
  469. foreach ($config_array as $key => $value){
  470. $key_position = $key + 1;
  471. if(! isset($upload[$key])) return [false, "第" . $key_position . "列表头缺失"];
  472. $tmp_v = trim($upload[$key]);
  473. if($tmp_v != $value['value']) return [false, "第" . $key_position . "列表头与模板不符合,请重新下载模板"];
  474. }
  475. return [true, $config_array];
  476. }
  477. //转换日期 ------------------------------------------
  478. function convertExcelCellToDate($cellValue) {
  479. // 尝试将单元格值转换为浮点数(Excel 日期序列号)
  480. $excelTimestamp = filter_var($cellValue, FILTER_VALIDATE_FLOAT);
  481. if ($excelTimestamp !== false && $excelTimestamp > 0) {
  482. // 如果成功转换并且值大于0,则认为是Excel日期序列号
  483. try {
  484. $dateTimeObject = Date::excelToDateTimeObject($cellValue);
  485. // if ($dateTimeObject->format('H:i:s') === '00:00:00') {
  486. // // 如果是,则将时间设置为 '23:59:00'
  487. // $dateTimeObject->setTime(23, 59);
  488. // }
  489. // 现在你可以格式化这个日期了
  490. $formattedDate = $dateTimeObject->format('Y-m-d');
  491. if(! strtotime($formattedDate)) return [false, ''];
  492. return [true, strtotime($formattedDate)];
  493. } catch (\Exception $e) {
  494. // 处理转换失败的情况
  495. return [false, '单元格日期格式转换时间戳失败'];
  496. }
  497. }
  498. // 如果不是有效的浮点数,则尝试按照多种日期格式解析
  499. if(! strtotime($cellValue)) return [false, '单元格文本格式转换时间戳失败'];
  500. return [true, strtotime($cellValue)];
  501. }
  502. }