ImportService.php 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604
  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\Fee;
  8. use App\Model\Item;
  9. use App\Model\ItemDetails;
  10. use App\Model\MonthlyPsOrder;
  11. use App\Model\MonthlyPwOrder;
  12. use Illuminate\Support\Facades\DB;
  13. use Maatwebsite\Excel\Facades\Excel;
  14. use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
  15. use PhpOffice\PhpSpreadsheet\IOFactory;
  16. use PhpOffice\PhpSpreadsheet\Shared\Date;
  17. class ImportService extends Service
  18. {
  19. public static $type = [
  20. 'device', // 设备
  21. 'item', // 项目
  22. 'fee', // 费用
  23. 'monthPwOrder', // 人员月度研发工时单
  24. 'monthDwOrder', // 设备月度研发工时单
  25. 'monthPsOrder', // 人员月度工资单
  26. ];
  27. public function getTableTitleXls($data,$user){
  28. if(empty($data['type'])) return [false,'缺少类型'];
  29. if(! in_array($data['type'],self::$type)) return [false,'类型不存在'];
  30. //获取配置文件
  31. $fuc = $data['type'];
  32. if (! method_exists(self::class, $fuc)) return [false, "导入文件获取不存在,请联系开发"];
  33. list($status,$return) = $this->$fuc($data,$user);
  34. list($msg,$filename) = $return;
  35. if(! $status) return [false, $msg];
  36. $headers = array_column($msg,'value');
  37. $comments = $enums = [];
  38. foreach ($msg as $value){
  39. if(! empty($value['comments'])) $comments[$value['value']] = $value['comments'];
  40. if(! empty($value['enums'])) $enums[$value['value']] = $value['enums'];
  41. }
  42. Excel::store(new TableHeadExport([], $headers, $comments, $enums),"/public/export/{$filename}", null, 'Xlsx', []);
  43. return [true, ['file' => $filename]];
  44. }
  45. private function getTableConfig($type = ""){
  46. if(empty($type)) return [];
  47. //获取配置文件
  48. $config = "excel." . $type;
  49. return config($config) ?? [];
  50. }
  51. private function device($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. private function item($data,$user){
  60. $config = $this->getTableConfig($data['type']);
  61. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  62. $config_array = $config['array'] ?? [];
  63. //生成下载文件
  64. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  65. return [true, [$config_array, $filename]];
  66. }
  67. private function fee($data,$user){
  68. $config = $this->getTableConfig($data['type']);
  69. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  70. $config_array = $config['array'] ?? [];
  71. //生成下载文件
  72. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  73. return [true, [$config_array, $filename]];
  74. }
  75. private function monthPwOrder($data,$user){
  76. $config = $this->getTableConfig($data['type']);
  77. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  78. $config_array = $config['array'] ?? [];
  79. //生成下载文件
  80. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  81. return [true, [$config_array, $filename]];
  82. }
  83. private function monthDwOrder($data,$user){
  84. $config = $this->getTableConfig($data['type']);
  85. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  86. $config_array = $config['array'] ?? [];
  87. //生成下载文件
  88. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  89. return [true, [$config_array, $filename]];
  90. }
  91. private function monthPsOrder($data,$user){
  92. $config = $this->getTableConfig($data['type']);
  93. if(empty($config)) return [false, ['导入配置表头文件不存在','']];
  94. $config_array = $config['array'] ?? [];
  95. //生成下载文件
  96. $filename = $config['name'] . "导入模板_" . time() . '.' . 'xlsx';
  97. return [true, [$config_array, $filename]];
  98. }
  99. //导入入口
  100. public function importAll($data,$user){
  101. // //不超时
  102. // ini_set('max_execution_time', 0);
  103. // //内存设置
  104. // ini_set('memory_limit', -1);
  105. // $reader = IOFactory::createReader('Xlsx');
  106. // $reader->setReadDataOnly(true); // 只读取有数据的单元格
  107. // $spreadsheet = $reader->load($data['file']);
  108. // dd($spreadsheet);
  109. // // 创建一个Reader对象
  110. // $reader = IOFactory::createReader('Xlsx'); // 根据你的文件格式选择合适的reader
  111. //
  112. //// 加载Excel文件
  113. // $spreadsheet = $reader->load($data['file']);
  114. //
  115. //// 获取第一个工作表
  116. // $worksheet = $spreadsheet->getActiveSheet();
  117. //
  118. //// 获取总行数
  119. // $totalRows = $worksheet->getHighestRow();dd($totalRows);
  120. if(empty($data['type'])) return [false,'缺少导入类型,导入失败'];
  121. if(! in_array($data['type'],self::$type)) return [false,'导入类型不存在,导入失败'];
  122. if(empty($data['file'])) return [false,'导入文件不能为空'];
  123. try {
  124. $import = new ImportAll();
  125. //设置导入人id
  126. $import->setCrt($user['id']);
  127. $import->setUser($user);
  128. $import->setType($data['type']);
  129. $other = $data;
  130. unset($other['file']);
  131. $import->setOtherParam($other);
  132. //导入
  133. \Maatwebsite\Excel\Facades\Excel::import($import,$data['file']);
  134. if($import->getMsg()) {
  135. $bool = $import->getIsLongText();
  136. if($bool) {
  137. return [0, $import->getMsg()];
  138. }else{
  139. return [false, $import->getMsg()];
  140. }
  141. }
  142. }catch (\Throwable $exception) {
  143. return [false, $exception->getMessage() . ' (Code: ' . $exception->getCode() . ', Line: ' . $exception->getLine() . ')'];
  144. }
  145. return [true, ''];
  146. }
  147. // 设备 ------------------------------------
  148. public function deviceImport($array, $user, $other_param){
  149. $upload = $array[0];
  150. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  151. if(!$status) return [false, $msg];
  152. $table_config = $msg;
  153. unset($array[0]);
  154. if(empty($array)) return [false, '导入数据不能为空'];
  155. // 公共校验
  156. list($array, $error) = $this->checkCommon($array, $table_config);
  157. if(!empty($error)) return [0, $error];
  158. // 详细校验 (这里 $array 传引用,内部会转换日期)
  159. list($error, $update_map) = $this->deviceCheck($array, $user, $table_config);
  160. if(!empty($error)) return [0, $error];
  161. $time = time();
  162. $insert = [];
  163. $update = [];
  164. foreach ($array as $key => $value){
  165. $main_tmp = [];
  166. foreach ($value as $k => $val){
  167. if(!empty($table_config[$k]['is_main'])){
  168. $main_tmp[$table_config[$k]['key']] = $val;
  169. }
  170. }
  171. if(isset($update_map[$key])){
  172. // 存入待更新数组
  173. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  174. } else {
  175. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  176. $main_tmp['crt_id'] = $user['id'];
  177. $main_tmp['crt_time'] = $time;
  178. $insert[] = $main_tmp;
  179. }
  180. }
  181. DB::beginTransaction();
  182. try {
  183. // 1. 批量新增
  184. if(!empty($insert)){
  185. foreach(array_chunk($insert, 500) as $chunkInsert){
  186. Device::insert($chunkInsert);
  187. }
  188. }
  189. // 批量更新
  190. foreach (array_chunk($update, 100) as $chunk) {
  191. foreach ($chunk as $item) {
  192. $id = $item['id'];
  193. unset($item['id']);
  194. Device::where('id', $id)->update($item);
  195. }
  196. }
  197. DB::commit();
  198. } catch (\Exception $e) {
  199. DB::rollBack();
  200. return [false, "错误:" . $e->getMessage() . " 行:" . $e->getLine()];
  201. }
  202. return [true, ''];
  203. }
  204. private function deviceCheck(&$array, $user, $table_config)
  205. {
  206. // 动态获取关键列的索引
  207. $codeIdx = array_search('code', array_column($table_config, 'key'));
  208. $dateIdx = array_search('in_time', array_column($table_config, 'key'));
  209. $typeIdx = array_search('type', array_column($table_config, 'key'));
  210. $type2Idx = array_search('is_use', array_column($table_config, 'key'));
  211. $code_map = $this->getDeviceList($array, $user, $codeIdx);
  212. $errors = [];
  213. $update = [];
  214. $map_type = array_flip(Device::$type);
  215. $map_type_2 = array_flip(Device::Use);
  216. foreach ($array as $rowIndex => $value) {
  217. $displayLine = $rowIndex + 1;
  218. $valCode = $value[$codeIdx] ?? '';
  219. // 记录更新 ID
  220. if(isset($code_map[$valCode])){
  221. $update[$rowIndex] = $code_map[$valCode];
  222. }
  223. if(empty($map_type[$value[$typeIdx]])){
  224. $errors[] = "第{$displayLine}行固定资产类型错误";
  225. }else{
  226. $array[$rowIndex][$typeIdx] = $map_type[$value[$typeIdx]];
  227. }
  228. if(empty($map_type_2[$value[$type2Idx]])){
  229. $errors[] = "第{$displayLine}行是否启用错误";
  230. }else{
  231. $array[$rowIndex][$type2Idx] = $map_type_2[$value[$type2Idx]];
  232. }
  233. // 日期处理
  234. if($dateIdx !== false && !empty($value[$dateIdx])){
  235. list($status, $msg) = $this->convertExcelCellToDate($value[$dateIdx]);
  236. if(!$status) {
  237. $errors[] = "第{$displayLine}行日期格式错误";
  238. } else {
  239. $array[$rowIndex][$dateIdx] = $msg;
  240. }
  241. }
  242. }
  243. $error_string = "";
  244. if(! empty($errors)) $error_string = implode('|', $errors);
  245. return [$error_string, $update];
  246. }
  247. private function getDeviceList($array, $user, $index){
  248. //查找设备
  249. $codes = array_unique(array_filter(array_column($array,$index)));
  250. return Device::where('del_time', 0)
  251. ->where('top_depart_id', $user['top_depart_id'])
  252. ->whereIn('code', $codes)
  253. ->pluck('id','code')
  254. ->toArray();
  255. }
  256. // 项目 -----------------------------------
  257. public function itemImport($array, $user, $other_param)
  258. {
  259. $upload = $array[0];
  260. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  261. if (!$status) return [false, $msg];
  262. $table_config = $msg;
  263. unset($array[0]);
  264. if (empty($array)) return [false, '导入数据不能为空'];
  265. // 1. 公共校验 (必填、唯一性等)
  266. list($array, $error) = $this->checkCommon($array, $table_config);
  267. if (!empty($error)) return [0, $error];
  268. // 2. 业务详细校验 (获取更新映射及明细数据)
  269. list($error, $update_map, $detail_data_map) = $this->itemCheck($array, $user, $table_config);
  270. if (!empty($error)) return [0, $error];
  271. $time = time();
  272. $insert_data = [];
  273. $update_data = [];
  274. $all_detail_insert = [];
  275. $update_main_ids = [];
  276. // 3. 数据分拣
  277. foreach ($array as $key => $value) {
  278. $main_tmp = [];
  279. foreach ($value as $k => $val) {
  280. if (!empty($table_config[$k]['is_main'])) {
  281. $main_tmp[$table_config[$k]['key']] = $val;
  282. }
  283. }
  284. if (isset($update_map[$key])) {
  285. // 更新逻辑
  286. $itemId = $update_map[$key];
  287. $update_data[] = array_merge($main_tmp, ['id' => $itemId]);
  288. $update_main_ids[] = $itemId;
  289. // 收集明细 (后续统一插入)
  290. if (isset($detail_data_map[$key])) {
  291. foreach ($detail_data_map[$key] as $d) {
  292. $all_detail_insert[] = array_merge($d, ['item_id' => $itemId, 'crt_time' => $time, 'top_depart_id' => $user['top_depart_id']]);
  293. }
  294. }
  295. } else {
  296. // 新增逻辑
  297. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  298. $main_tmp['crt_id'] = $user['id'];
  299. $main_tmp['crt_time'] = $time;
  300. // 以 code 为键,方便后续回填 ID
  301. $insert_data[$main_tmp['code']] = $main_tmp;
  302. if (isset($detail_data_map[$key])) {
  303. foreach ($detail_data_map[$key] as $d) {
  304. $all_detail_insert[] = array_merge($d, ['_code' => $main_tmp['code'], 'crt_time' => $time, 'top_depart_id' => $main_tmp['top_depart_id']]);
  305. }
  306. }
  307. }
  308. }
  309. DB::beginTransaction();
  310. try {
  311. // 4. 执行新增主表
  312. if (!empty($insert_data)) {
  313. foreach (array_chunk($insert_data, 500) as $chunk) {
  314. Item::insert($chunk);
  315. }
  316. // 获取新插入数据的 ID 映射
  317. $new_item_maps = Item::whereIn('code', array_keys($insert_data))
  318. ->where('del_time', 0)
  319. ->where('top_depart_id', $user['top_depart_id'])
  320. ->pluck('id', 'code')->toArray();
  321. }
  322. // 5. 执行更新主表 (分批更新)
  323. if (!empty($update_data)) {
  324. foreach (array_chunk($update_data, 100) as $chunk) {
  325. foreach ($chunk as $uItem) {
  326. $id = $uItem['id'];
  327. unset($uItem['id']);
  328. Item::where('id', $id)->update($uItem);
  329. }
  330. }
  331. }
  332. // 6. 处理明细表 (先删后插策略)
  333. // 删除旧明细 (逻辑删除)
  334. if (!empty($update_main_ids)) {
  335. ItemDetails::whereIn('item_id', $update_main_ids)
  336. ->where('del_time', 0)
  337. ->update(['del_time' => $time]);
  338. }
  339. // 回填新增主表的 ID 到明细数组
  340. foreach ($all_detail_insert as &$di) {
  341. if (isset($di['_code'])) {
  342. $di['item_id'] = $new_item_maps[$di['_code']] ?? 0;
  343. unset($di['_code']);
  344. }
  345. }
  346. unset($di);
  347. // 批量插入所有明细
  348. if (!empty($all_detail_insert)) {
  349. foreach (array_chunk($all_detail_insert, 500) as $chunk) {
  350. ItemDetails::insert($chunk);
  351. }
  352. }
  353. DB::commit();
  354. } catch (\Exception $e) {
  355. DB::rollBack();
  356. return [false, "入库失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  357. }
  358. return [true, '导入成功'];
  359. }
  360. private function getItemList($array, $user, $index){
  361. //查找设备
  362. $codes = array_unique(array_filter(array_column($array,$index)));
  363. return Item::where('del_time', 0)
  364. ->where('top_depart_id', $user['top_depart_id'])
  365. ->whereIn('code', $codes)
  366. ->pluck('id','code')
  367. ->toArray();
  368. }
  369. private function itemCheck(&$array, $user, $table_config)
  370. {
  371. $keys = array_column($table_config, 'key');
  372. $codeIdx = array_search('code', $keys);
  373. $stateIdx = array_search('state', $keys);
  374. $manIdx = array_search('man_list', $keys);
  375. $deviceIdx = array_search('device_list', $keys);
  376. // 日期索引
  377. $dateIdx = array_search('start_time', $keys);
  378. $date2Idx = array_search('end_time', $keys);
  379. $code_map = $this->getItemList($array, $user, $codeIdx);
  380. list($man_map, $device_map) = $this->getDataList($array, $user, $manIdx, $deviceIdx);
  381. $errors = [];
  382. $update_mapping = [];
  383. $detail_storage = [];
  384. $state_type_map = array_flip(Item::State_Type);
  385. foreach ($array as $rowIndex => $rowValue) {
  386. $displayLine = $rowIndex + 1;
  387. $valCode = $rowValue[$codeIdx] ?? '';
  388. // 1. 判定更新还是新增
  389. if (isset($code_map[$valCode])) {
  390. $update_mapping[$rowIndex] = $code_map[$valCode];
  391. }
  392. // 2. 状态校验
  393. $state_text = $rowValue[$stateIdx] ?? '';
  394. if (!isset($state_type_map[$state_text])) {
  395. $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
  396. } else {
  397. $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
  398. }
  399. // 3. 日期转换
  400. foreach ([$dateIdx, $date2Idx] as $dIdx) {
  401. if ($dIdx !== false && !empty($rowValue[$dIdx])) {
  402. list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
  403. if (!$s) $errors[] = "第{$displayLine}行:日期格式非法";
  404. else $array[$rowIndex][$dIdx] = $m;
  405. }
  406. }
  407. // 4. 解析人员 (明细类型1)
  408. if ($manIdx !== false && !empty($rowValue[$manIdx])) {
  409. foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
  410. $mNum = trim($mNum);
  411. if (!isset($man_map[$mNum])) {
  412. $errors[] = "第{$displayLine}行:人员工号[{$mNum}]不存在";
  413. } else {
  414. $detail_storage[$rowIndex][] = [
  415. 'type' => ItemDetails::type_one,
  416. 'data_id' => $man_map[$mNum],
  417. ];
  418. }
  419. }
  420. }
  421. // 5. 解析设备 (明细类型2)
  422. if ($deviceIdx !== false && !empty($rowValue[$deviceIdx])) {
  423. foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
  424. $dCode = trim($dCode);
  425. if (!isset($device_map[$dCode])) {
  426. $errors[] = "第{$displayLine}行:设备的资产编码[{$dCode}]不存在";
  427. } else {
  428. $detail_storage[$rowIndex][] = [
  429. 'type' => ItemDetails::type_two,
  430. 'data_id' => $device_map[$dCode],
  431. ];
  432. }
  433. }
  434. }
  435. }
  436. $error_str = !empty($errors) ? implode('|', $errors) : "";
  437. return [$error_str, $update_mapping, $detail_storage];
  438. }
  439. private function getDataList($array, $user, $index1, $index2)
  440. {
  441. $manNums = [];
  442. $devCodes = [];
  443. // 去重收集
  444. foreach ($array as $row) {
  445. if (!empty($row[$index1])) {
  446. foreach (explode(',', $row[$index1]) as $v) $manNums[trim($v)] = true;
  447. }
  448. if (!empty($row[$index2])) {
  449. foreach (explode(',', $row[$index2]) as $v) $devCodes[trim($v)] = true;
  450. }
  451. }
  452. $manMap = Employee::where('del_time', 0)
  453. ->where('top_depart_id', $user['top_depart_id'])
  454. ->whereIn('number', array_keys($manNums))
  455. ->pluck('id', 'number')->toArray();
  456. $devMap = Device::where('del_time', 0)
  457. ->where('top_depart_id', $user['top_depart_id'])
  458. ->whereIn('code', array_keys($devCodes))
  459. ->pluck('id', 'code')->toArray();
  460. return [$manMap, $devMap];
  461. }
  462. // 费用 ----------------------------------
  463. public function feeImport($array, $user, $other_param)
  464. {
  465. $upload = $array[0];
  466. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  467. if (!$status) return [false, $msg];
  468. $table_config = $msg;
  469. unset($array[0]);
  470. if (empty($array)) return [false, '导入数据不能为空'];
  471. list($array, $error) = $this->checkCommon($array, $table_config);
  472. if (!empty($error)) return [0, $error];
  473. // 2. 详细校验
  474. list($error, $update_map, $parent_code_map) = $this->feeCheck($array, $user, $table_config);
  475. if (!empty($error)) return [0, $error];
  476. $time = time();
  477. $insert = [];
  478. $update = [];
  479. $all_codes = [];
  480. // --- 修正点 1: 必须确保索引提取准确 ---
  481. $keys = array_column($table_config, 'key');
  482. $codeIdx = array_search('code', $keys);
  483. // ------------------------------------
  484. foreach ($array as $key => $value) {
  485. $cCode = trim($value[$codeIdx] ?? '');
  486. if($cCode === '') continue;
  487. $all_codes[] = $cCode;
  488. $main_tmp = [];
  489. foreach ($value as $k => $val){
  490. if(!empty($table_config[$k]['is_main'])){
  491. if ($table_config[$k]['key'] !== 'parent_id') {
  492. $main_tmp[$table_config[$k]['key']] = $val;
  493. }
  494. }
  495. }
  496. if (isset($update_map[$key])) {
  497. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  498. } else {
  499. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  500. $main_tmp['crt_time'] = $time;
  501. $main_tmp['parent_id'] = 0;
  502. $insert[] = $main_tmp;
  503. }
  504. }
  505. DB::beginTransaction();
  506. try {
  507. if (!empty($insert)) {
  508. foreach (array_chunk($insert, 500) as $chunk) {
  509. Fee::insert($chunk);
  510. }
  511. }
  512. if (!empty($update)) {
  513. foreach ($update as $item) {
  514. $uId = $item['id']; unset($item['id']);
  515. Fee::where('id', $uId)->update($item);
  516. }
  517. }
  518. // --- 修正点 2: 核心回填逻辑 ---
  519. $newCodeToIdMap = Fee::where('del_time', 0)
  520. ->where('top_depart_id', $user['top_depart_id'])
  521. ->whereIn('code', array_unique($all_codes))
  522. ->pluck('id', 'code')
  523. ->toArray();
  524. foreach ($parent_code_map as $rowIndex => $pCode) {
  525. // 这里必须通过 $rowIndex 从原始 $array 中精准获取当前行的编码
  526. $currentCode = isset($array[$rowIndex][$codeIdx]) ? trim($array[$rowIndex][$codeIdx]) : '';
  527. $pCode = trim($pCode);
  528. if ($currentCode === '' || $pCode === '') continue;
  529. $currentId = $newCodeToIdMap[$currentCode] ?? null;
  530. $parentId = $newCodeToIdMap[$pCode] ?? null;
  531. // 严谨判断:只有当前记录存在,且上级记录也存在,且两者不是同一个 ID 时才更新
  532. if ($currentId && $parentId && $currentId != $parentId) {
  533. Fee::where('id', $currentId)->update(['parent_id' => $parentId]);
  534. }
  535. }
  536. DB::commit();
  537. } catch (\Exception $e) {
  538. DB::rollBack();
  539. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  540. }
  541. return [true, ''];
  542. }
  543. private function feeCheck(&$array, $user, $table_config)
  544. {
  545. $keys = array_column($table_config, 'key');
  546. $codeIdx = array_search('code', $keys);
  547. $parentIdx = array_search('parent_id', $keys);
  548. // 1. 获取基础数据
  549. list($dbFeeMap, $excelCodesMap) = $this->getFeeList($array, $user, $codeIdx);
  550. $errors = [];
  551. $update = [];
  552. $parent_code_map = [];
  553. // 2. 建立 Excel 内部父子关系映射(用于环路追溯)
  554. $currentExcelMap = [];
  555. foreach ($array as $row) {
  556. $c = trim($row[$codeIdx] ?? '');
  557. $p = trim($row[$parentIdx] ?? '');
  558. if ($c !== '') $currentExcelMap[$c] = $p;
  559. }
  560. // 3. 逐行校验
  561. foreach ($array as $rowIndex => $value) {
  562. $displayLine = $rowIndex + 1;
  563. $valCode = trim($value[$codeIdx] ?? '');
  564. $valParentCode = trim($value[$parentIdx] ?? '');
  565. if ($valCode === '') continue;
  566. // 更新状态记录
  567. if (isset($dbFeeMap[$valCode])) {
  568. $update[$rowIndex] = $dbFeeMap[$valCode]['id'];
  569. }
  570. if ($valParentCode !== '') {
  571. // --- A. 存在性校验 ---
  572. // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
  573. if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
  574. $errors[] = "第{$displayLine}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
  575. continue;
  576. }
  577. // --- B. 自引用校验 ---
  578. if ($valCode === $valParentCode) {
  579. $errors[] = "第{$displayLine}行:上级编码不能是自身";
  580. continue;
  581. }
  582. // --- C. 环路追溯 ---
  583. $visited = [];
  584. $res = $this->findLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
  585. if ($res !== false) {
  586. if ($res['type'] === 'LOOP_SELF') {
  587. $errors[] = "第{$displayLine}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
  588. } else {
  589. $errors[] = "第{$displayLine}行:上级[{$valParentCode}]的溯源链条已成环";
  590. }
  591. continue;
  592. }
  593. // 记录有效的父级关系
  594. $parent_code_map[$rowIndex] = $valParentCode;
  595. }
  596. }
  597. $error_string = !empty($errors) ? implode('|', $errors) : "";
  598. return [$error_string, $update, $parent_code_map];
  599. }
  600. private function getFeeList($array, $user, $index)
  601. {
  602. // 关键:一定要把 Excel 里所有的 code 这一列全部拿出来,并去除空值
  603. $codesInExcel = [];
  604. foreach ($array as $row) {
  605. $c = trim($row[$index] ?? '');
  606. if ($c !== '') {
  607. $codesInExcel[$c] = true;
  608. }
  609. }
  610. $allFees = Fee::where('del_time', 0)
  611. ->where('top_depart_id', $user['top_depart_id'])
  612. ->select('id', 'code', 'parent_id')
  613. ->get();
  614. $dbFeeMap = [];
  615. foreach ($allFees as $fee) {
  616. $dbFeeMap[$fee->code] = [
  617. 'id' => $fee->id,
  618. 'code' => $fee->code,
  619. 'parent_id' => $fee->parent_id
  620. ];
  621. }
  622. // 返回数据库映射和 Excel 编码映射
  623. return [$dbFeeMap, $codesInExcel];
  624. }
  625. private function findLoopInAncestors($startParentCode, $targetCode, $excelMap, $dbFeeMap, &$visited)
  626. {
  627. $current = $startParentCode;
  628. while ($current !== '' && $current !== 0) {
  629. if ($current === $targetCode) {
  630. return ['type' => 'LOOP_SELF', 'code' => $current];
  631. }
  632. if (isset($visited[$current])) {
  633. return ['type' => 'EXISTING_LOOP', 'code' => $current];
  634. }
  635. $visited[$current] = true;
  636. if (isset($excelMap[$current]) && $excelMap[$current] !== '') {
  637. $current = $excelMap[$current];
  638. } elseif (isset($dbFeeMap[$current])) {
  639. $parentId = $dbFeeMap[$current]['parent_id'];
  640. $parentCode = '';
  641. foreach ($dbFeeMap as $item) {
  642. if ($item['id'] == $parentId) {
  643. $parentCode = $item['code'];
  644. break;
  645. }
  646. }
  647. $current = $parentCode;
  648. } else {
  649. break;
  650. }
  651. }
  652. return false;
  653. }
  654. // 人员月度工时单 ------------------------------
  655. public function monthPwOrderImport($array, $user, $other_param)
  656. {
  657. $upload = $array[0];
  658. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  659. if (!$status) return [false, $msg];
  660. $table_config = $msg;
  661. unset($array[0]);
  662. if (empty($array)) return [false, '导入数据不能为空'];
  663. list($array, $error) = $this->checkCommon($array, $table_config);
  664. if (!empty($error)) return [0, $error];
  665. // 2. 详细校验 (这里会返回 update_map 和 dbEmps 映射)
  666. list($error, $update_map, $dbEmps) = $this->monthPwOrderCheck($array, $user, $table_config);
  667. if (!empty($error)) return [0, $error];
  668. $keys = array_column($table_config, 'key');
  669. $codeIdx = array_search('code', $keys);
  670. $monthIdx = array_search('month', $keys);
  671. $empIdx = array_search('employee_id', $keys);
  672. // --- 步骤 1: 数据聚合分组 ---
  673. // 将 Excel 数据按“单据”维度归类
  674. $groups = [];
  675. foreach ($array as $rowIndex => $row) {
  676. $cCode = trim($row[$codeIdx] ?? '');
  677. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  678. // 聚合 Key:有编码用编码,没编码用月份标记
  679. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  680. if (!isset($groups[$aggKey])) {
  681. $groups[$aggKey] = [
  682. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  683. 'code' => $cCode,
  684. 'month' => $cMonthTs,
  685. 'details' => []
  686. ];
  687. }
  688. // 准备详情行数据
  689. $detailTmp = [];
  690. foreach ($table_config as $k => $conf) {
  691. if (!$conf['is_main']) {
  692. $fieldKey = $conf['key'];
  693. $fieldVal = $row[$k];
  694. // 如果是人员,转换为 ID
  695. if ($fieldKey == 'employee_id') {
  696. $fieldVal = $dbEmps[$fieldVal] ?? 0;
  697. }
  698. $detailTmp[$fieldKey] = $fieldVal;
  699. }
  700. }
  701. $groups[$aggKey]['details'][] = $detailTmp;
  702. }
  703. // --- 步骤 2: 开启事务写入 ---
  704. DB::beginTransaction();
  705. try {
  706. $time = time();
  707. foreach ($groups as $aggKey => $group) {
  708. $mainId = $group['main_id'];
  709. if ($mainId > 0) {
  710. // 删除旧详情
  711. DB::table('monthly_pw_order_details')->where('del_time',0)
  712. ->where('main_id', $mainId)
  713. ->update(['del_time' => $time]);
  714. } else {
  715. // B. 新增逻辑
  716. $newCode = $this->generateBillNo([
  717. 'top_depart_id' => $user['top_depart_id'],
  718. 'type' => MonthlyPwOrder::Order_type,
  719. 'period' => date("Ym", $group['month'])
  720. ]);
  721. $mainId = DB::table('monthly_pw_order')->insertGetId([
  722. 'code' => $newCode,
  723. 'month' => $group['month'],
  724. 'top_depart_id' => $user['top_depart_id'],
  725. 'crt_id' => $user['id'],
  726. 'crt_time' => $time,
  727. 'upd_time' => $time,
  728. 'del_time' => 0
  729. ]);
  730. }
  731. // C. 批量插入详情
  732. $insertDetails = [];
  733. foreach ($group['details'] as $detail) {
  734. $detail['main_id'] = $mainId;
  735. $detail['top_depart_id']= $user['top_depart_id'];
  736. $detail['crt_time'] = $time;
  737. $detail['del_time'] = 0;
  738. $insertDetails[] = $detail;
  739. }
  740. if (!empty($insertDetails)) {
  741. DB::table('monthly_pw_order_details')->insert($insertDetails);
  742. }
  743. }
  744. DB::commit();
  745. } catch (\Exception $e) {
  746. DB::rollBack();
  747. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  748. }
  749. return [true, ''];
  750. }
  751. private function monthPwOrderCheck(&$array, $user, $table_config)
  752. {
  753. $keys = array_column($table_config, 'key');
  754. $codeIdx = array_search('code', $keys);
  755. $monthIdx = array_search('month', $keys);
  756. $empIdx = array_search('employee_id', $keys);
  757. $startIdx = array_search('start_time', $keys);
  758. $endIdx = array_search('end_time', $keys);
  759. $numIdx = array_search('total_days', $keys);
  760. $num2Idx = array_search('rd_total_days', $keys);
  761. $num3Idx = array_search('total_hours', $keys);
  762. $num4Idx = array_search('rd_total_hours', $keys);
  763. // 1. 预加载基础数据
  764. $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
  765. $dbEmps = Employee::where('del_time', 0)
  766. ->whereIn('number', $allEmpNumbers)
  767. ->where('top_depart_id', $user['top_depart_id'])
  768. ->pluck('id', 'number')->toArray();
  769. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  770. $dbOrders = DB::table('monthly_pw_order')
  771. ->whereIn('code', $allCodes)
  772. ->where('top_depart_id', $user['top_depart_id'])
  773. ->get()->keyBy('code');
  774. $existingMonths = DB::table('monthly_pw_order')
  775. ->where('top_depart_id', $user['top_depart_id'])
  776. ->pluck('month')
  777. ->toArray();
  778. $existingMonthsMap = array_fill_keys($existingMonths, true);
  779. $excelAggregator = [];
  780. $errors = [];
  781. $update_map = [];
  782. foreach ($array as $rowIndex => $row) {
  783. // 用户看到的行号(假设数据从第2行开始)
  784. $displayLine = $rowIndex + 1;
  785. $valCode = trim($row[$codeIdx] ?? '');
  786. $valMonthRaw = trim($row[$monthIdx] ?? '');
  787. $valEmp = trim($row[$empIdx] ?? '');
  788. $valStartRaw = $row[$startIdx] ?? '';
  789. $valEndRaw = $row[$endIdx] ?? '';
  790. $valNumRaw = $row[$numIdx] ?? 0;
  791. $valNum2Raw = $row[$num2Idx] ?? 0;
  792. $valNum3Raw = $row[$num3Idx] ?? 0;
  793. $valNum4Raw = $row[$num4Idx] ?? 0;
  794. // --- A. 日期转换校验 ---
  795. // 转换月份
  796. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  797. if (!$mStatus) {
  798. $errors[] = "第{$displayLine}行:月份格式错误({$valMonthRaw})";
  799. continue;
  800. }
  801. // 强制格式化为当月1号的时间戳,确保聚合逻辑一致
  802. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  803. // 转换开始日期
  804. list($sStatus, $startTime) = $this->convertExcelCellToDate($valStartRaw);
  805. if (!$sStatus) {
  806. $errors[] = "第{$displayLine}行:开始日期格式错误";
  807. continue;
  808. }
  809. // 转换结束日期
  810. list($eStatus, $endTime) = $this->convertExcelCellToDate($valEndRaw);
  811. if (!$eStatus) {
  812. $errors[] = "第{$displayLine}行:结束日期格式错误";
  813. continue;
  814. }
  815. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  816. // --- B. 校验单据与月份的一致性 ---
  817. if ($valCode && isset($dbOrders[$valCode])) {
  818. $dbOrder = $dbOrders[$valCode];
  819. if ($dbOrder->month != $valMonthTs) {
  820. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  821. }
  822. $update_map[$rowIndex] = $dbOrder->id;
  823. }
  824. // --- C. 校验人员存在性与单据内唯一性 ---
  825. if (!isset($dbEmps[$valEmp])) {
  826. $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
  827. } else {
  828. if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
  829. $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同一单据中重复";
  830. }
  831. $excelAggregator[$aggKey]['emps'][$valEmp] = true;
  832. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  833. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  834. }
  835. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  836. }
  837. // --- D. 逻辑校验:日期范围 ---
  838. if ($startTime > $endTime) {
  839. $errors[] = "第{$displayLine}行:开始日期大于结束日期";
  840. } else {
  841. $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
  842. if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
  843. $errors[] = "第{$displayLine}行:日期超出[ " . date('Y-m', $valMonthTs) . " ]范围";
  844. }
  845. }
  846. // --- E. 月份唯一单据校验 (新增) ---
  847. if (!$valCode) {
  848. if (isset($existingMonthsMap[$valMonthTs])) {
  849. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  850. }
  851. }
  852. // --- F. 数字校验
  853. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  854. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
  855. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  856. if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
  857. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  858. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
  859. $res = $this->checkNumber($valNum4Raw,2,'non-negative');
  860. if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
  861. // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
  862. $array[$rowIndex][$monthIdx] = $valMonthTs;
  863. $array[$rowIndex][$startIdx] = $startTime;
  864. $array[$rowIndex][$endIdx] = $endTime;
  865. }
  866. $error_string = !empty($errors) ? implode('|', $errors) : "";
  867. return [$error_string, $update_map, $dbEmps];
  868. }
  869. // 设备月度工时单
  870. public function monthDwOrderImport($array, $user, $other_param)
  871. {
  872. $upload = $array[0];
  873. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  874. if (!$status) return [false, $msg];
  875. $table_config = $msg;
  876. unset($array[0]);
  877. if (empty($array)) return [false, '导入数据不能为空'];
  878. list($array, $error) = $this->checkCommon($array, $table_config);
  879. if (!empty($error)) return [0, $error];
  880. // 2. 详细校验 (这里会返回 update_map 和 dbDevs 映射)
  881. list($error, $update_map, $dbDevs) = $this->monthDwOrderCheck($array, $user, $table_config);
  882. if (!empty($error)) return [0, $error];
  883. $keys = array_column($table_config, 'key');
  884. $codeIdx = array_search('code', $keys);
  885. $monthIdx = array_search('month', $keys);
  886. $empIdx = array_search('device_id', $keys);
  887. // --- 步骤 1: 数据聚合分组 ---
  888. // 将 Excel 数据按“单据”维度归类
  889. $groups = [];
  890. foreach ($array as $rowIndex => $row) {
  891. $cCode = trim($row[$codeIdx] ?? '');
  892. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  893. // 聚合 Key:有编码用编码,没编码用月份标记
  894. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  895. if (!isset($groups[$aggKey])) {
  896. $groups[$aggKey] = [
  897. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  898. 'code' => $cCode,
  899. 'month' => $cMonthTs,
  900. 'details' => []
  901. ];
  902. }
  903. // 准备详情行数据
  904. $detailTmp = [];
  905. foreach ($table_config as $k => $conf) {
  906. if (!$conf['is_main']) {
  907. $fieldKey = $conf['key'];
  908. $fieldVal = $row[$k];
  909. // 如果是人员,转换为 ID
  910. if ($fieldKey == 'device_id') {
  911. $fieldVal = $dbDevs[$fieldVal] ?? 0;
  912. }
  913. $detailTmp[$fieldKey] = $fieldVal;
  914. }
  915. }
  916. $groups[$aggKey]['details'][] = $detailTmp;
  917. }
  918. // --- 步骤 2: 开启事务写入 ---
  919. DB::beginTransaction();
  920. try {
  921. $time = time();
  922. foreach ($groups as $aggKey => $group) {
  923. $mainId = $group['main_id'];
  924. if ($mainId > 0) {
  925. // 删除旧详情
  926. DB::table('monthly_dw_order_details')->where('del_time',0)
  927. ->where('main_id', $mainId)
  928. ->update(['del_time' => $time]);
  929. } else {
  930. // B. 新增逻辑
  931. $newCode = $this->generateBillNo([
  932. 'top_depart_id' => $user['top_depart_id'],
  933. 'type' => MonthlyPwOrder::Order_type,
  934. 'period' => date("Ym", $group['month'])
  935. ]);
  936. $mainId = DB::table('monthly_dw_order')->insertGetId([
  937. 'code' => $newCode,
  938. 'month' => $group['month'],
  939. 'top_depart_id' => $user['top_depart_id'],
  940. 'crt_id' => $user['id'],
  941. 'crt_time' => $time,
  942. 'upd_time' => $time,
  943. 'del_time' => 0
  944. ]);
  945. }
  946. // C. 批量插入详情
  947. $insertDetails = [];
  948. foreach ($group['details'] as $detail) {
  949. $detail['main_id'] = $mainId;
  950. $detail['top_depart_id']= $user['top_depart_id'];
  951. $detail['crt_time'] = $time;
  952. $detail['del_time'] = 0;
  953. $insertDetails[] = $detail;
  954. }
  955. if (!empty($insertDetails)) {
  956. DB::table('monthly_dw_order_details')->insert($insertDetails);
  957. }
  958. }
  959. DB::commit();
  960. } catch (\Exception $e) {
  961. DB::rollBack();
  962. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  963. }
  964. return [true, ''];
  965. }
  966. private function monthDwOrderCheck(&$array, $user, $table_config)
  967. {
  968. $keys = array_column($table_config, 'key');
  969. $codeIdx = array_search('code', $keys);
  970. $monthIdx = array_search('month', $keys);
  971. $devIdx = array_search('device_id', $keys);
  972. $startIdx = array_search('start_time', $keys);
  973. $endIdx = array_search('end_time', $keys);
  974. $numIdx = array_search('total_days', $keys);
  975. $num2Idx = array_search('rd_total_days', $keys);
  976. $num3Idx = array_search('total_hours', $keys);
  977. $num4Idx = array_search('rd_total_hours', $keys);
  978. // 1. 预加载基础数据
  979. $allEmpNumbers = array_filter(array_unique(array_column($array, $devIdx)));
  980. $dbDevs = Device::where('del_time', 0)
  981. ->whereIn('code', $allEmpNumbers)
  982. ->where('top_depart_id', $user['top_depart_id'])
  983. ->pluck('id', 'code')->toArray();
  984. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  985. $dbOrders = DB::table('monthly_dw_order')
  986. ->whereIn('code', $allCodes)
  987. ->where('top_depart_id', $user['top_depart_id'])
  988. ->get()->keyBy('code');
  989. $existingMonths = DB::table('monthly_dw_order')
  990. ->where('top_depart_id', $user['top_depart_id'])
  991. ->pluck('month')
  992. ->toArray();
  993. $existingMonthsMap = array_fill_keys($existingMonths, true);
  994. $excelAggregator = [];
  995. $errors = [];
  996. $update_map = [];
  997. foreach ($array as $rowIndex => $row) {
  998. // 用户看到的行号(假设数据从第2行开始)
  999. $displayLine = $rowIndex + 1;
  1000. $valCode = trim($row[$codeIdx] ?? '');
  1001. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1002. $valDev = trim($row[$devIdx] ?? '');
  1003. $valStartRaw = $row[$startIdx] ?? '';
  1004. $valEndRaw = $row[$endIdx] ?? '';
  1005. $valNumRaw = $row[$numIdx] ?? 0;
  1006. $valNum2Raw = $row[$num2Idx] ?? 0;
  1007. $valNum3Raw = $row[$num3Idx] ?? 0;
  1008. $valNum4Raw = $row[$num4Idx] ?? 0;
  1009. // --- A. 日期转换校验 ---
  1010. // 转换月份
  1011. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1012. if (!$mStatus) {
  1013. $errors[] = "第{$displayLine}行:月份格式错误({$valMonthRaw})";
  1014. continue;
  1015. }
  1016. // 强制格式化为当月1号的时间戳,确保聚合逻辑一致
  1017. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1018. // 转换开始日期
  1019. list($sStatus, $startTime) = $this->convertExcelCellToDate($valStartRaw);
  1020. if (!$sStatus) {
  1021. $errors[] = "第{$displayLine}行:开始日期格式错误";
  1022. continue;
  1023. }
  1024. // 转换结束日期
  1025. list($eStatus, $endTime) = $this->convertExcelCellToDate($valEndRaw);
  1026. if (!$eStatus) {
  1027. $errors[] = "第{$displayLine}行:结束日期格式错误";
  1028. continue;
  1029. }
  1030. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1031. // --- B. 校验单据与月份的一致性 ---
  1032. if ($valCode && isset($dbOrders[$valCode])) {
  1033. $dbOrder = $dbOrders[$valCode];
  1034. if ($dbOrder->month != $valMonthTs) {
  1035. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1036. }
  1037. $update_map[$rowIndex] = $dbOrder->id;
  1038. }
  1039. // --- C. 校验设备存在性与单据内唯一性 ---
  1040. if (!isset($dbDevs[$valDev])) {
  1041. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]不存在";
  1042. } else {
  1043. if (isset($excelAggregator[$aggKey]['devs'][$valDev])) {
  1044. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]在同一单据中重复";
  1045. }
  1046. $excelAggregator[$aggKey]['devs'][$valDev] = true;
  1047. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1048. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1049. }
  1050. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1051. }
  1052. // --- D. 逻辑校验:日期范围 ---
  1053. if ($startTime > $endTime) {
  1054. $errors[] = "第{$displayLine}行:开始日期大于结束日期";
  1055. } else {
  1056. $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
  1057. if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
  1058. $errors[] = "第{$displayLine}行:日期超出[ " . date('Y-m', $valMonthTs) . " ]范围";
  1059. }
  1060. }
  1061. // --- E. 月份唯一单据校验 (新增) ---
  1062. if (!$valCode) {
  1063. if (isset($existingMonthsMap[$valMonthTs])) {
  1064. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1065. }
  1066. }
  1067. // --- F. 数字校验
  1068. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  1069. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
  1070. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  1071. if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
  1072. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  1073. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
  1074. $res = $this->checkNumber($valNum4Raw,2,'non-negative');
  1075. if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
  1076. // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
  1077. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1078. $array[$rowIndex][$startIdx] = $startTime;
  1079. $array[$rowIndex][$endIdx] = $endTime;
  1080. }
  1081. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1082. return [$error_string, $update_map, $dbDevs];
  1083. }
  1084. // 人员月度工资单
  1085. public function monthPsOrderImport($array, $user, $other_param)
  1086. {
  1087. $upload = $array[0];
  1088. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1089. if (!$status) return [false, $msg];
  1090. $table_config = $msg;
  1091. unset($array[0]);
  1092. if (empty($array)) return [false, '导入数据不能为空'];
  1093. list($array, $error) = $this->checkCommon($array, $table_config);
  1094. if (!empty($error)) return [0, $error];
  1095. // 2. 详细校验 (这里会返回 update_map 和 dbEmps 映射)
  1096. list($error, $update_map, $dbEmps) = $this->monthPsOrderCheck($array, $user, $table_config);
  1097. if (!empty($error)) return [0, $error];
  1098. $keys = array_column($table_config, 'key');
  1099. $codeIdx = array_search('code', $keys);
  1100. $monthIdx = array_search('month', $keys);
  1101. $empIdx = array_search('employee_id', $keys);
  1102. // --- 步骤 1: 数据聚合分组 ---
  1103. // 将 Excel 数据按“单据”维度归类
  1104. $groups = [];
  1105. foreach ($array as $rowIndex => $row) {
  1106. $cCode = trim($row[$codeIdx] ?? '');
  1107. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  1108. // 聚合 Key:有编码用编码,没编码用月份标记
  1109. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  1110. if (!isset($groups[$aggKey])) {
  1111. $groups[$aggKey] = [
  1112. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  1113. 'code' => $cCode,
  1114. 'month' => $cMonthTs,
  1115. 'details' => []
  1116. ];
  1117. }
  1118. // 准备详情行数据
  1119. $detailTmp = [];
  1120. foreach ($table_config as $k => $conf) {
  1121. if (!$conf['is_main']) {
  1122. $fieldKey = $conf['key'];
  1123. $fieldVal = $row[$k];
  1124. // 如果是人员,转换为 ID
  1125. if ($fieldKey == 'employee_id') {
  1126. $fieldVal = $dbEmps[$fieldVal] ?? 0;
  1127. }
  1128. $detailTmp[$fieldKey] = $fieldVal;
  1129. }
  1130. }
  1131. $groups[$aggKey]['details'][] = $detailTmp;
  1132. }
  1133. // --- 步骤 2: 开启事务写入 ---
  1134. DB::beginTransaction();
  1135. try {
  1136. $time = time();
  1137. foreach ($groups as $aggKey => $group) {
  1138. $mainId = $group['main_id'];
  1139. if ($mainId > 0) {
  1140. // 删除旧详情
  1141. DB::table('monthly_ps_order_details')->where('del_time',0)
  1142. ->where('main_id', $mainId)
  1143. ->update(['del_time' => $time]);
  1144. } else {
  1145. // B. 新增逻辑
  1146. $newCode = $this->generateBillNo([
  1147. 'top_depart_id' => $user['top_depart_id'],
  1148. 'type' => MonthlyPsOrder::Order_type,
  1149. 'period' => date("Ym", $group['month'])
  1150. ]);
  1151. $mainId = DB::table('monthly_ps_order')->insertGetId([
  1152. 'code' => $newCode,
  1153. 'month' => $group['month'],
  1154. 'top_depart_id' => $user['top_depart_id'],
  1155. 'crt_id' => $user['id'],
  1156. 'crt_time' => $time,
  1157. 'upd_time' => $time,
  1158. 'del_time' => 0
  1159. ]);
  1160. }
  1161. // C. 批量插入详情
  1162. $insertDetails = [];
  1163. foreach ($group['details'] as $detail) {
  1164. $detail['main_id'] = $mainId;
  1165. $detail['top_depart_id']= $user['top_depart_id'];
  1166. $detail['crt_time'] = $time;
  1167. $detail['del_time'] = 0;
  1168. $insertDetails[] = $detail;
  1169. }
  1170. if (!empty($insertDetails)) {
  1171. DB::table('monthly_ps_order_details')->insert($insertDetails);
  1172. }
  1173. }
  1174. DB::commit();
  1175. } catch (\Exception $e) {
  1176. DB::rollBack();
  1177. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  1178. }
  1179. return [true, ''];
  1180. }
  1181. private function monthPsOrderCheck(&$array, $user, $table_config)
  1182. {
  1183. $keys = array_column($table_config, 'key');
  1184. $codeIdx = array_search('code', $keys);
  1185. $monthIdx = array_search('month', $keys);
  1186. $empIdx = array_search('employee_id', $keys);
  1187. $numIdx = array_search('salary', $keys);
  1188. $num2Idx = array_search('social_insurance', $keys);
  1189. $num3Idx = array_search('public_housing_fund', $keys);
  1190. // 1. 预加载基础数据
  1191. $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
  1192. $dbEmps = Employee::where('del_time', 0)
  1193. ->whereIn('number', $allEmpNumbers)
  1194. ->where('top_depart_id', $user['top_depart_id'])
  1195. ->pluck('id', 'number')->toArray();
  1196. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  1197. $dbOrders = DB::table('monthly_ps_order')
  1198. ->whereIn('code', $allCodes)
  1199. ->where('top_depart_id', $user['top_depart_id'])
  1200. ->get()->keyBy('code');
  1201. $existingMonths = DB::table('monthly_ps_order')
  1202. ->where('top_depart_id', $user['top_depart_id'])
  1203. ->pluck('month')
  1204. ->toArray();
  1205. $existingMonthsMap = array_fill_keys($existingMonths, true);
  1206. $excelAggregator = [];
  1207. $errors = [];
  1208. $update_map = [];
  1209. foreach ($array as $rowIndex => $row) {
  1210. // 用户看到的行号(假设数据从第2行开始)
  1211. $displayLine = $rowIndex + 1;
  1212. $valCode = trim($row[$codeIdx] ?? '');
  1213. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1214. $valEmp = trim($row[$empIdx] ?? '');
  1215. $valNumRaw = $row[$numIdx] ?? 0;
  1216. $valNum2Raw = $row[$num2Idx] ?? 0;
  1217. $valNum3Raw = $row[$num3Idx] ?? 0;
  1218. // --- A. 日期转换校验 ---
  1219. // 转换月份
  1220. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1221. if (!$mStatus) {
  1222. $errors[] = "第{$displayLine}行:月份格式错误({$valMonthRaw})";
  1223. continue;
  1224. }
  1225. // 强制格式化为当月1号的时间戳,确保聚合逻辑一致
  1226. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1227. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1228. // --- B. 校验单据与月份的一致性 ---
  1229. if ($valCode && isset($dbOrders[$valCode])) {
  1230. $dbOrder = $dbOrders[$valCode];
  1231. if ($dbOrder->month != $valMonthTs) {
  1232. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1233. }
  1234. $update_map[$rowIndex] = $dbOrder->id;
  1235. }
  1236. // --- C. 校验人员存在性与单据内唯一性 ---
  1237. if (!isset($dbEmps[$valEmp])) {
  1238. $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
  1239. } else {
  1240. if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
  1241. $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同一单据中重复";
  1242. }
  1243. $excelAggregator[$aggKey]['emps'][$valEmp] = true;
  1244. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1245. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1246. }
  1247. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1248. }
  1249. // --- E. 月份唯一单据校验 (新增) ---
  1250. if (!$valCode) {
  1251. if (isset($existingMonthsMap[$valMonthTs])) {
  1252. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1253. }
  1254. }
  1255. // --- F. 数字校验
  1256. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  1257. if(! $res['valid']) $errors[] = "第{$displayLine}行工资总额:" . $res['error'];
  1258. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  1259. if(! $res['valid']) $errors[] = "第{$displayLine}行社保:" . $res['error'];
  1260. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  1261. if(! $res['valid']) $errors[] = "第{$displayLine}行公积金:" . $res['error'];
  1262. // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
  1263. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1264. }
  1265. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1266. return [$error_string, $update_map, $dbEmps];
  1267. }
  1268. //公共校验 -----------------------------------------
  1269. private function checkCommon($array, $table_config) {
  1270. $error = [];
  1271. $uniqueCheck = []; // 格式:[$column_index => [$value => $first_line]]
  1272. foreach ($array as $line => $row) {
  1273. $rowData = array_filter($row);
  1274. if (empty($rowData)) {
  1275. unset($array[$line]);
  1276. continue;
  1277. }
  1278. foreach ($row as $colIndex => $value) {
  1279. $value = trim($value);
  1280. $config = $table_config[$colIndex] ?? null;
  1281. // 1. 基础存在性检查
  1282. if (!$config) {
  1283. $error[] = "第{$line}行第{$colIndex}列配置不存在";
  1284. continue;
  1285. }
  1286. $fieldName = $config['value'];
  1287. // 2. 必填校验
  1288. if (!empty($config['required']) && ($value === '' || !isset($value))) {
  1289. $error[] = "第{$line}行:[{$fieldName}] 必填";
  1290. }
  1291. // 3. 默认值填充
  1292. if ($value === '' && isset($config['default'])) {
  1293. $value = $config['default'];
  1294. }
  1295. // 4. 唯一性校验(重点:一次遍历解决)
  1296. if (!empty($config['unique']) && $value !== '') {
  1297. if (isset($uniqueCheck[$colIndex][$value])) {
  1298. $prevLine = $uniqueCheck[$colIndex][$value];
  1299. $error[] = "第{$line}行:[{$fieldName}] 与第{$prevLine}行重复,重复值:{$value}";
  1300. } else {
  1301. // 记录该值第一次出现的位置
  1302. $uniqueCheck[$colIndex][$value] = $line;
  1303. }
  1304. }
  1305. $row[$colIndex] = $value;
  1306. }
  1307. $array[$line] = $row;
  1308. }
  1309. $error_string = !empty($error) ? implode('|', $error) : "";
  1310. return [$array, $error_string];
  1311. }
  1312. //模板校验 -----------------------------------------
  1313. private function compareTableAndReturn($upload, $param){
  1314. if(empty($upload)) return [false, '表头不能为空'];
  1315. $config = $this->getTableConfig($param['type']);
  1316. $config_array = $config['array'];
  1317. if(empty($config_array)) return [false, '导入配置表头文件不存在'];
  1318. foreach ($config_array as $key => $value){
  1319. $key_position = $key + 1;
  1320. if(! isset($upload[$key])) return [false, "第" . $key_position . "列表头缺失"];
  1321. $tmp_v = trim($upload[$key]);
  1322. if($tmp_v != $value['value']) return [false, "第" . $key_position . "列表头与模板不符合,请重新下载模板"];
  1323. }
  1324. return [true, $config_array];
  1325. }
  1326. //转换日期 ------------------------------------------
  1327. function convertExcelCellToDate($cellValue) {
  1328. // 尝试将单元格值转换为浮点数(Excel 日期序列号)
  1329. $excelTimestamp = filter_var($cellValue, FILTER_VALIDATE_FLOAT);
  1330. if ($excelTimestamp !== false && $excelTimestamp > 0) {
  1331. // 如果成功转换并且值大于0,则认为是Excel日期序列号
  1332. try {
  1333. $dateTimeObject = Date::excelToDateTimeObject($cellValue);
  1334. // if ($dateTimeObject->format('H:i:s') === '00:00:00') {
  1335. // // 如果是,则将时间设置为 '23:59:00'
  1336. // $dateTimeObject->setTime(23, 59);
  1337. // }
  1338. // 现在你可以格式化这个日期了
  1339. $formattedDate = $dateTimeObject->format('Y-m-d');
  1340. if(! strtotime($formattedDate)) return [false, ''];
  1341. return [true, strtotime($formattedDate)];
  1342. } catch (\Exception $e) {
  1343. // 处理转换失败的情况
  1344. return [false, '单元格日期格式转换时间戳失败'];
  1345. }
  1346. }
  1347. // 如果不是有效的浮点数,则尝试按照多种日期格式解析
  1348. if(! strtotime($cellValue)) return [false, '单元格文本格式转换时间戳失败'];
  1349. return [true, strtotime($cellValue)];
  1350. }
  1351. }