ProductionSummary.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  5. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  6. use PhpOffice\PhpSpreadsheet\Style\Border;
  7. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  8. /**
  9. * 车间生产信息汇总
  10. */
  11. class ProductionSummary extends Api
  12. {
  13. protected $noNeedLogin = ['*'];
  14. protected $noNeedRight = ['*'];
  15. /** @var ProductionInformationStatistics */
  16. protected $statisticsService;
  17. public function _initialize()
  18. {
  19. parent::_initialize();
  20. $this->statisticsService = new ProductionInformationStatistics();
  21. }
  22. /**
  23. * 导出车间生产信息汇总Excel
  24. * 按月份天数生成 sheet:全月汇总 + 1~N日,样式相同
  25. * @ApiMethod (GET)
  26. * @param string month 月份,格式 Y-m,如 2026-07;不传则取当前月
  27. */
  28. public function export()
  29. {
  30. $month = $this->request->param('month', date('Y-m'));
  31. $timestamp = strtotime($month . '-01');
  32. if ($timestamp === false) {
  33. $this->error('月份格式错误,请传 Y-m,如 2026-07');
  34. }
  35. $daysInMonth = (int)date('t', $timestamp);
  36. $monthStr = date('Y-m', $timestamp);
  37. $spreadsheet = new Spreadsheet();
  38. // 第一个 sheet:全月汇总
  39. $summarySheet = $spreadsheet->getActiveSheet();
  40. $summarySheet->setTitle('全月汇总');
  41. $this->fillSheetTemplate($summarySheet, $monthStr);
  42. // 按日生成 sheet:1 ~ 当月天数
  43. for ($day = 1; $day <= $daysInMonth; $day++) {
  44. $daySheet = $spreadsheet->createSheet();
  45. $daySheet->setTitle((string)$day);
  46. $dayDate = sprintf('%s-%02d', $monthStr, $day);
  47. $this->fillSheetTemplate($daySheet, $dayDate);
  48. }
  49. $spreadsheet->setActiveSheetIndex(0);
  50. $fileName = '车间生产信息汇总_' . date('Ym', $timestamp) . '_' . date('His');
  51. $folderPath = 'uploads/folder';
  52. if (!is_dir($folderPath)) {
  53. mkdir($folderPath, 0777, true);
  54. }
  55. $tempPath = $folderPath . '/' . $fileName . '.xlsx';
  56. $writer = new Xlsx($spreadsheet);
  57. $writer->save($tempPath);
  58. $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
  59. $host = $_SERVER['HTTP_HOST'];
  60. $siteUrl = $protocol . '://' . $host . '/' . $tempPath;
  61. $this->success('成功', [
  62. 'url' => $siteUrl,
  63. 'month' => $monthStr,
  64. 'days' => $daysInMonth,
  65. 'sheets'=> $daysInMonth + 1,
  66. ]);
  67. }
  68. /**
  69. * 填充单个 sheet 的统一样式模板及数据
  70. */
  71. protected function fillSheetTemplate($sheet, $date)
  72. {
  73. $this->buildHeader($sheet);
  74. $templateRows = $this->getTemplateRows();
  75. $lastRow = $this->buildLeftFixedData($sheet, $templateRows);
  76. $dataList = $this->statisticsService->getExportData($date);
  77. $startRow = 5;
  78. foreach ($templateRows as $i => $templateRow) {
  79. $matched = $this->matchExportRow($templateRow, $dataList);
  80. if ($matched) {
  81. $this->fillExportRowValues($sheet, $startRow + $i, $matched);
  82. }
  83. }
  84. $this->applyCommonStyle($sheet, $lastRow);
  85. $this->setColumnWidths($sheet);
  86. }
  87. /**
  88. * 将计算后的数据写入 Excel 行(H-AA列)
  89. */
  90. protected function fillExportRowValues($sheet, $rowNum, array $data)
  91. {
  92. $values = [
  93. 'H' => $data['理论速度'],
  94. 'I' => $data['计划生产时间'],
  95. 'J' => $data['计划保养时间'],
  96. 'K' => $data['计划早会交接班时间'],
  97. 'L' => $data['设备故障时间'],
  98. 'M' => $data['换型时间'],
  99. 'N' => $data['打样时间'],
  100. 'O' => $data['异常时间'],
  101. 'P' => $data['计划完成产量'],
  102. 'Q' => $data['实际产量'],
  103. 'R' => $data['不良品数量'],
  104. 'S' => $data['负荷时间'],
  105. 'T' => $data['实际运行时间'],
  106. 'U' => $data['生产计划达成率'],
  107. 'V' => $data['设备综合效率'],
  108. 'W' => $data['时间利用率'],
  109. 'X' => $data['速度利用率'],
  110. 'Y' => $data['质量合格率'],
  111. 'AA' => $data['标准产量'],
  112. ];
  113. foreach ($values as $col => $value) {
  114. if ($value === '' || $value === null) {
  115. continue;
  116. }
  117. $sheet->setCellValue($col . $rowNum, $value);
  118. }
  119. }
  120. /**
  121. * 模板行与统计数据匹配
  122. */
  123. protected function matchExportRow(array $templateRow, array $dataList)
  124. {
  125. list($workshop, $group, $machineName,) = $templateRow;
  126. // 1. 机台名称精确匹配
  127. foreach ($dataList as $row) {
  128. if ($row['机台名称'] === $machineName) {
  129. return $row;
  130. }
  131. }
  132. // 2. 去「汇总」后缀匹配
  133. $normName = $this->normalizeExportName($machineName);
  134. foreach ($dataList as $row) {
  135. if ($this->normalizeExportName($row['机台名称']) === $normName) {
  136. return $row;
  137. }
  138. }
  139. // 3. 车间合计行:机台名称含「全部工序合计」且车间匹配
  140. if (strpos($machineName, '全部工序合计') !== false) {
  141. foreach ($dataList as $row) {
  142. if ($row['row_type'] !== 'workshop') {
  143. continue;
  144. }
  145. if ($this->workshopMatches($workshop, $row['车间']) &&
  146. strpos($row['机台名称'], '全部工序合计') !== false) {
  147. return $row;
  148. }
  149. }
  150. }
  151. // 4. 机组汇总行
  152. if (strpos($machineName, '汇总') !== false && $group !== '全部机组' && $group !== '全公司') {
  153. $shortName = str_replace('汇总', '', $machineName);
  154. $shortName = str_replace('工序', '', $shortName);
  155. foreach ($dataList as $row) {
  156. if ($row['row_type'] !== 'group') {
  157. continue;
  158. }
  159. if ($this->groupMatches($group, $row['机组']) &&
  160. strpos($row['机台名称'], $shortName) !== false) {
  161. return $row;
  162. }
  163. }
  164. }
  165. // 5. 班组汇总行(A班/B班/白班/夜班)
  166. foreach ($dataList as $row) {
  167. if ($row['row_type'] !== 'group_shift') {
  168. continue;
  169. }
  170. if (!$this->groupMatches($group, $row['机组'])) {
  171. continue;
  172. }
  173. if ($this->normalizeExportName($row['机台名称']) === $normName) {
  174. return $row;
  175. }
  176. if (strpos($machineName, $row['班组']) !== false &&
  177. $this->groupMatches($group, $row['机组'])) {
  178. return $row;
  179. }
  180. }
  181. return null;
  182. }
  183. protected function normalizeExportName($name)
  184. {
  185. $name = trim((string)$name);
  186. return preg_replace('/汇总$/u', '', $name);
  187. }
  188. protected function workshopMatches($templateWorkshop, $dataWorkshop)
  189. {
  190. $templateWorkshop = trim($templateWorkshop);
  191. $dataWorkshop = trim($dataWorkshop);
  192. if ($templateWorkshop === $dataWorkshop) {
  193. return true;
  194. }
  195. return strpos($dataWorkshop, $templateWorkshop) !== false
  196. || strpos($templateWorkshop, $dataWorkshop) !== false;
  197. }
  198. protected function groupMatches($templateGroup, $dataGroup)
  199. {
  200. $templateGroup = trim($templateGroup);
  201. $dataGroup = trim($dataGroup);
  202. if ($templateGroup === $dataGroup) {
  203. return true;
  204. }
  205. $templateKey = str_replace('机组', '', $templateGroup);
  206. return strpos($dataGroup, $templateKey) !== false;
  207. }
  208. /**
  209. * 表头:第1-4行,A-AA列
  210. */
  211. protected function buildHeader($sheet)
  212. {
  213. $fixedHeaders = [
  214. 'A' => '车间',
  215. 'B' => '机组',
  216. 'C' => '机台名称',
  217. 'D' => '班次',
  218. 'E' => '机长',
  219. 'F' => '产品名称',
  220. 'G' => '工序名称',
  221. ];
  222. foreach ($fixedHeaders as $col => $title) {
  223. $sheet->setCellValue($col . '1', $title);
  224. $sheet->mergeCells($col . '1:' . $col . '3');
  225. }
  226. $sheet->setCellValue('H1', '理论速度');
  227. $sheet->mergeCells('H1:H2');
  228. $sheet->setCellValue('I1', '计划生产时间');
  229. $sheet->mergeCells('I1:I2');
  230. $sheet->setCellValue('J1', '计划内固定停机时间');
  231. $sheet->mergeCells('J1:K1');
  232. $sheet->setCellValue('J2', '标准计划保养时间');
  233. $sheet->setCellValue('K2', "标准早会、交接班时间\n(须按机时填写)");
  234. $sheet->setCellValue('L1', '非计划停机时间');
  235. $sheet->mergeCells('L1:O1');
  236. $sheet->setCellValue('L2', '设备故障时间');
  237. $sheet->setCellValue('M2', '实际换型时间');
  238. $sheet->setCellValue('N2', '实际打样时间');
  239. $sheet->setCellValue('O2', '其他异常时间');
  240. $mainHeaders = [
  241. 'P' => '计划完成产量',
  242. 'Q' => '实际产量',
  243. 'R' => '不良品数量',
  244. 'S' => '负荷时间',
  245. 'T' => '实际运行时间',
  246. 'U' => '生产计划达成率',
  247. 'V' => "设备综合效率\n(OEE)",
  248. 'W' => '时间利用率',
  249. 'X' => '速度利用率',
  250. 'Y' => '质量合格率',
  251. 'Z' => '备注',
  252. 'AA' => "标准产量\n(辅助计算列)",
  253. ];
  254. foreach ($mainHeaders as $col => $title) {
  255. $sheet->setCellValue($col . '1', $title);
  256. $sheet->mergeCells($col . '1:' . $col . '2');
  257. }
  258. $units = [
  259. 'H' => "米/小时\n个/小时", 'I' => '小时', 'J' => '小时', 'K' => '小时',
  260. 'L' => '小时', 'M' => '小时', 'N' => '小时', 'O' => '小时',
  261. 'P' => "大米\n个", 'Q' => "大米\n个", 'R' => "大米\n个",
  262. 'S' => '小时', 'T' => '小时', 'U' => '%', 'V' => '%', 'W' => '%',
  263. 'X' => '%', 'Y' => '%', 'Z' => '', 'AA' => "大米\n个",
  264. ];
  265. foreach ($units as $col => $unit) {
  266. $sheet->setCellValue($col . '3', $unit);
  267. }
  268. for ($i = 1; $i <= 7; $i++) {
  269. $sheet->setCellValueByColumnAndRow($i, 4, '');
  270. }
  271. $numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 18, 15, 16, 17, '', 18];
  272. foreach ($numbers as $idx => $num) {
  273. $sheet->setCellValueByColumnAndRow($idx + 8, 4, $num);
  274. }
  275. }
  276. /**
  277. * 左侧固定层级数据(从第5行起)
  278. */
  279. protected function getTemplateRows()
  280. {
  281. return [
  282. ['全公司', '全公司', '印刷+印后全部工序合计', '/', 'company'],
  283. ['全公司', '全公司', '检验+糊盒全部工序合计', '/', 'company'],
  284. ['印刷车间', '全部机组', '印刷车间全部工序合计', '/', 'workshop'],
  285. ['印刷车间', '卷凹机组', '卷凹汇总', '/', 'group'],
  286. ['印刷车间', '卷凹机组', '卷凹A班', '/', 'shift'],
  287. ['印刷车间', '卷凹机组', '卷凹B班', '/', 'shift'],
  288. ['印刷车间', '胶印机组', '胶印汇总', '/', 'group'],
  289. ['印刷车间', '胶印机组', '胶印A班', '/', 'shift'],
  290. ['印刷车间', '胶印机组', '胶印B班', '/', 'shift'],
  291. ['印刷车间', '单凹机组', '单凹汇总', '/', 'group'],
  292. ['印刷车间', '单凹机组', '单凹A班', '/', 'shift'],
  293. ['印刷车间', '单凹机组', '单凹B班', '/', 'shift'],
  294. ['印刷车间', '覆膜机组', '覆膜汇总', '/', 'group'],
  295. ['印刷车间', '覆膜机组', '覆膜A班', '/', 'shift'],
  296. ['印刷车间', '覆膜机组', '覆膜B班', '/', 'shift'],
  297. ['印刷车间', '丝印机组', '丝印汇总', '/', 'group'],
  298. ['印刷车间', '丝印机组', '丝印A班', '/', 'shift'],
  299. ['印刷车间', '丝印机组', '丝印B班', '/', 'shift'],
  300. ['印刷车间', '喷码机组', '喷码汇总', '/', 'group'],
  301. ['印刷车间', '喷码机组', '喷码A班', '/', 'shift'],
  302. ['印刷车间', '喷码机组', '喷码B班', '/', 'shift'],
  303. ['印后车间(印刷)', '全部机组', '印后车间(印刷)全部工序合计', '/', 'workshop'],
  304. ['印后车间(印刷)', '烫金机组', '烫金工序汇总', '/', 'group'],
  305. ['印后车间(印刷)', '烫金机组', '烫金A班汇总', '/', 'shift'],
  306. ['印后车间(印刷)', '烫金机组', '烫金B班汇总', '/', 'shift'],
  307. ['印后车间(印刷)', '模切机组', '模切工序汇总', '/', 'group'],
  308. ['印后车间(印刷)', '模切机组', '模切工序A班汇总', '/', 'shift'],
  309. ['印后车间(印刷)', '模切机组', '模切工序B班汇总', '/', 'shift'],
  310. ['检验车间', '全部机组', '检验车间全部工序合计', '/', 'workshop'],
  311. ['检验车间', '手检', '手检工序汇总', '/', 'group'],
  312. ['检验车间', '机检', '机检工序汇总', '/', 'group'],
  313. ['检验车间', '机检', '机检A班', '/', 'shift'],
  314. ['检验车间', '机检', '机检B班', '/', 'shift'],
  315. ['印后车间(糊盒)', '全部机组', '印后车间(糊盒)全部工序合计', '/', 'workshop'],
  316. ['印后车间(糊盒)', '印后(糊盒)', '印后(糊盒)白班汇总', '/', 'shift'],
  317. ['印后车间(糊盒)', '印后(糊盒)', '印后(糊盒)夜班汇总', '/', 'shift'],
  318. ['糊盒车间', '全部机组', '糊盒车间全部工序合计', '/', 'workshop'],
  319. ['糊盒车间', '手工', '手工工序汇总', '/', 'group'],
  320. ['糊盒车间', '条盒糊盒', '条盒糊盒工序汇总', '/', 'group'],
  321. ['糊盒车间', '条盒糊盒', '条盒糊盒白班汇总', '/', 'shift'],
  322. ['糊盒车间', '条盒糊盒', '条盒糊盒夜班汇总', '/', 'shift'],
  323. ['糊盒车间', '皮壳组', '皮壳组工序汇总', '/', 'group'],
  324. ['糊盒车间', '皮壳组', '皮壳组白班汇总', '/', 'shift'],
  325. ['糊盒车间', '皮壳组', '皮壳组夜班汇总', '/', 'shift'],
  326. ['糊盒车间', '小盒糊盒', '小盒糊盒工序汇总', '/', 'group'],
  327. ['糊盒车间', '小盒糊盒', '小盒糊盒白班汇总', '/', 'shift'],
  328. ['糊盒车间', '小盒糊盒', '小盒糊盒夜班汇总', '/', 'shift'],
  329. ['糊盒车间', '小盒切割', '小盒切割工序汇总', '/', 'group'],
  330. ['糊盒车间', '小盒切割', '小盒切割白班汇总', '/', 'shift'],
  331. ['糊盒车间', '小盒切割', '小盒切割夜班汇总', '/', 'shift'],
  332. ['糊盒车间', '贴硫酸纸', '贴硫酸纸工序汇总', '/', 'group'],
  333. ['糊盒车间', '贴硫酸纸', '贴硫酸纸白班汇总', '/', 'shift'],
  334. ['糊盒车间', '贴硫酸纸', '贴硫酸纸夜班汇总', '/', 'shift'],
  335. ];
  336. }
  337. protected function buildLeftFixedData($sheet, array $rows)
  338. {
  339. $startRow = 5;
  340. foreach ($rows as $i => $row) {
  341. $r = $startRow + $i;
  342. $sheet->setCellValue('A' . $r, $row[0]);
  343. $sheet->setCellValue('B' . $r, $row[1]);
  344. $sheet->setCellValue('C' . $r, $row[2]);
  345. $sheet->setCellValue('D' . $r, $row[3]);
  346. $sheet->setCellValue('E' . $r, '/');
  347. $sheet->setCellValue('F' . $r, '/');
  348. $sheet->setCellValue('G' . $r, '/');
  349. }
  350. $lastRow = $startRow + count($rows) - 1;
  351. $this->mergeVerticalByValue($sheet, 'A', 7, $lastRow);
  352. $this->mergeVerticalByValue($sheet, 'B', 7, $lastRow);
  353. return $lastRow;
  354. }
  355. protected function mergeVerticalByValue($sheet, $col, $startRow, $endRow)
  356. {
  357. $mergeStart = $startRow;
  358. $prev = (string)$sheet->getCell($col . $startRow)->getValue();
  359. for ($r = $startRow + 1; $r <= $endRow + 1; $r++) {
  360. $curr = $r <= $endRow ? (string)$sheet->getCell($col . $r)->getValue() : null;
  361. if ($curr !== $prev) {
  362. if ($r - 1 > $mergeStart) {
  363. $sheet->mergeCells($col . $mergeStart . ':' . $col . ($r - 1));
  364. }
  365. $mergeStart = $r;
  366. $prev = $curr;
  367. }
  368. }
  369. }
  370. protected function applyCommonStyle($sheet, $lastRow)
  371. {
  372. $range = 'A1:AA' . $lastRow;
  373. $sheet->getStyle($range)->applyFromArray([
  374. 'alignment' => [
  375. 'horizontal' => Alignment::HORIZONTAL_CENTER,
  376. 'vertical' => Alignment::VERTICAL_CENTER,
  377. 'wrapText' => true,
  378. ],
  379. 'borders' => [
  380. 'allBorders' => [
  381. 'borderStyle' => Border::BORDER_THIN,
  382. 'color' => ['rgb' => '000000'],
  383. ],
  384. ],
  385. 'font' => [
  386. 'name' => '宋体',
  387. 'size' => 10,
  388. ],
  389. ]);
  390. $sheet->getStyle('A1:AA4')->getFont()->setBold(true);
  391. $sheet->getRowDimension(1)->setRowHeight(22);
  392. $sheet->getRowDimension(2)->setRowHeight(36);
  393. $sheet->getRowDimension(3)->setRowHeight(30);
  394. $sheet->getRowDimension(4)->setRowHeight(18);
  395. for ($r = 5; $r <= $lastRow; $r++) {
  396. $sheet->getRowDimension($r)->setRowHeight(20);
  397. }
  398. }
  399. protected function setColumnWidths($sheet)
  400. {
  401. $widths = [
  402. 'A' => 16, 'B' => 14, 'C' => 28, 'D' => 10, 'E' => 10,
  403. 'F' => 12, 'G' => 12, 'H' => 10, 'I' => 10, 'J' => 10,
  404. 'K' => 12, 'L' => 10, 'M' => 10, 'N' => 10, 'O' => 10,
  405. 'P' => 10, 'Q' => 10, 'R' => 10, 'S' => 10, 'T' => 10,
  406. 'U' => 10, 'V' => 12, 'W' => 10, 'X' => 10, 'Y' => 10,
  407. 'Z' => 10, 'AA' => 12,
  408. ];
  409. foreach ($widths as $col => $w) {
  410. $sheet->getColumnDimension($col)->setWidth($w);
  411. }
  412. }
  413. }