UnifiedCostCalculationService.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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 = []; // 成本v23_月度成本明细
  14. protected $monthlyUtilities = []; // 成本_各月水电气
  15. protected $allocationFactors = []; // 成本_各月分摊系数
  16. // 配置常量
  17. const FLOOR_GROUP_MAP = [
  18. '1' => ['02、胶印机组', '03、卷凹机组', '06、单凹机组', '05、圆切机组', '04、圆烫机组', '10、模切机组', '09、烫金机组'],
  19. '2' => ['01、切纸机组', '11、检品机组', '07、丝印机组', '12、覆膜机组', '08、喷码机组'],
  20. ];
  21. /**
  22. * 主入口:执行所有成本计算并统一入库
  23. */
  24. public function calculateAndSaveAll(array $param): array
  25. {
  26. Db::startTrans();
  27. try {
  28. $month = $param['month'];
  29. $sysId = $param['sys_id'] ?? '';
  30. // 1. 清空旧数据
  31. $this->clearOldData($month);
  32. // 2. 执行五项计算
  33. $this->calculateDirectLabor($param); // 直接人工
  34. $this->calculateDirectUtilities($param); // 直接水电
  35. $this->calculateIndirectMaterials($month); // 间接材料分摊
  36. $this->calculateIndirectLabor($month); // 间接人工分摊
  37. $this->calculateApportionedUtilities($param); // 分摊水电
  38. // 3. 统一插入数据
  39. $this->saveAllData($month, $sysId);
  40. Db::commit();
  41. Log::info("成本核算完成", [
  42. 'month' => $month,
  43. '月度成本明细记录数' => count($this->monthlyCostDetails),
  44. '水电记录数' => count($this->monthlyUtilities),
  45. '分摊系数记录数' => count($this->allocationFactors)
  46. ]);
  47. return [
  48. 'success' => true,
  49. 'message' => '成本核算完成',
  50. 'stats' => [
  51. 'monthly_cost_details' => count($this->monthlyCostDetails),
  52. 'utilities' => count($this->monthlyUtilities),
  53. 'allocation_factors' => count($this->allocationFactors)
  54. ]
  55. ];
  56. } catch (Exception $e) {
  57. Db::rollback();
  58. Log::error("统一成本核算失败: " . $e->getMessage());
  59. return [
  60. 'success' => false,
  61. 'message' => '成本核算失败: ' . $e->getMessage()
  62. ];
  63. }
  64. }
  65. /**
  66. * 清空旧数据
  67. */
  68. protected function clearOldData(string $month): void
  69. {
  70. // 这里只记录要删除,实际删除操作在最后统一执行
  71. // 所有数据都缓存在内存中,最后统一插入
  72. $this->monthlyCostDetails = [];
  73. $this->monthlyUtilities = [];
  74. $this->allocationFactors = [];
  75. }
  76. /**
  77. * 1. 计算直接人工
  78. */
  79. protected function calculateDirectLabor(array $param): void
  80. {
  81. $month = $param['month'];
  82. $sysId = $param['sys_id'] ?? '';
  83. $list = Db::name('绩效工资汇总')
  84. ->alias('a')
  85. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  86. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  87. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  88. ->field('a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  89. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,a.sys_rq as 年月,
  90. a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,a.千件工价')
  91. ->where('a.sys_ny', $month)
  92. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  93. ->select();
  94. $now = date('Y-m-d H:i:s');
  95. foreach ($list as $k => $v) {
  96. // 处理特殊值
  97. $banju = $v['版距'] === '0.0' ? 1000 : $v['版距'];
  98. $moshushu = $v['墨色数'] === '0.00' ? 1 : $v['墨色数'];
  99. if (strpos($v['工序名称'], '切废')) {
  100. $moshushu = 0.2;
  101. }
  102. $chanliang = $v['班组车头产量'] * $v['工序难度系数'] + $v['班组换算产量'];
  103. $renGongFenTan = ($chanliang / 1000) * $v['千件工价'];
  104. $this->monthlyCostDetails[] = [
  105. '车间名称' => $v['使用部门'] ?? '',
  106. 'sys_ny' => $month,
  107. 'sczl_gdbh' => $v['工单编号'],
  108. '印件名称' => $v['印件名称'] ?? '',
  109. 'sczl_yjno' => $v['印件号'],
  110. 'sczl_gxh' => $v['工序号'],
  111. '工序名称' => $v['工序名称'] ?? '',
  112. 'sczl_jtbh' => $v['sczl_jtbh'] ?? '',
  113. '卷张换算系数' => floatval($banju) / 1000,
  114. '占用机时' => floatval($v['占用机时']) ?? 0,
  115. '班组车头产量' => floatval($v['班组车头产量']) ?? 0,
  116. 'sczl_ms' => floatval($moshushu),
  117. '工序难度系数' => floatval($v['工序难度系数']) ?? 1,
  118. '班组换算产量' => floatval($v['班组换算产量']) ?? 0,
  119. '千件工价' => floatval($v['千件工价']) ?? 0,
  120. '计件产量' => floatval($chanliang),
  121. '水电分摊因子' => floatval($v['占用机时']) ?? 0,
  122. '材料分摊因子' => floatval($chanliang),
  123. '人工分摊因子' => floatval($renGongFenTan),
  124. '直接水电' => 0, // 后续计算
  125. '分摊材料' => 0, // 后续计算
  126. '车间人工' => 0, // 后续计算
  127. '部门人工附加' => 0, // 后续计算
  128. '分摊水电' => 0, // 后续计算
  129. '废气处理' => 0, // 后续计算
  130. '锅炉' => 0, // 后续计算
  131. '空压机' => 0, // 后续计算
  132. '热水锅炉' => 0, // 后续计算
  133. '真空鼓风机' => 0, // 后续计算
  134. '中央空调' => 0, // 后续计算
  135. 'Sys_id' => $sysId,
  136. 'Sys_rq' => $now
  137. ];
  138. }
  139. }
  140. /**
  141. * 2. 计算直接水电
  142. */
  143. protected function calculateDirectUtilities(array $param): void
  144. {
  145. $month = $param['month'];
  146. $sysId = $param['sys_id'] ?? '';
  147. $monthStr = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  148. $sist = ['胶印车间', '凹丝印车间', '印后车间'];
  149. $list = Db::name('设备_产量计酬')
  150. ->alias('a')
  151. ->join('设备_基本资料 b', 'a.sczl_jtbh = b.设备编号')
  152. ->where('a.sczl_rq', 'like', $monthStr . '%')
  153. ->where('b.sys_sbID', '<>', '')
  154. ->where('b.使用部门', 'in', $sist)
  155. ->field('a.sczl_jtbh,sum(a.sczl_设备运行工时) as 通电工时,b.使用部门,rtrim(b.设备名称) as 设备名称')
  156. ->order('b.使用部门,a.sczl_jtbh')
  157. ->group('a.sczl_jtbh')
  158. ->select();
  159. $now = date('Y-m-d H:i:s');
  160. foreach ($list as $v) {
  161. $this->monthlyUtilities[] = [
  162. 'Sys_ny' => $month,
  163. '部门名称' => $v['使用部门'] ?? '',
  164. '费用类型' => '直接',
  165. '设备编号' => $v['sczl_jtbh'] ?? '',
  166. '科目名称' => $v['设备名称'] ?? '',
  167. '耗电量' => floatval($v['通电工时']) ?? 0,
  168. '单位电价' => 0.69,
  169. '耗气量' => 0,
  170. '单位气价' => 0,
  171. 'Sys_id' => $sysId,
  172. 'Sys_rq' => $now,
  173. ];
  174. }
  175. }
  176. /**
  177. * 3. 计算间接材料分摊
  178. */
  179. protected function calculateIndirectMaterials(string $month): void
  180. {
  181. if (empty($this->monthlyCostDetails)) {
  182. return;
  183. }
  184. $date = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  185. // 获取分摊材料总金额
  186. $totalMoney = Db::name('材料出库单列表')
  187. ->where([
  188. '出库日期' => ['like', $date . '%'],
  189. '部门' => '印刷成本中心'
  190. ])
  191. ->whereNull('表体生产订单号')
  192. ->field('SUM(金额) as money')
  193. ->find();
  194. if (!$totalMoney || $totalMoney['money'] <= 0) {
  195. return;
  196. }
  197. // 计算总色度数
  198. $totalChroma = 0;
  199. foreach ($this->monthlyCostDetails as $detail) {
  200. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  201. $totalChroma += $chroma;
  202. }
  203. if ($totalChroma <= 0) {
  204. return;
  205. }
  206. // 分摊到每个记录
  207. foreach ($this->monthlyCostDetails as &$detail) {
  208. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  209. $money = round($totalMoney['money'] * ($chroma / $totalChroma), 2);
  210. $detail['分摊材料'] = $money;
  211. }
  212. }
  213. /**
  214. * 4. 计算间接人工分摊
  215. */
  216. protected function calculateIndirectLabor(string $month): void
  217. {
  218. // 获取工资比例
  219. $wageRatio = $this->getWageRatio($month);
  220. if (empty($wageRatio)) {
  221. return;
  222. }
  223. // 获取月度工资数据
  224. $monthWage = Db::name('成本_各月其他费用')
  225. ->where('sys_ny', $month)
  226. ->field('部门人员工资,管理人员工资')
  227. ->find();
  228. if (empty($monthWage)) {
  229. return;
  230. }
  231. // 计算每个车间的分配数据
  232. foreach ($wageRatio as $workshopName => $ratio) {
  233. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  234. if (empty($chromaData['list']) || $chromaData['total'] == 0) {
  235. continue;
  236. }
  237. // 计算两种工资类型的分配
  238. $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData);
  239. }
  240. }
  241. /**
  242. * 获取车间色度数数据
  243. */
  244. protected function getChromaDataForWorkshop(string $workshop): array
  245. {
  246. $data = [
  247. 'total' => 0,
  248. 'list' => []
  249. ];
  250. foreach ($this->monthlyCostDetails as $detail) {
  251. if ($detail['车间名称'] === $workshop) {
  252. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  253. $data['total'] += $chroma;
  254. $data['list'][] = [
  255. 'sczl_gdbh' => $detail['sczl_gdbh'],
  256. 'sczl_yjno' => $detail['sczl_yjno'],
  257. 'sczl_gxh' => $detail['sczl_gxh'],
  258. 'sczl_jtbh' => $detail['sczl_jtbh'],
  259. 'chroma' => $chroma
  260. ];
  261. }
  262. }
  263. return $data;
  264. }
  265. /**
  266. * 分配工资到车间
  267. */
  268. protected function allocateWageToWorkshop(string $workshop, float $ratio, array $monthWage, array $chromaData): void
  269. {
  270. $updateTypes = [
  271. '部门人员工资' => '车间人工',
  272. '管理人员工资' => '部门人工附加'
  273. ];
  274. foreach ($updateTypes as $wageType => $fieldName) {
  275. if (!isset($monthWage[$wageType]) || $monthWage[$wageType] <= 0) {
  276. continue;
  277. }
  278. $money = $ratio * $monthWage[$wageType];
  279. if ($chromaData['total'] <= 0) {
  280. continue;
  281. }
  282. // 找到对应记录并更新
  283. foreach ($this->monthlyCostDetails as &$detail) {
  284. if ($detail['车间名称'] === $workshop) {
  285. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  286. $perAmount = round($chroma / $chromaData['total'] * $money, 2);
  287. if ($fieldName === '车间人工') {
  288. $detail['车间人工'] += $perAmount;
  289. } else {
  290. $detail['部门人工附加'] += $perAmount;
  291. }
  292. }
  293. }
  294. }
  295. }
  296. /**
  297. * 获取工资比例
  298. */
  299. protected function getWageRatio(string $month): array
  300. {
  301. // 从缓存数据中计算
  302. $workshopTotals = [];
  303. foreach ($this->monthlyCostDetails as $detail) {
  304. $workshop = $detail['车间名称'];
  305. $amount = floatval($detail['人工分摊因子']);
  306. if (!isset($workshopTotals[$workshop])) {
  307. $workshopTotals[$workshop] = 0;
  308. }
  309. $workshopTotals[$workshop] += $amount;
  310. }
  311. $total = array_sum($workshopTotals);
  312. if ($total <= 0) {
  313. return [];
  314. }
  315. $ratios = [];
  316. foreach ($workshopTotals as $workshop => $workshopTotal) {
  317. $ratios[$workshop] = round($workshopTotal / $total, 4);
  318. }
  319. return $ratios;
  320. }
  321. /**
  322. * 5. 计算分摊水电
  323. */
  324. protected function calculateApportionedUtilities(array $param): void
  325. {
  326. $month = $param['month'];
  327. $sysId = $param['sys_id'] ?? '';
  328. // 获取分摊水电数据
  329. $utilityData = Db::name('成本_各月水电气')
  330. ->where('Sys_ny', $month)
  331. ->whereLike('费用类型', '%分摊%')
  332. ->select();
  333. if (empty($utilityData)) {
  334. return;
  335. }
  336. // 计算各机台的分摊金额
  337. $machineAllocations = $this->calculateMachineAllocations($utilityData, $month);
  338. // 生成分摊系数记录
  339. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  340. // 分配到工单
  341. $this->allocateUtilitiesToWorkOrders($machineAllocations, $month);
  342. }
  343. /**
  344. * 计算机台分摊金额
  345. */
  346. protected function calculateMachineAllocations(array $utilityData, string $month): array
  347. {
  348. $allocations = [];
  349. // 先按设备编号分组计算总机时
  350. $machineHours = $this->getMachineHours($month);
  351. // 按科目处理分摊
  352. foreach ($utilityData as $item) {
  353. $subject = $this->simplifySubjectName($item['科目名称']);
  354. $amount = $this->calculateUtilityAmount($item);
  355. if ($amount <= 0) {
  356. continue;
  357. }
  358. // 根据科目类型选择分摊方式
  359. if ($subject === '待分摊总额') {
  360. $this->allocateByFloor($allocations, $amount, $machineHours, $month);
  361. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  362. $this->allocateToRollCoater($allocations, $amount, $machineHours, $month);
  363. } else {
  364. $this->allocateGlobally($allocations, $amount, $machineHours);
  365. }
  366. }
  367. return $allocations;
  368. }
  369. /**
  370. * 获取机台运行时间
  371. */
  372. protected function getMachineHours(string $month): array
  373. {
  374. $hours = [];
  375. foreach ($this->monthlyCostDetails as $detail) {
  376. $machine = $detail['sczl_jtbh'];
  377. $hour = floatval($detail['占用机时']);
  378. if (!isset($hours[$machine])) {
  379. $hours[$machine] = 0;
  380. }
  381. $hours[$machine] += $hour;
  382. }
  383. return $hours;
  384. }
  385. /**
  386. * 按楼层分摊
  387. */
  388. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours, string $month): void
  389. {
  390. // 按楼层分组
  391. $floorHours = ['1' => 0, '2' => 0];
  392. $floorMachines = ['1' => [], '2' => []];
  393. // 先计算每个楼层总机时
  394. foreach ($machineHours as $machine => $hours) {
  395. $floor = $this->getFloorByMachine($machine);
  396. if ($floor && isset($floorHours[$floor])) {
  397. $floorHours[$floor] += $hours;
  398. $floorMachines[$floor][] = $machine;
  399. }
  400. }
  401. // 按楼层比例分摊
  402. $totalFloorHours = array_sum($floorHours);
  403. if ($totalFloorHours <= 0) {
  404. return;
  405. }
  406. foreach ($floorHours as $floor => $hours) {
  407. if ($hours <= 0) continue;
  408. $floorAmount = $amount * ($hours / $totalFloorHours);
  409. // 在楼层内按机台分摊
  410. foreach ($floorMachines[$floor] as $machine) {
  411. if (!isset($allocations[$machine])) {
  412. $allocations[$machine] = [];
  413. }
  414. $machineAmount = round($floorAmount * ($machineHours[$machine] / $hours), 2);
  415. $allocations[$machine]['待分摊总额'] = ($allocations[$machine]['待分摊总额'] ?? 0) + $machineAmount;
  416. }
  417. }
  418. }
  419. /**
  420. * 只分摊到卷凹机组
  421. */
  422. protected function allocateToRollCoater(array &$allocations, float $amount, array $machineHours, string $month): void
  423. {
  424. // 筛选卷凹机组的机台
  425. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  426. $totalHours = 0;
  427. foreach ($rollCoaterMachines as $machine) {
  428. $totalHours += $machineHours[$machine];
  429. }
  430. if ($totalHours <= 0) {
  431. return;
  432. }
  433. foreach ($rollCoaterMachines as $machine) {
  434. if (!isset($allocations[$machine])) {
  435. $allocations[$machine] = [];
  436. }
  437. $machineAmount = round($amount * ($machineHours[$machine] / $totalHours), 2);
  438. $allocations[$machine]['锅炉'] = ($allocations[$machine]['锅炉'] ?? 0) + $machineAmount;
  439. }
  440. }
  441. /**
  442. * 全局分摊
  443. */
  444. protected function allocateGlobally(array &$allocations, float $amount, array $machineHours): void
  445. {
  446. $totalHours = array_sum($machineHours);
  447. if ($totalHours <= 0) {
  448. return;
  449. }
  450. foreach ($machineHours as $machine => $hours) {
  451. if ($hours <= 0) continue;
  452. if (!isset($allocations[$machine])) {
  453. $allocations[$machine] = [];
  454. }
  455. $machineAmount = round($amount * ($hours / $totalHours), 2);
  456. $allocations[$machine][$this->getSubjectKey($amount)] = ($allocations[$machine][$this->getSubjectKey($amount)] ?? 0) + $machineAmount;
  457. }
  458. }
  459. /**
  460. * 根据机台获取楼层
  461. */
  462. protected function getFloorByMachine(string $machine): ?string
  463. {
  464. // 简化实现,实际应从数据库查询
  465. $groups = Db::name('设备_基本资料')
  466. ->where('设备编号', $machine)
  467. ->value('设备编组');
  468. if (!$groups) {
  469. return null;
  470. }
  471. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  472. foreach ($groupNames as $groupName) {
  473. if (strpos($groups, $groupName) !== false) {
  474. return $floor;
  475. }
  476. }
  477. }
  478. return null;
  479. }
  480. /**
  481. * 筛选卷凹机组的机台
  482. */
  483. protected function filterRollCoaterMachines(array $machines): array
  484. {
  485. $rollCoater = [];
  486. foreach ($machines as $machine) {
  487. $group = Db::name('设备_基本资料')
  488. ->where('设备编号', $machine)
  489. ->value('设备编组');
  490. if ($group && strpos($group, '03、卷凹机组') !== false) {
  491. $rollCoater[] = $machine;
  492. }
  493. }
  494. return $rollCoater;
  495. }
  496. /**
  497. * 简化科目名称
  498. */
  499. protected function simplifySubjectName(string $subject): string
  500. {
  501. $map = [
  502. '废气处理' => '废气处理',
  503. '锅炉' => '锅炉',
  504. '空压机' => '空压机',
  505. '热水锅炉' => '热水锅炉',
  506. '真空鼓风机' => '真空鼓风机',
  507. '中央空调' => '中央空调',
  508. '待分摊总额' => '待分摊总额',
  509. ];
  510. foreach ($map as $keyword => $simple) {
  511. if (strpos($subject, $keyword) !== false) {
  512. return $simple;
  513. }
  514. }
  515. return $subject;
  516. }
  517. /**
  518. * 计算水电金额
  519. */
  520. protected function calculateUtilityAmount(array $item): float
  521. {
  522. $electricity = floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0);
  523. $gas = floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0);
  524. return round($electricity + $gas, 2);
  525. }
  526. /**
  527. * 获取科目键名
  528. */
  529. protected function getSubjectKey(float $amount): string
  530. {
  531. // 简化的实现
  532. return '分摊其他';
  533. }
  534. /**
  535. * 生成分摊系数记录
  536. */
  537. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  538. {
  539. $now = date('Y-m-d H:i:s');
  540. foreach ($allocations as $machine => $subjects) {
  541. foreach ($subjects as $subject => $amount) {
  542. $this->allocationFactors[] = [
  543. 'Sys_ny' => $month,
  544. '科目名称' => $subject,
  545. '设备编号' => $machine,
  546. '分摊系数' => 1,
  547. '分摊金额' => $amount,
  548. 'Sys_id' => $sysId,
  549. 'Sys_rq' => $now,
  550. ];
  551. }
  552. }
  553. }
  554. /**
  555. * 分配水电到工单
  556. */
  557. protected function allocateUtilitiesToWorkOrders(array $machineAllocations, string $month): void
  558. {
  559. // 计算机台每机时费用
  560. $machineRates = $this->calculateMachineRates($machineAllocations);
  561. // 更新月度成本明细
  562. foreach ($this->monthlyCostDetails as &$detail) {
  563. $machine = $detail['sczl_jtbh'];
  564. $hours = floatval($detail['占用机时']);
  565. if ($hours <= 0 || !isset($machineRates[$machine])) {
  566. continue;
  567. }
  568. // 直接水电费
  569. $detail['直接水电'] = round($hours * 0.69, 2);
  570. // 分摊水电费
  571. foreach ($machineRates[$machine] as $subject => $rate) {
  572. $field = $this->getUtilityFieldName($subject);
  573. $detail[$field] = round($hours * $rate, 2);
  574. }
  575. }
  576. }
  577. /**
  578. * 计算机台每机时费用
  579. */
  580. protected function calculateMachineRates(array $allocations): array
  581. {
  582. $rates = [];
  583. $machineHours = [];
  584. // 先计算机台总机时
  585. foreach ($this->monthlyCostDetails as $detail) {
  586. $machine = $detail['sczl_jtbh'];
  587. $hours = floatval($detail['占用机时']);
  588. if (!isset($machineHours[$machine])) {
  589. $machineHours[$machine] = 0;
  590. }
  591. $machineHours[$machine] += $hours;
  592. }
  593. // 计算每机时费用
  594. foreach ($allocations as $machine => $subjects) {
  595. $totalHours = $machineHours[$machine] ?? 0;
  596. if ($totalHours <= 0) {
  597. continue;
  598. }
  599. $rates[$machine] = [];
  600. foreach ($subjects as $subject => $amount) {
  601. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  602. }
  603. }
  604. return $rates;
  605. }
  606. /**
  607. * 获取水电字段名
  608. */
  609. protected function getUtilityFieldName(string $subject): string
  610. {
  611. $map = [
  612. '待分摊总额' => '分摊水电',
  613. '废气处理' => '废气处理',
  614. '锅炉' => '锅炉',
  615. '空压机' => '空压机',
  616. '热水锅炉' => '热水锅炉',
  617. '真空鼓风机' => '真空鼓风机',
  618. '中央空调' => '中央空调',
  619. ];
  620. return $map[$subject] ?? $subject;
  621. }
  622. /**
  623. * 统一保存所有数据
  624. */
  625. protected function saveAllData(string $month, string $sysId): void
  626. {
  627. $now = date('Y-m-d H:i:s');
  628. // 1. 删除旧数据
  629. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  630. Db::name('成本_各月水电气')->where('Sys_ny', $month)->delete();
  631. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  632. // 2. 插入前检查数据结构
  633. $this->validateAndFixData();
  634. // 3. 插入新数据(使用分批插入避免单次数据量过大)
  635. $this->insertDataInBatches();
  636. }
  637. /**
  638. * 验证并修复数据
  639. */
  640. protected function validateAndFixData(): void
  641. {
  642. if (empty($this->monthlyCostDetails)) {
  643. return;
  644. }
  645. // 获取表结构
  646. $tableName = '成本v23_月度成本明细';
  647. $columns = Db::query("DESCRIBE `{$tableName}`");
  648. $columnNames = array_column($columns, 'Field');
  649. Log::info("表{$tableName}结构: " . implode(', ', $columnNames));
  650. Log::info("插入数据字段数: " . count(array_keys($this->monthlyCostDetails[0])));
  651. // 检查每条数据的字段数
  652. foreach ($this->monthlyCostDetails as $index => $row) {
  653. $rowCount = count($row);
  654. if ($rowCount !== count($columnNames)) {
  655. Log::error("第{$index}行字段数不匹配: 数据有{$rowCount}个字段,表有" . count($columnNames) . "个字段");
  656. Log::error("数据字段: " . implode(', ', array_keys($row)));
  657. // 修正第58行数据(从0开始计数,第58行是索引57)
  658. if ($index === 57) {
  659. $this->fixRowData($row, $columnNames);
  660. }
  661. }
  662. }
  663. }
  664. /**
  665. * 修复行数据
  666. */
  667. protected function fixRowData(array &$row, array $columnNames): void
  668. {
  669. $fixedRow = [];
  670. foreach ($columnNames as $column) {
  671. $fixedRow[$column] = $row[$column] ?? null;
  672. }
  673. // 替换原数据
  674. $this->monthlyCostDetails[57] = $fixedRow;
  675. Log::info("已修复第58行数据");
  676. }
  677. /**
  678. * 分批插入数据
  679. */
  680. protected function insertDataInBatches(): void
  681. {
  682. // 分批插入月度成本明细
  683. if (!empty($this->monthlyCostDetails)) {
  684. $batchSize = 100;
  685. $total = count($this->monthlyCostDetails);
  686. for ($i = 0; $i < $total; $i += $batchSize) {
  687. $batch = array_slice($this->monthlyCostDetails, $i, $batchSize);
  688. // 构建确保字段顺序正确的SQL
  689. $firstRow = reset($batch);
  690. $fields = array_keys($firstRow);
  691. $fieldStr = '`' . implode('`,`', $fields) . '`';
  692. $values = [];
  693. foreach ($batch as $row) {
  694. $rowValues = [];
  695. foreach ($fields as $field) {
  696. $value = $row[$field] ?? null;
  697. $rowValues[] = is_numeric($value) ? $value : "'" . addslashes($value) . "'";
  698. }
  699. $values[] = '(' . implode(',', $rowValues) . ')';
  700. }
  701. $sql = "INSERT INTO `成本v23_月度成本明细` ({$fieldStr}) VALUES " . implode(',', $values);
  702. try {
  703. Db::execute($sql);
  704. Log::info("成功插入批次 " . ($i/$batchSize + 1));
  705. } catch (\Exception $e) {
  706. Log::error("插入批次失败: " . $e->getMessage());
  707. throw $e;
  708. }
  709. }
  710. }
  711. if (!empty($this->monthlyUtilities)) {
  712. $sql = Db::name('成本_各月水电气')->fetchSql(true)->insertAll($this->monthlyUtilities);
  713. Db::query($sql);
  714. }
  715. if (!empty($this->allocationFactors)) {
  716. $sql = Db::name('成本_各月分摊系数')->fetchSql(true)->insertAll($this->allocationFactors);
  717. Db::query($sql);
  718. }
  719. }
  720. }