['02、胶印机组', '03、卷凹机组', '06、单凹机组', '05、圆切机组', '04、圆烫机组', '10、模切机组', '09、烫金机组'], '2' => ['01、切纸机组', '11、检品机组', '07、丝印机组', '12、覆膜机组', '08、喷码机组'], ]; // 科目名称映射 const SUBJECT_MAPPING = [ '废气处理' => '废气处理', '锅炉' => '锅炉', '空压机' => '空压机', '热水锅炉' => '热水锅炉', '真空鼓风机' => '真空鼓风机', '中央空调' => '中央空调', '待分摊总额' => '待分摊总额', ]; // 字段默认值 const DEFAULT_FIELD_VALUES = [ '直接水电' => 0, '分摊材料' => 0, '车间人工' => 0, '部门人工附加' => 0, '分摊水电' => 0, '废气处理' => 0, '锅炉' => 0, '空压机' => 0, '热水锅炉' => 0, '真空鼓风机' => 0, '中央空调' => 0, '分摊其他' => 0, 'A_小时折旧额' => 0, '直接材料' => 0, '考核直接材料' => 0, '直接折旧' => 0, '分摊折旧' => 0, '场地租金' => 0, '成本合计' => 0, ]; // 添加科目名称到数据库字段的映射 const SUBJECT_TO_FIELD_MAP = [ // 真空鼓风机相关 '真空鼓风机' => '真空鼓风机', '真空鼓风' => '真空鼓风机', '6#楼真空鼓风' => '真空鼓风机', '真空风机' => '真空鼓风机', // 废气处理相关 '废气处理' => '废气处理', '废气' => '废气处理', // 锅炉相关 '锅炉' => '锅炉', '热水锅炉' => '热水锅炉', // 空压机相关 '空压机' => '空压机', '空压' => '空压机', // 中央空调相关 '中央空调' => '中央空调', '空调' => '中央空调', // 待分摊总额 '待分摊总额' => '分摊水电', '分摊总额' => '分摊水电', ]; // 批次大小 const BATCH_SIZE = 100; /** * 主入口:执行所有成本计算并统一入库 */ public function calculateAndSaveAll(array $param): array { Log::info("开始成本计算,参数: " . json_encode($param)); Db::startTrans(); try { if (!isset($param['month'])) { throw new Exception("缺少必要参数: month"); } $month = $param['month']; $sysId = $param['sys_id'] ?? uniqid(); Log::info("开始清空旧数据"); $this->clearOldData($month); Log::info("开始计算直接人工"); $this->calculateDirectLabor($param); Log::info("直接人工计算完成,记录数: " . count($this->monthlyCostDetails)); Log::info("开始计算直接水电"); $this->calculateDirectUtilities($param); Log::info("开始计算间接材料"); $this->calculateIndirectMaterials($month); Log::info("开始计算间接人工"); $this->calculateIndirectLabor($month); Log::info("开始计算分摊水电"); $this->calculateApportionedUtilities($param); Log::info("开始保存数据"); $this->saveAllData($month, $sysId); Db::commit(); $this->logSuccess($month); return $this->buildSuccessResponse($month); } catch (\Throwable $t) { // 使用 Throwable 而不是 Exception Db::rollback(); // 记录详细的错误信息 $errorInfo = [ 'message' => $t->getMessage(), 'code' => $t->getCode(), 'file' => $t->getFile(), 'line' => $t->getLine(), 'trace' => $t->getTraceAsString(), 'param' => $param ]; Log::error("统一成本核算失败: " . json_encode($errorInfo, JSON_UNESCAPED_UNICODE)); return [ 'success' => false, 'message' => '成本核算失败: ' . $t->getMessage(), 'error' => $t->getMessage(), 'error_details' => $errorInfo ]; } } /** * 清空旧数据 */ protected function clearOldData(string $month): void { $this->monthlyCostDetails = []; $this->allocationFactors = []; } /** * 1. 计算直接人工 */ protected function calculateDirectLabor(array $param): void { try { $month = $param['month']; $sysId = $param['sys_id'] ?? ''; $now = date('Y-m-d H:i:s'); $list = Db::name('绩效工资汇总') ->alias('a') ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh') ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT') ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno') ->field($this->getDirectLaborFields()) ->where('a.sys_ny', $month) ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh') ->select(); // 转换为数组(如果返回的是对象) if (!is_array($list)) { $list = $list->toArray(); } Log::info("直接人工查询结果数: " . count($list)); foreach ($list as $item) { // 确保 $item 是数组 $itemArray = is_object($item) ? (array)$item : $item; if (!is_array($itemArray)) { Log::warning("直接人工数据项格式不正确: " . gettype($item)); continue; } $this->monthlyCostDetails[] = $this->buildMonthlyCostDetail($itemArray, $month, $sysId, $now); } } catch (\Exception $e) { Log::error("计算直接人工失败: " . $e->getMessage()); throw new Exception("直接人工计算失败: " . $e->getMessage()); } } /** * 获取直接人工查询字段 */ protected function getDirectLaborFields(): string { return 'a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号, sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称, a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数, a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时, a.sys_rq as 年月,a.工序难度系数,sum(a.班组换算产量) as 班组换算产量, a.千件工价'; } /** * 构建月度成本明细记录 */ protected function buildMonthlyCostDetail(array $data, string $month, string $sysId, string $now): array { $banju = $data['版距'] === '0.0' ? 1000 : $data['版距']; $moshushu = $this->calculateMoshushu($data); $chanliang = $this->calculateChanliang($data); $renGongFenTan = $this->calculateRenGongFenTan($chanliang, $data); return array_merge([ '车间名称' => $data['使用部门'] ?? '', 'sys_ny' => $month, 'sczl_gdbh' => $data['工单编号'], '印件名称' => $data['印件名称'] ?? '', 'sczl_yjno' => $data['印件号'], 'sczl_gxh' => $data['工序号'], '工序名称' => $data['工序名称'] ?? '', 'sczl_jtbh' => $data['sczl_jtbh'] ?? '', '卷张换算系数' => floatval($banju) / 1000, '占用机时' => floatval($data['占用机时']) ?? 0, '班组车头产量' => floatval($data['班组车头产量']) ?? 0, 'sczl_ms' => floatval($moshushu), '工序难度系数' => floatval($data['工序难度系数']) ?? 1, '班组换算产量' => floatval($data['班组换算产量']) ?? 0, '千件工价' => floatval($data['千件工价']) ?? 0, '计件产量' => floatval($chanliang), '水电分摊因子' => floatval($data['占用机时']) ?? 0, '材料分摊因子' => floatval($chanliang), '人工分摊因子' => floatval($renGongFenTan), 'Sys_id' => $sysId, 'Sys_rq' => $now ], self::DEFAULT_FIELD_VALUES); } /** * 计算墨色数 */ protected function calculateMoshushu(array $data): float { if (strpos($data['工序名称'] ?? '', '切废') !== false) { return 0.2; } return $data['墨色数'] === '0.00' ? 1.0 : floatval($data['墨色数']); } /** * 计算产量 */ protected function calculateChanliang(array $data): float { return ($data['班组车头产量'] * $data['工序难度系数']) + $data['班组换算产量']; } /** * 计算人工分摊 */ protected function calculateRenGongFenTan(float $chanliang, array $data): float { return ($chanliang / 1000) * $data['千件工价']; } /** * 2. 计算直接水电 */ protected function calculateDirectUtilities(array $param): void { $month = $param['month']; if (empty($this->monthlyCostDetails)) { throw new Exception("请先执行直接人工计算"); } $utilityData = $this->fetchDirectUtilities($month); if (empty($utilityData)) { Log::info("{$month}月份未找到直接水电费用数据"); return; } $machineUtilities = $this->calculateMachineUtilities($utilityData); $workOrderHours = $this->getMachineWorkHours(); $machineTotalHours = $this->calculateMachineTotalHours($workOrderHours); $allocationResults = $this->allocateUtilitiesToWorkOrders( $machineUtilities, $workOrderHours, $machineTotalHours ); $this->updateDirectUtilitiesToCostDetails($allocationResults); } /** * 获取直接水电数据 */ protected function fetchDirectUtilities(string $month): array { return Db::name('成本_各月水电气') ->where('Sys_ny', $month) ->where('费用类型', '直接') ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价') ->select(); } /** * 计算机台水电费用 */ protected function calculateMachineUtilities(array $utilityData): array { $machineUtilities = []; foreach ($utilityData as $item) { $machineCode = $item['设备编号'] ?? ''; if (empty($machineCode)) { continue; } $electricityCost = $this->calculateElectricityCost($item); $gasCost = $this->calculateGasCost($item); $totalCost = $electricityCost + $gasCost; if (!isset($machineUtilities[$machineCode])) { $machineUtilities[$machineCode] = [ '机器编号' => $machineCode, '部门名称' => $item['部门名称'] ?? '', '总费用' => 0, '电费' => 0, '气费' => 0, ]; } $machineUtilities[$machineCode]['总费用'] += $totalCost; $machineUtilities[$machineCode]['电费'] += $electricityCost; $machineUtilities[$machineCode]['气费'] += $gasCost; } return $machineUtilities; } /** * 计算电费 */ protected function calculateElectricityCost(array $item): float { return round(floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0), 2); } /** * 计算气费 */ protected function calculateGasCost(array $item): float { return round(floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0), 2); } /** * 获取机台工作小时数 */ protected function getMachineWorkHours(): array { $workHours = []; foreach ($this->monthlyCostDetails as $detail) { try { // 确保 $detail 是数组 $detailArray = is_object($detail) ? (array)$detail : $detail; if (!is_array($detailArray)) { Log::warning("成本明细数据格式不正确: " . gettype($detail)); continue; } $machineCode = $detailArray['sczl_jtbh'] ?? ''; $hours = floatval($detailArray['占用机时'] ?? 0); if (!empty($machineCode) && $hours > 0) { $workHours[] = [ 'sczl_gdbh' => $detailArray['sczl_gdbh'] ?? '', 'sczl_yjno' => $detailArray['sczl_yjno'] ?? '', 'sczl_gxh' => $detailArray['sczl_gxh'] ?? '', 'sczl_jtbh' => $machineCode, '占用机时' => $hours, '车间名称' => $detailArray['车间名称'] ?? '', ]; } } catch (\Exception $e) { Log::error("处理机台工作小时数时出错: " . $e->getMessage()); continue; } } Log::info("从月度成本明细数据中获取机台运行时间,记录数: " . count($workHours)); return $workHours; } /** * 计算机台总小时数 */ protected function calculateMachineTotalHours(array $workOrderHours): array { $machineTotalHours = []; foreach ($workOrderHours as $workOrder) { $machineCode = $workOrder['sczl_jtbh'] ?? ''; $hours = floatval($workOrder['占用机时'] ?? 0); if (!empty($machineCode) && $hours > 0) { $machineTotalHours[$machineCode] = ($machineTotalHours[$machineCode] ?? 0) + $hours; } } return $machineTotalHours; } /** * 分摊水电到工单 */ protected function allocateUtilitiesToWorkOrders( array $machineUtilities, array $workOrderHours, array $machineTotalHours ): array { $allocationResults = []; $processedCount = 0; $skippedCount = 0; foreach ($workOrderHours as $workOrder) { try { // 确保 $workOrder 是数组 $orderArray = is_object($workOrder) ? (array)$workOrder : $workOrder; if (!is_array($orderArray)) { Log::warning("工单小时数据格式不正确: " . gettype($workOrder)); $skippedCount++; continue; } $machineCode = $orderArray['sczl_jtbh'] ?? ''; $hours = floatval($orderArray['占用机时'] ?? 0); if (empty($machineCode)) { Log::debug("工单缺少机器编号: " . json_encode($orderArray)); $skippedCount++; continue; } if (!isset($machineUtilities[$machineCode])) { Log::debug("机器 {$machineCode} 没有水电费用数据"); $skippedCount++; continue; } if ($machineUtilities[$machineCode]['总费用'] <= 0) { Log::debug("机器 {$machineCode} 水电费用为0"); $skippedCount++; continue; } if (!isset($machineTotalHours[$machineCode]) || $machineTotalHours[$machineCode] <= 0) { Log::warning("机器 {$machineCode} 总运行时间为0,无法分摊"); $skippedCount++; continue; } $allocationRatio = $hours / $machineTotalHours[$machineCode]; $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2); $uniqueKey = $this->getWorkOrderUniqueKey($orderArray); $allocationResults[$uniqueKey] = [ 'unique_key' => $uniqueKey, 'sczl_gdbh' => $orderArray['sczl_gdbh'] ?? '', 'sczl_yjno' => $orderArray['sczl_yjno'] ?? '', 'sczl_gxh' => $orderArray['sczl_gxh'] ?? '', 'sczl_jtbh' => $machineCode, '占用机时' => $hours, '分摊比例' => $allocationRatio, '分摊金额' => $allocatedAmount, '机台总费用' => $machineUtilities[$machineCode]['总费用'], '机台总工时' => $machineTotalHours[$machineCode], ]; $processedCount++; } catch (\Exception $e) { Log::error("分摊水电到工单时出错: " . $e->getMessage()); $skippedCount++; continue; } } Log::info("分摊水电处理完成,成功: {$processedCount}, 跳过: {$skippedCount}, 总计: " . count($workOrderHours)); return $allocationResults; } /** * 获取工单唯一键 */ protected function getWorkOrderUniqueKey($workOrder): string { // 记录传入参数的类型和内容(用于调试) $type = gettype($workOrder); Log::debug("getWorkOrderUniqueKey 接收参数类型: {$type}"); // 如果是对象,转换为数组 if (is_object($workOrder)) { $workOrder = (array)$workOrder; } // 确保是数组 if (!is_array($workOrder)) { Log::error("getWorkOrderUniqueKey 参数不是数组: {$type}, 值: " . print_r($workOrder, true)); return 'invalid-' . uniqid(); } // 调试:记录数组内容 if (count($workOrder) < 4) { Log::debug("getWorkOrderUniqueKey 数组内容: " . json_encode($workOrder)); } // 安全地获取所有可能键名 $gdbh = ''; $yjno = ''; $gxh = ''; $jtbh = ''; // 尝试各种可能的键名 if (isset($workOrder['sczl_gdbh'])) { $gdbh = $workOrder['sczl_gdbh']; } elseif (isset($workOrder['工单编号'])) { $gdbh = $workOrder['工单编号']; } if (isset($workOrder['sczl_yjno'])) { $yjno = $workOrder['sczl_yjno']; } elseif (isset($workOrder['印件号'])) { $yjno = $workOrder['印件号']; } if (isset($workOrder['sczl_gxh'])) { $gxh = $workOrder['sczl_gxh']; } elseif (isset($workOrder['工序号'])) { $gxh = $workOrder['工序号']; } if (isset($workOrder['sczl_jtbh'])) { $jtbh = $workOrder['sczl_jtbh']; } elseif (isset($workOrder['机器编号'])) { $jtbh = $workOrder['机器编号']; } // 验证所有字段都不为空 if (empty($gdbh) || empty($yjno) || empty($gxh) || empty($jtbh)) { Log::warning("工单唯一键字段不完整: gdbh={$gdbh}, yjno={$yjno}, gxh={$gxh}, jtbh={$jtbh}"); } $key = sprintf('%s-%s-%s-%s', $gdbh, $yjno, $gxh, $jtbh); Log::debug("生成的唯一键: {$key}"); return $key; } /** * 更新直接水电到成本明细 */ protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void { if (empty($allocationResults) || empty($this->monthlyCostDetails)) { Log::warning("水电费分配结果或成本明细数据为空,无法更新"); return; } $updatedCount = 0; $totalAllocatedAmount = 0; $notFoundCount = 0; foreach ($this->monthlyCostDetails as &$costDetail) { try { // 确保 $costDetail 是数组 $detailArray = is_object($costDetail) ? (array)$costDetail : $costDetail; $uniqueKey = $this->getWorkOrderUniqueKey($detailArray); if (isset($allocationResults[$uniqueKey])) { $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'] ?? 0; $costDetail['直接水电'] = $allocatedAmount; $totalAllocatedAmount += $allocatedAmount; $updatedCount++; } else { $notFoundCount++; } } catch (\Exception $e) { Log::error("更新直接水电费时出错: " . $e->getMessage()); continue; } } Log::info("已更新直接水电费的工单数量: {$updatedCount}, 未找到的工单数: {$notFoundCount}, 分配总金额: {$totalAllocatedAmount}"); // 如果有大量未找到的工单,记录详细信息 if ($notFoundCount > 0 && $notFoundCount > $updatedCount) { Log::warning("有大量工单未匹配到水电费分配结果,可能键名不匹配"); } } /** * 3. 计算间接材料分摊 */ protected function calculateIndirectMaterials(string $month): void { if (empty($this->monthlyCostDetails)) { return; } $date = substr($month, 0, 4) . '-' . substr($month, 4, 2); $totalMoney = $this->getIndirectMaterialTotal($date); if (!$totalMoney || $totalMoney <= 0) { return; } $totalChroma = $this->calculateTotalChroma(); if ($totalChroma <= 0) { return; } $this->allocateIndirectMaterials($totalMoney, $totalChroma); } /** * 获取间接材料总额 */ protected function getIndirectMaterialTotal(string $date): float { $result = Db::name('材料出库单列表') ->where([ '出库日期' => ['like', $date . '%'], '部门' => '印刷成本中心' ]) ->whereNull('表体生产订单号') ->field('SUM(金额) as money') ->find(); return $result ? floatval($result['money']) : 0; } /** * 计算总色度数 */ protected function calculateTotalChroma(): float { $totalChroma = 0; foreach ($this->monthlyCostDetails as $detail) { $totalChroma += floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']); } return $totalChroma; } /** * 分摊间接材料 */ protected function allocateIndirectMaterials(float $totalMoney, float $totalChroma): void { foreach ($this->monthlyCostDetails as &$detail) { $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']); $money = round($totalMoney * ($chroma / $totalChroma), 2); $detail['分摊材料'] = $money; } } /** * 4. 计算间接人工分摊 */ protected function calculateIndirectLabor(string $month): void { $wageRatio = $this->getWageRatio(); if (empty($wageRatio)) { return; } $monthWage = $this->getMonthlyWage($month); if (empty($monthWage)) { return; } $this->allocateIndirectLabor($wageRatio, $monthWage); } /** * 获取工资比例 */ protected function getWageRatio(): array { $workshopTotals = []; foreach ($this->monthlyCostDetails as $detail) { $workshop = $detail['车间名称']; $amount = floatval($detail['人工分摊因子']); $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount; } $total = array_sum($workshopTotals); if ($total <= 0) { return []; } $ratios = []; foreach ($workshopTotals as $workshop => $workshopTotal) { $ratios[$workshop] = round($workshopTotal / $total, 4); } return $ratios; } /** * 获取月度工资数据 */ protected function getMonthlyWage(string $month): array { return Db::name('成本_各月其他费用') ->where('sys_ny', $month) ->field('部门人员工资,管理人员工资') ->find() ?: []; } /** * 分摊间接人工 */ protected function allocateIndirectLabor(array $wageRatio, array $monthWage): void { foreach ($wageRatio as $workshopName => $ratio) { $chromaData = $this->getChromaDataForWorkshop($workshopName); if (empty($chromaData['list']) || $chromaData['total'] == 0) { continue; } $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData); } } /** * 获取车间色度数数据 */ protected function getChromaDataForWorkshop(string $workshop): array { $data = ['total' => 0, 'list' => []]; foreach ($this->monthlyCostDetails as $detail) { if ($detail['车间名称'] === $workshop) { $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']); $data['total'] += $chroma; $data['list'][] = [ 'sczl_gdbh' => $detail['sczl_gdbh'], 'sczl_yjno' => $detail['sczl_yjno'], 'sczl_gxh' => $detail['sczl_gxh'], 'sczl_jtbh' => $detail['sczl_jtbh'], 'chroma' => $chroma ]; } } return $data; } /** * 分配工资到车间 */ protected function allocateWageToWorkshop( string $workshop, float $ratio, array $monthWage, array $chromaData ): void { $wageTypes = [ '部门人员工资' => '车间人工', '管理人员工资' => '部门人工附加' ]; foreach ($wageTypes as $wageType => $fieldName) { if (empty($monthWage[$wageType]) || $monthWage[$wageType] <= 0) { continue; } $workshopAmount = $ratio * $monthWage[$wageType]; if ($chromaData['total'] <= 0) { continue; } $this->allocateWageToDetails($workshop, $workshopAmount, $chromaData, $fieldName); } } /** * 分配工资到明细记录 */ protected function allocateWageToDetails( string $workshop, float $workshopAmount, array $chromaData, string $fieldName ): void { foreach ($this->monthlyCostDetails as &$detail) { if ($detail['车间名称'] === $workshop) { $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']); $amount = round($chroma / $chromaData['total'] * $workshopAmount, 2); $detail[$fieldName] += $amount; } } } /** * 5. 计算分摊水电 */ protected function calculateApportionedUtilities(array $param): void { try { $month = $param['month']; $sysId = $param['sys_id'] ?? ''; $utilityData = $this->fetchApportionedUtilities($month); if (empty($utilityData)) { Log::info("{$month}月份未找到分摊水电数据"); return; } // 记录原始科目名称用于调试 $originalSubjects = array_unique(array_column($utilityData, '科目名称')); Log::info("原始科目名称: " . implode(', ', $originalSubjects)); $machineAllocations = $this->calculateMachineAllocations($utilityData); if (!empty($machineAllocations)) { $this->generateAllocationFactors($machineAllocations, $month, $sysId); $this->allocateApportionedUtilities($machineAllocations); } } catch (\Exception $e) { Log::error("计算分摊水电时出错: " . $e->getMessage()); throw new Exception("分摊水电计算失败: " . $e->getMessage()); } } /** * 获取分摊水电数据 */ protected function fetchApportionedUtilities(string $month): array { return Db::name('成本_各月水电气') ->where('Sys_ny', $month) ->whereLike('费用类型', '%分摊%') ->select(); } /** * 计算机台分摊金额 */ protected function calculateMachineAllocations(array $utilityData): array { $allocations = []; $machineHours = $this->getMachineHours(); foreach ($utilityData as $item) { try { $subject = $this->simplifySubjectName($item['科目名称']); $amount = $this->calculateUtilityAmount($item); if ($amount <= 0) { continue; } $this->allocateBySubject($allocations, $subject, $amount, $machineHours); } catch (\Exception $e) { Log::error("计算机台分摊金额时出错: " . $e->getMessage()); continue; } } return $allocations; } /** * 获取机台运行时间 */ protected function getMachineHours(): array { $hours = []; foreach ($this->monthlyCostDetails as $detail) { $machine = $detail['sczl_jtbh']; $hour = floatval($detail['占用机时']); if (!empty($machine)) { $hours[$machine] = ($hours[$machine] ?? 0) + $hour; } } return $hours; } /** * 按科目分摊 */ protected function allocateBySubject( array &$allocations, string $subject, float $amount, array $machineHours ): void { try { if ($subject === '待分摊总额') { $this->allocateByFloor($allocations, $amount, $machineHours); } elseif (in_array($subject, ['锅炉', '热水锅炉'])) { $this->allocateToRollCoater($allocations, $subject, $amount, $machineHours); } else { $this->allocateGlobally($allocations, $subject, $amount, $machineHours); } } catch (\Exception $e) { Log::error("按科目分摊时出错 (科目: {$subject}): " . $e->getMessage()); } } /** * 按楼层分摊 */ protected function allocateByFloor(array &$allocations, float $amount, array $machineHours): void { $floorData = $this->groupMachinesByFloor($machineHours); if ($floorData['totalHours'] <= 0) { return; } foreach ($floorData['floors'] as $floor => $data) { if ($data['hours'] <= 0) { continue; } $floorAmount = $amount * ($data['hours'] / $floorData['totalHours']); $this->allocateToFloorMachines($allocations, $floorAmount, $data['machines'], $machineHours); } } /** * 按楼层分组机台 */ protected function groupMachinesByFloor(array $machineHours): array { $floors = ['1' => ['hours' => 0, 'machines' => []], '2' => ['hours' => 0, 'machines' => []]]; $totalHours = 0; foreach ($machineHours as $machine => $hours) { $floor = $this->getFloorByMachine($machine); if ($floor && isset($floors[$floor])) { $floors[$floor]['hours'] += $hours; $floors[$floor]['machines'][] = $machine; $totalHours += $hours; } } return ['floors' => $floors, 'totalHours' => $totalHours]; } /** * 分摊到楼层机台 */ protected function allocateToFloorMachines( array &$allocations, float $floorAmount, array $machines, array $machineHours ): void { $floorHours = 0; foreach ($machines as $machine) { $floorHours += $machineHours[$machine]; } if ($floorHours <= 0) { return; } foreach ($machines as $machine) { $machineHoursAmount = $machineHours[$machine]; if ($machineHoursAmount <= 0) { continue; } $allocations[$machine]['待分摊总额'] = ($allocations[$machine]['待分摊总额'] ?? 0) + round($floorAmount * ($machineHoursAmount / $floorHours), 2); } } /** * 只分摊到卷凹机组 */ protected function allocateToRollCoater( array &$allocations, string $subject, float $amount, array $machineHours ): void { $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours)); $totalHours = 0; foreach ($rollCoaterMachines as $machine) { $totalHours += $machineHours[$machine]; } if ($totalHours <= 0) { return; } foreach ($rollCoaterMachines as $machine) { $machineHoursAmount = $machineHours[$machine]; if ($machineHoursAmount <= 0) { continue; } $allocations[$machine][$subject] = ($allocations[$machine][$subject] ?? 0) + round($amount * ($machineHoursAmount / $totalHours), 2); } } /** * 全局分摊 */ protected function allocateGlobally( array &$allocations, string $subject, float $amount, array $machineHours ): void { $totalHours = array_sum($machineHours); if ($totalHours <= 0) { Log::warning("全局分摊失败:总机时为0"); return; } foreach ($machineHours as $machine => $hours) { try { // 确保 $machine 是有效的字符串键名 if (!is_string($machine) && !is_numeric($machine)) { Log::warning("无效的机器键名: " . print_r($machine, true)); continue; } $machine = (string)$machine; if ($hours <= 0) { continue; } if (!isset($allocations[$machine])) { $allocations[$machine] = []; } $machineAmount = round($amount * ($hours / $totalHours), 2); $allocations[$machine][$subject] = ($allocations[$machine][$subject] ?? 0) + $machineAmount; } catch (\Exception $e) { Log::error("全局分摊到机器 {$machine} 时出错: " . $e->getMessage()); continue; } } } /** * 根据机台获取楼层 */ protected function getFloorByMachine(string $machine): ?string { $group = Db::name('设备_基本资料') ->where('设备编号', $machine) ->value('设备编组'); if (!$group) { return null; } foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) { foreach ($groupNames as $groupName) { if (strpos($group, $groupName) !== false) { return $floor; } } } return null; } /** * 筛选卷凹机组的机台 */ protected function filterRollCoaterMachines(array $machines): array { $rollCoater = []; foreach ($machines as $machine) { $group = Db::name('设备_基本资料') ->where('设备编号', $machine) ->value('设备编组'); if ($group && strpos($group, '03、卷凹机组') !== false) { $rollCoater[] = $machine; } } return $rollCoater; } /** * 简化科目名称 */ protected function simplifySubjectName(string $subject): string { // 先尝试完全匹配 $subject = trim($subject); foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) { if ($subject === $keyword) { return $mappedField; } } // 尝试部分匹配 foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) { if (strpos($subject, $keyword) !== false) { Log::debug("科目名称部分匹配: {$subject} => {$mappedField} (关键词: {$keyword})"); return $mappedField; } } // 如果都不匹配,使用默认映射 Log::warning("未识别的科目名称: {$subject}, 将映射到 '分摊水电'"); return '分摊水电'; } /** * 计算水电金额 */ protected function calculateUtilityAmount(array $item): float { $electricity = $this->calculateElectricityCost($item); $gas = $this->calculateGasCost($item); return $electricity + $gas; } /** * 生成分摊系数记录 */ protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void { $now = date('Y-m-d H:i:s'); foreach ($allocations as $machine => $subjects) { if (!is_string($machine) && !is_numeric($machine)) { Log::warning("无效的机台标识: " . print_r($machine, true)); continue; } foreach ($subjects as $subject => $amount) { if (!is_string($subject)) { Log::warning("无效的科目名称: " . print_r($subject, true)); continue; } // 使用简化后的科目名称 $simplifiedSubject = $this->simplifySubjectName($subject); $this->allocationFactors[] = [ 'Sys_ny' => $month, '科目名称' => $subject, // 原始科目名称 '简化科目名称' => $simplifiedSubject, // 简化后的科目名称 '设备编号' => (string)$machine, '分摊系数' => 1, '分摊金额' => floatval($amount), 'Sys_id' => $sysId, 'Sys_rq' => $now, ]; } } } /** * 分配分摊水电 */ protected function allocateApportionedUtilities(array $machineAllocations): void { $machineRates = $this->calculateMachineRates($machineAllocations); foreach ($this->monthlyCostDetails as &$detail) { $machine = $detail['sczl_jtbh']; $hours = floatval($detail['占用机时']); if ($hours <= 0 || !isset($machineRates[$machine])) { continue; } // 直接水电费(保持不变) $detail['直接水电'] = round($hours * 0.69, 2); // 分摊水电费 - 使用映射后的字段名 foreach ($machineRates[$machine] as $subject => $rate) { $field = $this->getUtilityFieldName($subject); if (!isset($detail[$field])) { Log::warning("数据库字段不存在: {$field},跳过该分摊"); continue; } $detail[$field] = round($hours * $rate, 2); } } } /** * 计算机台每机时费用 */ protected function calculateMachineRates(array $allocations): array { $rates = []; $machineHours = $this->getMachineHours(); foreach ($allocations as $machine => $subjects) { $totalHours = $machineHours[$machine] ?? 0; if ($totalHours <= 0) { continue; } $rates[$machine] = []; foreach ($subjects as $subject => $amount) { $rates[$machine][$subject] = round($amount / $totalHours, 4); } } return $rates; } /** * 获取水电字段名 */ protected function getUtilityFieldName(string $subject): string { // 使用映射表 if (isset(self::SUBJECT_TO_FIELD_MAP[$subject])) { return self::SUBJECT_TO_FIELD_MAP[$subject]; } // 尝试部分匹配 foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) { if (strpos($subject, $keyword) !== false) { return $mappedField; } } // 默认映射到 '分摊水电' Log::warning("未知的科目字段: {$subject}, 映射到 '分摊水电'"); return '分摊水电'; } /** * 统一保存所有数据 */ protected function saveAllData(string $month, string $sysId): void { // 1. 删除旧数据 $this->deleteOldData($month); // 2. 插入前检查数据结构 $this->validateDataStructure(); // 3. 插入新数据 $this->insertAllData(); } /** * 删除旧数据 */ protected function deleteOldData(string $month): void { Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete(); Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete(); } /** * 验证数据结构 */ protected function validateDataStructure(): void { if (empty($this->monthlyCostDetails)) { Log::warning("月度成本明细数据为空"); return; } $tableName = '成本v23_月度成本明细'; try { // 获取数据库表结构 $columns = Db::query("DESCRIBE `{$tableName}`"); $columnNames = array_column($columns, 'Field'); Log::info("表{$tableName}结构字段数: " . count($columnNames)); Log::debug("表字段列表: " . implode(', ', $columnNames)); // 检查并修正每行数据 foreach ($this->monthlyCostDetails as $index => &$row) { // 确保行是数组 $rowArray = is_object($row) ? (array)$row : $row; // 1. 移除表中不存在的字段 foreach ($rowArray as $key => $value) { if (!in_array($key, $columnNames)) { Log::warning("移除无效字段 (行 {$index}): {$key}"); unset($rowArray[$key]); } } // 2. 确保所有表字段都存在 foreach ($columnNames as $column) { if (!array_key_exists($column, $rowArray)) { // 设置默认值 if (in_array($column, ['直接水电', '分摊材料', '车间人工', '部门人工附加', '分摊水电', '废气处理', '锅炉', '空压机', '热水锅炉', '真空鼓风机', '中央空调', '分摊其他'])) { $rowArray[$column] = 0; } else { $rowArray[$column] = null; } Log::debug("添加缺失字段 (行 {$index}): {$column}"); } } // 3. 确保字段顺序与表一致 $orderedRow = []; foreach ($columnNames as $column) { $orderedRow[$column] = $rowArray[$column] ?? null; } // 4. 修复字段名中的特殊字符 $this->sanitizeFieldNames($orderedRow); $row = $orderedRow; // 记录第一行的字段信息用于调试 if ($index === 0) { Log::debug("第一行字段数: " . count($orderedRow) . ", 字段: " . implode(', ', array_keys($orderedRow))); } } Log::info("数据结构验证完成,总记录数: " . count($this->monthlyCostDetails)); } catch (\Exception $e) { Log::error("验证数据结构时出错: " . $e->getMessage()); } } /** * 修复行数据 */ protected function fixRowData(array &$row, array $columnNames, int $index): void { $fixedRow = []; foreach ($columnNames as $column) { $fixedRow[$column] = $row[$column] ?? 0; } $this->monthlyCostDetails[$index] = $fixedRow; Log::info("已修复第" . ($index + 1) . "行数据"); } /** * 插入所有数据 */ protected function insertAllData(): void { $this->insertMonthlyCostDetails(); $this->insertAllocationFactors(); } /** * 插入月度成本明细 */ protected function insertMonthlyCostDetails(): void { if (empty($this->monthlyCostDetails)) { return; } $total = count($this->monthlyCostDetails); for ($i = 0; $i < $total; $i += self::BATCH_SIZE) { $batch = array_slice($this->monthlyCostDetails, $i, self::BATCH_SIZE); $this->insertBatch($batch, '成本v23_月度成本明细', $i); } } /** * 插入批次数据 */ protected function insertBatch(array $batch, string $tableName, int $startIndex): void { if (empty($batch)) { Log::warning("批次数据为空,跳过插入"); return; } $firstRow = reset($batch); $fields = array_keys($firstRow); // 验证字段名不包含特殊字符 foreach ($fields as $field) { if (preg_match('/[#@$%^&*()+\-=\[\]{}|;:"<>,.?\/]/', $field)) { Log::error("字段名包含特殊字符: {$field}"); throw new Exception("字段名 '{$field}' 包含非法字符"); } } $fieldStr = '`' . implode('`,`', $fields) . '`'; $values = []; foreach ($batch as $rowIndex => $row) { $rowValues = []; foreach ($fields as $field) { $value = $row[$field] ?? null; if (is_numeric($value)) { $rowValues[] = $value; } elseif (is_null($value)) { $rowValues[] = 'NULL'; } else { $rowValues[] = "'" . addslashes((string)$value) . "'"; } } $values[] = '(' . implode(',', $rowValues) . ')'; // 记录第一行数据用于调试 if ($rowIndex === 0 && $startIndex === 0) { Log::debug("第一行数据字段: " . implode(', ', $fields)); Log::debug("第一行数据值: " . implode(', ', $rowValues)); } } $sql = "INSERT INTO `{$tableName}` ({$fieldStr}) VALUES " . implode(',', $values); // 记录SQL语句(前200个字符) Log::debug("SQL语句: " . substr($sql, 0, 200) . "..."); try { $result = Db::execute($sql); Log::info("成功插入批次 " . (($startIndex / self::BATCH_SIZE) + 1) . ", 影响行数: " . $result); } catch (\Exception $e) { Log::error("插入批次失败: " . $e->getMessage()); Log::error("失败SQL: " . $sql); throw $e; } } /** * 插入分摊系数 */ protected function insertAllocationFactors(): void { if (!empty($this->allocationFactors)) { $total = count($this->allocationFactors); for ($i = 0; $i < $total; $i += self::BATCH_SIZE) { $batch = array_slice($this->allocationFactors, $i, self::BATCH_SIZE); $this->insertBatch($batch, '成本_各月分摊系数', $i); } } } /** * 记录成功日志 */ protected function logSuccess(string $month): void { Log::info("成本核算完成", [ 'month' => $month, '月度成本明细记录数' => count($this->monthlyCostDetails), '分摊系数记录数' => count($this->allocationFactors) ]); } /** * 构建成功响应 */ protected function buildSuccessResponse(string $month): array { return [ 'success' => true, 'message' => '成本核算完成', 'month' => $month, 'stats' => [ 'monthly_cost_details' => count($this->monthlyCostDetails), 'allocation_factors' => count($this->allocationFactors) ] ]; } /** * 记录错误日志 */ protected function logError(\Throwable $t): void { $errorDetails = [ '时间' => date('Y-m-d H:i:s'), '错误类型' => get_class($t), '错误信息' => $t->getMessage(), '错误代码' => $t->getCode(), '文件' => $t->getFile(), '行号' => $t->getLine(), '堆栈跟踪' => $t->getTraceAsString(), '月度成本明细数' => count($this->monthlyCostDetails), '分摊系数数' => count($this->allocationFactors), ]; Log::error("统一成本核算失败详情: " . json_encode($errorDetails, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } /** * 构建错误响应 */ protected function buildErrorResponse(Exception $e): array { return [ 'success' => false, 'message' => '成本核算失败: ' . $e->getMessage(), 'error' => $e->getMessage() ]; } /** * 清理字段名中的特殊字符 */ protected function sanitizeFieldNames(array &$row): void { $sanitized = []; foreach ($row as $key => $value) { // 移除字段名中的特殊字符,只保留字母、数字、下划线和中文字符 $cleanKey = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', '', $key); if ($cleanKey !== $key) { Log::debug("清理字段名: {$key} => {$cleanKey}"); } $sanitized[$cleanKey] = $value; } $row = $sanitized; } }