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