UnifiedCostCalculationService.php 28 KB

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