UnifiedCostCalculationService.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  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. ];
  45. // 批次大小
  46. const BATCH_SIZE = 100;
  47. /**
  48. * 主入口:执行所有成本计算并统一入库
  49. */
  50. public function calculateAndSaveAll(array $param): array
  51. {
  52. Db::startTrans();
  53. try {
  54. $month = $param['month'];
  55. $sysId = $param['sys_id'] ?? '';
  56. // 1. 清空旧数据
  57. $this->clearOldData($month);
  58. // 2. 执行五项计算
  59. $this->calculateDirectLabor($param);
  60. $this->calculateDirectUtilities($param);
  61. $this->calculateIndirectMaterials($month);
  62. $this->calculateIndirectLabor($month);
  63. $this->calculateApportionedUtilities($param);
  64. // 3. 统一插入数据
  65. $this->saveAllData($month, $sysId);
  66. Db::commit();
  67. $this->logSuccess($month);
  68. return $this->buildSuccessResponse($month);
  69. } catch (Exception $e) {
  70. Db::rollback();
  71. $this->logError($e);
  72. return $this->buildErrorResponse($e);
  73. }
  74. }
  75. /**
  76. * 清空旧数据
  77. */
  78. protected function clearOldData(string $month): void
  79. {
  80. $this->monthlyCostDetails = [];
  81. $this->allocationFactors = [];
  82. }
  83. /**
  84. * 1. 计算直接人工
  85. */
  86. protected function calculateDirectLabor(array $param): void
  87. {
  88. $month = $param['month'];
  89. $sysId = $param['sys_id'] ?? '';
  90. $now = date('Y-m-d H:i:s');
  91. $list = Db::name('绩效工资汇总')
  92. ->alias('a')
  93. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  94. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  95. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  96. ->field($this->getDirectLaborFields())
  97. ->where('a.sys_ny', $month)
  98. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  99. ->select();
  100. foreach ($list as $item) {
  101. $this->monthlyCostDetails[] = $this->buildMonthlyCostDetail($item, $month, $sysId, $now);
  102. }
  103. Log::info("直接人工计算完成,记录数: " . count($list));
  104. }
  105. /**
  106. * 获取直接人工查询字段
  107. */
  108. protected function getDirectLaborFields(): string
  109. {
  110. return 'a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,
  111. sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  112. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,
  113. a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,
  114. a.sys_rq as 年月,a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,
  115. a.千件工价';
  116. }
  117. /**
  118. * 构建月度成本明细记录
  119. */
  120. protected function buildMonthlyCostDetail(array $data, string $month, string $sysId, string $now): array
  121. {
  122. $banju = $data['版距'] === '0.0' ? 1000 : $data['版距'];
  123. $moshushu = $this->calculateMoshushu($data);
  124. $chanliang = $this->calculateChanliang($data);
  125. $renGongFenTan = $this->calculateRenGongFenTan($chanliang, $data);
  126. return array_merge([
  127. '车间名称' => $data['使用部门'] ?? '',
  128. 'sys_ny' => $month,
  129. 'sczl_gdbh' => $data['工单编号'],
  130. '印件名称' => $data['印件名称'] ?? '',
  131. 'sczl_yjno' => $data['印件号'],
  132. 'sczl_gxh' => $data['工序号'],
  133. '工序名称' => $data['工序名称'] ?? '',
  134. 'sczl_jtbh' => $data['sczl_jtbh'] ?? '',
  135. '卷张换算系数' => floatval($banju) / 1000,
  136. '占用机时' => floatval($data['占用机时']) ?? 0,
  137. '班组车头产量' => floatval($data['班组车头产量']) ?? 0,
  138. 'sczl_ms' => floatval($moshushu),
  139. '工序难度系数' => floatval($data['工序难度系数']) ?? 1,
  140. '班组换算产量' => floatval($data['班组换算产量']) ?? 0,
  141. '千件工价' => floatval($data['千件工价']) ?? 0,
  142. '计件产量' => floatval($chanliang),
  143. '水电分摊因子' => floatval($data['占用机时']) ?? 0,
  144. '材料分摊因子' => floatval($chanliang),
  145. '人工分摊因子' => floatval($renGongFenTan),
  146. 'Sys_id' => $sysId,
  147. 'Sys_rq' => $now
  148. ], self::DEFAULT_FIELD_VALUES);
  149. }
  150. /**
  151. * 计算墨色数
  152. */
  153. protected function calculateMoshushu(array $data): float
  154. {
  155. if (strpos($data['工序名称'] ?? '', '切废') !== false) {
  156. return 0.2;
  157. }
  158. return $data['墨色数'] === '0.00' ? 1.0 : floatval($data['墨色数']);
  159. }
  160. /**
  161. * 计算产量
  162. */
  163. protected function calculateChanliang(array $data): float
  164. {
  165. return ($data['班组车头产量'] * $data['工序难度系数']) + $data['班组换算产量'];
  166. }
  167. /**
  168. * 计算人工分摊
  169. */
  170. protected function calculateRenGongFenTan(float $chanliang, array $data): float
  171. {
  172. return ($chanliang / 1000) * $data['千件工价'];
  173. }
  174. /**
  175. * 2. 计算直接水电
  176. */
  177. protected function calculateDirectUtilities(array $param): void
  178. {
  179. $month = $param['month'];
  180. if (empty($this->monthlyCostDetails)) {
  181. throw new Exception("请先执行直接人工计算");
  182. }
  183. $utilityData = $this->fetchDirectUtilities($month);
  184. if (empty($utilityData)) {
  185. Log::info("{$month}月份未找到直接水电费用数据");
  186. return;
  187. }
  188. $machineUtilities = $this->calculateMachineUtilities($utilityData);
  189. $workOrderHours = $this->getMachineWorkHours();
  190. $machineTotalHours = $this->calculateMachineTotalHours($workOrderHours);
  191. $allocationResults = $this->allocateUtilitiesToWorkOrders(
  192. $machineUtilities,
  193. $workOrderHours,
  194. $machineTotalHours
  195. );
  196. $this->updateDirectUtilitiesToCostDetails($allocationResults);
  197. }
  198. /**
  199. * 获取直接水电数据
  200. */
  201. protected function fetchDirectUtilities(string $month): array
  202. {
  203. return Db::name('成本_各月水电气')
  204. ->where('Sys_ny', $month)
  205. ->where('费用类型', '直接')
  206. ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价')
  207. ->select();
  208. }
  209. /**
  210. * 计算机台水电费用
  211. */
  212. protected function calculateMachineUtilities(array $utilityData): array
  213. {
  214. $machineUtilities = [];
  215. foreach ($utilityData as $item) {
  216. $machineCode = $item['设备编号'] ?? '';
  217. if (empty($machineCode)) {
  218. continue;
  219. }
  220. $electricityCost = $this->calculateElectricityCost($item);
  221. $gasCost = $this->calculateGasCost($item);
  222. $totalCost = $electricityCost + $gasCost;
  223. if (!isset($machineUtilities[$machineCode])) {
  224. $machineUtilities[$machineCode] = [
  225. '机器编号' => $machineCode,
  226. '部门名称' => $item['部门名称'] ?? '',
  227. '总费用' => 0,
  228. '电费' => 0,
  229. '气费' => 0,
  230. ];
  231. }
  232. $machineUtilities[$machineCode]['总费用'] += $totalCost;
  233. $machineUtilities[$machineCode]['电费'] += $electricityCost;
  234. $machineUtilities[$machineCode]['气费'] += $gasCost;
  235. }
  236. return $machineUtilities;
  237. }
  238. /**
  239. * 计算电费
  240. */
  241. protected function calculateElectricityCost(array $item): float
  242. {
  243. return round(floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0), 2);
  244. }
  245. /**
  246. * 计算气费
  247. */
  248. protected function calculateGasCost(array $item): float
  249. {
  250. return round(floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0), 2);
  251. }
  252. /**
  253. * 获取机台工作小时数
  254. */
  255. protected function getMachineWorkHours(): array
  256. {
  257. $workHours = [];
  258. foreach ($this->monthlyCostDetails as $detail) {
  259. $machineCode = $detail['sczl_jtbh'] ?? '';
  260. $hours = floatval($detail['占用机时'] ?? 0);
  261. if (!empty($machineCode) && $hours > 0) {
  262. $workHours[] = [
  263. 'sczl_gdbh' => $detail['sczl_gdbh'] ?? '',
  264. 'sczl_yjno' => $detail['sczl_yjno'] ?? '',
  265. 'sczl_gxh' => $detail['sczl_gxh'] ?? '',
  266. 'sczl_jtbh' => $machineCode,
  267. '占用机时' => $hours,
  268. '车间名称' => $detail['车间名称'] ?? '',
  269. ];
  270. }
  271. }
  272. return $workHours;
  273. }
  274. /**
  275. * 计算机台总小时数
  276. */
  277. protected function calculateMachineTotalHours(array $workOrderHours): array
  278. {
  279. $machineTotalHours = [];
  280. foreach ($workOrderHours as $workOrder) {
  281. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  282. $hours = floatval($workOrder['占用机时'] ?? 0);
  283. if (!empty($machineCode) && $hours > 0) {
  284. $machineTotalHours[$machineCode] = ($machineTotalHours[$machineCode] ?? 0) + $hours;
  285. }
  286. }
  287. return $machineTotalHours;
  288. }
  289. /**
  290. * 分摊水电到工单
  291. */
  292. protected function allocateUtilitiesToWorkOrders(
  293. array $machineUtilities,
  294. array $workOrderHours,
  295. array $machineTotalHours
  296. ): array {
  297. $allocationResults = [];
  298. foreach ($workOrderHours as $workOrder) {
  299. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  300. $hours = floatval($workOrder['占用机时'] ?? 0);
  301. if (empty($machineCode) ||
  302. !isset($machineUtilities[$machineCode]) ||
  303. $machineUtilities[$machineCode]['总费用'] <= 0 ||
  304. !isset($machineTotalHours[$machineCode]) ||
  305. $machineTotalHours[$machineCode] <= 0) {
  306. continue;
  307. }
  308. $allocationRatio = $hours / $machineTotalHours[$machineCode];
  309. $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2);
  310. $uniqueKey = $this->getWorkOrderUniqueKey($workOrder);
  311. $allocationResults[$uniqueKey] = [
  312. 'unique_key' => $uniqueKey,
  313. 'sczl_gdbh' => $workOrder['sczl_gdbh'] ?? '',
  314. 'sczl_yjno' => $workOrder['sczl_yjno'] ?? '',
  315. 'sczl_gxh' => $workOrder['sczl_gxh'] ?? '',
  316. 'sczl_jtbh' => $machineCode,
  317. '占用机时' => $hours,
  318. '分摊比例' => $allocationRatio,
  319. '分摊金额' => $allocatedAmount,
  320. '机台总费用' => $machineUtilities[$machineCode]['总费用'],
  321. '机台总工时' => $machineTotalHours[$machineCode],
  322. ];
  323. }
  324. return $allocationResults;
  325. }
  326. /**
  327. * 获取工单唯一键
  328. */
  329. protected function getWorkOrderUniqueKey(array $workOrder): string
  330. {
  331. return implode('-', [
  332. $workOrder['sczl_gdbh'] ?? '',
  333. $workOrder['sczl_yjno'] ?? '',
  334. $workOrder['sczl_gxh'] ?? '',
  335. $workOrder['sczl_jtbh'] ?? ''
  336. ]);
  337. }
  338. /**
  339. * 更新直接水电到成本明细
  340. */
  341. protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void
  342. {
  343. if (empty($allocationResults)) {
  344. return;
  345. }
  346. $updatedCount = 0;
  347. $totalAllocatedAmount = 0;
  348. foreach ($this->monthlyCostDetails as &$costDetail) {
  349. $uniqueKey = $this->getWorkOrderUniqueKey($costDetail);
  350. if (isset($allocationResults[$uniqueKey])) {
  351. $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'];
  352. $costDetail['直接水电'] = $allocatedAmount;
  353. $totalAllocatedAmount += $allocatedAmount;
  354. $updatedCount++;
  355. }
  356. }
  357. Log::info("已更新直接水电费的工单数量: {$updatedCount}, 分配总金额: {$totalAllocatedAmount}");
  358. }
  359. /**
  360. * 3. 计算间接材料分摊
  361. */
  362. protected function calculateIndirectMaterials(string $month): void
  363. {
  364. if (empty($this->monthlyCostDetails)) {
  365. return;
  366. }
  367. $date = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  368. $totalMoney = $this->getIndirectMaterialTotal($date);
  369. if (!$totalMoney || $totalMoney <= 0) {
  370. return;
  371. }
  372. $totalChroma = $this->calculateTotalChroma();
  373. if ($totalChroma <= 0) {
  374. return;
  375. }
  376. $this->allocateIndirectMaterials($totalMoney, $totalChroma);
  377. }
  378. /**
  379. * 获取间接材料总额
  380. */
  381. protected function getIndirectMaterialTotal(string $date): float
  382. {
  383. $result = Db::name('材料出库单列表')
  384. ->where([
  385. '出库日期' => ['like', $date . '%'],
  386. '部门' => '印刷成本中心'
  387. ])
  388. ->whereNull('表体生产订单号')
  389. ->field('SUM(金额) as money')
  390. ->find();
  391. return $result ? floatval($result['money']) : 0;
  392. }
  393. /**
  394. * 计算总色度数
  395. */
  396. protected function calculateTotalChroma(): float
  397. {
  398. $totalChroma = 0;
  399. foreach ($this->monthlyCostDetails as $detail) {
  400. $totalChroma += floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  401. }
  402. return $totalChroma;
  403. }
  404. /**
  405. * 分摊间接材料
  406. */
  407. protected function allocateIndirectMaterials(float $totalMoney, float $totalChroma): void
  408. {
  409. foreach ($this->monthlyCostDetails as &$detail) {
  410. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  411. $money = round($totalMoney * ($chroma / $totalChroma), 2);
  412. $detail['分摊材料'] = $money;
  413. }
  414. }
  415. /**
  416. * 4. 计算间接人工分摊
  417. */
  418. protected function calculateIndirectLabor(string $month): void
  419. {
  420. $wageRatio = $this->getWageRatio();
  421. if (empty($wageRatio)) {
  422. return;
  423. }
  424. $monthWage = $this->getMonthlyWage($month);
  425. if (empty($monthWage)) {
  426. return;
  427. }
  428. $this->allocateIndirectLabor($wageRatio, $monthWage);
  429. }
  430. /**
  431. * 获取工资比例
  432. */
  433. protected function getWageRatio(): array
  434. {
  435. $workshopTotals = [];
  436. foreach ($this->monthlyCostDetails as $detail) {
  437. $workshop = $detail['车间名称'];
  438. $amount = floatval($detail['人工分摊因子']);
  439. $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount;
  440. }
  441. $total = array_sum($workshopTotals);
  442. if ($total <= 0) {
  443. return [];
  444. }
  445. $ratios = [];
  446. foreach ($workshopTotals as $workshop => $workshopTotal) {
  447. $ratios[$workshop] = round($workshopTotal / $total, 4);
  448. }
  449. return $ratios;
  450. }
  451. /**
  452. * 获取月度工资数据
  453. */
  454. protected function getMonthlyWage(string $month): array
  455. {
  456. return Db::name('成本_各月其他费用')
  457. ->where('sys_ny', $month)
  458. ->field('部门人员工资,管理人员工资')
  459. ->find() ?: [];
  460. }
  461. /**
  462. * 分摊间接人工
  463. */
  464. protected function allocateIndirectLabor(array $wageRatio, array $monthWage): void
  465. {
  466. foreach ($wageRatio as $workshopName => $ratio) {
  467. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  468. if (empty($chromaData['list']) || $chromaData['total'] == 0) {
  469. continue;
  470. }
  471. $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData);
  472. }
  473. }
  474. /**
  475. * 获取车间色度数数据
  476. */
  477. protected function getChromaDataForWorkshop(string $workshop): array
  478. {
  479. $data = ['total' => 0, 'list' => []];
  480. foreach ($this->monthlyCostDetails as $detail) {
  481. if ($detail['车间名称'] === $workshop) {
  482. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  483. $data['total'] += $chroma;
  484. $data['list'][] = [
  485. 'sczl_gdbh' => $detail['sczl_gdbh'],
  486. 'sczl_yjno' => $detail['sczl_yjno'],
  487. 'sczl_gxh' => $detail['sczl_gxh'],
  488. 'sczl_jtbh' => $detail['sczl_jtbh'],
  489. 'chroma' => $chroma
  490. ];
  491. }
  492. }
  493. return $data;
  494. }
  495. /**
  496. * 分配工资到车间
  497. */
  498. protected function allocateWageToWorkshop(
  499. string $workshop,
  500. float $ratio,
  501. array $monthWage,
  502. array $chromaData
  503. ): void {
  504. $wageTypes = [
  505. '部门人员工资' => '车间人工',
  506. '管理人员工资' => '部门人工附加'
  507. ];
  508. foreach ($wageTypes as $wageType => $fieldName) {
  509. if (empty($monthWage[$wageType]) || $monthWage[$wageType] <= 0) {
  510. continue;
  511. }
  512. $workshopAmount = $ratio * $monthWage[$wageType];
  513. if ($chromaData['total'] <= 0) {
  514. continue;
  515. }
  516. $this->allocateWageToDetails($workshop, $workshopAmount, $chromaData, $fieldName);
  517. }
  518. }
  519. /**
  520. * 分配工资到明细记录
  521. */
  522. protected function allocateWageToDetails(
  523. string $workshop,
  524. float $workshopAmount,
  525. array $chromaData,
  526. string $fieldName
  527. ): void {
  528. foreach ($this->monthlyCostDetails as &$detail) {
  529. if ($detail['车间名称'] === $workshop) {
  530. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  531. $amount = round($chroma / $chromaData['total'] * $workshopAmount, 2);
  532. $detail[$fieldName] += $amount;
  533. }
  534. }
  535. }
  536. /**
  537. * 5. 计算分摊水电
  538. */
  539. protected function calculateApportionedUtilities(array $param): void
  540. {
  541. $month = $param['month'];
  542. $sysId = $param['sys_id'] ?? '';
  543. $utilityData = $this->fetchApportionedUtilities($month);
  544. if (empty($utilityData)) {
  545. return;
  546. }
  547. $machineAllocations = $this->calculateMachineAllocations($utilityData);
  548. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  549. $this->allocateApportionedUtilities($machineAllocations);
  550. }
  551. /**
  552. * 获取分摊水电数据
  553. */
  554. protected function fetchApportionedUtilities(string $month): array
  555. {
  556. return Db::name('成本_各月水电气')
  557. ->where('Sys_ny', $month)
  558. ->whereLike('费用类型', '%分摊%')
  559. ->select();
  560. }
  561. /**
  562. * 计算机台分摊金额
  563. */
  564. protected function calculateMachineAllocations(array $utilityData): array
  565. {
  566. $allocations = [];
  567. $machineHours = $this->getMachineHours();
  568. foreach ($utilityData as $item) {
  569. $subject = $this->simplifySubjectName($item['科目名称']);
  570. $amount = $this->calculateUtilityAmount($item);
  571. if ($amount <= 0) {
  572. continue;
  573. }
  574. $this->allocateBySubject($allocations, $subject, $amount, $machineHours);
  575. }
  576. return $allocations;
  577. }
  578. /**
  579. * 获取机台运行时间
  580. */
  581. protected function getMachineHours(): array
  582. {
  583. $hours = [];
  584. foreach ($this->monthlyCostDetails as $detail) {
  585. $machine = $detail['sczl_jtbh'];
  586. $hour = floatval($detail['占用机时']);
  587. if (!empty($machine)) {
  588. $hours[$machine] = ($hours[$machine] ?? 0) + $hour;
  589. }
  590. }
  591. return $hours;
  592. }
  593. /**
  594. * 按科目分摊
  595. */
  596. protected function allocateBySubject(
  597. array &$allocations,
  598. string $subject,
  599. float $amount,
  600. array $machineHours
  601. ): void {
  602. if ($subject === '待分摊总额') {
  603. $this->allocateByFloor($allocations, $amount, $machineHours);
  604. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  605. $this->allocateToRollCoater($allocations, $subject, $amount, $machineHours);
  606. } else {
  607. $this->allocateGlobally($allocations, $subject, $amount, $machineHours);
  608. }
  609. }
  610. /**
  611. * 按楼层分摊
  612. */
  613. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours): void
  614. {
  615. $floorData = $this->groupMachinesByFloor($machineHours);
  616. if ($floorData['totalHours'] <= 0) {
  617. return;
  618. }
  619. foreach ($floorData['floors'] as $floor => $data) {
  620. if ($data['hours'] <= 0) {
  621. continue;
  622. }
  623. $floorAmount = $amount * ($data['hours'] / $floorData['totalHours']);
  624. $this->allocateToFloorMachines($allocations, $floorAmount, $data['machines'], $machineHours);
  625. }
  626. }
  627. /**
  628. * 按楼层分组机台
  629. */
  630. protected function groupMachinesByFloor(array $machineHours): array
  631. {
  632. $floors = ['1' => ['hours' => 0, 'machines' => []], '2' => ['hours' => 0, 'machines' => []]];
  633. $totalHours = 0;
  634. foreach ($machineHours as $machine => $hours) {
  635. $floor = $this->getFloorByMachine($machine);
  636. if ($floor && isset($floors[$floor])) {
  637. $floors[$floor]['hours'] += $hours;
  638. $floors[$floor]['machines'][] = $machine;
  639. $totalHours += $hours;
  640. }
  641. }
  642. return ['floors' => $floors, 'totalHours' => $totalHours];
  643. }
  644. /**
  645. * 分摊到楼层机台
  646. */
  647. protected function allocateToFloorMachines(
  648. array &$allocations,
  649. float $floorAmount,
  650. array $machines,
  651. array $machineHours
  652. ): void {
  653. $floorHours = 0;
  654. foreach ($machines as $machine) {
  655. $floorHours += $machineHours[$machine];
  656. }
  657. if ($floorHours <= 0) {
  658. return;
  659. }
  660. foreach ($machines as $machine) {
  661. $machineHoursAmount = $machineHours[$machine];
  662. if ($machineHoursAmount <= 0) {
  663. continue;
  664. }
  665. $allocations[$machine]['待分摊总额'] =
  666. ($allocations[$machine]['待分摊总额'] ?? 0) +
  667. round($floorAmount * ($machineHoursAmount / $floorHours), 2);
  668. }
  669. }
  670. /**
  671. * 只分摊到卷凹机组
  672. */
  673. protected function allocateToRollCoater(
  674. array &$allocations,
  675. string $subject,
  676. float $amount,
  677. array $machineHours
  678. ): void {
  679. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  680. $totalHours = 0;
  681. foreach ($rollCoaterMachines as $machine) {
  682. $totalHours += $machineHours[$machine];
  683. }
  684. if ($totalHours <= 0) {
  685. return;
  686. }
  687. foreach ($rollCoaterMachines as $machine) {
  688. $machineHoursAmount = $machineHours[$machine];
  689. if ($machineHoursAmount <= 0) {
  690. continue;
  691. }
  692. $allocations[$machine][$subject] =
  693. ($allocations[$machine][$subject] ?? 0) +
  694. round($amount * ($machineHoursAmount / $totalHours), 2);
  695. }
  696. }
  697. /**
  698. * 全局分摊
  699. */
  700. protected function allocateGlobally(
  701. array &$allocations,
  702. string $subject,
  703. float $amount,
  704. array $machineHours
  705. ): void {
  706. $totalHours = array_sum($machineHours);
  707. if ($totalHours <= 0) {
  708. return;
  709. }
  710. foreach ($machineHours as $machine => $hours) {
  711. if ($hours <= 0) {
  712. continue;
  713. }
  714. $allocations[$machine][$subject] =
  715. ($allocations[$machine][$subject] ?? 0) +
  716. round($amount * ($hours / $totalHours), 2);
  717. }
  718. }
  719. /**
  720. * 根据机台获取楼层
  721. */
  722. protected function getFloorByMachine(string $machine): ?string
  723. {
  724. $group = Db::name('设备_基本资料')
  725. ->where('设备编号', $machine)
  726. ->value('设备编组');
  727. if (!$group) {
  728. return null;
  729. }
  730. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  731. foreach ($groupNames as $groupName) {
  732. if (strpos($group, $groupName) !== false) {
  733. return $floor;
  734. }
  735. }
  736. }
  737. return null;
  738. }
  739. /**
  740. * 筛选卷凹机组的机台
  741. */
  742. protected function filterRollCoaterMachines(array $machines): array
  743. {
  744. $rollCoater = [];
  745. foreach ($machines as $machine) {
  746. $group = Db::name('设备_基本资料')
  747. ->where('设备编号', $machine)
  748. ->value('设备编组');
  749. if ($group && strpos($group, '03、卷凹机组') !== false) {
  750. $rollCoater[] = $machine;
  751. }
  752. }
  753. return $rollCoater;
  754. }
  755. /**
  756. * 简化科目名称
  757. */
  758. protected function simplifySubjectName(string $subject): string
  759. {
  760. foreach (self::SUBJECT_MAPPING as $keyword => $simple) {
  761. if (strpos($subject, $keyword) !== false) {
  762. return $simple;
  763. }
  764. }
  765. return $subject;
  766. }
  767. /**
  768. * 计算水电金额
  769. */
  770. protected function calculateUtilityAmount(array $item): float
  771. {
  772. $electricity = $this->calculateElectricityCost($item);
  773. $gas = $this->calculateGasCost($item);
  774. return $electricity + $gas;
  775. }
  776. /**
  777. * 生成分摊系数记录
  778. */
  779. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  780. {
  781. $now = date('Y-m-d H:i:s');
  782. foreach ($allocations as $machine => $subjects) {
  783. foreach ($subjects as $subject => $amount) {
  784. $this->allocationFactors[] = [
  785. 'Sys_ny' => $month,
  786. '科目名称' => $subject,
  787. '设备编号' => $machine,
  788. '分摊系数' => 1,
  789. '分摊金额' => $amount,
  790. 'Sys_id' => $sysId,
  791. 'Sys_rq' => $now,
  792. ];
  793. }
  794. }
  795. }
  796. /**
  797. * 分配分摊水电
  798. */
  799. protected function allocateApportionedUtilities(array $machineAllocations): void
  800. {
  801. $machineRates = $this->calculateMachineRates($machineAllocations);
  802. foreach ($this->monthlyCostDetails as &$detail) {
  803. $machine = $detail['sczl_jtbh'];
  804. $hours = floatval($detail['占用机时']);
  805. if ($hours <= 0 || !isset($machineRates[$machine])) {
  806. continue;
  807. }
  808. $detail['直接水电'] = round($hours * 0.69, 2);
  809. foreach ($machineRates[$machine] as $subject => $rate) {
  810. $field = $this->getUtilityFieldName($subject);
  811. $detail[$field] = round($hours * $rate, 2);
  812. }
  813. }
  814. }
  815. /**
  816. * 计算机台每机时费用
  817. */
  818. protected function calculateMachineRates(array $allocations): array
  819. {
  820. $rates = [];
  821. $machineHours = $this->getMachineHours();
  822. foreach ($allocations as $machine => $subjects) {
  823. $totalHours = $machineHours[$machine] ?? 0;
  824. if ($totalHours <= 0) {
  825. continue;
  826. }
  827. $rates[$machine] = [];
  828. foreach ($subjects as $subject => $amount) {
  829. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  830. }
  831. }
  832. return $rates;
  833. }
  834. /**
  835. * 获取水电字段名
  836. */
  837. protected function getUtilityFieldName(string $subject): string
  838. {
  839. $map = [
  840. '待分摊总额' => '分摊水电',
  841. '废气处理' => '废气处理',
  842. '锅炉' => '锅炉',
  843. '空压机' => '空压机',
  844. '热水锅炉' => '热水锅炉',
  845. '真空鼓风机' => '真空鼓风机',
  846. '中央空调' => '中央空调',
  847. ];
  848. return $map[$subject] ?? $subject;
  849. }
  850. /**
  851. * 统一保存所有数据
  852. */
  853. protected function saveAllData(string $month, string $sysId): void
  854. {
  855. // 1. 删除旧数据
  856. $this->deleteOldData($month);
  857. // 2. 插入前检查数据结构
  858. $this->validateDataStructure();
  859. // 3. 插入新数据
  860. $this->insertAllData();
  861. }
  862. /**
  863. * 删除旧数据
  864. */
  865. protected function deleteOldData(string $month): void
  866. {
  867. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  868. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  869. }
  870. /**
  871. * 验证数据结构
  872. */
  873. protected function validateDataStructure(): void
  874. {
  875. if (empty($this->monthlyCostDetails)) {
  876. return;
  877. }
  878. $tableName = '成本v23_月度成本明细';
  879. $columns = Db::query("DESCRIBE `{$tableName}`");
  880. $columnNames = array_column($columns, 'Field');
  881. foreach ($this->monthlyCostDetails as $index => &$row) {
  882. if (count($row) !== count($columnNames)) {
  883. $this->fixRowData($row, $columnNames, $index);
  884. }
  885. }
  886. }
  887. /**
  888. * 修复行数据
  889. */
  890. protected function fixRowData(array &$row, array $columnNames, int $index): void
  891. {
  892. $fixedRow = [];
  893. foreach ($columnNames as $column) {
  894. $fixedRow[$column] = $row[$column] ?? null;
  895. }
  896. $this->monthlyCostDetails[$index] = $fixedRow;
  897. Log::info("已修复第" . ($index + 1) . "行数据");
  898. }
  899. /**
  900. * 插入所有数据
  901. */
  902. protected function insertAllData(): void
  903. {
  904. $this->insertMonthlyCostDetails();
  905. $this->insertAllocationFactors();
  906. }
  907. /**
  908. * 插入月度成本明细
  909. */
  910. protected function insertMonthlyCostDetails(): void
  911. {
  912. if (empty($this->monthlyCostDetails)) {
  913. return;
  914. }
  915. $total = count($this->monthlyCostDetails);
  916. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  917. $batch = array_slice($this->monthlyCostDetails, $i, self::BATCH_SIZE);
  918. $this->insertBatch($batch, '成本v23_月度成本明细', $i);
  919. }
  920. }
  921. /**
  922. * 插入批次数据
  923. */
  924. protected function insertBatch(array $batch, string $tableName, int $startIndex): void
  925. {
  926. $firstRow = reset($batch);
  927. $fields = array_keys($firstRow);
  928. $fieldStr = '`' . implode('`,`', $fields) . '`';
  929. $values = [];
  930. foreach ($batch as $row) {
  931. $rowValues = [];
  932. foreach ($fields as $field) {
  933. $value = $row[$field] ?? null;
  934. $rowValues[] = is_numeric($value) ? $value : "'" . addslashes($value) . "'";
  935. }
  936. $values[] = '(' . implode(',', $rowValues) . ')';
  937. }
  938. $sql = "INSERT INTO `{$tableName}` ({$fieldStr}) VALUES " . implode(',', $values);
  939. try {
  940. Db::execute($sql);
  941. Log::info("成功插入批次 " . (($startIndex / self::BATCH_SIZE) + 1));
  942. } catch (\Exception $e) {
  943. Log::error("插入批次失败: " . $e->getMessage());
  944. throw $e;
  945. }
  946. }
  947. /**
  948. * 插入分摊系数
  949. */
  950. protected function insertAllocationFactors(): void
  951. {
  952. if (!empty($this->allocationFactors)) {
  953. $total = count($this->allocationFactors);
  954. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  955. $batch = array_slice($this->allocationFactors, $i, self::BATCH_SIZE);
  956. $this->insertBatch($batch, '成本_各月分摊系数', $i);
  957. }
  958. }
  959. }
  960. /**
  961. * 记录成功日志
  962. */
  963. protected function logSuccess(string $month): void
  964. {
  965. Log::info("成本核算完成", [
  966. 'month' => $month,
  967. '月度成本明细记录数' => count($this->monthlyCostDetails),
  968. '分摊系数记录数' => count($this->allocationFactors)
  969. ]);
  970. }
  971. /**
  972. * 构建成功响应
  973. */
  974. protected function buildSuccessResponse(string $month): array
  975. {
  976. return [
  977. 'success' => true,
  978. 'message' => '成本核算完成',
  979. 'month' => $month,
  980. 'stats' => [
  981. 'monthly_cost_details' => count($this->monthlyCostDetails),
  982. 'allocation_factors' => count($this->allocationFactors)
  983. ]
  984. ];
  985. }
  986. /**
  987. * 记录错误日志
  988. */
  989. protected function logError(Exception $e): void
  990. {
  991. Log::error("统一成本核算失败", [
  992. 'message' => $e->getMessage(),
  993. 'trace' => $e->getTraceAsString()
  994. ]);
  995. }
  996. /**
  997. * 构建错误响应
  998. */
  999. protected function buildErrorResponse(Exception $e): array
  1000. {
  1001. return [
  1002. 'success' => false,
  1003. 'message' => '成本核算失败: ' . $e->getMessage(),
  1004. 'error' => $e->getMessage()
  1005. ];
  1006. }
  1007. }