UnifiedCostCalculationService.php 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. use think\Exception;
  5. use think\Log;
  6. /**
  7. * 统一成本核算服务类
  8. * 五项计算完成后统一插入数据库
  9. */
  10. class UnifiedCostCalculationService
  11. {
  12. // 存储中间数据的数组
  13. protected $monthlyCostDetails = [];
  14. protected $allocationFactors = [];
  15. // 配置常量
  16. const FLOOR_GROUP_MAP = [
  17. '1' => ['02、胶印机组', '03、卷凹机组', '06、单凹机组', '05、圆切机组', '04、圆烫机组', '10、模切机组', '09、烫金机组'],
  18. '2' => ['01、切纸机组', '11、检品机组', '07、丝印机组', '12、覆膜机组', '08、喷码机组'],
  19. ];
  20. // 科目名称映射
  21. const SUBJECT_MAPPING = [
  22. '废气处理' => '废气处理',
  23. '锅炉' => '锅炉',
  24. '空压机' => '空压机',
  25. '热水锅炉' => '热水锅炉',
  26. '真空鼓风机' => '真空鼓风机',
  27. '中央空调' => '中央空调',
  28. '待分摊总额' => '待分摊总额',
  29. ];
  30. // 字段默认值
  31. const DEFAULT_FIELD_VALUES = [
  32. '直接水电' => 0,
  33. '分摊材料' => 0,
  34. '车间人工' => 0,
  35. '部门人工附加' => 0,
  36. '分摊水电' => 0,
  37. '废气处理' => 0,
  38. '锅炉' => 0,
  39. '空压机' => 0,
  40. '热水锅炉' => 0,
  41. '真空鼓风机' => 0,
  42. '中央空调' => 0,
  43. '分摊其他' => 0,
  44. 'A_小时折旧额' => 0,
  45. '直接材料' => 0,
  46. '考核直接材料' => 0,
  47. '直接折旧' => 0,
  48. '分摊折旧' => 0,
  49. '场地租金' => 0,
  50. '成本合计' => 0,
  51. ];
  52. // 添加科目名称到数据库字段的映射
  53. const SUBJECT_TO_FIELD_MAP = [
  54. // 真空鼓风机相关
  55. '真空鼓风机' => '真空鼓风机',
  56. '真空鼓风' => '真空鼓风机',
  57. '6#楼真空鼓风' => '真空鼓风机',
  58. '真空风机' => '真空鼓风机',
  59. // 废气处理相关
  60. '废气处理' => '废气处理',
  61. '废气' => '废气处理',
  62. // 锅炉相关
  63. '锅炉' => '锅炉',
  64. '热水锅炉' => '热水锅炉',
  65. // 空压机相关
  66. '空压机' => '空压机',
  67. '空压' => '空压机',
  68. // 中央空调相关
  69. '中央空调' => '中央空调',
  70. '空调' => '中央空调',
  71. // 待分摊总额
  72. '待分摊总额' => '分摊水电',
  73. '分摊总额' => '分摊水电',
  74. ];
  75. // 批次大小
  76. const BATCH_SIZE = 100;
  77. /**
  78. * 主入口:执行所有成本计算并统一入库
  79. */
  80. public function calculateAndSaveAll(array $param): array
  81. {
  82. Db::startTrans();
  83. try {
  84. if (!isset($param['month'])) {
  85. throw new Exception("缺少必要参数: month");
  86. }
  87. $month = $param['month'];
  88. $sysId = $param['sys_id'] ?? uniqid();
  89. // 重置数据数组,确保是空数组
  90. $this->monthlyCostDetails = [];
  91. $this->allocationFactors = [];
  92. // 1. 清空旧数据
  93. $this->clearOldData($month);
  94. // 2. 执行五项计算
  95. $this->calculateDirectLabor($param);
  96. Log::info("直接人工计算完成", ['记录数' => is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 0]);
  97. $this->calculateDirectUtilities($param);
  98. $this->calculateIndirectMaterials($month);
  99. $this->calculateIndirectLabor($month);
  100. $this->calculateApportionedUtilities($param);
  101. // 3. 统一插入数据
  102. $this->saveAllData($month, $sysId);
  103. Db::commit();
  104. $this->logSuccess($month);
  105. return $this->buildSuccessResponse($month);
  106. } catch (\Throwable $t) {
  107. Db::rollback();
  108. // 直接记录错误,不通过 logError 方法
  109. try {
  110. error_log("成本核算失败: " . $t->getMessage() . " at " . $t->getFile() . ":" . $t->getLine());
  111. error_log("堆栈跟踪: " . $t->getTraceAsString());
  112. // 尝试记录到系统日志
  113. if (class_exists('think\Log')) {
  114. Log::error("成本核算失败(直接记录)", [
  115. 'message' => $t->getMessage(),
  116. 'file' => $t->getFile(),
  117. 'line' => $t->getLine()
  118. ]);
  119. }
  120. } catch (\Throwable $logError) {
  121. // 忽略日志记录错误
  122. }
  123. return [
  124. 'success' => false,
  125. 'message' => '成本核算失败: ' . $t->getMessage(),
  126. 'error' => $t->getMessage(),
  127. 'file' => $t->getFile(),
  128. 'line' => $t->getLine()
  129. ];
  130. }
  131. }
  132. /**
  133. * 清空旧数据
  134. */
  135. protected function clearOldData(string $month): void
  136. {
  137. // 确保数据数组是有效的数组
  138. $this->monthlyCostDetails = [];
  139. $this->allocationFactors = [];
  140. // 记录清空操作
  141. Log::info("清空旧数据", ['month' => $month]);
  142. }
  143. /**
  144. * 1. 计算直接人工
  145. */
  146. protected function calculateDirectLabor(array $param): void
  147. {
  148. try {
  149. $month = $param['month'];
  150. $sysId = $param['sys_id'] ?? '';
  151. $now = date('Y-m-d H:i:s');
  152. $list = Db::name('绩效工资汇总')
  153. ->alias('a')
  154. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  155. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  156. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  157. ->field($this->getDirectLaborFields())
  158. ->where('a.sys_ny', $month)
  159. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  160. ->select();
  161. // 转换为数组(如果返回的是对象)
  162. if (!is_array($list)) {
  163. $list = $list->toArray();
  164. }
  165. Log::info("直接人工查询结果数: " . count($list));
  166. foreach ($list as $item) {
  167. // 确保 $item 是数组
  168. $itemArray = is_object($item) ? (array)$item : $item;
  169. if (!is_array($itemArray)) {
  170. Log::warning("直接人工数据项格式不正确: " . gettype($item));
  171. continue;
  172. }
  173. $this->monthlyCostDetails[] = $this->buildMonthlyCostDetail($itemArray, $month, $sysId, $now);
  174. }
  175. } catch (\Exception $e) {
  176. Log::error("计算直接人工失败: " . $e->getMessage());
  177. throw new Exception("直接人工计算失败: " . $e->getMessage());
  178. }
  179. }
  180. /**
  181. * 获取直接人工查询字段
  182. */
  183. protected function getDirectLaborFields(): string
  184. {
  185. return 'a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,
  186. sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  187. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,
  188. a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,
  189. a.sys_rq as 年月,a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,
  190. a.千件工价';
  191. }
  192. /**
  193. * 构建月度成本明细记录
  194. */
  195. protected function buildMonthlyCostDetail(array $data, string $month, string $sysId, string $now): array
  196. {
  197. $banju = $data['版距'] === '0.0' ? 1000 : $data['版距'];
  198. $moshushu = $this->calculateMoshushu($data);
  199. $chanliang = $this->calculateChanliang($data);
  200. $renGongFenTan = $this->calculateRenGongFenTan($chanliang, $data);
  201. return array_merge([
  202. '车间名称' => $data['使用部门'] ?? '',
  203. 'sys_ny' => $month,
  204. 'sczl_gdbh' => $data['工单编号'],
  205. '印件名称' => $data['印件名称'] ?? '',
  206. 'sczl_yjno' => $data['印件号'],
  207. 'sczl_gxh' => $data['工序号'],
  208. '工序名称' => $data['工序名称'] ?? '',
  209. 'sczl_jtbh' => $data['sczl_jtbh'] ?? '',
  210. '卷张换算系数' => floatval($banju) / 1000,
  211. '占用机时' => floatval($data['占用机时']) ?? 0,
  212. '班组车头产量' => floatval($data['班组车头产量']) ?? 0,
  213. 'sczl_ms' => floatval($moshushu),
  214. '工序难度系数' => floatval($data['工序难度系数']) ?? 1,
  215. '班组换算产量' => floatval($data['班组换算产量']) ?? 0,
  216. '千件工价' => floatval($data['千件工价']) ?? 0,
  217. '计件产量' => floatval($chanliang),
  218. '水电分摊因子' => floatval($data['占用机时']) ?? 0,
  219. '材料分摊因子' => floatval($chanliang),
  220. '人工分摊因子' => floatval($renGongFenTan),
  221. 'Sys_id' => $sysId,
  222. 'Sys_rq' => $now
  223. ], self::DEFAULT_FIELD_VALUES);
  224. }
  225. /**
  226. * 计算墨色数
  227. */
  228. protected function calculateMoshushu(array $data): float
  229. {
  230. if (strpos($data['工序名称'] ?? '', '切废') !== false) {
  231. return 0.2;
  232. }
  233. return $data['墨色数'] === '0.00' ? 1.0 : floatval($data['墨色数']);
  234. }
  235. /**
  236. * 计算产量
  237. */
  238. protected function calculateChanliang(array $data): float
  239. {
  240. return ($data['班组车头产量'] * $data['工序难度系数']) + $data['班组换算产量'];
  241. }
  242. /**
  243. * 计算人工分摊
  244. */
  245. protected function calculateRenGongFenTan(float $chanliang, array $data): float
  246. {
  247. return ($chanliang / 1000) * $data['千件工价'];
  248. }
  249. /**
  250. * 2. 计算直接水电
  251. */
  252. protected function calculateDirectUtilities(array $param): void
  253. {
  254. $month = $param['month'];
  255. if (empty($this->monthlyCostDetails)) {
  256. throw new Exception("请先执行直接人工计算");
  257. }
  258. $utilityData = $this->fetchDirectUtilities($month);
  259. if (empty($utilityData)) {
  260. Log::info("{$month}月份未找到直接水电费用数据");
  261. return;
  262. }
  263. $machineUtilities = $this->calculateMachineUtilities($utilityData);
  264. $workOrderHours = $this->getMachineWorkHours();
  265. $machineTotalHours = $this->calculateMachineTotalHours($workOrderHours);
  266. $allocationResults = $this->allocateUtilitiesToWorkOrders(
  267. $machineUtilities,
  268. $workOrderHours,
  269. $machineTotalHours
  270. );
  271. $this->updateDirectUtilitiesToCostDetails($allocationResults);
  272. }
  273. /**
  274. * 获取直接水电数据
  275. */
  276. protected function fetchDirectUtilities(string $month): array
  277. {
  278. return Db::name('成本_各月水电气')
  279. ->where('Sys_ny', $month)
  280. ->where('费用类型', '直接')
  281. ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价')
  282. ->select();
  283. }
  284. /**
  285. * 计算机台水电费用
  286. */
  287. protected function calculateMachineUtilities(array $utilityData): array
  288. {
  289. $machineUtilities = [];
  290. foreach ($utilityData as $item) {
  291. $machineCode = $item['设备编号'] ?? '';
  292. if (empty($machineCode)) {
  293. continue;
  294. }
  295. $electricityCost = $this->calculateElectricityCost($item);
  296. $gasCost = $this->calculateGasCost($item);
  297. $totalCost = $electricityCost + $gasCost;
  298. if (!isset($machineUtilities[$machineCode])) {
  299. $machineUtilities[$machineCode] = [
  300. '机器编号' => $machineCode,
  301. '部门名称' => $item['部门名称'] ?? '',
  302. '总费用' => 0,
  303. '电费' => 0,
  304. '气费' => 0,
  305. ];
  306. }
  307. $machineUtilities[$machineCode]['总费用'] += $totalCost;
  308. $machineUtilities[$machineCode]['电费'] += $electricityCost;
  309. $machineUtilities[$machineCode]['气费'] += $gasCost;
  310. }
  311. return $machineUtilities;
  312. }
  313. /**
  314. * 计算电费
  315. */
  316. protected function calculateElectricityCost(array $item): float
  317. {
  318. return round(floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0), 2);
  319. }
  320. /**
  321. * 计算气费
  322. */
  323. protected function calculateGasCost(array $item): float
  324. {
  325. return round(floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0), 2);
  326. }
  327. /**
  328. * 获取机台工作小时数
  329. */
  330. protected function getMachineWorkHours(): array
  331. {
  332. $workHours = [];
  333. foreach ($this->monthlyCostDetails as $detail) {
  334. try {
  335. // 确保 $detail 是数组
  336. $detailArray = is_object($detail) ? (array)$detail : $detail;
  337. if (!is_array($detailArray)) {
  338. Log::warning("成本明细数据格式不正确: " . gettype($detail));
  339. continue;
  340. }
  341. $machineCode = $detailArray['sczl_jtbh'] ?? '';
  342. $hours = floatval($detailArray['占用机时'] ?? 0);
  343. if (!empty($machineCode) && $hours > 0) {
  344. $workHours[] = [
  345. 'sczl_gdbh' => $detailArray['sczl_gdbh'] ?? '',
  346. 'sczl_yjno' => $detailArray['sczl_yjno'] ?? '',
  347. 'sczl_gxh' => $detailArray['sczl_gxh'] ?? '',
  348. 'sczl_jtbh' => $machineCode,
  349. '占用机时' => $hours,
  350. '车间名称' => $detailArray['车间名称'] ?? '',
  351. ];
  352. }
  353. } catch (\Exception $e) {
  354. Log::error("处理机台工作小时数时出错: " . $e->getMessage());
  355. continue;
  356. }
  357. }
  358. Log::info("从月度成本明细数据中获取机台运行时间,记录数: " . count($workHours));
  359. return $workHours;
  360. }
  361. /**
  362. * 计算机台总小时数
  363. */
  364. protected function calculateMachineTotalHours(array $workOrderHours): array
  365. {
  366. $machineTotalHours = [];
  367. foreach ($workOrderHours as $workOrder) {
  368. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  369. $hours = floatval($workOrder['占用机时'] ?? 0);
  370. if (!empty($machineCode) && $hours > 0) {
  371. $machineTotalHours[$machineCode] = ($machineTotalHours[$machineCode] ?? 0) + $hours;
  372. }
  373. }
  374. return $machineTotalHours;
  375. }
  376. /**
  377. * 分摊水电到工单
  378. */
  379. protected function allocateUtilitiesToWorkOrders(
  380. array $machineUtilities,
  381. array $workOrderHours,
  382. array $machineTotalHours
  383. ): array {
  384. $allocationResults = [];
  385. $processedCount = 0;
  386. $skippedCount = 0;
  387. foreach ($workOrderHours as $workOrder) {
  388. try {
  389. // 确保 $workOrder 是数组
  390. $orderArray = is_object($workOrder) ? (array)$workOrder : $workOrder;
  391. if (!is_array($orderArray)) {
  392. Log::warning("工单小时数据格式不正确: " . gettype($workOrder));
  393. $skippedCount++;
  394. continue;
  395. }
  396. $machineCode = $orderArray['sczl_jtbh'] ?? '';
  397. $hours = floatval($orderArray['占用机时'] ?? 0);
  398. if (empty($machineCode)) {
  399. Log::debug("工单缺少机器编号: " . json_encode($orderArray));
  400. $skippedCount++;
  401. continue;
  402. }
  403. if (!isset($machineUtilities[$machineCode])) {
  404. Log::debug("机器 {$machineCode} 没有水电费用数据");
  405. $skippedCount++;
  406. continue;
  407. }
  408. if ($machineUtilities[$machineCode]['总费用'] <= 0) {
  409. Log::debug("机器 {$machineCode} 水电费用为0");
  410. $skippedCount++;
  411. continue;
  412. }
  413. if (!isset($machineTotalHours[$machineCode]) || $machineTotalHours[$machineCode] <= 0) {
  414. Log::warning("机器 {$machineCode} 总运行时间为0,无法分摊");
  415. $skippedCount++;
  416. continue;
  417. }
  418. $allocationRatio = $hours / $machineTotalHours[$machineCode];
  419. $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2);
  420. $uniqueKey = $this->getWorkOrderUniqueKey($orderArray);
  421. $allocationResults[$uniqueKey] = [
  422. 'unique_key' => $uniqueKey,
  423. 'sczl_gdbh' => $orderArray['sczl_gdbh'] ?? '',
  424. 'sczl_yjno' => $orderArray['sczl_yjno'] ?? '',
  425. 'sczl_gxh' => $orderArray['sczl_gxh'] ?? '',
  426. 'sczl_jtbh' => $machineCode,
  427. '占用机时' => $hours,
  428. '分摊比例' => $allocationRatio,
  429. '分摊金额' => $allocatedAmount,
  430. '机台总费用' => $machineUtilities[$machineCode]['总费用'],
  431. '机台总工时' => $machineTotalHours[$machineCode],
  432. ];
  433. $processedCount++;
  434. } catch (\Exception $e) {
  435. Log::error("分摊水电到工单时出错: " . $e->getMessage());
  436. $skippedCount++;
  437. continue;
  438. }
  439. }
  440. Log::info("分摊水电处理完成,成功: {$processedCount}, 跳过: {$skippedCount}, 总计: " . count($workOrderHours));
  441. return $allocationResults;
  442. }
  443. /**
  444. * 获取工单唯一键
  445. */
  446. protected function getWorkOrderUniqueKey($workOrder): string
  447. {
  448. // 记录传入参数的类型和内容(用于调试)
  449. $type = gettype($workOrder);
  450. Log::debug("getWorkOrderUniqueKey 接收参数类型: {$type}");
  451. // 如果是对象,转换为数组
  452. if (is_object($workOrder)) {
  453. $workOrder = (array)$workOrder;
  454. }
  455. // 确保是数组
  456. if (!is_array($workOrder)) {
  457. Log::error("getWorkOrderUniqueKey 参数不是数组: {$type}, 值: " . print_r($workOrder, true));
  458. return 'invalid-' . uniqid();
  459. }
  460. // 调试:记录数组内容
  461. if (count($workOrder) < 4) {
  462. Log::debug("getWorkOrderUniqueKey 数组内容: " . json_encode($workOrder));
  463. }
  464. // 安全地获取所有可能键名
  465. $gdbh = '';
  466. $yjno = '';
  467. $gxh = '';
  468. $jtbh = '';
  469. // 尝试各种可能的键名
  470. if (isset($workOrder['sczl_gdbh'])) {
  471. $gdbh = $workOrder['sczl_gdbh'];
  472. } elseif (isset($workOrder['工单编号'])) {
  473. $gdbh = $workOrder['工单编号'];
  474. }
  475. if (isset($workOrder['sczl_yjno'])) {
  476. $yjno = $workOrder['sczl_yjno'];
  477. } elseif (isset($workOrder['印件号'])) {
  478. $yjno = $workOrder['印件号'];
  479. }
  480. if (isset($workOrder['sczl_gxh'])) {
  481. $gxh = $workOrder['sczl_gxh'];
  482. } elseif (isset($workOrder['工序号'])) {
  483. $gxh = $workOrder['工序号'];
  484. }
  485. if (isset($workOrder['sczl_jtbh'])) {
  486. $jtbh = $workOrder['sczl_jtbh'];
  487. } elseif (isset($workOrder['机器编号'])) {
  488. $jtbh = $workOrder['机器编号'];
  489. }
  490. // 验证所有字段都不为空
  491. if (empty($gdbh) || empty($yjno) || empty($gxh) || empty($jtbh)) {
  492. Log::warning("工单唯一键字段不完整: gdbh={$gdbh}, yjno={$yjno}, gxh={$gxh}, jtbh={$jtbh}");
  493. }
  494. $key = sprintf('%s-%s-%s-%s', $gdbh, $yjno, $gxh, $jtbh);
  495. Log::debug("生成的唯一键: {$key}");
  496. return $key;
  497. }
  498. /**
  499. * 更新直接水电到成本明细
  500. */
  501. protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void
  502. {
  503. if (empty($allocationResults) || empty($this->monthlyCostDetails)) {
  504. Log::warning("水电费分配结果或成本明细数据为空,无法更新");
  505. return;
  506. }
  507. $updatedCount = 0;
  508. $totalAllocatedAmount = 0;
  509. $notFoundCount = 0;
  510. foreach ($this->monthlyCostDetails as &$costDetail) {
  511. try {
  512. // 确保 $costDetail 是数组
  513. $detailArray = is_object($costDetail) ? (array)$costDetail : $costDetail;
  514. $uniqueKey = $this->getWorkOrderUniqueKey($detailArray);
  515. if (isset($allocationResults[$uniqueKey])) {
  516. $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'] ?? 0;
  517. $costDetail['直接水电'] = $allocatedAmount;
  518. $totalAllocatedAmount += $allocatedAmount;
  519. $updatedCount++;
  520. } else {
  521. $notFoundCount++;
  522. }
  523. } catch (\Exception $e) {
  524. Log::error("更新直接水电费时出错: " . $e->getMessage());
  525. continue;
  526. }
  527. }
  528. Log::info("已更新直接水电费的工单数量: {$updatedCount}, 未找到的工单数: {$notFoundCount}, 分配总金额: {$totalAllocatedAmount}");
  529. // 如果有大量未找到的工单,记录详细信息
  530. if ($notFoundCount > 0 && $notFoundCount > $updatedCount) {
  531. Log::warning("有大量工单未匹配到水电费分配结果,可能键名不匹配");
  532. }
  533. }
  534. /**
  535. * 3. 计算间接材料分摊
  536. */
  537. protected function calculateIndirectMaterials(string $month): void
  538. {
  539. if (empty($this->monthlyCostDetails)) {
  540. return;
  541. }
  542. $date = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  543. $totalMoney = $this->getIndirectMaterialTotal($date);
  544. if (!$totalMoney || $totalMoney <= 0) {
  545. return;
  546. }
  547. $totalChroma = $this->calculateTotalChroma();
  548. if ($totalChroma <= 0) {
  549. return;
  550. }
  551. $this->allocateIndirectMaterials($totalMoney, $totalChroma);
  552. }
  553. /**
  554. * 获取间接材料总额
  555. */
  556. protected function getIndirectMaterialTotal(string $date): float
  557. {
  558. $result = Db::name('材料出库单列表')
  559. ->where([
  560. '出库日期' => ['like', $date . '%'],
  561. '部门' => '印刷成本中心'
  562. ])
  563. ->whereNull('表体生产订单号')
  564. ->field('SUM(金额) as money')
  565. ->find();
  566. return $result ? floatval($result['money']) : 0;
  567. }
  568. /**
  569. * 计算总色度数
  570. */
  571. protected function calculateTotalChroma(): float
  572. {
  573. $totalChroma = 0;
  574. foreach ($this->monthlyCostDetails as $detail) {
  575. $totalChroma += floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  576. }
  577. return $totalChroma;
  578. }
  579. /**
  580. * 分摊间接材料
  581. */
  582. protected function allocateIndirectMaterials(float $totalMoney, float $totalChroma): void
  583. {
  584. foreach ($this->monthlyCostDetails as &$detail) {
  585. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  586. $money = round($totalMoney * ($chroma / $totalChroma), 2);
  587. $detail['分摊材料'] = $money;
  588. }
  589. }
  590. /**
  591. * 4. 计算间接人工分摊
  592. */
  593. protected function calculateIndirectLabor(string $month): void
  594. {
  595. $wageRatio = $this->getWageRatio();
  596. if (empty($wageRatio)) {
  597. return;
  598. }
  599. $monthWage = $this->getMonthlyWage($month);
  600. if (empty($monthWage)) {
  601. return;
  602. }
  603. $this->allocateIndirectLabor($wageRatio, $monthWage);
  604. }
  605. /**
  606. * 获取工资比例
  607. */
  608. protected function getWageRatio(): array
  609. {
  610. $workshopTotals = [];
  611. foreach ($this->monthlyCostDetails as $detail) {
  612. $workshop = $detail['车间名称'];
  613. $amount = floatval($detail['人工分摊因子']);
  614. $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount;
  615. }
  616. $total = array_sum($workshopTotals);
  617. if ($total <= 0) {
  618. return [];
  619. }
  620. $ratios = [];
  621. foreach ($workshopTotals as $workshop => $workshopTotal) {
  622. $ratios[$workshop] = round($workshopTotal / $total, 4);
  623. }
  624. return $ratios;
  625. }
  626. /**
  627. * 获取月度工资数据
  628. */
  629. protected function getMonthlyWage(string $month): array
  630. {
  631. return Db::name('成本_各月其他费用')
  632. ->where('sys_ny', $month)
  633. ->field('部门人员工资,管理人员工资')
  634. ->find() ?: [];
  635. }
  636. /**
  637. * 分摊间接人工
  638. */
  639. protected function allocateIndirectLabor(array $wageRatio, array $monthWage): void
  640. {
  641. foreach ($wageRatio as $workshopName => $ratio) {
  642. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  643. if (empty($chromaData['list']) || $chromaData['total'] == 0) {
  644. continue;
  645. }
  646. $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData);
  647. }
  648. }
  649. /**
  650. * 获取车间色度数数据
  651. */
  652. protected function getChromaDataForWorkshop(string $workshop): array
  653. {
  654. $data = ['total' => 0, 'list' => []];
  655. foreach ($this->monthlyCostDetails as $detail) {
  656. if ($detail['车间名称'] === $workshop) {
  657. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  658. $data['total'] += $chroma;
  659. $data['list'][] = [
  660. 'sczl_gdbh' => $detail['sczl_gdbh'],
  661. 'sczl_yjno' => $detail['sczl_yjno'],
  662. 'sczl_gxh' => $detail['sczl_gxh'],
  663. 'sczl_jtbh' => $detail['sczl_jtbh'],
  664. 'chroma' => $chroma
  665. ];
  666. }
  667. }
  668. return $data;
  669. }
  670. /**
  671. * 分配工资到车间
  672. */
  673. protected function allocateWageToWorkshop(
  674. string $workshop,
  675. float $ratio,
  676. array $monthWage,
  677. array $chromaData
  678. ): void {
  679. $wageTypes = [
  680. '部门人员工资' => '车间人工',
  681. '管理人员工资' => '部门人工附加'
  682. ];
  683. foreach ($wageTypes as $wageType => $fieldName) {
  684. if (empty($monthWage[$wageType]) || $monthWage[$wageType] <= 0) {
  685. continue;
  686. }
  687. $workshopAmount = $ratio * $monthWage[$wageType];
  688. if ($chromaData['total'] <= 0) {
  689. continue;
  690. }
  691. $this->allocateWageToDetails($workshop, $workshopAmount, $chromaData, $fieldName);
  692. }
  693. }
  694. /**
  695. * 分配工资到明细记录
  696. */
  697. protected function allocateWageToDetails(
  698. string $workshop,
  699. float $workshopAmount,
  700. array $chromaData,
  701. string $fieldName
  702. ): void {
  703. foreach ($this->monthlyCostDetails as &$detail) {
  704. if ($detail['车间名称'] === $workshop) {
  705. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  706. $amount = round($chroma / $chromaData['total'] * $workshopAmount, 2);
  707. $detail[$fieldName] += $amount;
  708. }
  709. }
  710. }
  711. /**
  712. * 5. 计算分摊水电
  713. */
  714. protected function calculateApportionedUtilities(array $param): void
  715. {
  716. try {
  717. $month = $param['month'];
  718. $sysId = $param['sys_id'] ?? '';
  719. $utilityData = $this->fetchApportionedUtilities($month);
  720. if (empty($utilityData)) {
  721. Log::info("{$month}月份未找到分摊水电数据");
  722. return;
  723. }
  724. // 记录原始科目名称用于调试
  725. $originalSubjects = array_unique(array_column($utilityData, '科目名称'));
  726. Log::info("原始科目名称: " . implode(', ', $originalSubjects));
  727. $machineAllocations = $this->calculateMachineAllocations($utilityData);
  728. if (!empty($machineAllocations)) {
  729. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  730. $this->allocateApportionedUtilities($machineAllocations);
  731. }
  732. } catch (\Exception $e) {
  733. Log::error("计算分摊水电时出错: " . $e->getMessage());
  734. throw new Exception("分摊水电计算失败: " . $e->getMessage());
  735. }
  736. }
  737. /**
  738. * 获取分摊水电数据
  739. */
  740. protected function fetchApportionedUtilities(string $month): array
  741. {
  742. return Db::name('成本_各月水电气')
  743. ->where('Sys_ny', $month)
  744. ->whereLike('费用类型', '%分摊%')
  745. ->select();
  746. }
  747. /**
  748. * 计算机台分摊金额
  749. */
  750. protected function calculateMachineAllocations(array $utilityData): array
  751. {
  752. $allocations = [];
  753. $machineHours = $this->getMachineHours();
  754. foreach ($utilityData as $item) {
  755. try {
  756. $subject = $this->simplifySubjectName($item['科目名称']);
  757. $amount = $this->calculateUtilityAmount($item);
  758. if ($amount <= 0) {
  759. continue;
  760. }
  761. $this->allocateBySubject($allocations, $subject, $amount, $machineHours);
  762. } catch (\Exception $e) {
  763. Log::error("计算机台分摊金额时出错: " . $e->getMessage());
  764. continue;
  765. }
  766. }
  767. return $allocations;
  768. }
  769. /**
  770. * 获取机台运行时间
  771. */
  772. protected function getMachineHours(): array
  773. {
  774. $hours = [];
  775. foreach ($this->monthlyCostDetails as $detail) {
  776. $machine = $detail['sczl_jtbh'];
  777. $hour = floatval($detail['占用机时']);
  778. if (!empty($machine)) {
  779. $hours[$machine] = ($hours[$machine] ?? 0) + $hour;
  780. }
  781. }
  782. return $hours;
  783. }
  784. /**
  785. * 按科目分摊
  786. */
  787. protected function allocateBySubject(
  788. array &$allocations,
  789. string $subject,
  790. float $amount,
  791. array $machineHours
  792. ): void {
  793. try {
  794. if ($subject === '待分摊总额') {
  795. $this->allocateByFloor($allocations, $amount, $machineHours);
  796. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  797. $this->allocateToRollCoater($allocations, $subject, $amount, $machineHours);
  798. } else {
  799. $this->allocateGlobally($allocations, $subject, $amount, $machineHours);
  800. }
  801. } catch (\Exception $e) {
  802. Log::error("按科目分摊时出错 (科目: {$subject}): " . $e->getMessage());
  803. }
  804. }
  805. /**
  806. * 按楼层分摊
  807. */
  808. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours): void
  809. {
  810. $floorData = $this->groupMachinesByFloor($machineHours);
  811. if ($floorData['totalHours'] <= 0) {
  812. return;
  813. }
  814. foreach ($floorData['floors'] as $floor => $data) {
  815. if ($data['hours'] <= 0) {
  816. continue;
  817. }
  818. $floorAmount = $amount * ($data['hours'] / $floorData['totalHours']);
  819. $this->allocateToFloorMachines($allocations, $floorAmount, $data['machines'], $machineHours);
  820. }
  821. }
  822. /**
  823. * 按楼层分组机台
  824. */
  825. protected function groupMachinesByFloor(array $machineHours): array
  826. {
  827. $floors = ['1' => ['hours' => 0, 'machines' => []], '2' => ['hours' => 0, 'machines' => []]];
  828. $totalHours = 0;
  829. foreach ($machineHours as $machine => $hours) {
  830. $floor = $this->getFloorByMachine($machine);
  831. if ($floor && isset($floors[$floor])) {
  832. $floors[$floor]['hours'] += $hours;
  833. $floors[$floor]['machines'][] = $machine;
  834. $totalHours += $hours;
  835. }
  836. }
  837. return ['floors' => $floors, 'totalHours' => $totalHours];
  838. }
  839. /**
  840. * 分摊到楼层机台
  841. */
  842. protected function allocateToFloorMachines(
  843. array &$allocations,
  844. float $floorAmount,
  845. array $machines,
  846. array $machineHours
  847. ): void {
  848. $floorHours = 0;
  849. foreach ($machines as $machine) {
  850. $floorHours += $machineHours[$machine];
  851. }
  852. if ($floorHours <= 0) {
  853. return;
  854. }
  855. foreach ($machines as $machine) {
  856. $machineHoursAmount = $machineHours[$machine];
  857. if ($machineHoursAmount <= 0) {
  858. continue;
  859. }
  860. $allocations[$machine]['待分摊总额'] =
  861. ($allocations[$machine]['待分摊总额'] ?? 0) +
  862. round($floorAmount * ($machineHoursAmount / $floorHours), 2);
  863. }
  864. }
  865. /**
  866. * 只分摊到卷凹机组
  867. */
  868. protected function allocateToRollCoater(
  869. array &$allocations,
  870. string $subject,
  871. float $amount,
  872. array $machineHours
  873. ): void {
  874. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  875. $totalHours = 0;
  876. foreach ($rollCoaterMachines as $machine) {
  877. $totalHours += $machineHours[$machine];
  878. }
  879. if ($totalHours <= 0) {
  880. return;
  881. }
  882. foreach ($rollCoaterMachines as $machine) {
  883. $machineHoursAmount = $machineHours[$machine];
  884. if ($machineHoursAmount <= 0) {
  885. continue;
  886. }
  887. $allocations[$machine][$subject] =
  888. ($allocations[$machine][$subject] ?? 0) +
  889. round($amount * ($machineHoursAmount / $totalHours), 2);
  890. }
  891. }
  892. /**
  893. * 全局分摊
  894. */
  895. protected function allocateGlobally(
  896. array &$allocations,
  897. string $subject,
  898. float $amount,
  899. array $machineHours
  900. ): void {
  901. $totalHours = array_sum($machineHours);
  902. if ($totalHours <= 0) {
  903. Log::warning("全局分摊失败:总机时为0");
  904. return;
  905. }
  906. foreach ($machineHours as $machine => $hours) {
  907. try {
  908. // 确保 $machine 是有效的字符串键名
  909. if (!is_string($machine) && !is_numeric($machine)) {
  910. Log::warning("无效的机器键名: " . print_r($machine, true));
  911. continue;
  912. }
  913. $machine = (string)$machine;
  914. if ($hours <= 0) {
  915. continue;
  916. }
  917. if (!isset($allocations[$machine])) {
  918. $allocations[$machine] = [];
  919. }
  920. $machineAmount = round($amount * ($hours / $totalHours), 2);
  921. $allocations[$machine][$subject] =
  922. ($allocations[$machine][$subject] ?? 0) + $machineAmount;
  923. } catch (\Exception $e) {
  924. Log::error("全局分摊到机器 {$machine} 时出错: " . $e->getMessage());
  925. continue;
  926. }
  927. }
  928. }
  929. /**
  930. * 根据机台获取楼层
  931. */
  932. protected function getFloorByMachine(string $machine): ?string
  933. {
  934. $group = Db::name('设备_基本资料')
  935. ->where('设备编号', $machine)
  936. ->value('设备编组');
  937. if (!$group) {
  938. return null;
  939. }
  940. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  941. foreach ($groupNames as $groupName) {
  942. if (strpos($group, $groupName) !== false) {
  943. return $floor;
  944. }
  945. }
  946. }
  947. return null;
  948. }
  949. /**
  950. * 筛选卷凹机组的机台
  951. */
  952. protected function filterRollCoaterMachines(array $machines): array
  953. {
  954. $rollCoater = [];
  955. foreach ($machines as $machine) {
  956. $group = Db::name('设备_基本资料')
  957. ->where('设备编号', $machine)
  958. ->value('设备编组');
  959. if ($group && strpos($group, '03、卷凹机组') !== false) {
  960. $rollCoater[] = $machine;
  961. }
  962. }
  963. return $rollCoater;
  964. }
  965. /**
  966. * 简化科目名称
  967. */
  968. protected function simplifySubjectName(string $subject): string
  969. {
  970. // 先尝试完全匹配
  971. $subject = trim($subject);
  972. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  973. if ($subject === $keyword) {
  974. return $mappedField;
  975. }
  976. }
  977. // 尝试部分匹配
  978. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  979. if (strpos($subject, $keyword) !== false) {
  980. Log::debug("科目名称部分匹配: {$subject} => {$mappedField} (关键词: {$keyword})");
  981. return $mappedField;
  982. }
  983. }
  984. // 如果都不匹配,使用默认映射
  985. Log::warning("未识别的科目名称: {$subject}, 将映射到 '分摊水电'");
  986. return '分摊水电';
  987. }
  988. /**
  989. * 计算水电金额
  990. */
  991. protected function calculateUtilityAmount(array $item): float
  992. {
  993. $electricity = $this->calculateElectricityCost($item);
  994. $gas = $this->calculateGasCost($item);
  995. return $electricity + $gas;
  996. }
  997. /**
  998. * 生成分摊系数记录
  999. */
  1000. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  1001. {
  1002. $now = date('Y-m-d H:i:s');
  1003. foreach ($allocations as $machine => $subjects) {
  1004. if (!is_string($machine) && !is_numeric($machine)) {
  1005. Log::warning("无效的机台标识: " . print_r($machine, true));
  1006. continue;
  1007. }
  1008. foreach ($subjects as $subject => $amount) {
  1009. if (!is_string($subject)) {
  1010. Log::warning("无效的科目名称: " . print_r($subject, true));
  1011. continue;
  1012. }
  1013. // 使用简化后的科目名称
  1014. $simplifiedSubject = $this->simplifySubjectName($subject);
  1015. $this->allocationFactors[] = [
  1016. 'Sys_ny' => $month,
  1017. '科目名称' => $simplifiedSubject, // 原始科目名称
  1018. '设备编号' => (string)$machine,
  1019. '分摊系数' => 1,
  1020. '分摊金额' => floatval($amount),
  1021. 'Sys_id' => $sysId,
  1022. 'Sys_rq' => $now,
  1023. ];
  1024. }
  1025. }
  1026. }
  1027. /**
  1028. * 分配分摊水电
  1029. */
  1030. protected function allocateApportionedUtilities(array $machineAllocations): void
  1031. {
  1032. $machineRates = $this->calculateMachineRates($machineAllocations);
  1033. foreach ($this->monthlyCostDetails as &$detail) {
  1034. $machine = $detail['sczl_jtbh'];
  1035. $hours = floatval($detail['占用机时']);
  1036. if ($hours <= 0 || !isset($machineRates[$machine])) {
  1037. continue;
  1038. }
  1039. // 直接水电费(保持不变)
  1040. $detail['直接水电'] = round($hours * 0.69, 2);
  1041. // 分摊水电费 - 使用映射后的字段名
  1042. foreach ($machineRates[$machine] as $subject => $rate) {
  1043. $field = $this->getUtilityFieldName($subject);
  1044. if (!isset($detail[$field])) {
  1045. Log::warning("数据库字段不存在: {$field},跳过该分摊");
  1046. continue;
  1047. }
  1048. $detail[$field] = round($hours * $rate, 2);
  1049. }
  1050. }
  1051. }
  1052. /**
  1053. * 计算机台每机时费用
  1054. */
  1055. protected function calculateMachineRates(array $allocations): array
  1056. {
  1057. $rates = [];
  1058. $machineHours = $this->getMachineHours();
  1059. foreach ($allocations as $machine => $subjects) {
  1060. $totalHours = $machineHours[$machine] ?? 0;
  1061. if ($totalHours <= 0) {
  1062. continue;
  1063. }
  1064. $rates[$machine] = [];
  1065. foreach ($subjects as $subject => $amount) {
  1066. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  1067. }
  1068. }
  1069. return $rates;
  1070. }
  1071. /**
  1072. * 获取水电字段名
  1073. */
  1074. protected function getUtilityFieldName(string $subject): string
  1075. {
  1076. // 使用映射表
  1077. if (isset(self::SUBJECT_TO_FIELD_MAP[$subject])) {
  1078. return self::SUBJECT_TO_FIELD_MAP[$subject];
  1079. }
  1080. // 尝试部分匹配
  1081. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  1082. if (strpos($subject, $keyword) !== false) {
  1083. return $mappedField;
  1084. }
  1085. }
  1086. // 默认映射到 '分摊水电'
  1087. Log::warning("未知的科目字段: {$subject}, 映射到 '分摊水电'");
  1088. return '分摊水电';
  1089. }
  1090. /**
  1091. * 统一保存所有数据
  1092. */
  1093. protected function saveAllData(string $month, string $sysId): void
  1094. {
  1095. // 1. 删除旧数据
  1096. $this->deleteOldData($month);
  1097. // 2. 插入前检查数据结构
  1098. $this->validateDataStructure();
  1099. // 3. 插入新数据
  1100. $this->insertAllData();
  1101. }
  1102. /**
  1103. * 删除旧数据
  1104. */
  1105. protected function deleteOldData(string $month): void
  1106. {
  1107. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  1108. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  1109. }
  1110. /**
  1111. * 验证数据结构
  1112. */
  1113. protected function validateDataStructure(): void
  1114. {
  1115. try {
  1116. if (!is_array($this->monthlyCostDetails)) {
  1117. Log::error("monthlyCostDetails 不是数组: " . gettype($this->monthlyCostDetails));
  1118. $this->monthlyCostDetails = [];
  1119. return;
  1120. }
  1121. if (empty($this->monthlyCostDetails)) {
  1122. Log::warning("月度成本明细数据为空");
  1123. return;
  1124. }
  1125. $tableName = '成本v23_月度成本明细';
  1126. $columns = Db::query("DESCRIBE `{$tableName}`");
  1127. if (empty($columns)) {
  1128. Log::error("无法获取表结构: {$tableName}");
  1129. return;
  1130. }
  1131. $columnNames = array_column($columns, 'Field');
  1132. Log::info("表{$tableName}结构字段数: " . count($columnNames));
  1133. // 验证并修复每条记录
  1134. foreach ($this->monthlyCostDetails as $index => &$row) {
  1135. // 确保行是数组
  1136. if (!is_array($row)) {
  1137. Log::warning("第{$index}行不是数组: " . gettype($row));
  1138. $row = [];
  1139. continue;
  1140. }
  1141. // 检查字段数是否匹配
  1142. $rowCount = count($row);
  1143. $columnCount = count($columnNames);
  1144. if ($rowCount !== $columnCount) {
  1145. Log::warning("第" . ($index + 1) . "行字段数不匹配: 数据{$rowCount}个,表{$columnCount}个");
  1146. $this->fixRowData($row, $columnNames, $index);
  1147. }
  1148. // 确保所有键名都是字符串
  1149. foreach ($row as $key => $value) {
  1150. if (!is_string($key) && !is_int($key)) {
  1151. Log::warning("发现非法键名类型 (行 {$index}): " . gettype($key));
  1152. // 移除非法键名
  1153. unset($row[$key]);
  1154. // 如果有值,尝试保存
  1155. if ($value !== null) {
  1156. $row['invalid_key_' . $index] = $value;
  1157. }
  1158. }
  1159. }
  1160. }
  1161. Log::info("数据结构验证完成,总记录数: " . count($this->monthlyCostDetails));
  1162. } catch (\Throwable $t) {
  1163. Log::error("验证数据结构时出错: " . $t->getMessage());
  1164. // 不抛出异常,继续执行
  1165. }
  1166. }
  1167. /**
  1168. * 修复行数据
  1169. */
  1170. protected function fixRowData(array &$row, array $columnNames, int $index): void
  1171. {
  1172. $fixedRow = [];
  1173. foreach ($columnNames as $column) {
  1174. // 确保列名是字符串
  1175. $column = (string)$column;
  1176. // 获取值,如果不存在则设置默认值
  1177. if (array_key_exists($column, $row)) {
  1178. $value = $row[$column];
  1179. } else {
  1180. // 根据列名设置默认值
  1181. if (in_array($column, ['直接水电', '分摊材料', '车间人工', '部门人工附加', '分摊水电',
  1182. '废气处理', '锅炉', '空压机', '热水锅炉', '真空鼓风机', '中央空调', '分摊其他'])) {
  1183. $value = 0;
  1184. } else {
  1185. $value = null;
  1186. }
  1187. }
  1188. $fixedRow[$column] = $value;
  1189. }
  1190. $this->monthlyCostDetails[$index] = $fixedRow;
  1191. Log::info("已修复第" . ($index + 1) . "行数据");
  1192. }
  1193. /**
  1194. * 插入所有数据
  1195. */
  1196. protected function insertAllData(): void
  1197. {
  1198. $this->insertMonthlyCostDetails();
  1199. $this->insertAllocationFactors();
  1200. }
  1201. /**
  1202. * 插入月度成本明细
  1203. */
  1204. protected function insertMonthlyCostDetails(): void
  1205. {
  1206. if (empty($this->monthlyCostDetails)) {
  1207. return;
  1208. }
  1209. $total = count($this->monthlyCostDetails);
  1210. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  1211. $batch = array_slice($this->monthlyCostDetails, $i, self::BATCH_SIZE);
  1212. $this->insertBatch($batch, '成本v23_月度成本明细', $i);
  1213. }
  1214. }
  1215. /**
  1216. * 插入批次数据
  1217. */
  1218. protected function insertBatch(array $batch, string $tableName, int $startIndex): void
  1219. {
  1220. if (empty($batch)) {
  1221. Log::warning("批次数据为空,跳过插入");
  1222. return;
  1223. }
  1224. $firstRow = reset($batch);
  1225. $fields = array_keys($firstRow);
  1226. // 验证字段名不包含特殊字符
  1227. foreach ($fields as $field) {
  1228. if (preg_match('/[#@$%^&*()+\-=\[\]{}|;:"<>,.?\/]/', $field)) {
  1229. Log::error("字段名包含特殊字符: {$field}");
  1230. throw new Exception("字段名 '{$field}' 包含非法字符");
  1231. }
  1232. }
  1233. $fieldStr = '`' . implode('`,`', $fields) . '`';
  1234. $values = [];
  1235. foreach ($batch as $rowIndex => $row) {
  1236. $rowValues = [];
  1237. foreach ($fields as $field) {
  1238. $value = $row[$field] ?? null;
  1239. if (is_numeric($value)) {
  1240. $rowValues[] = $value;
  1241. } elseif (is_null($value)) {
  1242. $rowValues[] = 'NULL';
  1243. } else {
  1244. $rowValues[] = "'" . addslashes((string)$value) . "'";
  1245. }
  1246. }
  1247. $values[] = '(' . implode(',', $rowValues) . ')';
  1248. // 记录第一行数据用于调试
  1249. if ($rowIndex === 0 && $startIndex === 0) {
  1250. Log::debug("第一行数据字段: " . implode(', ', $fields));
  1251. Log::debug("第一行数据值: " . implode(', ', $rowValues));
  1252. }
  1253. }
  1254. $sql = "INSERT INTO `{$tableName}` ({$fieldStr}) VALUES " . implode(',', $values);
  1255. // 记录SQL语句(前200个字符)
  1256. Log::debug("SQL语句: " . substr($sql, 0, 200) . "...");
  1257. try {
  1258. $result = Db::execute($sql);
  1259. Log::info("成功插入批次 " . (($startIndex / self::BATCH_SIZE) + 1) . ", 影响行数: " . $result);
  1260. } catch (\Exception $e) {
  1261. Log::error("插入批次失败: " . $e->getMessage());
  1262. Log::error("失败SQL: " . $sql);
  1263. throw $e;
  1264. }
  1265. }
  1266. /**
  1267. * 插入分摊系数
  1268. */
  1269. protected function insertAllocationFactors(): void
  1270. {
  1271. if (!empty($this->allocationFactors)) {
  1272. $total = count($this->allocationFactors);
  1273. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  1274. $batch = array_slice($this->allocationFactors, $i, self::BATCH_SIZE);
  1275. $this->insertBatch($batch, '成本_各月分摊系数', $i);
  1276. }
  1277. }
  1278. }
  1279. /**
  1280. * 记录成功日志
  1281. */
  1282. protected function logSuccess(string $month): void
  1283. {
  1284. try {
  1285. // 确保记录数为整数
  1286. $monthlyCostDetailsCount = is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 0;
  1287. $allocationFactorsCount = is_countable($this->allocationFactors) ? count($this->allocationFactors) : 0;
  1288. $logData = [
  1289. 'month' => $month,
  1290. '月度成本明细记录数' => $monthlyCostDetailsCount,
  1291. '分摊系数记录数' => $allocationFactorsCount,
  1292. 'timestamp' => date('Y-m-d H:i:s')
  1293. ];
  1294. // 记录每个字段的数据类型,用于调试
  1295. if (!empty($this->monthlyCostDetails)) {
  1296. $firstRecord = reset($this->monthlyCostDetails);
  1297. $logData['first_record_keys'] = is_array($firstRecord) ? array_keys($firstRecord) : 'invalid';
  1298. $logData['first_record_type'] = gettype($firstRecord);
  1299. }
  1300. Log::info("成本核算完成", $logData);
  1301. } catch (\Throwable $t) {
  1302. // 如果日志记录失败,至少输出到标准错误
  1303. error_log("成本核算完成(日志记录失败): " . $t->getMessage());
  1304. }
  1305. }
  1306. /**
  1307. * 构建成功响应
  1308. */
  1309. protected function buildSuccessResponse(string $month): array
  1310. {
  1311. try {
  1312. return [
  1313. 'success' => true,
  1314. 'message' => '成本核算完成',
  1315. 'month' => $month,
  1316. 'stats' => [
  1317. 'monthly_cost_details' => is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 0,
  1318. 'allocation_factors' => is_countable($this->allocationFactors) ? count($this->allocationFactors) : 0
  1319. ],
  1320. 'timestamp' => date('Y-m-d H:i:s')
  1321. ];
  1322. } catch (\Throwable $t) {
  1323. // 即使构建响应失败,也返回基本成功信息
  1324. return [
  1325. 'success' => true,
  1326. 'message' => '成本核算完成(统计信息获取失败)',
  1327. 'month' => $month,
  1328. 'error' => $t->getMessage()
  1329. ];
  1330. }
  1331. }
  1332. /**
  1333. * 记录错误日志
  1334. */
  1335. protected function logError(\Throwable $t): void
  1336. {
  1337. try {
  1338. $errorDetails = [
  1339. '时间' => date('Y-m-d H:i:s'),
  1340. '错误类型' => get_class($t),
  1341. '错误信息' => $t->getMessage(),
  1342. '错误代码' => $t->getCode(),
  1343. '文件' => $t->getFile(),
  1344. '行号' => $t->getLine(),
  1345. '堆栈跟踪' => $t->getTraceAsString(),
  1346. '月度成本明细数' => is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 'N/A',
  1347. '分摊系数数' => is_countable($this->allocationFactors) ? count($this->allocationFactors) : 'N/A',
  1348. ];
  1349. Log::error("统一成本核算失败详情", $errorDetails);
  1350. } catch (\Throwable $logError) {
  1351. // 如果日志记录也失败,至少输出到标准错误
  1352. error_log("无法记录错误日志: " . $logError->getMessage());
  1353. error_log("原始错误: " . $t->getMessage());
  1354. }
  1355. }
  1356. /**
  1357. * 构建错误响应
  1358. */
  1359. protected function buildErrorResponse(Exception $e): array
  1360. {
  1361. return [
  1362. 'success' => false,
  1363. 'message' => '成本核算失败: ' . $e->getMessage(),
  1364. 'error' => $e->getMessage()
  1365. ];
  1366. }
  1367. /**
  1368. * 清理字段名中的特殊字符
  1369. */
  1370. protected function sanitizeFieldNames(array &$row): void
  1371. {
  1372. $sanitized = [];
  1373. foreach ($row as $key => $value) {
  1374. // 移除字段名中的特殊字符,只保留字母、数字、下划线和中文字符
  1375. $cleanKey = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', '', $key);
  1376. if ($cleanKey !== $key) {
  1377. Log::debug("清理字段名: {$key} => {$cleanKey}");
  1378. }
  1379. $sanitized[$cleanKey] = $value;
  1380. }
  1381. $row = $sanitized;
  1382. }
  1383. }