ImportService.php 98 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505
  1. <?php
  2. namespace App\Service;
  3. use App\Exports\TableHeadExport;
  4. use App\Import\ImportAll;
  5. use App\Model\Depart;
  6. use App\Model\Device;
  7. use App\Model\Employee;
  8. use App\Model\EmployeeDepartPermission;
  9. use App\Model\Fee;
  10. use App\Model\Item;
  11. use App\Model\ItemDetails;
  12. use App\Model\MonthlyDdOrder;
  13. use App\Model\MonthlyPsOrder;
  14. use App\Model\MonthlyPwOrder;
  15. use App\Model\RuleSet;
  16. use App\Model\RuleSetDetails;
  17. use Illuminate\Support\Facades\DB;
  18. use Maatwebsite\Excel\Facades\Excel;
  19. use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
  20. use PhpOffice\PhpSpreadsheet\IOFactory;
  21. use PhpOffice\PhpSpreadsheet\Shared\Date;
  22. class ImportService extends Service
  23. {
  24. public static $type = [
  25. 'depart', // 部门
  26. 'employee', // 用户
  27. 'device', // 设备
  28. 'item', // 项目
  29. 'fee', // 费用
  30. 'monthPwOrder', // 人员月度研发工时单
  31. 'monthDwOrder', // 设备月度研发工时单
  32. 'monthPsOrder', // 人员月度工资单
  33. 'monthDdOrder', // 设备月度折旧单
  34. 'ruleSet', // 规则配置单
  35. ];
  36. public function getTableTitleXls($data,$user){
  37. if(empty($data['type'])) return [false,'缺少类型'];
  38. //获取配置文件
  39. // $fuc = $data['type'];
  40. // if (! method_exists(self::class, $fuc)) return [false, "导入文件方法获取不存在,请联系开发"];
  41. list($status,$return) = $this->title($data,$user);
  42. list($msg,$filename) = $return;
  43. if(! $status) return [false, $msg];
  44. $headers = array_column($msg,'value');
  45. $comments = $enums = [];
  46. foreach ($msg as $value){
  47. if(! empty($value['comments'])) $comments[$value['value']] = $value['comments'];
  48. if(! empty($value['enums'])) $enums[$value['value']] = $value['enums'];
  49. }
  50. Excel::store(new TableHeadExport([], $headers, $comments, $enums),"/public/export/{$filename}", null, 'Xlsx', []);
  51. return [true, ['file' => $filename]];
  52. }
  53. private function getTableConfig($type = ""){
  54. if(empty($type)) return [];
  55. //获取配置文件
  56. $config = "excel." . $type;
  57. return config($config) ?? [];
  58. }
  59. private function title($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. //导入入口
  68. public function importAll($data,$user){
  69. // //不超时
  70. // ini_set('max_execution_time', 0);
  71. // //内存设置
  72. // ini_set('memory_limit', -1);
  73. // $reader = IOFactory::createReader('Xlsx');
  74. // $reader->setReadDataOnly(true); // 只读取有数据的单元格
  75. // $spreadsheet = $reader->load($data['file']);
  76. // dd($spreadsheet);
  77. // // 创建一个Reader对象
  78. // $reader = IOFactory::createReader('Xlsx'); // 根据你的文件格式选择合适的reader
  79. //
  80. //// 加载Excel文件
  81. // $spreadsheet = $reader->load($data['file']);
  82. //
  83. //// 获取第一个工作表
  84. // $worksheet = $spreadsheet->getActiveSheet();
  85. //
  86. //// 获取总行数
  87. // $totalRows = $worksheet->getHighestRow();dd($totalRows);
  88. if(empty($data['type'])) return [false,'缺少导入类型,导入失败'];
  89. if(! in_array($data['type'],self::$type)) return [false,'导入类型不存在,导入失败'];
  90. if(empty($data['file'])) return [false,'导入文件不能为空'];
  91. try {
  92. $import = new ImportAll();
  93. //设置导入人id
  94. $import->setCrt($user['id']);
  95. $import->setUser($user);
  96. $import->setType($data['type']);
  97. $other = $data;
  98. unset($other['file']);
  99. $import->setOtherParam($other);
  100. //导入
  101. \Maatwebsite\Excel\Facades\Excel::import($import,$data['file']);
  102. if($import->getMsg()) {
  103. $bool = $import->getIsLongText();
  104. if($bool) {
  105. return [0, $import->getMsg()];
  106. }else{
  107. return [false, $import->getMsg()];
  108. }
  109. }
  110. }catch (\Throwable $exception) {
  111. return [false, $exception->getMessage() . ' (Code: ' . $exception->getCode() . ', Line: ' . $exception->getLine() . ')'];
  112. }
  113. return [true, ''];
  114. }
  115. // 项目 -----------------------------------
  116. public function employeeImport($array, $user, $other_param)
  117. {
  118. $upload = $array[0];
  119. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  120. if (!$status) return [false, $msg];
  121. $table_config = $msg;
  122. unset($array[0]);
  123. if (empty($array)) return [false, '导入数据不能为空'];
  124. // 1. 公共校验 (必填、唯一性等)
  125. list($array, $error) = $this->checkCommon($array, $table_config);
  126. if (!empty($error)) return [0, $error];
  127. // 2. 业务详细校验 (获取更新映射及明细数据)
  128. list($error, $update_map, $detail_data_map) = $this->employeeCheck($array, $user, $table_config);
  129. if (!empty($error)) return [0, $error];
  130. $time = time();
  131. $insert_data = [];
  132. $update_data = [];
  133. $all_detail_insert = [];
  134. $update_main_ids = [];
  135. // 3. 数据分拣
  136. foreach ($array as $key => $value) {
  137. $main_tmp = [];
  138. foreach ($value as $k => $val) {
  139. if (!empty($table_config[$k]['is_main'])) {
  140. $main_tmp[$table_config[$k]['key']] = $val;
  141. }
  142. }
  143. if (isset($update_map[$key])) {
  144. // 更新逻辑
  145. $empId = $update_map[$key];
  146. $update_data[] = array_merge($main_tmp, ['id' => $empId]);
  147. $update_main_ids[] = $empId;
  148. // 收集明细 (后续统一插入)
  149. if (isset($detail_data_map[$key])) {
  150. foreach ($detail_data_map[$key] as $d) {
  151. $all_detail_insert[] = array_merge($d, ['employee_id' => $empId, 'top_depart_id' => $user['top_depart_id']]);
  152. }
  153. }
  154. } else {
  155. // 新增逻辑
  156. $main_tmp['account'] = $user['top_depart_code'] . "_" . $main_tmp['number'];
  157. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  158. $main_tmp['crt_id'] = $user['id'];
  159. $main_tmp['crt_time'] = $time;
  160. // 以 number 为键,方便后续回填 ID
  161. $insert_data[$main_tmp['number']] = $main_tmp;
  162. if (isset($detail_data_map[$key])) {
  163. foreach ($detail_data_map[$key] as $d) {
  164. $all_detail_insert[] = array_merge($d, ['_number' => $main_tmp['number'], 'top_depart_id' => $main_tmp['top_depart_id']]);
  165. }
  166. }
  167. }
  168. }
  169. DB::beginTransaction();
  170. try {
  171. // 4. 执行新增主表
  172. if (!empty($insert_data)) {
  173. foreach (array_chunk($insert_data, 500) as $chunk) {
  174. Employee::insert($chunk);
  175. }
  176. // 获取新插入数据的 ID 映射
  177. $new_item_maps = Employee::whereIn('number', array_keys($insert_data))
  178. ->where('del_time', 0)
  179. ->where('top_depart_id', $user['top_depart_id'])
  180. ->pluck('id', 'number')->toArray();
  181. }
  182. // 5. 执行更新主表 (分批更新)
  183. if (!empty($update_data)) {
  184. foreach (array_chunk($update_data, 100) as $chunk) {
  185. foreach ($chunk as $uItem) {
  186. $id = $uItem['id'];
  187. unset($uItem['id']);
  188. Employee::where('id', $id)->update($uItem);
  189. }
  190. }
  191. }
  192. // 6. 处理明细表 (先删后插策略)
  193. // 删除旧明细 (逻辑删除)
  194. if (!empty($update_main_ids)) EmployeeDepartPermission::whereIn('employee_id', $update_main_ids)->delete();
  195. // 回填新增主表的 ID 到明细数组
  196. foreach ($all_detail_insert as &$di) {
  197. if (isset($di['_number'])) {
  198. $di['employee_id'] = $new_item_maps[$di['_number']] ?? 0;
  199. unset($di['_number']);
  200. }
  201. }
  202. unset($di);
  203. // 批量插入所有明细
  204. if (!empty($all_detail_insert)) {
  205. foreach (array_chunk($all_detail_insert, 500) as $chunk) {
  206. EmployeeDepartPermission::insert($chunk);
  207. }
  208. }
  209. DB::commit();
  210. } catch (\Exception $e) {
  211. DB::rollBack();
  212. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  213. }
  214. return [true, ''];
  215. }
  216. private function getEmployeeList($array, $user, $index){
  217. //查找设备
  218. $codes = array_unique(array_filter(array_column($array,$index)));
  219. return Employee::where('del_time', 0)
  220. ->where('top_depart_id', $user['top_depart_id'])
  221. ->whereIn('number', $codes)
  222. ->pluck('id','number')
  223. ->toArray();
  224. }
  225. private function employeeCheck(&$array, $user, $table_config)
  226. {
  227. $keys = array_column($table_config, 'key');
  228. $codeIdx = array_search('number', $keys);
  229. $sexIdx = array_search('sex', $keys);
  230. $eductionIdx = array_search('education', $keys);
  231. $stateIdx = array_search('state', $keys);
  232. $depIdx = array_search('depart_list', $keys);
  233. $code_map = $this->getEmployeeList($array, $user, $codeIdx);
  234. $dep_map = $this->getEDataList($array, $user, $depIdx);
  235. $errors = [];
  236. $update_mapping = [];
  237. $detail_storage = [];
  238. $sex_map = array_flip(Employee::SEX_TYPE);
  239. $e_map = array_flip(Employee::Education);
  240. $state_map = array_flip(Employee::State_Type);
  241. foreach ($array as $rowIndex => $rowValue) {
  242. $displayLine = $rowIndex + 1;
  243. $valCode = $rowValue[$codeIdx] ?? '';
  244. // 1. 判定更新还是新增
  245. if (isset($code_map[$valCode])) {
  246. $update_mapping[$rowIndex] = $code_map[$valCode];
  247. }
  248. // 2. 校验
  249. $sex_text = $rowValue[$sexIdx] ?? '';
  250. if(! empty($sex_text)){
  251. if (!isset($sex_map[$sex_text])) {
  252. $errors[] = "第{$displayLine}行:性别[{$sex_text}]无效";
  253. } else {
  254. $array[$rowIndex][$sexIdx] = $sex_map[$sex_text];
  255. }
  256. }
  257. $e_text = $rowValue[$eductionIdx] ?? '';
  258. if(! empty($e_text)){
  259. if (!isset($e_map[$e_text])) {
  260. $errors[] = "第{$displayLine}行:性别[{$e_text}]无效";
  261. } else {
  262. $array[$rowIndex][$eductionIdx] = $e_map[$e_text];
  263. }
  264. }
  265. $state_text = $rowValue[$stateIdx] ?? '';
  266. if (!isset($state_map[$state_text])) {
  267. $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
  268. } else {
  269. $array[$rowIndex][$stateIdx] = $state_map[$state_text];
  270. }
  271. // 4. 解析部门 (明细类型1)
  272. if ($depIdx !== false && !empty($rowValue[$depIdx])) {
  273. foreach (explode(',', $rowValue[$depIdx]) as $mNum) {
  274. $mNum = trim($mNum);
  275. if (!isset($dep_map[$mNum])) {
  276. $errors[] = "第{$displayLine}行:部门编码[{$mNum}]不存在";
  277. } else {
  278. $detail_storage[$rowIndex][] = [
  279. 'depart_id' => $dep_map[$mNum],
  280. ];
  281. }
  282. }
  283. }
  284. }
  285. $error_str = !empty($errors) ? implode('|', $errors) : "";
  286. return [$error_str, $update_mapping, $detail_storage];
  287. }
  288. private function getEDataList($array, $user, $index1)
  289. {
  290. $depNums = [];
  291. // 去重收集
  292. foreach ($array as $row) {
  293. if (!empty($row[$index1])) {
  294. foreach (explode(',', $row[$index1]) as $v) $depNums[trim($v)] = true;
  295. }
  296. }
  297. return Depart::where('del_time', 0)
  298. ->where('top_depart_id', $user['top_depart_id'])
  299. ->whereIn('code', array_keys($depNums))
  300. ->pluck('id', 'code')->toArray();
  301. }
  302. // 部门 ----------------------------------
  303. public function departImport($array, $user, $other_param)
  304. {
  305. $upload = $array[0];
  306. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  307. if (!$status) return [false, $msg];
  308. $table_config = $msg;
  309. unset($array[0]);
  310. if (empty($array)) return [false, '导入数据不能为空'];
  311. list($array, $error) = $this->checkCommon($array, $table_config);
  312. if (!empty($error)) return [0, $error];
  313. // 2. 详细校验
  314. list($error, $update_map, $parent_code_map) = $this->departCheck($array, $user, $table_config);
  315. if (!empty($error)) return [0, $error];
  316. $time = time();
  317. $insert = [];
  318. $update = [];
  319. $all_codes = [];
  320. $all_parent_codes = [];
  321. // --- 修正点 1: 必须确保索引提取准确 ---
  322. $keys = array_column($table_config, 'key');
  323. $codeIdx = array_search('code', $keys);
  324. // ------------------------------------
  325. foreach ($array as $key => $value) {
  326. $cCode = trim($value[$codeIdx] ?? '');
  327. if($cCode === '') continue;
  328. $all_codes[] = $cCode;
  329. if (isset($parent_code_map[$key])) {
  330. $all_parent_codes[] = $parent_code_map[$key];
  331. }
  332. $main_tmp = [];
  333. foreach ($value as $k => $val){
  334. if(!empty($table_config[$k]['is_main'])){
  335. if ($table_config[$k]['key'] !== 'parent_id') {
  336. $main_tmp[$table_config[$k]['key']] = $val;
  337. }
  338. }
  339. }
  340. if (isset($update_map[$key])) {
  341. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  342. } else {
  343. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  344. $main_tmp['crt_time'] = $time;
  345. $main_tmp['parent_id'] = 0;
  346. $insert[] = $main_tmp;
  347. }
  348. }
  349. $searchCodes = array_unique(array_merge($all_codes, $all_parent_codes));
  350. DB::beginTransaction();
  351. try {
  352. if (!empty($insert)) {
  353. foreach (array_chunk($insert, 500) as $chunk) {
  354. Depart::insert($chunk);
  355. }
  356. }
  357. if (!empty($update)) {
  358. foreach ($update as $item) {
  359. $uId = $item['id']; unset($item['id']);
  360. Depart::where('id', $uId)->update($item);
  361. }
  362. }
  363. // --- 修正点 2: 核心回填逻辑 ---
  364. $newCodeToIdMap = Depart::where('del_time', 0)
  365. ->where('top_depart_id', $user['top_depart_id'])
  366. ->whereIn('code', $searchCodes)
  367. ->pluck('id', 'code')
  368. ->toArray();
  369. foreach ($parent_code_map as $rowIndex => $pCode) {
  370. // 这里必须通过 $rowIndex 从原始 $array 中精准获取当前行的编码
  371. $currentCode = isset($array[$rowIndex][$codeIdx]) ? trim($array[$rowIndex][$codeIdx]) : '';
  372. $pCode = trim($pCode);
  373. if ($currentCode === '' || $pCode === '') continue;
  374. $currentId = $newCodeToIdMap[$currentCode] ?? null;
  375. $parentId = $newCodeToIdMap[$pCode] ?? null;
  376. // 严谨判断:只有当前记录存在,且上级记录也存在,且两者不是同一个 ID 时才更新
  377. if ($currentId && $parentId && $currentId != $parentId) {
  378. Depart::where('id', $currentId)->update(['parent_id' => $parentId]);
  379. }
  380. }
  381. DB::commit();
  382. } catch (\Exception $e) {
  383. DB::rollBack();
  384. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  385. }
  386. return [true, ''];
  387. }
  388. private function departCheck(&$array, $user, $table_config)
  389. {
  390. $keys = array_column($table_config, 'key');
  391. $codeIdx = array_search('code', $keys);
  392. $parentIdx = array_search('parent_id', $keys);
  393. // 1. 获取基础数据
  394. list($dbFeeMap, $excelCodesMap) = $this->getDepartList($array, $user, $codeIdx);
  395. $errors = [];
  396. $update = [];
  397. $parent_code_map = [];
  398. // 2. 建立 Excel 内部父子关系映射(用于环路追溯)
  399. $currentExcelMap = [];
  400. foreach ($array as $row) {
  401. $c = trim($row[$codeIdx] ?? '');
  402. $p = trim($row[$parentIdx] ?? '');
  403. if ($c !== '') $currentExcelMap[$c] = $p ?: $user['top_depart_code'] ;
  404. }
  405. // 3. 逐行校验
  406. foreach ($array as $rowIndex => $value) {
  407. $displayLine = $rowIndex + 1;
  408. $valCode = trim($value[$codeIdx] ?? '');
  409. $valParentCode = trim($value[$parentIdx] ?? '');
  410. if ($valCode === '') continue;
  411. // 更新状态记录
  412. if (isset($dbFeeMap[$valCode])) {
  413. $update[$rowIndex] = $dbFeeMap[$valCode]['id'];
  414. }
  415. if ($valParentCode !== '') {
  416. // --- A. 存在性校验 ---
  417. // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
  418. if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
  419. $errors[] = "第{$displayLine}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
  420. continue;
  421. }
  422. // --- B. 自引用校验 ---
  423. if ($valCode === $valParentCode) {
  424. $errors[] = "第{$displayLine}行:上级编码不能是自身";
  425. continue;
  426. }
  427. // --- C. 环路追溯 ---
  428. $visited = [];
  429. $res = $this->findDLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
  430. if ($res !== false) {
  431. if ($res['type'] === 'LOOP_SELF') {
  432. $errors[] = "第{$displayLine}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
  433. } else {
  434. $errors[] = "第{$displayLine}行:上级[{$valParentCode}]的溯源链条已成环";
  435. }
  436. continue;
  437. }
  438. // 记录有效的父级关系
  439. $parent_code_map[$rowIndex] = $valParentCode;
  440. }else{
  441. $parent_code_map[$rowIndex] = $user['top_depart_code'];
  442. }
  443. }
  444. $error_string = !empty($errors) ? implode('|', $errors) : "";
  445. return [$error_string, $update, $parent_code_map];
  446. }
  447. private function getDepartList($array, $user, $index)
  448. {
  449. // 关键:一定要把 Excel 里所有的 code 这一列全部拿出来,并去除空值
  450. $codesInExcel = [];
  451. foreach ($array as $row) {
  452. $c = trim($row[$index] ?? '');
  453. if ($c !== '') {
  454. $codesInExcel[$c] = true;
  455. }
  456. }
  457. $allFees = Depart::where('del_time', 0)
  458. ->where('top_depart_id', $user['top_depart_id'])
  459. ->select('id', 'code', 'parent_id')
  460. ->get();
  461. $dbFeeMap = [];
  462. foreach ($allFees as $fee) {
  463. $dbFeeMap[$fee->code] = [
  464. 'id' => $fee->id,
  465. 'code' => $fee->code,
  466. 'parent_id' => $fee->parent_id
  467. ];
  468. }
  469. // 返回数据库映射和 Excel 编码映射
  470. return [$dbFeeMap, $codesInExcel];
  471. }
  472. private function findDLoopInAncestors($startParentCode, $targetCode, $excelMap, $dbFeeMap, &$visited)
  473. {
  474. $current = $startParentCode;
  475. while ($current !== '' && $current !== 0) {
  476. if ($current === $targetCode) {
  477. return ['type' => 'LOOP_SELF', 'code' => $current];
  478. }
  479. if (isset($visited[$current])) {
  480. return ['type' => 'EXISTING_LOOP', 'code' => $current];
  481. }
  482. $visited[$current] = true;
  483. if (isset($excelMap[$current]) && $excelMap[$current] !== '') {
  484. $current = $excelMap[$current];
  485. } elseif (isset($dbFeeMap[$current])) {
  486. $parentId = $dbFeeMap[$current]['parent_id'];
  487. $parentCode = '';
  488. foreach ($dbFeeMap as $item) {
  489. if ($item['id'] == $parentId) {
  490. $parentCode = $item['code'];
  491. break;
  492. }
  493. }
  494. $current = $parentCode;
  495. } else {
  496. break;
  497. }
  498. }
  499. return false;
  500. }
  501. // 设备 ------------------------------------
  502. public function deviceImport($array, $user, $other_param){
  503. $upload = $array[0];
  504. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  505. if(!$status) return [false, $msg];
  506. $table_config = $msg;
  507. unset($array[0]);
  508. if(empty($array)) return [false, '导入数据不能为空'];
  509. // 公共校验
  510. list($array, $error) = $this->checkCommon($array, $table_config);
  511. if(!empty($error)) return [0, $error];
  512. // 详细校验 (这里 $array 传引用,内部会转换日期)
  513. list($error, $update_map) = $this->deviceCheck($array, $user, $table_config);
  514. if(!empty($error)) return [0, $error];
  515. $time = time();
  516. $insert = [];
  517. $update = [];
  518. foreach ($array as $key => $value){
  519. $main_tmp = [];
  520. foreach ($value as $k => $val){
  521. if(!empty($table_config[$k]['is_main'])){
  522. $main_tmp[$table_config[$k]['key']] = $val;
  523. }
  524. }
  525. if(isset($update_map[$key])){
  526. // 存入待更新数组
  527. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  528. } else {
  529. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  530. $main_tmp['crt_id'] = $user['id'];
  531. $main_tmp['crt_time'] = $time;
  532. $insert[] = $main_tmp;
  533. }
  534. }
  535. DB::beginTransaction();
  536. try {
  537. // 1. 批量新增
  538. if(!empty($insert)){
  539. foreach(array_chunk($insert, 500) as $chunkInsert){
  540. Device::insert($chunkInsert);
  541. }
  542. }
  543. // 批量更新
  544. foreach (array_chunk($update, 100) as $chunk) {
  545. foreach ($chunk as $item) {
  546. $id = $item['id'];
  547. unset($item['id']);
  548. Device::where('id', $id)->update($item);
  549. }
  550. }
  551. DB::commit();
  552. } catch (\Exception $e) {
  553. DB::rollBack();
  554. return [false, "错误:" . $e->getMessage() . " 行:" . $e->getLine()];
  555. }
  556. return [true, ''];
  557. }
  558. private function deviceCheck(&$array, $user, $table_config)
  559. {
  560. // 动态获取关键列的索引
  561. $codeIdx = array_search('code', array_column($table_config, 'key'));
  562. $dateIdx = array_search('in_time', array_column($table_config, 'key'));
  563. $typeIdx = array_search('type', array_column($table_config, 'key'));
  564. $type2Idx = array_search('is_use', array_column($table_config, 'key'));
  565. $code_map = $this->getDeviceList($array, $user, $codeIdx);
  566. $errors = [];
  567. $update = [];
  568. $map_type = array_flip(Device::$type);
  569. $map_type_2 = array_flip(Device::Use);
  570. foreach ($array as $rowIndex => $value) {
  571. $displayLine = $rowIndex + 1;
  572. $valCode = $value[$codeIdx] ?? '';
  573. // 记录更新 ID
  574. if(isset($code_map[$valCode])){
  575. $update[$rowIndex] = $code_map[$valCode];
  576. }
  577. if(empty($map_type[$value[$typeIdx]])){
  578. $errors[] = "第{$displayLine}行固定资产类型错误";
  579. }else{
  580. $array[$rowIndex][$typeIdx] = $map_type[$value[$typeIdx]];
  581. }
  582. if(empty($map_type_2[$value[$type2Idx]])){
  583. $errors[] = "第{$displayLine}行是否启用错误";
  584. }else{
  585. $array[$rowIndex][$type2Idx] = $map_type_2[$value[$type2Idx]];
  586. }
  587. // 日期处理
  588. if($dateIdx !== false && !empty($value[$dateIdx])){
  589. list($status, $msg) = $this->convertExcelCellToDate($value[$dateIdx]);
  590. if(!$status) {
  591. $errors[] = "第{$displayLine}行日期格式错误";
  592. } else {
  593. $array[$rowIndex][$dateIdx] = $msg;
  594. }
  595. }
  596. }
  597. $error_string = "";
  598. if(! empty($errors)) $error_string = implode('|', $errors);
  599. return [$error_string, $update];
  600. }
  601. private function getDeviceList($array, $user, $index){
  602. //查找设备
  603. $codes = array_unique(array_filter(array_column($array,$index)));
  604. return Device::where('del_time', 0)
  605. ->where('top_depart_id', $user['top_depart_id'])
  606. ->whereIn('code', $codes)
  607. ->pluck('id','code')
  608. ->toArray();
  609. }
  610. // 项目 -----------------------------------
  611. public function itemImport($array, $user, $other_param)
  612. {
  613. $upload = $array[0];
  614. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  615. if (!$status) return [false, $msg];
  616. $table_config = $msg;
  617. unset($array[0]);
  618. if (empty($array)) return [false, '导入数据不能为空'];
  619. // 1. 公共校验 (必填、唯一性等)
  620. list($array, $error) = $this->checkCommon($array, $table_config);
  621. if (!empty($error)) return [0, $error];
  622. // 2. 业务详细校验 (获取更新映射及明细数据)
  623. list($error, $update_map, $detail_data_map) = $this->itemCheck($array, $user, $table_config);
  624. if (!empty($error)) return [0, $error];
  625. $time = time();
  626. $insert_data = [];
  627. $update_data = [];
  628. $all_detail_insert = [];
  629. $update_main_ids = [];
  630. // 3. 数据分拣
  631. foreach ($array as $key => $value) {
  632. $main_tmp = [];
  633. foreach ($value as $k => $val) {
  634. if (!empty($table_config[$k]['is_main'])) {
  635. $main_tmp[$table_config[$k]['key']] = $val;
  636. }
  637. }
  638. if (isset($update_map[$key])) {
  639. // 更新逻辑
  640. $itemId = $update_map[$key];
  641. $update_data[] = array_merge($main_tmp, ['id' => $itemId]);
  642. $update_main_ids[] = $itemId;
  643. // 收集明细 (后续统一插入)
  644. if (isset($detail_data_map[$key])) {
  645. foreach ($detail_data_map[$key] as $d) {
  646. $all_detail_insert[] = array_merge($d, ['item_id' => $itemId, 'crt_time' => $time, 'top_depart_id' => $user['top_depart_id']]);
  647. }
  648. }
  649. } else {
  650. // 新增逻辑
  651. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  652. $main_tmp['crt_id'] = $user['id'];
  653. $main_tmp['crt_time'] = $time;
  654. // 以 code 为键,方便后续回填 ID
  655. $insert_data[$main_tmp['code']] = $main_tmp;
  656. if (isset($detail_data_map[$key])) {
  657. foreach ($detail_data_map[$key] as $d) {
  658. $all_detail_insert[] = array_merge($d, ['_code' => $main_tmp['code'], 'crt_time' => $time, 'top_depart_id' => $main_tmp['top_depart_id']]);
  659. }
  660. }
  661. }
  662. }
  663. DB::beginTransaction();
  664. try {
  665. // 4. 执行新增主表
  666. if (!empty($insert_data)) {
  667. foreach (array_chunk($insert_data, 500) as $chunk) {
  668. Item::insert($chunk);
  669. }
  670. // 获取新插入数据的 ID 映射
  671. $new_item_maps = Item::whereIn('code', array_keys($insert_data))
  672. ->where('del_time', 0)
  673. ->where('top_depart_id', $user['top_depart_id'])
  674. ->pluck('id', 'code')->toArray();
  675. }
  676. // 5. 执行更新主表 (分批更新)
  677. if (!empty($update_data)) {
  678. foreach (array_chunk($update_data, 100) as $chunk) {
  679. foreach ($chunk as $uItem) {
  680. $id = $uItem['id'];
  681. unset($uItem['id']);
  682. Item::where('id', $id)->update($uItem);
  683. }
  684. }
  685. }
  686. // 6. 处理明细表 (先删后插策略)
  687. // 删除旧明细 (逻辑删除)
  688. if (!empty($update_main_ids)) {
  689. ItemDetails::whereIn('item_id', $update_main_ids)
  690. ->where('del_time', 0)
  691. ->update(['del_time' => $time]);
  692. }
  693. // 回填新增主表的 ID 到明细数组
  694. foreach ($all_detail_insert as &$di) {
  695. if (isset($di['_code'])) {
  696. $di['item_id'] = $new_item_maps[$di['_code']] ?? 0;
  697. unset($di['_code']);
  698. }
  699. }
  700. unset($di);
  701. // 批量插入所有明细
  702. if (!empty($all_detail_insert)) {
  703. foreach (array_chunk($all_detail_insert, 500) as $chunk) {
  704. ItemDetails::insert($chunk);
  705. }
  706. }
  707. DB::commit();
  708. } catch (\Exception $e) {
  709. DB::rollBack();
  710. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  711. }
  712. return [true, ''];
  713. }
  714. private function getItemList($array, $user, $index){
  715. //查找设备
  716. $codes = array_unique(array_filter(array_column($array,$index)));
  717. return Item::where('del_time', 0)
  718. ->where('top_depart_id', $user['top_depart_id'])
  719. ->whereIn('code', $codes)
  720. ->pluck('id','code')
  721. ->toArray();
  722. }
  723. private function itemCheck(&$array, $user, $table_config)
  724. {
  725. $keys = array_column($table_config, 'key');
  726. $codeIdx = array_search('code', $keys);
  727. $stateIdx = array_search('state', $keys);
  728. $manIdx = array_search('man_list', $keys);
  729. $deviceIdx = array_search('device_list', $keys);
  730. // 日期索引
  731. $dateIdx = array_search('start_time', $keys);
  732. $date2Idx = array_search('end_time', $keys);
  733. $code_map = $this->getItemList($array, $user, $codeIdx);
  734. list($man_map, $device_map) = $this->getDataList($array, $user, $manIdx, $deviceIdx);
  735. $errors = [];
  736. $update_mapping = [];
  737. $detail_storage = [];
  738. $state_type_map = array_flip(Item::State_Type);
  739. foreach ($array as $rowIndex => $rowValue) {
  740. $displayLine = $rowIndex + 1;
  741. $valCode = $rowValue[$codeIdx] ?? '';
  742. // 1. 判定更新还是新增
  743. if (isset($code_map[$valCode])) {
  744. $update_mapping[$rowIndex] = $code_map[$valCode];
  745. }
  746. // 2. 状态校验
  747. $state_text = $rowValue[$stateIdx] ?? '';
  748. if (!isset($state_type_map[$state_text])) {
  749. $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
  750. } else {
  751. $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
  752. }
  753. // 3. 日期转换
  754. foreach ([$dateIdx, $date2Idx] as $dIdx) {
  755. if ($dIdx !== false && !empty($rowValue[$dIdx])) {
  756. list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
  757. if (!$s) $errors[] = "第{$displayLine}行:日期格式非法";
  758. else $array[$rowIndex][$dIdx] = $m;
  759. }
  760. }
  761. // 4. 解析人员 (明细类型1)
  762. if ($manIdx !== false && !empty($rowValue[$manIdx])) {
  763. foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
  764. $mNum = trim($mNum);
  765. if (!isset($man_map[$mNum])) {
  766. $errors[] = "第{$displayLine}行:人员工号[{$mNum}]不存在";
  767. } else {
  768. $detail_storage[$rowIndex][] = [
  769. 'type' => ItemDetails::type_one,
  770. 'data_id' => $man_map[$mNum],
  771. ];
  772. }
  773. }
  774. }
  775. // 5. 解析设备 (明细类型2)
  776. if ($deviceIdx !== false && !empty($rowValue[$deviceIdx])) {
  777. foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
  778. $dCode = trim($dCode);
  779. if (!isset($device_map[$dCode])) {
  780. $errors[] = "第{$displayLine}行:设备的资产编码[{$dCode}]不存在";
  781. } else {
  782. $detail_storage[$rowIndex][] = [
  783. 'type' => ItemDetails::type_two,
  784. 'data_id' => $device_map[$dCode],
  785. ];
  786. }
  787. }
  788. }
  789. }
  790. $error_str = !empty($errors) ? implode('|', $errors) : "";
  791. return [$error_str, $update_mapping, $detail_storage];
  792. }
  793. private function getDataList($array, $user, $index1, $index2)
  794. {
  795. $manNums = [];
  796. $devCodes = [];
  797. // 去重收集
  798. foreach ($array as $row) {
  799. if (!empty($row[$index1])) {
  800. foreach (explode(',', $row[$index1]) as $v) $manNums[trim($v)] = true;
  801. }
  802. if (!empty($row[$index2])) {
  803. foreach (explode(',', $row[$index2]) as $v) $devCodes[trim($v)] = true;
  804. }
  805. }
  806. $manMap = Employee::where('del_time', 0)
  807. ->where('top_depart_id', $user['top_depart_id'])
  808. ->whereIn('number', array_keys($manNums))
  809. ->pluck('id', 'number')->toArray();
  810. $devMap = Device::where('del_time', 0)
  811. ->where('top_depart_id', $user['top_depart_id'])
  812. ->whereIn('code', array_keys($devCodes))
  813. ->pluck('id', 'code')->toArray();
  814. return [$manMap, $devMap];
  815. }
  816. // 费用 ----------------------------------
  817. public function feeImport($array, $user, $other_param)
  818. {
  819. $upload = $array[0];
  820. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  821. if (!$status) return [false, $msg];
  822. $table_config = $msg;
  823. unset($array[0]);
  824. if (empty($array)) return [false, '导入数据不能为空'];
  825. list($array, $error) = $this->checkCommon($array, $table_config);
  826. if (!empty($error)) return [0, $error];
  827. // 2. 详细校验
  828. list($error, $update_map, $parent_code_map) = $this->feeCheck($array, $user, $table_config);
  829. if (!empty($error)) return [0, $error];
  830. $time = time();
  831. $insert = [];
  832. $update = [];
  833. $all_codes = [];
  834. // --- 修正点 1: 必须确保索引提取准确 ---
  835. $keys = array_column($table_config, 'key');
  836. $codeIdx = array_search('code', $keys);
  837. // ------------------------------------
  838. foreach ($array as $key => $value) {
  839. $cCode = trim($value[$codeIdx] ?? '');
  840. if($cCode === '') continue;
  841. $all_codes[] = $cCode;
  842. $main_tmp = [];
  843. foreach ($value as $k => $val){
  844. if(!empty($table_config[$k]['is_main'])){
  845. if ($table_config[$k]['key'] !== 'parent_id') {
  846. $main_tmp[$table_config[$k]['key']] = $val;
  847. }
  848. }
  849. }
  850. if (isset($update_map[$key])) {
  851. $update[] = array_merge($main_tmp, ['id' => $update_map[$key]]);
  852. } else {
  853. $main_tmp['top_depart_id'] = $user['top_depart_id'];
  854. $main_tmp['crt_time'] = $time;
  855. $main_tmp['parent_id'] = 0;
  856. $insert[] = $main_tmp;
  857. }
  858. }
  859. $all_parent_codes = array_values($parent_code_map);
  860. $searchCodes = array_unique(array_merge($all_codes, $all_parent_codes));
  861. DB::beginTransaction();
  862. try {
  863. if (!empty($insert)) {
  864. foreach (array_chunk($insert, 500) as $chunk) {
  865. Fee::insert($chunk);
  866. }
  867. }
  868. if (!empty($update)) {
  869. foreach ($update as $item) {
  870. $uId = $item['id']; unset($item['id']);
  871. Fee::where('id', $uId)->update($item);
  872. }
  873. }
  874. // --- 修正点 2: 核心回填逻辑 ---
  875. $newCodeToIdMap = Fee::where('del_time', 0)
  876. ->where('top_depart_id', $user['top_depart_id'])
  877. ->whereIn('code', $searchCodes)
  878. ->pluck('id', 'code')
  879. ->toArray();
  880. foreach ($parent_code_map as $rowIndex => $pCode) {
  881. // 这里必须通过 $rowIndex 从原始 $array 中精准获取当前行的编码
  882. $currentCode = isset($array[$rowIndex][$codeIdx]) ? trim($array[$rowIndex][$codeIdx]) : '';
  883. $pCode = trim($pCode);
  884. if ($currentCode === '' || $pCode === '') continue;
  885. $currentId = $newCodeToIdMap[$currentCode] ?? null;
  886. $parentId = $newCodeToIdMap[$pCode] ?? null;
  887. // 严谨判断:只有当前记录存在,且上级记录也存在,且两者不是同一个 ID 时才更新
  888. if ($currentId && $parentId && $currentId != $parentId) {
  889. Fee::where('id', $currentId)->update(['parent_id' => $parentId]);
  890. }
  891. }
  892. DB::commit();
  893. } catch (\Exception $e) {
  894. DB::rollBack();
  895. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  896. }
  897. return [true, ''];
  898. }
  899. private function feeCheck(&$array, $user, $table_config)
  900. {
  901. $keys = array_column($table_config, 'key');
  902. $codeIdx = array_search('code', $keys);
  903. $parentIdx = array_search('parent_id', $keys);
  904. // 1. 获取基础数据
  905. list($dbFeeMap, $excelCodesMap) = $this->getFeeList($array, $user, $codeIdx);
  906. $errors = [];
  907. $update = [];
  908. $parent_code_map = [];
  909. // 2. 建立 Excel 内部父子关系映射(用于环路追溯)
  910. $currentExcelMap = [];
  911. foreach ($array as $row) {
  912. $c = trim($row[$codeIdx] ?? '');
  913. $p = trim($row[$parentIdx] ?? '');
  914. if ($c !== '') $currentExcelMap[$c] = $p;
  915. }
  916. // 3. 逐行校验
  917. foreach ($array as $rowIndex => $value) {
  918. $displayLine = $rowIndex + 1;
  919. $valCode = trim($value[$codeIdx] ?? '');
  920. $valParentCode = trim($value[$parentIdx] ?? '');
  921. if ($valCode === '') continue;
  922. // 更新状态记录
  923. if (isset($dbFeeMap[$valCode])) {
  924. $update[$rowIndex] = $dbFeeMap[$valCode]['id'];
  925. }
  926. if ($valParentCode !== '') {
  927. // --- A. 存在性校验 ---
  928. // 这里会检查 006 是否在数据库,或者是否在本次 Excel 的其他行中
  929. if (!isset($dbFeeMap[$valParentCode]) && !isset($excelCodesMap[$valParentCode])) {
  930. $errors[] = "第{$displayLine}行:上级编码[{$valParentCode}]在系统和文件中均不存在";
  931. continue;
  932. }
  933. // --- B. 自引用校验 ---
  934. if ($valCode === $valParentCode) {
  935. $errors[] = "第{$displayLine}行:上级编码不能是自身";
  936. continue;
  937. }
  938. // --- C. 环路追溯 ---
  939. $visited = [];
  940. $res = $this->findLoopInAncestors($valParentCode, $valCode, $currentExcelMap, $dbFeeMap, $visited);
  941. if ($res !== false) {
  942. if ($res['type'] === 'LOOP_SELF') {
  943. $errors[] = "第{$displayLine}行:编码[{$valCode}]与上级[{$valParentCode}]存在循环引用";
  944. } else {
  945. $errors[] = "第{$displayLine}行:上级[{$valParentCode}]的溯源链条已成环";
  946. }
  947. continue;
  948. }
  949. // 记录有效的父级关系
  950. $parent_code_map[$rowIndex] = $valParentCode;
  951. }
  952. }
  953. $error_string = !empty($errors) ? implode('|', $errors) : "";
  954. return [$error_string, $update, $parent_code_map];
  955. }
  956. private function getFeeList($array, $user, $index)
  957. {
  958. // 关键:一定要把 Excel 里所有的 code 这一列全部拿出来,并去除空值
  959. $codesInExcel = [];
  960. foreach ($array as $row) {
  961. $c = trim($row[$index] ?? '');
  962. if ($c !== '') {
  963. $codesInExcel[$c] = true;
  964. }
  965. }
  966. $allFees = Fee::where('del_time', 0)
  967. ->where('top_depart_id', $user['top_depart_id'])
  968. ->select('id', 'code', 'parent_id')
  969. ->get();
  970. $dbFeeMap = [];
  971. foreach ($allFees as $fee) {
  972. $dbFeeMap[$fee->code] = [
  973. 'id' => $fee->id,
  974. 'code' => $fee->code,
  975. 'parent_id' => $fee->parent_id
  976. ];
  977. }
  978. // 返回数据库映射和 Excel 编码映射
  979. return [$dbFeeMap, $codesInExcel];
  980. }
  981. private function findLoopInAncestors($startParentCode, $targetCode, $excelMap, $dbFeeMap, &$visited)
  982. {
  983. $current = $startParentCode;
  984. while ($current !== '' && $current !== 0) {
  985. if ($current === $targetCode) {
  986. return ['type' => 'LOOP_SELF', 'code' => $current];
  987. }
  988. if (isset($visited[$current])) {
  989. return ['type' => 'EXISTING_LOOP', 'code' => $current];
  990. }
  991. $visited[$current] = true;
  992. if (isset($excelMap[$current]) && $excelMap[$current] !== '') {
  993. $current = $excelMap[$current];
  994. } elseif (isset($dbFeeMap[$current])) {
  995. $parentId = $dbFeeMap[$current]['parent_id'];
  996. $parentCode = '';
  997. foreach ($dbFeeMap as $item) {
  998. if ($item['id'] == $parentId) {
  999. $parentCode = $item['code'];
  1000. break;
  1001. }
  1002. }
  1003. $current = $parentCode;
  1004. } else {
  1005. break;
  1006. }
  1007. }
  1008. return false;
  1009. }
  1010. // 人员月度工时单 ------------------------------
  1011. public function monthPwOrderImport($array, $user, $other_param)
  1012. {
  1013. $upload = $array[0];
  1014. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1015. if (!$status) return [false, $msg];
  1016. $table_config = $msg;
  1017. unset($array[0]);
  1018. if (empty($array)) return [false, '导入数据不能为空'];
  1019. list($array, $error) = $this->checkCommon($array, $table_config);
  1020. if (!empty($error)) return [0, $error];
  1021. // 2. 详细校验 (这里会返回 update_map 和 dbEmps 映射)
  1022. list($error, $update_map, $dbEmps) = $this->monthPwOrderCheck($array, $user, $table_config);
  1023. if (!empty($error)) return [0, $error];
  1024. $keys = array_column($table_config, 'key');
  1025. $codeIdx = array_search('code', $keys);
  1026. $monthIdx = array_search('month', $keys);
  1027. $empIdx = array_search('employee_id', $keys);
  1028. // --- 步骤 1: 数据聚合分组 ---
  1029. // 将 Excel 数据按“单据”维度归类
  1030. $groups = [];
  1031. foreach ($array as $rowIndex => $row) {
  1032. $cCode = trim($row[$codeIdx] ?? '');
  1033. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  1034. // 聚合 Key:有编码用编码,没编码用月份标记
  1035. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  1036. if (!isset($groups[$aggKey])) {
  1037. $groups[$aggKey] = [
  1038. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  1039. 'code' => $cCode,
  1040. 'month' => $cMonthTs,
  1041. 'details' => []
  1042. ];
  1043. }
  1044. // 准备详情行数据
  1045. $detailTmp = [];
  1046. foreach ($table_config as $k => $conf) {
  1047. if (!$conf['is_main']) {
  1048. $fieldKey = $conf['key'];
  1049. $fieldVal = $row[$k];
  1050. // 如果是人员,转换为 ID
  1051. if ($fieldKey == 'employee_id') {
  1052. $fieldVal = $dbEmps[$fieldVal] ?? 0;
  1053. }
  1054. $detailTmp[$fieldKey] = $fieldVal;
  1055. }
  1056. }
  1057. $groups[$aggKey]['details'][] = $detailTmp;
  1058. }
  1059. // --- 步骤 2: 开启事务写入 ---
  1060. DB::beginTransaction();
  1061. try {
  1062. $time = time();
  1063. foreach ($groups as $aggKey => $group) {
  1064. $mainId = $group['main_id'];
  1065. if ($mainId > 0) {
  1066. // 删除旧详情
  1067. DB::table('monthly_pw_order_details')->where('del_time',0)
  1068. ->where('main_id', $mainId)
  1069. ->update(['del_time' => $time]);
  1070. } else {
  1071. // B. 新增逻辑
  1072. $newCode = $this->generateBillNo([
  1073. 'top_depart_id' => $user['top_depart_id'],
  1074. 'type' => MonthlyPwOrder::Order_type,
  1075. 'period' => date("Ym", $group['month'])
  1076. ]);
  1077. $mainId = DB::table('monthly_pw_order')->insertGetId([
  1078. 'code' => $newCode,
  1079. 'month' => $group['month'],
  1080. 'top_depart_id' => $user['top_depart_id'],
  1081. 'crt_id' => $user['id'],
  1082. 'crt_time' => $time,
  1083. 'upd_time' => $time,
  1084. 'del_time' => 0
  1085. ]);
  1086. }
  1087. // C. 批量插入详情
  1088. $insertDetails = [];
  1089. foreach ($group['details'] as $detail) {
  1090. $detail['main_id'] = $mainId;
  1091. $detail['top_depart_id']= $user['top_depart_id'];
  1092. $detail['crt_time'] = $time;
  1093. $detail['del_time'] = 0;
  1094. $insertDetails[] = $detail;
  1095. }
  1096. if (!empty($insertDetails)) {
  1097. DB::table('monthly_pw_order_details')->insert($insertDetails);
  1098. }
  1099. }
  1100. DB::commit();
  1101. } catch (\Exception $e) {
  1102. DB::rollBack();
  1103. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  1104. }
  1105. return [true, ''];
  1106. }
  1107. private function monthPwOrderCheck(&$array, $user, $table_config)
  1108. {
  1109. $keys = array_column($table_config, 'key');
  1110. $codeIdx = array_search('code', $keys);
  1111. $monthIdx = array_search('month', $keys);
  1112. $empIdx = array_search('employee_id', $keys);
  1113. $startIdx = array_search('start_time', $keys);
  1114. $endIdx = array_search('end_time', $keys);
  1115. $numIdx = array_search('total_days', $keys);
  1116. $num2Idx = array_search('rd_total_days', $keys);
  1117. $num3Idx = array_search('total_hours', $keys);
  1118. $num4Idx = array_search('rd_total_hours', $keys);
  1119. // 1. 预加载基础数据
  1120. $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
  1121. $dbEmps = Employee::where('del_time', 0)
  1122. ->whereIn('number', $allEmpNumbers)
  1123. ->where('top_depart_id', $user['top_depart_id'])
  1124. ->pluck('id', 'number')->toArray();
  1125. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  1126. $dbOrders = DB::table('monthly_pw_order')
  1127. ->whereIn('code', $allCodes)
  1128. ->where('top_depart_id', $user['top_depart_id'])
  1129. ->get()->keyBy('code');
  1130. $errors = [];
  1131. //时间转换
  1132. foreach ($array as $rowIndex => $row) {
  1133. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1134. if ($valMonthRaw === '') continue;
  1135. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1136. if (!$mStatus) {
  1137. $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
  1138. continue;
  1139. }
  1140. // 标准化并存回数组,这样后面的循环直接拿到的就是时间戳
  1141. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1142. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1143. $uniqueMonths[] = $valMonthTs;
  1144. }
  1145. $existingMonthsMap = [];
  1146. if (!empty($uniqueMonths)) {
  1147. $existingMonths = DB::table('monthly_pw_order')
  1148. ->where('top_depart_id', $user['top_depart_id'])
  1149. ->where('del_time', 0)
  1150. ->whereIn('month', array_unique($uniqueMonths))
  1151. ->pluck('month')
  1152. ->toArray();
  1153. $existingMonthsMap = array_fill_keys($existingMonths, true);
  1154. }
  1155. $excelAggregator = [];
  1156. $update_map = [];
  1157. foreach ($array as $rowIndex => $row) {
  1158. // 用户看到的行号(假设数据从第2行开始)
  1159. $displayLine = $rowIndex + 1;
  1160. $valCode = trim($row[$codeIdx] ?? '');
  1161. $valMonthTs = trim($row[$monthIdx] ?? '');
  1162. $valEmp = trim($row[$empIdx] ?? '');
  1163. $valStartRaw = $row[$startIdx] ?? '';
  1164. $valEndRaw = $row[$endIdx] ?? '';
  1165. $valNumRaw = $row[$numIdx] ?? 0;
  1166. $valNum2Raw = $row[$num2Idx] ?? 0;
  1167. $valNum3Raw = $row[$num3Idx] ?? 0;
  1168. $valNum4Raw = $row[$num4Idx] ?? 0;
  1169. if (!is_numeric($valMonthTs)) continue;
  1170. // 转换开始日期
  1171. list($sStatus, $startTime) = $this->convertExcelCellToDate($valStartRaw);
  1172. if (!$sStatus) {
  1173. $errors[] = "第{$displayLine}行:开始日期格式错误";
  1174. continue;
  1175. }
  1176. // 转换结束日期
  1177. list($eStatus, $endTime) = $this->convertExcelCellToDate($valEndRaw);
  1178. if (!$eStatus) {
  1179. $errors[] = "第{$displayLine}行:结束日期格式错误";
  1180. continue;
  1181. }
  1182. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1183. // --- B. 校验单据与月份的一致性 ---
  1184. if ($valCode && isset($dbOrders[$valCode])) {
  1185. $dbOrder = $dbOrders[$valCode];
  1186. if ($dbOrder->month != $valMonthTs) {
  1187. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1188. }
  1189. $update_map[$rowIndex] = $dbOrder->id;
  1190. }
  1191. // --- C. 校验人员存在性与单据内唯一性 ---
  1192. if (!isset($dbEmps[$valEmp])) {
  1193. $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
  1194. } else {
  1195. if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
  1196. $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同一单据中重复";
  1197. }
  1198. $excelAggregator[$aggKey]['emps'][$valEmp] = true;
  1199. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1200. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1201. }
  1202. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1203. }
  1204. // --- D. 逻辑校验:日期范围 ---
  1205. if ($startTime > $endTime) {
  1206. $errors[] = "第{$displayLine}行:开始日期大于结束日期";
  1207. } else {
  1208. $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
  1209. if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
  1210. $errors[] = "第{$displayLine}行:日期超出[ " . date('Y-m', $valMonthTs) . " ]范围";
  1211. }
  1212. }
  1213. // --- E. 月份唯一单据校验 (新增) ---
  1214. if (!$valCode) {
  1215. if (isset($existingMonthsMap[$valMonthTs])) {
  1216. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1217. }
  1218. }
  1219. // --- F. 数字校验
  1220. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  1221. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
  1222. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  1223. if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
  1224. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  1225. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
  1226. $res = $this->checkNumber($valNum4Raw,2,'non-negative');
  1227. if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
  1228. // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
  1229. $array[$rowIndex][$startIdx] = $startTime;
  1230. $array[$rowIndex][$endIdx] = $endTime;
  1231. }
  1232. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1233. return [$error_string, $update_map, $dbEmps];
  1234. }
  1235. // 设备月度工时单
  1236. public function monthDwOrderImport($array, $user, $other_param)
  1237. {
  1238. $upload = $array[0];
  1239. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1240. if (!$status) return [false, $msg];
  1241. $table_config = $msg;
  1242. unset($array[0]);
  1243. if (empty($array)) return [false, '导入数据不能为空'];
  1244. list($array, $error) = $this->checkCommon($array, $table_config);
  1245. if (!empty($error)) return [0, $error];
  1246. // 2. 详细校验 (这里会返回 update_map 和 dbDevs 映射)
  1247. list($error, $update_map, $dbDevs) = $this->monthDwOrderCheck($array, $user, $table_config);
  1248. if (!empty($error)) return [0, $error];
  1249. $keys = array_column($table_config, 'key');
  1250. $codeIdx = array_search('code', $keys);
  1251. $monthIdx = array_search('month', $keys);
  1252. $empIdx = array_search('device_id', $keys);
  1253. // --- 步骤 1: 数据聚合分组 ---
  1254. // 将 Excel 数据按“单据”维度归类
  1255. $groups = [];
  1256. foreach ($array as $rowIndex => $row) {
  1257. $cCode = trim($row[$codeIdx] ?? '');
  1258. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  1259. // 聚合 Key:有编码用编码,没编码用月份标记
  1260. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  1261. if (!isset($groups[$aggKey])) {
  1262. $groups[$aggKey] = [
  1263. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  1264. 'code' => $cCode,
  1265. 'month' => $cMonthTs,
  1266. 'details' => []
  1267. ];
  1268. }
  1269. // 准备详情行数据
  1270. $detailTmp = [];
  1271. foreach ($table_config as $k => $conf) {
  1272. if (!$conf['is_main']) {
  1273. $fieldKey = $conf['key'];
  1274. $fieldVal = $row[$k];
  1275. // 如果是人员,转换为 ID
  1276. if ($fieldKey == 'device_id') {
  1277. $fieldVal = $dbDevs[$fieldVal] ?? 0;
  1278. }
  1279. $detailTmp[$fieldKey] = $fieldVal;
  1280. }
  1281. }
  1282. $groups[$aggKey]['details'][] = $detailTmp;
  1283. }
  1284. // --- 步骤 2: 开启事务写入 ---
  1285. DB::beginTransaction();
  1286. try {
  1287. $time = time();
  1288. foreach ($groups as $aggKey => $group) {
  1289. $mainId = $group['main_id'];
  1290. if ($mainId > 0) {
  1291. // 删除旧详情
  1292. DB::table('monthly_dw_order_details')->where('del_time',0)
  1293. ->where('main_id', $mainId)
  1294. ->update(['del_time' => $time]);
  1295. } else {
  1296. // B. 新增逻辑
  1297. $newCode = $this->generateBillNo([
  1298. 'top_depart_id' => $user['top_depart_id'],
  1299. 'type' => MonthlyPwOrder::Order_type,
  1300. 'period' => date("Ym", $group['month'])
  1301. ]);
  1302. $mainId = DB::table('monthly_dw_order')->insertGetId([
  1303. 'code' => $newCode,
  1304. 'month' => $group['month'],
  1305. 'top_depart_id' => $user['top_depart_id'],
  1306. 'crt_id' => $user['id'],
  1307. 'crt_time' => $time,
  1308. 'upd_time' => $time,
  1309. 'del_time' => 0
  1310. ]);
  1311. }
  1312. // C. 批量插入详情
  1313. $insertDetails = [];
  1314. foreach ($group['details'] as $detail) {
  1315. $detail['main_id'] = $mainId;
  1316. $detail['top_depart_id']= $user['top_depart_id'];
  1317. $detail['crt_time'] = $time;
  1318. $detail['del_time'] = 0;
  1319. $insertDetails[] = $detail;
  1320. }
  1321. if (!empty($insertDetails)) {
  1322. DB::table('monthly_dw_order_details')->insert($insertDetails);
  1323. }
  1324. }
  1325. DB::commit();
  1326. } catch (\Exception $e) {
  1327. DB::rollBack();
  1328. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  1329. }
  1330. return [true, ''];
  1331. }
  1332. private function monthDwOrderCheck(&$array, $user, $table_config)
  1333. {
  1334. $keys = array_column($table_config, 'key');
  1335. $codeIdx = array_search('code', $keys);
  1336. $monthIdx = array_search('month', $keys);
  1337. $devIdx = array_search('device_id', $keys);
  1338. $startIdx = array_search('start_time', $keys);
  1339. $endIdx = array_search('end_time', $keys);
  1340. $numIdx = array_search('total_days', $keys);
  1341. $num2Idx = array_search('rd_total_days', $keys);
  1342. $num3Idx = array_search('total_hours', $keys);
  1343. $num4Idx = array_search('rd_total_hours', $keys);
  1344. // 1. 预加载基础数据
  1345. $allEmpNumbers = array_filter(array_unique(array_column($array, $devIdx)));
  1346. $dbDevs = Device::where('del_time', 0)
  1347. ->whereIn('code', $allEmpNumbers)
  1348. ->where('top_depart_id', $user['top_depart_id'])
  1349. ->pluck('id', 'code')->toArray();
  1350. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  1351. $dbOrders = DB::table('monthly_dw_order')
  1352. ->whereIn('code', $allCodes)
  1353. ->where('top_depart_id', $user['top_depart_id'])
  1354. ->get()->keyBy('code');
  1355. $errors = [];
  1356. //时间转换
  1357. foreach ($array as $rowIndex => $row) {
  1358. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1359. if ($valMonthRaw === '') continue;
  1360. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1361. if (!$mStatus) {
  1362. $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
  1363. continue;
  1364. }
  1365. // 标准化并存回数组,这样后面的循环直接拿到的就是时间戳
  1366. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1367. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1368. $uniqueMonths[] = $valMonthTs;
  1369. }
  1370. $existingMonthsMap = [];
  1371. if (!empty($uniqueMonths)) {
  1372. $existingMonths = DB::table('monthly_dw_order')
  1373. ->where('top_depart_id', $user['top_depart_id'])
  1374. ->where('del_time', 0)
  1375. ->whereIn('month', array_unique($uniqueMonths))
  1376. ->pluck('month')
  1377. ->toArray();
  1378. $existingMonthsMap = array_fill_keys($existingMonths, true);
  1379. }
  1380. $excelAggregator = [];
  1381. $update_map = [];
  1382. foreach ($array as $rowIndex => $row) {
  1383. // 用户看到的行号(假设数据从第2行开始)
  1384. $displayLine = $rowIndex + 1;
  1385. $valCode = trim($row[$codeIdx] ?? '');
  1386. $valMonthTs = trim($row[$monthIdx] ?? '');
  1387. $valDev = trim($row[$devIdx] ?? '');
  1388. $valStartRaw = $row[$startIdx] ?? '';
  1389. $valEndRaw = $row[$endIdx] ?? '';
  1390. $valNumRaw = $row[$numIdx] ?? 0;
  1391. $valNum2Raw = $row[$num2Idx] ?? 0;
  1392. $valNum3Raw = $row[$num3Idx] ?? 0;
  1393. $valNum4Raw = $row[$num4Idx] ?? 0;
  1394. if (!is_numeric($valMonthTs)) continue;
  1395. // 转换开始日期
  1396. list($sStatus, $startTime) = $this->convertExcelCellToDate($valStartRaw);
  1397. if (!$sStatus) {
  1398. $errors[] = "第{$displayLine}行:开始日期格式错误";
  1399. continue;
  1400. }
  1401. // 转换结束日期
  1402. list($eStatus, $endTime) = $this->convertExcelCellToDate($valEndRaw);
  1403. if (!$eStatus) {
  1404. $errors[] = "第{$displayLine}行:结束日期格式错误";
  1405. continue;
  1406. }
  1407. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1408. // --- B. 校验单据与月份的一致性 ---
  1409. if ($valCode && isset($dbOrders[$valCode])) {
  1410. $dbOrder = $dbOrders[$valCode];
  1411. if ($dbOrder->month != $valMonthTs) {
  1412. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1413. }
  1414. $update_map[$rowIndex] = $dbOrder->id;
  1415. }
  1416. // --- C. 校验设备存在性与单据内唯一性 ---
  1417. if (!isset($dbDevs[$valDev])) {
  1418. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]不存在";
  1419. } else {
  1420. if (isset($excelAggregator[$aggKey]['devs'][$valDev])) {
  1421. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]在同一单据中重复";
  1422. }
  1423. $excelAggregator[$aggKey]['devs'][$valDev] = true;
  1424. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1425. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1426. }
  1427. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1428. }
  1429. // --- D. 逻辑校验:日期范围 ---
  1430. if ($startTime > $endTime) {
  1431. $errors[] = "第{$displayLine}行:开始日期大于结束日期";
  1432. } else {
  1433. $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
  1434. if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
  1435. $errors[] = "第{$displayLine}行:日期超出[ " . date('Y-m', $valMonthTs) . " ]范围";
  1436. }
  1437. }
  1438. // --- E. 月份唯一单据校验 (新增) ---
  1439. if (!$valCode) {
  1440. if (isset($existingMonthsMap[$valMonthTs])) {
  1441. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1442. }
  1443. }
  1444. // --- F. 数字校验
  1445. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  1446. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总天数:" . $res['error'];
  1447. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  1448. if(! $res['valid']) $errors[] = "第{$displayLine}行研发出勤总天数:" . $res['error'];
  1449. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  1450. if(! $res['valid']) $errors[] = "第{$displayLine}行出勤总工时:" . $res['error'];
  1451. $res = $this->checkNumber($valNum4Raw,2,'non-negative');
  1452. if(! $res['valid']) $errors[] = "第{$displayLine}行研发总工时:" . $res['error'];
  1453. // 将转换后的时间戳写回原数组,方便后续写入数据库时直接使用
  1454. $array[$rowIndex][$startIdx] = $startTime;
  1455. $array[$rowIndex][$endIdx] = $endTime;
  1456. }
  1457. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1458. return [$error_string, $update_map, $dbDevs];
  1459. }
  1460. // 人员月度工资单
  1461. public function monthPsOrderImport($array, $user, $other_param)
  1462. {
  1463. $upload = $array[0];
  1464. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1465. if (!$status) return [false, $msg];
  1466. $table_config = $msg;
  1467. unset($array[0]);
  1468. if (empty($array)) return [false, '导入数据不能为空'];
  1469. list($array, $error) = $this->checkCommon($array, $table_config);
  1470. if (!empty($error)) return [0, $error];
  1471. // 2. 详细校验 (这里会返回 update_map 和 dbEmps 映射)
  1472. list($error, $update_map, $dbEmps) = $this->monthPsOrderCheck($array, $user, $table_config);
  1473. if (!empty($error)) return [0, $error];
  1474. $keys = array_column($table_config, 'key');
  1475. $codeIdx = array_search('code', $keys);
  1476. $monthIdx = array_search('month', $keys);
  1477. $empIdx = array_search('employee_id', $keys);
  1478. // --- 步骤 1: 数据聚合分组 ---
  1479. // 将 Excel 数据按“单据”维度归类
  1480. $groups = [];
  1481. foreach ($array as $rowIndex => $row) {
  1482. $cCode = trim($row[$codeIdx] ?? '');
  1483. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  1484. // 聚合 Key:有编码用编码,没编码用月份标记
  1485. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  1486. if (!isset($groups[$aggKey])) {
  1487. $groups[$aggKey] = [
  1488. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  1489. 'code' => $cCode,
  1490. 'month' => $cMonthTs,
  1491. 'details' => []
  1492. ];
  1493. }
  1494. // 准备详情行数据
  1495. $detailTmp = [];
  1496. foreach ($table_config as $k => $conf) {
  1497. if (!$conf['is_main']) {
  1498. $fieldKey = $conf['key'];
  1499. $fieldVal = $row[$k];
  1500. // 如果是人员,转换为 ID
  1501. if ($fieldKey == 'employee_id') {
  1502. $fieldVal = $dbEmps[$fieldVal] ?? 0;
  1503. }
  1504. $detailTmp[$fieldKey] = $fieldVal;
  1505. }
  1506. }
  1507. $groups[$aggKey]['details'][] = $detailTmp;
  1508. }
  1509. // --- 步骤 2: 开启事务写入 ---
  1510. DB::beginTransaction();
  1511. try {
  1512. $time = time();
  1513. foreach ($groups as $aggKey => $group) {
  1514. $mainId = $group['main_id'];
  1515. if ($mainId > 0) {
  1516. // 删除旧详情
  1517. DB::table('monthly_ps_order_details')->where('del_time',0)
  1518. ->where('main_id', $mainId)
  1519. ->update(['del_time' => $time]);
  1520. } else {
  1521. // B. 新增逻辑
  1522. $newCode = $this->generateBillNo([
  1523. 'top_depart_id' => $user['top_depart_id'],
  1524. 'type' => MonthlyPsOrder::Order_type,
  1525. 'period' => date("Ym", $group['month'])
  1526. ]);
  1527. $mainId = DB::table('monthly_ps_order')->insertGetId([
  1528. 'code' => $newCode,
  1529. 'month' => $group['month'],
  1530. 'top_depart_id' => $user['top_depart_id'],
  1531. 'crt_id' => $user['id'],
  1532. 'crt_time' => $time,
  1533. 'upd_time' => $time,
  1534. 'del_time' => 0
  1535. ]);
  1536. }
  1537. // C. 批量插入详情
  1538. $insertDetails = [];
  1539. foreach ($group['details'] as $detail) {
  1540. $detail['main_id'] = $mainId;
  1541. $detail['top_depart_id']= $user['top_depart_id'];
  1542. $detail['crt_time'] = $time;
  1543. $detail['del_time'] = 0;
  1544. $insertDetails[] = $detail;
  1545. }
  1546. if (!empty($insertDetails)) {
  1547. DB::table('monthly_ps_order_details')->insert($insertDetails);
  1548. }
  1549. }
  1550. DB::commit();
  1551. } catch (\Exception $e) {
  1552. DB::rollBack();
  1553. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  1554. }
  1555. return [true, ''];
  1556. }
  1557. private function monthPsOrderCheck(&$array, $user, $table_config)
  1558. {
  1559. $keys = array_column($table_config, 'key');
  1560. $codeIdx = array_search('code', $keys);
  1561. $monthIdx = array_search('month', $keys);
  1562. $empIdx = array_search('employee_id', $keys);
  1563. $numIdx = array_search('salary', $keys);
  1564. $num2Idx = array_search('social_insurance', $keys);
  1565. $num3Idx = array_search('public_housing_fund', $keys);
  1566. // 1. 预加载基础数据
  1567. $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
  1568. $dbEmps = Employee::where('del_time', 0)
  1569. ->whereIn('number', $allEmpNumbers)
  1570. ->where('top_depart_id', $user['top_depart_id'])
  1571. ->pluck('id', 'number')->toArray();
  1572. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  1573. $dbOrders = DB::table('monthly_ps_order')
  1574. ->whereIn('code', $allCodes)
  1575. ->where('top_depart_id', $user['top_depart_id'])
  1576. ->get()->keyBy('code');
  1577. $errors = [];
  1578. //时间转换
  1579. foreach ($array as $rowIndex => $row) {
  1580. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1581. if ($valMonthRaw === '') continue;
  1582. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1583. if (!$mStatus) {
  1584. $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
  1585. continue;
  1586. }
  1587. // 标准化并存回数组,这样后面的循环直接拿到的就是时间戳
  1588. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1589. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1590. $uniqueMonths[] = $valMonthTs;
  1591. }
  1592. $existingMonthsMap = [];
  1593. if (!empty($uniqueMonths)) {
  1594. $existingMonths = DB::table('monthly_ps_order')
  1595. ->where('top_depart_id', $user['top_depart_id'])
  1596. ->where('del_time', 0)
  1597. ->whereIn('month', array_unique($uniqueMonths))
  1598. ->pluck('month')
  1599. ->toArray();
  1600. $existingMonthsMap = array_fill_keys($existingMonths, true);
  1601. }
  1602. $excelAggregator = [];
  1603. $update_map = [];
  1604. foreach ($array as $rowIndex => $row) {
  1605. // 用户看到的行号(假设数据从第2行开始)
  1606. $displayLine = $rowIndex + 1;
  1607. $valCode = trim($row[$codeIdx] ?? '');
  1608. $valMonthTs = trim($row[$monthIdx] ?? '');
  1609. $valEmp = trim($row[$empIdx] ?? '');
  1610. $valNumRaw = $row[$numIdx] ?? 0;
  1611. $valNum2Raw = $row[$num2Idx] ?? 0;
  1612. $valNum3Raw = $row[$num3Idx] ?? 0;
  1613. if (!is_numeric($valMonthTs)) continue;
  1614. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1615. // --- B. 校验单据与月份的一致性 ---
  1616. if ($valCode && isset($dbOrders[$valCode])) {
  1617. $dbOrder = $dbOrders[$valCode];
  1618. if ($dbOrder->month != $valMonthTs) {
  1619. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1620. }
  1621. $update_map[$rowIndex] = $dbOrder->id;
  1622. }
  1623. // --- C. 校验人员存在性与单据内唯一性 ---
  1624. if (!isset($dbEmps[$valEmp])) {
  1625. $errors[] = "第{$displayLine}行:人员工号[{$valEmp}]不存在";
  1626. } else {
  1627. if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
  1628. $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同一单据中重复";
  1629. }
  1630. $excelAggregator[$aggKey]['emps'][$valEmp] = true;
  1631. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1632. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1633. }
  1634. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1635. }
  1636. // --- E. 月份唯一单据校验 (新增) ---
  1637. if (!$valCode) {
  1638. if (isset($existingMonthsMap[$valMonthTs])) {
  1639. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1640. }
  1641. }
  1642. // --- F. 数字校验
  1643. $res = $this->checkNumber($valNumRaw,0,'non-negative');
  1644. if(! $res['valid']) $errors[] = "第{$displayLine}行工资总额:" . $res['error'];
  1645. $res = $this->checkNumber($valNum2Raw,0,'non-negative');
  1646. if(! $res['valid']) $errors[] = "第{$displayLine}行社保:" . $res['error'];
  1647. $res = $this->checkNumber($valNum3Raw,2,'non-negative');
  1648. if(! $res['valid']) $errors[] = "第{$displayLine}行公积金:" . $res['error'];
  1649. }
  1650. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1651. return [$error_string, $update_map, $dbEmps];
  1652. }
  1653. // 设备月度折旧单
  1654. public function monthDdOrderImport($array, $user, $other_param)
  1655. {
  1656. $upload = $array[0];
  1657. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1658. if (!$status) return [false, $msg];
  1659. $table_config = $msg;
  1660. unset($array[0]);
  1661. if (empty($array)) return [false, '导入数据不能为空'];
  1662. list($array, $error) = $this->checkCommon($array, $table_config);
  1663. if (!empty($error)) return [0, $error];
  1664. // 2. 详细校验 (这里会返回 update_map 和 dbDevs 映射)
  1665. list($error, $update_map, $dbDevs) = $this->monthDdOrderCheck($array, $user, $table_config);
  1666. if (!empty($error)) return [0, $error];
  1667. $keys = array_column($table_config, 'key');
  1668. $codeIdx = array_search('code', $keys);
  1669. $monthIdx = array_search('month', $keys);
  1670. $empIdx = array_search('device_id', $keys);
  1671. // --- 步骤 1: 数据聚合分组 ---
  1672. // 将 Excel 数据按“单据”维度归类
  1673. $groups = [];
  1674. foreach ($array as $rowIndex => $row) {
  1675. $cCode = trim($row[$codeIdx] ?? '');
  1676. $cMonthTs = $row[$monthIdx]; // 校验阶段已转为时间戳
  1677. // 聚合 Key:有编码用编码,没编码用月份标记
  1678. $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
  1679. if (!isset($groups[$aggKey])) {
  1680. $groups[$aggKey] = [
  1681. 'main_id' => $update_map[$rowIndex] ?? 0, // 如果存在则是编辑
  1682. 'code' => $cCode,
  1683. 'month' => $cMonthTs,
  1684. 'details' => []
  1685. ];
  1686. }
  1687. // 准备详情行数据
  1688. $detailTmp = [];
  1689. foreach ($table_config as $k => $conf) {
  1690. if (!$conf['is_main']) {
  1691. $fieldKey = $conf['key'];
  1692. $fieldVal = $row[$k];
  1693. // 如果是人员,转换为 ID
  1694. if ($fieldKey == 'device_id') {
  1695. $fieldVal = $dbDevs[$fieldVal] ?? 0;
  1696. }
  1697. $detailTmp[$fieldKey] = $fieldVal;
  1698. }
  1699. }
  1700. $groups[$aggKey]['details'][] = $detailTmp;
  1701. }
  1702. // --- 步骤 2: 开启事务写入 ---
  1703. DB::beginTransaction();
  1704. try {
  1705. $time = time();
  1706. foreach ($groups as $aggKey => $group) {
  1707. $mainId = $group['main_id'];
  1708. if ($mainId > 0) {
  1709. // 删除旧详情
  1710. DB::table('monthly_dd_order_details')->where('del_time',0)
  1711. ->where('main_id', $mainId)
  1712. ->update(['del_time' => $time]);
  1713. } else {
  1714. // B. 新增逻辑
  1715. $newCode = $this->generateBillNo([
  1716. 'top_depart_id' => $user['top_depart_id'],
  1717. 'type' => MonthlyDdOrder::Order_type,
  1718. 'period' => date("Ym", $group['month'])
  1719. ]);
  1720. $mainId = DB::table('monthly_dd_order')->insertGetId([
  1721. 'code' => $newCode,
  1722. 'month' => $group['month'],
  1723. 'top_depart_id' => $user['top_depart_id'],
  1724. 'crt_id' => $user['id'],
  1725. 'crt_time' => $time,
  1726. 'upd_time' => $time,
  1727. 'del_time' => 0
  1728. ]);
  1729. }
  1730. // C. 批量插入详情
  1731. $insertDetails = [];
  1732. foreach ($group['details'] as $detail) {
  1733. $detail['main_id'] = $mainId;
  1734. $detail['top_depart_id']= $user['top_depart_id'];
  1735. $detail['crt_time'] = $time;
  1736. $detail['del_time'] = 0;
  1737. $insertDetails[] = $detail;
  1738. }
  1739. if (!empty($insertDetails)) {
  1740. DB::table('monthly_dd_order_details')->insert($insertDetails);
  1741. }
  1742. }
  1743. DB::commit();
  1744. } catch (\Exception $e) {
  1745. DB::rollBack();
  1746. return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
  1747. }
  1748. return [true, ''];
  1749. }
  1750. private function monthDdOrderCheck(&$array, $user, $table_config)
  1751. {
  1752. $keys = array_column($table_config, 'key');
  1753. $codeIdx = array_search('code', $keys);
  1754. $monthIdx = array_search('month', $keys);
  1755. $devIdx = array_search('device_id', $keys);
  1756. $numIdx = array_search('depreciation_amount', $keys);
  1757. // 1. 预加载基础数据
  1758. $allEmpNumbers = array_filter(array_unique(array_column($array, $devIdx)));
  1759. $dbDevs = Device::where('del_time', 0)
  1760. ->whereIn('code', $allEmpNumbers)
  1761. ->where('top_depart_id', $user['top_depart_id'])
  1762. ->pluck('id', 'code')->toArray();
  1763. $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
  1764. $dbOrders = DB::table('monthly_dd_order')
  1765. ->whereIn('code', $allCodes)
  1766. ->where('top_depart_id', $user['top_depart_id'])
  1767. ->get()->keyBy('code');
  1768. $errors = [];
  1769. //时间转换
  1770. foreach ($array as $rowIndex => $row) {
  1771. $valMonthRaw = trim($row[$monthIdx] ?? '');
  1772. if ($valMonthRaw === '') continue;
  1773. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
  1774. if (!$mStatus) {
  1775. $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
  1776. continue;
  1777. }
  1778. // 标准化并存回数组,这样后面的循环直接拿到的就是时间戳
  1779. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1780. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1781. $uniqueMonths[] = $valMonthTs;
  1782. }
  1783. $existingMonthsMap = [];
  1784. if (!empty($uniqueMonths)) {
  1785. $existingMonths = DB::table('monthly_dd_order')
  1786. ->where('top_depart_id', $user['top_depart_id'])
  1787. ->where('del_time', 0)
  1788. ->whereIn('month', array_unique($uniqueMonths))
  1789. ->pluck('month')
  1790. ->toArray();
  1791. $existingMonthsMap = array_fill_keys($existingMonths, true);
  1792. }
  1793. $excelAggregator = [];
  1794. $update_map = [];
  1795. foreach ($array as $rowIndex => $row) {
  1796. // 用户看到的行号(假设数据从第2行开始)
  1797. $displayLine = $rowIndex + 1;
  1798. $valCode = trim($row[$codeIdx] ?? '');
  1799. $valMonthTs = trim($row[$monthIdx] ?? '');
  1800. $valDev = trim($row[$devIdx] ?? '');
  1801. $valNumRaw = $row[$numIdx] ?? 0;
  1802. if (!is_numeric($valMonthTs)) continue;
  1803. $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
  1804. // --- B. 校验单据与月份的一致性 ---
  1805. if ($valCode && isset($dbOrders[$valCode])) {
  1806. $dbOrder = $dbOrders[$valCode];
  1807. if ($dbOrder->month != $valMonthTs) {
  1808. $errors[] = "第{$displayLine}行:单据编码[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与导入月份不符";
  1809. }
  1810. $update_map[$rowIndex] = $dbOrder->id;
  1811. }
  1812. // --- C. 校验设备存在性与单据内唯一性 ---
  1813. if (!isset($dbDevs[$valDev])) {
  1814. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]不存在";
  1815. } else {
  1816. if (isset($excelAggregator[$aggKey]['devs'][$valDev])) {
  1817. $errors[] = "第{$displayLine}行:设备的资产编码[{$valDev}]在同一单据中重复";
  1818. }
  1819. $excelAggregator[$aggKey]['devs'][$valDev] = true;
  1820. if (isset($excelAggregator[$aggKey]['month_ts']) && $excelAggregator[$aggKey]['month_ts'] !== $valMonthTs) {
  1821. $errors[] = "第{$displayLine}行:同组数据的月份不统一";
  1822. }
  1823. $excelAggregator[$aggKey]['month_ts'] = $valMonthTs;
  1824. }
  1825. // --- E. 月份唯一单据校验 (新增) ---
  1826. if (!$valCode) {
  1827. if (isset($existingMonthsMap[$valMonthTs])) {
  1828. $errors[] = "第{$displayLine}行:月份[ " . date('Y-m', $valMonthTs) . " ]已存在单据,请填编码编辑";
  1829. }
  1830. }
  1831. // --- F. 数字校验
  1832. $res = $this->checkNumber($valNumRaw,2,'non-negative');
  1833. if(! $res['valid']) $errors[] = "第{$displayLine}行月折旧额:" . $res['error'];
  1834. }
  1835. $error_string = !empty($errors) ? implode('|', $errors) : "";
  1836. return [$error_string, $update_map, $dbDevs];
  1837. }
  1838. // 规则配置单
  1839. public function ruleSetImport($array, $user, $other_param){
  1840. $upload = $array[0];
  1841. list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
  1842. if (!$status) return [false, $msg];
  1843. $table_config = $msg;
  1844. unset($array[0]);
  1845. if (empty($array)) return [false, '导入数据不能为空'];
  1846. list($array, $error) = $this->checkCommon($array, $table_config);
  1847. if (!empty($error)) return [0, $error];
  1848. // 2. 详细校验
  1849. list($error, $update_map, $dbDevs) = $this->ruleSetCheck($array, $user, $table_config);
  1850. if (!empty($error)) return [0, $error];
  1851. $keys = array_column($table_config, 'key');
  1852. $codeIdx = array_search('code', $keys);
  1853. $monthIdx = array_search('month', $keys);
  1854. $rateIdx = array_search('rate', $keys);
  1855. $groups = [];
  1856. foreach ($array as $rowIndex => $row) {
  1857. $mCode = trim($row[$codeIdx] ?? '');
  1858. $month = $row[$monthIdx];
  1859. $aggKey = $mCode ?: "NEW_" . $month;
  1860. if (!isset($groups[$aggKey])) {
  1861. $groups[$aggKey] = [
  1862. 'id' => $update_map[$rowIndex] ?? 0,
  1863. 'month' => $month,
  1864. 'details' => []
  1865. ];
  1866. }
  1867. $groups[$aggKey]['details'][] = [
  1868. 'item_id' => $row['parsed_item_id'],
  1869. 'data_id' => $row['parsed_data_id'],
  1870. 'type' => $row['parsed_type'],
  1871. 'rate' => $row[$rateIdx],
  1872. ];
  1873. }
  1874. $time = time();
  1875. DB::beginTransaction();
  1876. try {
  1877. foreach ($groups as $group) {
  1878. $mainId = $group['id'];
  1879. if ($mainId) {
  1880. DB::table('rule_set_details')
  1881. ->where('del_time',0)
  1882. ->where('main_id', $mainId)->update(['del_time' => $time]);
  1883. } else {
  1884. $newCode = $this->generateBillNo([
  1885. 'top_depart_id' => $user['top_depart_id'],
  1886. 'type' => RuleSet::Order_type,
  1887. 'period' => date("Ym", $group['month'])
  1888. ]);
  1889. $mainId = DB::table('rule_set')->insertGetId([
  1890. 'code' => $newCode,
  1891. 'month' => $group['month'],
  1892. 'top_depart_id' => $user['top_depart_id'],
  1893. 'crt_id' => $user['id'],
  1894. 'crt_time' => $time
  1895. ]);
  1896. }
  1897. foreach ($group['details'] as &$d) {
  1898. $d['main_id'] = $mainId;
  1899. $d['top_depart_id'] = $user['top_depart_id'];
  1900. $d['crt_time'] = $time;
  1901. }
  1902. DB::table('rule_set_details')->insert($group['details']);
  1903. }
  1904. DB::commit();
  1905. } catch (\Exception $e) {
  1906. DB::rollBack();
  1907. return [false, "失败: " . $e->getMessage()];
  1908. }
  1909. return [true, ''];
  1910. }
  1911. private function ruleSetCheck(&$array, $user, $table_config)
  1912. {
  1913. $keys = array_column($table_config, 'key');
  1914. // 1. 索引提取
  1915. $mCodeIdx = array_search('code', $keys); // 主表单号
  1916. $monthIdx = array_search('month', $keys); // 月份
  1917. $typeIdx = array_search('type', $keys); // 明细类型
  1918. $dCodeIdx = array_search('code_2', $keys); // 明细编码 (工号或资产编号)
  1919. $itemIdIdx = array_search('item_id', $keys); // 项目编码
  1920. $rateIdx = array_search('rate', $keys); // 比例
  1921. // 预处理扫描变量
  1922. $uniqueMonths = [];
  1923. $empCodes = [];
  1924. $devCodes = [];
  1925. $itemCodes = [];
  1926. $type_map = array_flip(RuleSetDetails::$type_name);
  1927. // --- 步骤 1: 预处理扫描 ---
  1928. foreach ($array as $rowIndex => $row) {
  1929. // 月份预转
  1930. list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($row[$monthIdx] ?? '');
  1931. if ($mStatus) {
  1932. $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
  1933. $array[$rowIndex][$monthIdx] = $valMonthTs;
  1934. $uniqueMonths[] = $valMonthTs;
  1935. }
  1936. $type = $type_map[$row[$typeIdx]] ?? 0;
  1937. $dCode = trim($row[$dCodeIdx] ?? '');
  1938. $iCode = trim($row[$itemIdIdx] ?? '');
  1939. if ($type == RuleSetDetails::type_one && $dCode !== '') $empCodes[] = $dCode;
  1940. if ($type == RuleSetDetails::type_two && $dCode !== '') $devCodes[] = $dCode;
  1941. if ($iCode !== '') $itemCodes[] = $iCode;
  1942. }
  1943. // --- 步骤 2: 批量预加载数据库数据 ---
  1944. $dbEmps = Employee::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
  1945. ->whereIn('number', array_unique($empCodes))->pluck('id', 'number')->toArray();
  1946. $dbDevs = Device::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
  1947. ->whereIn('code', array_unique($devCodes))->pluck('id', 'code')->toArray();
  1948. $dbItems = Item::where('del_time', 0)->where('top_depart_id', $user['top_depart_id'])
  1949. ->whereIn('code', array_unique($itemCodes))->pluck('id', 'code')->toArray();
  1950. // 加载已有单据
  1951. $allMCodes = array_filter(array_unique(array_column($array, $mCodeIdx)));
  1952. $dbOrders = DB::table('rule_set')->where('top_depart_id', $user['top_depart_id'])
  1953. ->whereIn('code', $allMCodes)->get()->keyBy('code');
  1954. $existingMonthsMap = DB::table('rule_set')->where('top_depart_id', $user['top_depart_id'])
  1955. ->whereIn('month', array_unique($uniqueMonths))->pluck('id', 'month')->toArray();
  1956. $errors = [];
  1957. $update_map = [];
  1958. $aggregator = []; // 用于校验分摊比例之和
  1959. $uniqueCheck = []; // 用于校验同一个单据+项目内 人/设备唯一性
  1960. // --- 步骤 3: 详细校验循环 ---
  1961. foreach ($array as $rowIndex => $row) {
  1962. $displayLine = $rowIndex + 1;
  1963. $valMCode = trim($row[$mCodeIdx] ?? '');
  1964. $valMonthTs = $row[$monthIdx];
  1965. $typeTitle = trim($row[$typeIdx] ?? '');
  1966. $type = $type_map[$typeTitle] ?? 0;
  1967. $valDCode = trim($row[$dCodeIdx] ?? '');
  1968. $valICode = trim($row[$itemIdIdx] ?? '');
  1969. $valRate = $row[$rateIdx];
  1970. if (!is_numeric($valMonthTs)) {
  1971. $errors[] = "第{$displayLine}行:月份格式非法";
  1972. continue;
  1973. }
  1974. // A. 编辑/新增逻辑校验
  1975. if ($valMCode !== '') {
  1976. if (!isset($dbOrders[$valMCode])) {
  1977. $errors[] = "第{$displayLine}行:系统不存在单号[{$valMCode}]";
  1978. } else {
  1979. if ($dbOrders[$valMCode]->month != $valMonthTs) {
  1980. $errors[] = "第{$displayLine}行:单号{$valMCode}编辑时不允许修改月份";
  1981. }
  1982. $update_map[$rowIndex] = $dbOrders[$valMCode]->id;
  1983. }
  1984. } else {
  1985. if (isset($existingMonthsMap[$valMonthTs])) {
  1986. $errors[] = "第{$displayLine}行:月份[" . date('Y-m', $valMonthTs) . "]已存在配置单,请填写单号进行编辑";
  1987. }
  1988. }
  1989. // B. 人员/设备/项目存在性校验
  1990. $dataId = 0;
  1991. if ($type == RuleSetDetails::type_one) {
  1992. $dataId = $dbEmps[$valDCode] ?? 0;
  1993. if (!$dataId) $errors[] = "第{$displayLine}行:工号[{$valDCode}]不存在";
  1994. } elseif ($type == RuleSetDetails::type_two) {
  1995. $dataId = $dbDevs[$valDCode] ?? 0;
  1996. if (!$dataId) $errors[] = "第{$displayLine}行:资产编码[{$valDCode}]不存在";
  1997. } else {
  1998. $errors[] = "第{$displayLine}行:明细类型错误";
  1999. }
  2000. $itemId = $dbItems[$valICode] ?? 0;
  2001. if (!$itemId) $errors[] = "第{$displayLine}行:项目编码[{$valICode}]不存在";
  2002. // C. 比例校验
  2003. $res = $this->checkNumber($valRate, 2, 'positive');
  2004. if (!$res['valid']) {
  2005. $errors[] = "第{$displayLine}行:比例{$res['error']}";
  2006. }
  2007. // --- D. 修正后的聚合校验 ---
  2008. if ($dataId && $itemId) {
  2009. // 当前业务聚合的 Key(以单号或月份区分单据)
  2010. $billKey = $valMCode ?: "MONTH_" . $valMonthTs;
  2011. // D1. 唯一性:同一个单据里,同一个项目,同一个[人/设备]只能出现一次
  2012. $rowUniqueKey = $billKey . '_' . $itemId . '_' . $type . '_' . $dataId;
  2013. if (isset($uniqueCheck[$rowUniqueKey])) {
  2014. $errors[] = "第{$displayLine}行:同一规则配置单,项目[{$valICode}]下人或设备不能重复录入";
  2015. }
  2016. $uniqueCheck[$rowUniqueKey] = true;
  2017. // D2. 比例和:同一个单据里,同一个人/设备在所有项目里的比例之和必须为100%
  2018. $totalRateKey = $billKey . '_' . $type . '_' . $dataId;
  2019. $aggregator[$totalRateKey]['total_rate'] = ($aggregator[$totalRateKey]['total_rate'] ?? 0) + $valRate;
  2020. $aggregator[$totalRateKey]['title'] = $valDCode;
  2021. $aggregator[$totalRateKey]['type_name'] = $typeTitle;
  2022. $aggregator[$totalRateKey]['month_str'] = date('Y-m', $valMonthTs); // 记录月份供报错使用
  2023. }
  2024. // 存入处理后的结果
  2025. $array[$rowIndex]['parsed_data_id'] = $dataId;
  2026. $array[$rowIndex]['parsed_item_id'] = $itemId;
  2027. $array[$rowIndex]['parsed_type'] = $type;
  2028. }
  2029. // --- 步骤 4: 比例汇总校验 ---
  2030. foreach ($aggregator as $key => $info) {
  2031. if (abs($info['total_rate'] - 100) > 0.0001) {
  2032. $errors[] = "汇总错误:月份[{$info['month_str']}]下,{$info['type_name']}[{$info['title']}]的所有项目分摊比例总和为 {$info['total_rate']}%,不等于 100%"; }
  2033. }
  2034. $error_string = !empty($errors) ? implode('|', $errors) : "";
  2035. return [$error_string, $update_map, $dbItems];
  2036. }
  2037. //公共校验 -----------------------------------------
  2038. private function checkCommon($array, $table_config) {
  2039. $error = [];
  2040. $uniqueCheck = []; // 格式:[$column_index => [$value => $first_line]]
  2041. foreach ($array as $line => $row) {
  2042. $rowData = array_filter($row);
  2043. if (empty($rowData)) {
  2044. unset($array[$line]);
  2045. continue;
  2046. }
  2047. foreach ($row as $colIndex => $value) {
  2048. $value = trim($value);
  2049. $config = $table_config[$colIndex] ?? null;
  2050. // 1. 基础存在性检查
  2051. if (!$config) {
  2052. $error[] = "第{$line}行第{$colIndex}列配置不存在";
  2053. continue;
  2054. }
  2055. $fieldName = $config['value'];
  2056. // 2. 必填校验
  2057. if (!empty($config['required']) && ($value === '' || !isset($value))) {
  2058. $error[] = "第{$line}行:[{$fieldName}] 必填";
  2059. }
  2060. // 3. 默认值填充
  2061. if ($value === '' && isset($config['default'])) {
  2062. $value = $config['default'];
  2063. }
  2064. // 4. 唯一性校验(重点:一次遍历解决)
  2065. if (!empty($config['unique']) && $value !== '') {
  2066. if (isset($uniqueCheck[$colIndex][$value])) {
  2067. $prevLine = $uniqueCheck[$colIndex][$value];
  2068. $error[] = "第{$line}行:[{$fieldName}] 与第{$prevLine}行重复,重复值:{$value}";
  2069. } else {
  2070. // 记录该值第一次出现的位置
  2071. $uniqueCheck[$colIndex][$value] = $line;
  2072. }
  2073. }
  2074. $row[$colIndex] = $value;
  2075. }
  2076. $array[$line] = $row;
  2077. }
  2078. $error_string = !empty($error) ? implode('|', $error) : "";
  2079. return [$array, $error_string];
  2080. }
  2081. //模板校验 -----------------------------------------
  2082. private function compareTableAndReturn($upload, $param){
  2083. if(empty($upload)) return [false, '表头不能为空'];
  2084. $config = $this->getTableConfig($param['type']);
  2085. $config_array = $config['array'];
  2086. if(empty($config_array)) return [false, '导入配置表头文件不存在'];
  2087. foreach ($config_array as $key => $value){
  2088. $key_position = $key + 1;
  2089. if(! isset($upload[$key])) return [false, "第" . $key_position . "列表头缺失"];
  2090. $tmp_v = trim($upload[$key]);
  2091. if($tmp_v != $value['value']) return [false, "第" . $key_position . "列表头与模板不符合,请重新下载模板"];
  2092. }
  2093. return [true, $config_array];
  2094. }
  2095. //转换日期 ------------------------------------------
  2096. function convertExcelCellToDate($cellValue) {
  2097. // 尝试将单元格值转换为浮点数(Excel 日期序列号)
  2098. $excelTimestamp = filter_var($cellValue, FILTER_VALIDATE_FLOAT);
  2099. if ($excelTimestamp !== false && $excelTimestamp > 0) {
  2100. // 如果成功转换并且值大于0,则认为是Excel日期序列号
  2101. try {
  2102. $dateTimeObject = Date::excelToDateTimeObject($cellValue);
  2103. // if ($dateTimeObject->format('H:i:s') === '00:00:00') {
  2104. // // 如果是,则将时间设置为 '23:59:00'
  2105. // $dateTimeObject->setTime(23, 59);
  2106. // }
  2107. // 现在你可以格式化这个日期了
  2108. $formattedDate = $dateTimeObject->format('Y-m-d');
  2109. if(! strtotime($formattedDate)) return [false, ''];
  2110. return [true, strtotime($formattedDate)];
  2111. } catch (\Exception $e) {
  2112. // 处理转换失败的情况
  2113. return [false, '单元格日期格式转换时间戳失败'];
  2114. }
  2115. }
  2116. // 如果不是有效的浮点数,则尝试按照多种日期格式解析
  2117. if(! strtotime($cellValue)) return [false, '单元格文本格式转换时间戳失败'];
  2118. return [true, strtotime($cellValue)];
  2119. }
  2120. }