Przeglądaj źródła

车间生产信息统计表

unknown 6 dni temu
rodzic
commit
83bacb5138

+ 497 - 47
application/api/controller/ProductionInformationStatistics.php

@@ -145,11 +145,7 @@ class ProductionInformationStatistics extends Api
         }
 
         $query = $this->parseQueryTime($time);
-        if ($query['type'] === 'day') {
-            $list = $this->queryDailyList($query['start'], $query['end']);
-        } else {
-            $list = $this->queryMonthlyList($query['start'], $query['end'], $query['month']);
-        }
+        $list = $this->buildDisplayList($query);
 
         $this->success('成功', $list);
     }
@@ -187,6 +183,42 @@ class ProductionInformationStatistics extends Api
         $this->error('时间格式错误,请传 Y-m-d(具体日期)或 Y-m(月份)');
     }
 
+    /**
+     * 供 Excel 导出使用的表格数据
+     * @param string $time Y-m-d 或 Y-m
+     * @return array
+     */
+    public function getExportData($time)
+    {
+        $time = trim((string)$time);
+        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $time)) {
+            $timestamp = strtotime($time);
+            if ($timestamp === false) {
+                return [];
+            }
+            $query = [
+                'type'  => 'day',
+                'start' => date('Y-m-d 00:00:00', $timestamp),
+                'end'   => date('Y-m-d 23:59:59', $timestamp),
+            ];
+        } elseif (preg_match('/^\d{4}-\d{2}$/', $time)) {
+            $timestamp = strtotime($time . '-01');
+            if ($timestamp === false) {
+                return [];
+            }
+            $query = [
+                'type'  => 'month',
+                'month' => $time,
+                'start' => date('Y-m-01 00:00:00', $timestamp),
+                'end'   => date('Y-m-t 23:59:59', $timestamp),
+            ];
+        } else {
+            return [];
+        }
+
+        return $this->buildDisplayList($query);
+    }
+
     /**
      * 3. 获取车间、机组、机台编号、机台名称、班组基础数据
      * @ApiMethod (GET)
@@ -197,34 +229,21 @@ class ProductionInformationStatistics extends Api
             $this->error('请求错误');
         }
 
-        $equipments = Db::name($this->equipmentTable)
-            ->where('设备编组', '<>', '')
-            ->where('sys_sbID', '<>', '')
-            ->where('使用部门', '<>', '研发中心')
-            ->field([
-                'rtrim(使用部门) as 车间',
-                'rtrim(设备编组) as 机组',
-                'rtrim(设备编号) as 机台编号',
-                'rtrim(设备名称) as 机台名称',
-                '平均车速',
-            ])
-            ->order('设备编组,设备编号')
-            ->select();
-
-        if (empty($equipments)) {
+        $equipmentMap = $this->fetchEquipmentMap(true);
+        if (empty($equipmentMap)) {
             $this->success('未获取到机台数据', []);
         }
 
         $list = [];
-        foreach ($equipments as $item) {
-            $code = $this->trimText($item['机台编号']);
+        foreach ($equipmentMap as $code => $equipment) {
             $list[] = [
-                '车间'       => $this->trimText($item['车间']),
-                '机组'       => $this->trimText($item['机组']),
+                '车间'       => $equipment['车间'],
+                '机组'       => $equipment['机组'],
+                '编组'       => $equipment['编组'],
                 '机台编号'   => $code,
-                '机台名称'   => $this->trimText($item['机台名称']),
+                '机台名称'   => $equipment['机台名称'],
                 '设备编号'   => $code,
-                '理论速度'   => (int)$this->formatNumber($item['平均车速']),
+                '理论速度'   => $equipment['理论速度'],
                 '班组列表'   => $this->defaultShifts,
             ];
         }
@@ -480,34 +499,181 @@ class ProductionInformationStatistics extends Api
     }
 
     /**
-     * 按日查询明细
+     * 构建带计算字段和汇总行的表格数据
+     */
+    protected function buildDisplayList(array $query)
+    {
+        if ($query['type'] === 'day') {
+            $statsRows = $this->fetchStatsRows($query['start'], $query['end']);
+            $dateMeta = [
+                'query_type'  => 'day',
+                'sczl_rq'     => date('Y-m-d 00:00:00', strtotime($query['start'])),
+                'query_month' => '',
+            ];
+        } else {
+            $statsRows = $this->fetchMonthlyStatsRows($query['start'], $query['end']);
+            $dateMeta = [
+                'query_type'  => 'month',
+                'sczl_rq'     => $query['month'] . '-01 00:00:00',
+                'query_month' => $query['month'],
+            ];
+        }
+
+        $equipmentMap = $this->fetchEquipmentMap(true);
+        $statsRows = $this->enrichStatsWithEquipment($statsRows, $equipmentMap);
+
+        $statsMap = [];
+        foreach ($statsRows as $row) {
+            $key = $this->trimText($row['sczl_jtbh']) . '|' . $this->normalizeShift($row['sczl_bzdh']);
+            $statsMap[$key] = $row;
+        }
+
+        $detailRows = [];
+        $processedKeys = [];
+        foreach ($equipmentMap as $code => $equipment) {
+            foreach ($this->defaultShifts as $shift) {
+                $key = $code . '|' . $shift;
+                $processedKeys[$key] = true;
+                $stat = isset($statsMap[$key]) ? $statsMap[$key] : [];
+                $detailRows[] = $this->buildDetailRow($equipment, $shift, $stat, $dateMeta);
+            }
+        }
+
+        // 补充统计表中有数据但未纳入骨架的行(按设备基本资料补全车间编组)
+        foreach ($statsMap as $key => $stat) {
+            if (isset($processedKeys[$key])) {
+                continue;
+            }
+            $code = $this->trimText($stat['sczl_jtbh']);
+            $shift = $this->normalizeShift($stat['sczl_bzdh']);
+            if ($code === '' || $shift === '' || !isset($equipmentMap[$code])) {
+                continue;
+            }
+            $detailRows[] = $this->buildDetailRow($equipmentMap[$code], $shift, $stat, $dateMeta);
+        }
+
+        return $this->assembleListWithSummaries($detailRows);
+    }
+
+    /**
+     * 获取设备基本资料映射(key=机台编号)
+     */
+    protected function fetchEquipmentMap($displayOnly = true)
+    {
+        $query = Db::name($this->equipmentTable)
+            ->where('使用部门', '<>', '研发中心');
+
+        if ($displayOnly) {
+            $query->where('设备编组', '<>', '')->where('sys_sbID', '<>', '');
+        }
+
+        $equipments = $query
+            ->field([
+                'rtrim(使用部门) as 车间',
+                'rtrim(设备编组) as 机组',
+                'rtrim(设备编号) as 机台编号',
+                'rtrim(设备名称) as 机台名称',
+                '平均车速',
+            ])
+            ->order('设备编组,设备编号')
+            ->select();
+
+        $map = [];
+        foreach ($equipments as $item) {
+            $code = $this->trimText($item['机台编号']);
+            if ($code === '') {
+                continue;
+            }
+            $map[$code] = $this->formatEquipmentItem($item, $code);
+        }
+
+        return $map;
+    }
+
+    /**
+     * 格式化设备基本资料项
+     */
+    protected function formatEquipmentItem(array $item, $code = null)
+    {
+        $code = $code ?: $this->trimText($item['机台编号']);
+        $group = $this->trimText($item['机组']);
+
+        return [
+            '车间'     => $this->trimText($item['车间']),
+            '机组'     => $group,
+            '编组'     => $group,
+            '机台编号' => $code,
+            '机台名称' => $this->trimText($item['机台名称']),
+            '理论速度' => (int)$this->formatNumber($item['平均车速']),
+        ];
+    }
+
+    /**
+     * 按机台编号用设备基本资料补全统计数据
+     */
+    protected function enrichStatsWithEquipment(array $statsRows, array $equipmentMap)
+    {
+        foreach ($statsRows as &$row) {
+            $code = $this->trimText($row['sczl_jtbh']);
+            if ($code === '' || !isset($equipmentMap[$code])) {
+                continue;
+            }
+            $equipment = $equipmentMap[$code];
+            $row['车间'] = $equipment['车间'];
+            $row['机组'] = $equipment['机组'];
+            $row['编组'] = $equipment['编组'];
+            $row['机台名称'] = $equipment['机台名称'];
+        }
+        unset($row);
+
+        return $statsRows;
+    }
+
+    /**
+     * 查询原始统计数据(按日)
      */
-    protected function queryDailyList($start, $end)
+    protected function fetchStatsRows($start, $end)
     {
-        $list = Db::name($this->tableName)
+        return Db::name($this->tableName)
             ->alias('a')
-            ->join($this->equipmentTable . ' b', 'a.sczl_jtbh = b.设备编号', 'LEFT')
             ->where('a.sczl_rq', '>=', $start)
             ->where('a.sczl_rq', '<=', $end)
-            ->field($this->buildSelectFields(false))
+            ->field([
+                'a.id',
+                'a.sczl_jtbh',
+                'a.sczl_bzdh',
+                'a.sczl_rq',
+                'a.theoretical_speed',
+                'a.scheduled_production',
+                'a.scheduled_maintenance',
+                'a.scheduled_handover',
+                'a.fault',
+                'a.changeover',
+                'a.proofing',
+                'a.abnormal',
+                'a.planned_output',
+                'a.actual_output',
+                'a.defective_product',
+                'a.sys_id',
+                'a.sys_rq',
+                'a.mod_id',
+                'a.mod_rq',
+            ])
             ->select();
-
-        return $this->formatList($list);
     }
 
     /**
-     * 按月汇总查询
+     * 查询原始统计数据(按月汇总)
      */
-    protected function queryMonthlyList($start, $end, $month)
+    protected function fetchMonthlyStatsRows($start, $end)
     {
         $sumExpr = [];
         foreach ($this->sumFields as $field) {
             $sumExpr[] = "SUM(a.{$field}) as {$field}";
         }
 
-        $list = Db::name($this->tableName)
+        return Db::name($this->tableName)
             ->alias('a')
-            ->join($this->equipmentTable . ' b', 'a.sczl_jtbh = b.设备编号', 'LEFT')
             ->where('a.sczl_rq', '>=', $start)
             ->where('a.sczl_rq', '<=', $end)
             ->field(array_merge([
@@ -515,21 +681,305 @@ class ProductionInformationStatistics extends Api
                 'a.sczl_jtbh',
                 'a.sczl_bzdh',
                 'MAX(a.theoretical_speed) as theoretical_speed',
-                'rtrim(b.使用部门) as 车间',
-                'rtrim(b.设备编组) as 机组',
-                'rtrim(b.设备名称) as 机台名称',
             ], $sumExpr))
-            ->group('a.sczl_jtbh,a.sczl_bzdh,b.使用部门,b.设备编组,b.设备名称')
+            ->group('a.sczl_jtbh,a.sczl_bzdh')
             ->select();
+    }
 
-        foreach ($list as &$row) {
-            $row['sczl_rq'] = $month . '-01 00:00:00';
-            $row['query_type'] = 'month';
-            $row['query_month'] = $month;
+    /**
+     * 构建明细行
+     */
+    protected function buildDetailRow(array $equipment, $shift, array $stat, array $dateMeta)
+    {
+        $row = [
+            'row_type'             => 'detail',
+            'id'                   => isset($stat['id']) ? (int)$stat['id'] : 0,
+            '车间'                 => $equipment['车间'],
+            '机组'                 => $equipment['机组'],
+            '编组'                 => $equipment['编组'],
+            '机台编号'             => $equipment['机台编号'],
+            '设备编号'             => $equipment['机台编号'],
+            '机台名称'             => $equipment['机台名称'],
+            '班组'                 => $shift,
+            'sczl_jtbh'            => $equipment['机台编号'],
+            'sczl_bzdh'            => $shift,
+            'sczl_rq'              => $dateMeta['sczl_rq'],
+            'query_type'           => $dateMeta['query_type'],
+            'query_month'          => $dateMeta['query_month'],
+            'theoretical_speed'    => !empty($stat['id'])
+                ? (int)$this->formatNumber($stat['theoretical_speed'])
+                : $equipment['理论速度'],
+            'scheduled_production' => $this->formatNumber(isset($stat['scheduled_production']) ? $stat['scheduled_production'] : 0),
+            'scheduled_maintenance'=> $this->formatNumber(isset($stat['scheduled_maintenance']) ? $stat['scheduled_maintenance'] : 0),
+            'scheduled_handover'   => $this->formatNumber(isset($stat['scheduled_handover']) ? $stat['scheduled_handover'] : 0),
+            'fault'                => $this->formatNumber(isset($stat['fault']) ? $stat['fault'] : 0),
+            'changeover'           => $this->formatNumber(isset($stat['changeover']) ? $stat['changeover'] : 0),
+            'proofing'             => $this->formatNumber(isset($stat['proofing']) ? $stat['proofing'] : 0),
+            'abnormal'             => $this->formatNumber(isset($stat['abnormal']) ? $stat['abnormal'] : 0),
+            'planned_output'       => $this->formatNumber(isset($stat['planned_output']) ? $stat['planned_output'] : 0),
+            'actual_output'        => $this->formatNumber(isset($stat['actual_output']) ? $stat['actual_output'] : 0),
+            'defective_product'    => $this->formatNumber(isset($stat['defective_product']) ? $stat['defective_product'] : 0),
+            'sys_id'               => isset($stat['sys_id']) ? $this->trimText($stat['sys_id']) : '',
+            'sys_rq'               => isset($stat['sys_rq']) ? $stat['sys_rq'] : '',
+            'mod_id'               => isset($stat['mod_id']) ? $this->trimText($stat['mod_id']) : '',
+            'mod_rq'               => isset($stat['mod_rq']) ? $stat['mod_rq'] : '',
+        ];
+
+        return $this->appendCalculatedFields($row);
+    }
+
+    /**
+     * 组装明细 + 汇总行
+     */
+    protected function assembleListWithSummaries(array $detailRows)
+    {
+        $result = [];
+        $workshopGroups = [];
+
+        foreach ($detailRows as $row) {
+            $workshop = $row['车间'];
+            $group = $row['机组'];
+            if (!isset($workshopGroups[$workshop])) {
+                $workshopGroups[$workshop] = [];
+            }
+            if (!isset($workshopGroups[$workshop][$group])) {
+                $workshopGroups[$workshop][$group] = [];
+            }
+            $workshopGroups[$workshop][$group][] = $row;
         }
-        unset($row);
 
-        return $this->formatList($list);
+        uksort($workshopGroups, function ($a, $b) {
+            return strcmp($a, $b);
+        });
+
+        foreach ($workshopGroups as $workshop => $groups) {
+            uksort($groups, function ($a, $b) {
+                $orderA = $this->extractGroupOrder($a);
+                $orderB = $this->extractGroupOrder($b);
+                if ($orderA !== $orderB) {
+                    return $orderA <=> $orderB;
+                }
+                return strcmp($a, $b);
+            });
+
+            $workshopRows = [];
+            $workshopBlock = [];
+            foreach ($groups as $group => $groupRows) {
+                $groupRows = $this->sortEquipmentList($groupRows);
+                $workshopRows = array_merge($workshopRows, $groupRows);
+
+                $workshopBlock[] = $this->buildSummaryRow(
+                    'group',
+                    $workshop,
+                    $group,
+                    $this->buildGroupShortName($group) . '汇总',
+                    '/',
+                    $groupRows
+                );
+
+                foreach ($this->defaultShifts as $shift) {
+                    $shiftRows = array_values(array_filter($groupRows, function ($item) use ($shift) {
+                        return $item['班组'] === $shift;
+                    }));
+                    if (!empty($shiftRows)) {
+                        $workshopBlock[] = $this->buildSummaryRow(
+                            'group_shift',
+                            $workshop,
+                            $group,
+                            $this->buildGroupShortName($group) . $shift,
+                            $shift,
+                            $shiftRows
+                        );
+                    }
+                }
+
+                $workshopBlock = array_merge($workshopBlock, $groupRows);
+            }
+
+            if (!empty($workshopRows)) {
+                array_unshift(
+                    $workshopBlock,
+                    $this->buildSummaryRow(
+                        'workshop',
+                        $workshop,
+                        '全部机组',
+                        $workshop . '全部工序合计',
+                        '/',
+                        $workshopRows
+                    )
+                );
+            }
+
+            $result = array_merge($result, $workshopBlock);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 提取机组简称(03、卷凹机组 -> 卷凹)
+     */
+    protected function buildGroupShortName($group)
+    {
+        $group = $this->trimText($group);
+        if (preg_match('/、(.+?)机组/u', $group, $matches)) {
+            return $matches[1];
+        }
+        if (preg_match('/、(.+)/u', $group, $matches)) {
+            return $matches[1];
+        }
+        return $group;
+    }
+
+    /**
+     * 构建汇总行
+     */
+    protected function buildSummaryRow($rowType, $workshop, $group, $machineName, $shift, array $sourceRows)
+    {
+        $aggregated = $this->aggregateBaseFields($sourceRows);
+        $row = [
+            'row_type'             => $rowType,
+            'id'                   => 0,
+            '车间'                 => $workshop,
+            '机组'                 => $group,
+            '编组'                 => $group,
+            '机台编号'             => '',
+            '设备编号'             => '',
+            '机台名称'             => $machineName,
+            '班组'                 => $shift,
+            'sczl_jtbh'            => '',
+            'sczl_bzdh'            => $shift === '/' ? '' : $shift,
+            'sczl_rq'              => $sourceRows[0]['sczl_rq'],
+            'query_type'           => $sourceRows[0]['query_type'],
+            'query_month'          => $sourceRows[0]['query_month'],
+            'theoretical_speed'    => 0,
+            'scheduled_production' => $aggregated['scheduled_production'],
+            'scheduled_maintenance'=> $aggregated['scheduled_maintenance'],
+            'scheduled_handover'   => $aggregated['scheduled_handover'],
+            'fault'                => $aggregated['fault'],
+            'changeover'           => $aggregated['changeover'],
+            'proofing'             => $aggregated['proofing'],
+            'abnormal'             => $aggregated['abnormal'],
+            'planned_output'       => $aggregated['planned_output'],
+            'actual_output'        => $aggregated['actual_output'],
+            'defective_product'    => $aggregated['defective_product'],
+            'sys_id'               => '',
+            'sys_rq'               => '',
+            'mod_id'               => '',
+            'mod_rq'               => '',
+        ];
+
+        return $this->appendCalculatedFields($row, $aggregated['standard_output']);
+    }
+
+    /**
+     * 汇总基础字段
+     */
+    protected function aggregateBaseFields(array $rows)
+    {
+        $totals = [
+            'scheduled_production' => 0,
+            'scheduled_maintenance'=> 0,
+            'scheduled_handover'   => 0,
+            'fault'                => 0,
+            'changeover'           => 0,
+            'proofing'             => 0,
+            'abnormal'             => 0,
+            'planned_output'       => 0,
+            'actual_output'        => 0,
+            'defective_product'    => 0,
+            'standard_output'      => 0,
+        ];
+
+        foreach ($rows as $row) {
+            foreach ($this->sumFields as $field) {
+                $totals[$field] += $this->formatNumber($row[$field]);
+            }
+            $totals['standard_output'] += $this->formatNumber($row['标准产量']);
+        }
+
+        return $totals;
+    }
+
+    /**
+     * 计算并追加衍生指标
+     */
+    protected function appendCalculatedFields(array $row, $standardOutputOverride = null)
+    {
+        $scheduledProduction = $this->formatNumber($row['scheduled_production']);
+        $scheduledMaintenance = $this->formatNumber($row['scheduled_maintenance']);
+        $scheduledHandover = $this->formatNumber($row['scheduled_handover']);
+        $fault = $this->formatNumber($row['fault']);
+        $changeover = $this->formatNumber($row['changeover']);
+        $proofing = $this->formatNumber($row['proofing']);
+        $abnormal = $this->formatNumber($row['abnormal']);
+        $plannedOutput = $this->formatNumber($row['planned_output']);
+        $actualOutput = $this->formatNumber($row['actual_output']);
+        $defectiveProduct = $this->formatNumber($row['defective_product']);
+        $theoreticalSpeed = $this->formatNumber($row['theoretical_speed']);
+
+        $loadTime = $scheduledProduction - ($scheduledMaintenance + $scheduledHandover);
+        if ($loadTime < 0) {
+            $loadTime = 0;
+        }
+
+        $actualRunTime = $loadTime - ($fault + $changeover + $proofing + $abnormal);
+        if ($actualRunTime < 0) {
+            $actualRunTime = 0;
+        }
+
+        $standardOutput = $standardOutputOverride !== null
+            ? $this->formatNumber($standardOutputOverride)
+            : ($theoreticalSpeed * $actualRunTime);
+
+        $planAchievementRate = $plannedOutput > 0
+            ? (($actualOutput - $defectiveProduct) / $plannedOutput)
+            : 0;
+        $timeUtilization = $loadTime > 0 ? ($actualRunTime / $loadTime) : 0;
+        $speedUtilization = $standardOutput > 0 ? ($actualOutput / $standardOutput) : 0;
+        $qualityRate = $actualOutput > 0
+            ? (($actualOutput - $defectiveProduct) / $actualOutput)
+            : 0;
+        $oee = $timeUtilization * $speedUtilization * $qualityRate;
+
+        $row['负荷时间'] = round($loadTime, 2);
+        $row['实际运行时间'] = round($actualRunTime, 2);
+        $row['生产计划达成率'] = $this->formatRate($planAchievementRate);
+        $row['标准产量'] = round($standardOutput, 2);
+        $row['时间利用率'] = $this->formatRate($timeUtilization);
+        $row['速度利用率'] = $this->formatRate($speedUtilization);
+        $row['质量合格率'] = $this->formatRate($qualityRate);
+        $row['设备综合效率'] = $this->formatRate($oee);
+        $row['load_time'] = $row['负荷时间'];
+        $row['actual_run_time'] = $row['实际运行时间'];
+        $row['plan_achievement_rate'] = $row['生产计划达成率'];
+        $row['standard_output'] = $row['标准产量'];
+        $row['time_utilization'] = $row['时间利用率'];
+        $row['speed_utilization'] = $row['速度利用率'];
+        $row['quality_rate'] = $row['质量合格率'];
+        $row['oee'] = $row['设备综合效率'];
+
+        $row['理论速度'] = (int)$theoreticalSpeed;
+        $row['计划生产时间'] = round($scheduledProduction, 2);
+        $row['计划保养时间'] = round($scheduledMaintenance, 2);
+        $row['计划早会交接班时间'] = round($scheduledHandover, 2);
+        $row['设备故障时间'] = round($fault, 2);
+        $row['换型时间'] = round($changeover, 2);
+        $row['打样时间'] = round($proofing, 2);
+        $row['异常时间'] = round($abnormal, 2);
+        $row['计划完成产量'] = round($plannedOutput, 2);
+        $row['实际产量'] = round($actualOutput, 2);
+        $row['不良品数量'] = round($defectiveProduct, 2);
+
+        return $row;
+    }
+
+    /**
+     * 格式化为百分比(0-100,保留2位)
+     */
+    protected function formatRate($value)
+    {
+        return round((float)$value * 100, 2);
     }
 
     /**

+ 458 - 0
application/api/controller/ProductionSummary.php

@@ -0,0 +1,458 @@
+<?php
+
+namespace app\api\controller;
+
+use app\common\controller\Api;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+/**
+ * 车间生产信息汇总
+ */
+class ProductionSummary extends Api
+{
+    protected $noNeedLogin = ['*'];
+    protected $noNeedRight = ['*'];
+
+    /** @var ProductionInformationStatistics */
+    protected $statisticsService;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->statisticsService = new ProductionInformationStatistics();
+    }
+
+    /**
+     * 导出车间生产信息汇总Excel
+     * 按月份天数生成 sheet:全月汇总 + 1~N日,样式相同
+     * @ApiMethod (GET)
+     * @param string month 月份,格式 Y-m,如 2026-07;不传则取当前月
+     */
+    public function export()
+    {
+        $month = $this->request->param('month', date('Y-m'));
+        $timestamp = strtotime($month . '-01');
+        if ($timestamp === false) {
+            $this->error('月份格式错误,请传 Y-m,如 2026-07');
+        }
+
+        $daysInMonth = (int)date('t', $timestamp);
+        $monthStr = date('Y-m', $timestamp);
+        $spreadsheet = new Spreadsheet();
+
+        // 第一个 sheet:全月汇总
+        $summarySheet = $spreadsheet->getActiveSheet();
+        $summarySheet->setTitle('全月汇总');
+        $this->fillSheetTemplate($summarySheet, $monthStr);
+
+        // 按日生成 sheet:1 ~ 当月天数
+        for ($day = 1; $day <= $daysInMonth; $day++) {
+            $daySheet = $spreadsheet->createSheet();
+            $daySheet->setTitle((string)$day);
+            $dayDate = sprintf('%s-%02d', $monthStr, $day);
+            $this->fillSheetTemplate($daySheet, $dayDate);
+        }
+
+        $spreadsheet->setActiveSheetIndex(0);
+
+        $fileName = '车间生产信息汇总_' . date('Ym', $timestamp) . '_' . date('His');
+        $folderPath = 'uploads/folder';
+        if (!is_dir($folderPath)) {
+            mkdir($folderPath, 0777, true);
+        }
+        $tempPath = $folderPath . '/' . $fileName . '.xlsx';
+
+        $writer = new Xlsx($spreadsheet);
+        $writer->save($tempPath);
+
+        $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+        $host = $_SERVER['HTTP_HOST'];
+        $siteUrl = $protocol . '://' . $host . '/' . $tempPath;
+
+        $this->success('成功', [
+            'url'   => $siteUrl,
+            'month' => $monthStr,
+            'days'  => $daysInMonth,
+            'sheets'=> $daysInMonth + 1,
+        ]);
+    }
+
+    /**
+     * 填充单个 sheet 的统一样式模板及数据
+     */
+    protected function fillSheetTemplate($sheet, $date)
+    {
+        $this->buildHeader($sheet);
+        $templateRows = $this->getTemplateRows();
+        $lastRow = $this->buildLeftFixedData($sheet, $templateRows);
+
+        $dataList = $this->statisticsService->getExportData($date);
+        $startRow = 5;
+        foreach ($templateRows as $i => $templateRow) {
+            $matched = $this->matchExportRow($templateRow, $dataList);
+            if ($matched) {
+                $this->fillExportRowValues($sheet, $startRow + $i, $matched);
+            }
+        }
+
+        $this->applyCommonStyle($sheet, $lastRow);
+        $this->setColumnWidths($sheet);
+    }
+
+    /**
+     * 将计算后的数据写入 Excel 行(H-AA列)
+     */
+    protected function fillExportRowValues($sheet, $rowNum, array $data)
+    {
+        $values = [
+            'H'  => $data['理论速度'],
+            'I'  => $data['计划生产时间'],
+            'J'  => $data['计划保养时间'],
+            'K'  => $data['计划早会交接班时间'],
+            'L'  => $data['设备故障时间'],
+            'M'  => $data['换型时间'],
+            'N'  => $data['打样时间'],
+            'O'  => $data['异常时间'],
+            'P'  => $data['计划完成产量'],
+            'Q'  => $data['实际产量'],
+            'R'  => $data['不良品数量'],
+            'S'  => $data['负荷时间'],
+            'T'  => $data['实际运行时间'],
+            'U'  => $data['生产计划达成率'],
+            'V'  => $data['设备综合效率'],
+            'W'  => $data['时间利用率'],
+            'X'  => $data['速度利用率'],
+            'Y'  => $data['质量合格率'],
+            'AA' => $data['标准产量'],
+        ];
+
+        foreach ($values as $col => $value) {
+            if ($value === '' || $value === null) {
+                continue;
+            }
+            $sheet->setCellValue($col . $rowNum, $value);
+        }
+    }
+
+    /**
+     * 模板行与统计数据匹配
+     */
+    protected function matchExportRow(array $templateRow, array $dataList)
+    {
+        list($workshop, $group, $machineName,) = $templateRow;
+
+        // 1. 机台名称精确匹配
+        foreach ($dataList as $row) {
+            if ($row['机台名称'] === $machineName) {
+                return $row;
+            }
+        }
+
+        // 2. 去「汇总」后缀匹配
+        $normName = $this->normalizeExportName($machineName);
+        foreach ($dataList as $row) {
+            if ($this->normalizeExportName($row['机台名称']) === $normName) {
+                return $row;
+            }
+        }
+
+        // 3. 车间合计行:机台名称含「全部工序合计」且车间匹配
+        if (strpos($machineName, '全部工序合计') !== false) {
+            foreach ($dataList as $row) {
+                if ($row['row_type'] !== 'workshop') {
+                    continue;
+                }
+                if ($this->workshopMatches($workshop, $row['车间']) &&
+                    strpos($row['机台名称'], '全部工序合计') !== false) {
+                    return $row;
+                }
+            }
+        }
+
+        // 4. 机组汇总行
+        if (strpos($machineName, '汇总') !== false && $group !== '全部机组' && $group !== '全公司') {
+            $shortName = str_replace('汇总', '', $machineName);
+            $shortName = str_replace('工序', '', $shortName);
+            foreach ($dataList as $row) {
+                if ($row['row_type'] !== 'group') {
+                    continue;
+                }
+                if ($this->groupMatches($group, $row['机组']) &&
+                    strpos($row['机台名称'], $shortName) !== false) {
+                    return $row;
+                }
+            }
+        }
+
+        // 5. 班组汇总行(A班/B班/白班/夜班)
+        foreach ($dataList as $row) {
+            if ($row['row_type'] !== 'group_shift') {
+                continue;
+            }
+            if (!$this->groupMatches($group, $row['机组'])) {
+                continue;
+            }
+            if ($this->normalizeExportName($row['机台名称']) === $normName) {
+                return $row;
+            }
+            if (strpos($machineName, $row['班组']) !== false &&
+                $this->groupMatches($group, $row['机组'])) {
+                return $row;
+            }
+        }
+
+        return null;
+    }
+
+    protected function normalizeExportName($name)
+    {
+        $name = trim((string)$name);
+        return preg_replace('/汇总$/u', '', $name);
+    }
+
+    protected function workshopMatches($templateWorkshop, $dataWorkshop)
+    {
+        $templateWorkshop = trim($templateWorkshop);
+        $dataWorkshop = trim($dataWorkshop);
+        if ($templateWorkshop === $dataWorkshop) {
+            return true;
+        }
+        return strpos($dataWorkshop, $templateWorkshop) !== false
+            || strpos($templateWorkshop, $dataWorkshop) !== false;
+    }
+
+    protected function groupMatches($templateGroup, $dataGroup)
+    {
+        $templateGroup = trim($templateGroup);
+        $dataGroup = trim($dataGroup);
+        if ($templateGroup === $dataGroup) {
+            return true;
+        }
+        $templateKey = str_replace('机组', '', $templateGroup);
+        return strpos($dataGroup, $templateKey) !== false;
+    }
+
+    /**
+     * 表头:第1-4行,A-AA列
+     */
+    protected function buildHeader($sheet)
+    {
+        $fixedHeaders = [
+            'A' => '车间',
+            'B' => '机组',
+            'C' => '机台名称',
+            'D' => '班次',
+            'E' => '机长',
+            'F' => '产品名称',
+            'G' => '工序名称',
+        ];
+        foreach ($fixedHeaders as $col => $title) {
+            $sheet->setCellValue($col . '1', $title);
+            $sheet->mergeCells($col . '1:' . $col . '3');
+        }
+
+        $sheet->setCellValue('H1', '理论速度');
+        $sheet->mergeCells('H1:H2');
+        $sheet->setCellValue('I1', '计划生产时间');
+        $sheet->mergeCells('I1:I2');
+
+        $sheet->setCellValue('J1', '计划内固定停机时间');
+        $sheet->mergeCells('J1:K1');
+        $sheet->setCellValue('J2', '标准计划保养时间');
+        $sheet->setCellValue('K2', "标准早会、交接班时间\n(须按机时填写)");
+
+        $sheet->setCellValue('L1', '非计划停机时间');
+        $sheet->mergeCells('L1:O1');
+        $sheet->setCellValue('L2', '设备故障时间');
+        $sheet->setCellValue('M2', '实际换型时间');
+        $sheet->setCellValue('N2', '实际打样时间');
+        $sheet->setCellValue('O2', '其他异常时间');
+
+        $mainHeaders = [
+            'P' => '计划完成产量',
+            'Q' => '实际产量',
+            'R' => '不良品数量',
+            'S' => '负荷时间',
+            'T' => '实际运行时间',
+            'U' => '生产计划达成率',
+            'V' => "设备综合效率\n(OEE)",
+            'W' => '时间利用率',
+            'X' => '速度利用率',
+            'Y' => '质量合格率',
+            'Z' => '备注',
+            'AA' => "标准产量\n(辅助计算列)",
+        ];
+        foreach ($mainHeaders as $col => $title) {
+            $sheet->setCellValue($col . '1', $title);
+            $sheet->mergeCells($col . '1:' . $col . '2');
+        }
+
+        $units = [
+            'H' => "米/小时\n个/小时", 'I' => '小时', 'J' => '小时', 'K' => '小时',
+            'L' => '小时', 'M' => '小时', 'N' => '小时', 'O' => '小时',
+            'P' => "大米\n个", 'Q' => "大米\n个", 'R' => "大米\n个",
+            'S' => '小时', 'T' => '小时', 'U' => '%', 'V' => '%', 'W' => '%',
+            'X' => '%', 'Y' => '%', 'Z' => '', 'AA' => "大米\n个",
+        ];
+        foreach ($units as $col => $unit) {
+            $sheet->setCellValue($col . '3', $unit);
+        }
+
+        for ($i = 1; $i <= 7; $i++) {
+            $sheet->setCellValueByColumnAndRow($i, 4, '');
+        }
+        $numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 18, 15, 16, 17, '', 18];
+        foreach ($numbers as $idx => $num) {
+            $sheet->setCellValueByColumnAndRow($idx + 8, 4, $num);
+        }
+    }
+
+    /**
+     * 左侧固定层级数据(从第5行起)
+     */
+    protected function getTemplateRows()
+    {
+        return [
+            ['全公司', '全公司', '印刷+印后全部工序合计', '/', 'company'],
+            ['全公司', '全公司', '检验+糊盒全部工序合计', '/', 'company'],
+            ['印刷车间', '全部机组', '印刷车间全部工序合计', '/', 'workshop'],
+            ['印刷车间', '卷凹机组', '卷凹汇总', '/', 'group'],
+            ['印刷车间', '卷凹机组', '卷凹A班', '/', 'shift'],
+            ['印刷车间', '卷凹机组', '卷凹B班', '/', 'shift'],
+            ['印刷车间', '胶印机组', '胶印汇总', '/', 'group'],
+            ['印刷车间', '胶印机组', '胶印A班', '/', 'shift'],
+            ['印刷车间', '胶印机组', '胶印B班', '/', 'shift'],
+            ['印刷车间', '单凹机组', '单凹汇总', '/', 'group'],
+            ['印刷车间', '单凹机组', '单凹A班', '/', 'shift'],
+            ['印刷车间', '单凹机组', '单凹B班', '/', 'shift'],
+            ['印刷车间', '覆膜机组', '覆膜汇总', '/', 'group'],
+            ['印刷车间', '覆膜机组', '覆膜A班', '/', 'shift'],
+            ['印刷车间', '覆膜机组', '覆膜B班', '/', 'shift'],
+            ['印刷车间', '丝印机组', '丝印汇总', '/', 'group'],
+            ['印刷车间', '丝印机组', '丝印A班', '/', 'shift'],
+            ['印刷车间', '丝印机组', '丝印B班', '/', 'shift'],
+            ['印刷车间', '喷码机组', '喷码汇总', '/', 'group'],
+            ['印刷车间', '喷码机组', '喷码A班', '/', 'shift'],
+            ['印刷车间', '喷码机组', '喷码B班', '/', 'shift'],
+            ['印后车间(印刷)', '全部机组', '印后车间(印刷)全部工序合计', '/', 'workshop'],
+            ['印后车间(印刷)', '烫金机组', '烫金工序汇总', '/', 'group'],
+            ['印后车间(印刷)', '烫金机组', '烫金A班汇总', '/', 'shift'],
+            ['印后车间(印刷)', '烫金机组', '烫金B班汇总', '/', 'shift'],
+            ['印后车间(印刷)', '模切机组', '模切工序汇总', '/', 'group'],
+            ['印后车间(印刷)', '模切机组', '模切工序A班汇总', '/', 'shift'],
+            ['印后车间(印刷)', '模切机组', '模切工序B班汇总', '/', 'shift'],
+            ['检验车间', '全部机组', '检验车间全部工序合计', '/', 'workshop'],
+            ['检验车间', '手检', '手检工序汇总', '/', 'group'],
+            ['检验车间', '机检', '机检工序汇总', '/', 'group'],
+            ['检验车间', '机检', '机检A班', '/', 'shift'],
+            ['检验车间', '机检', '机检B班', '/', 'shift'],
+            ['印后车间(糊盒)', '全部机组', '印后车间(糊盒)全部工序合计', '/', 'workshop'],
+            ['印后车间(糊盒)', '印后(糊盒)', '印后(糊盒)白班汇总', '/', 'shift'],
+            ['印后车间(糊盒)', '印后(糊盒)', '印后(糊盒)夜班汇总', '/', 'shift'],
+            ['糊盒车间', '全部机组', '糊盒车间全部工序合计', '/', 'workshop'],
+            ['糊盒车间', '手工', '手工工序汇总', '/', 'group'],
+            ['糊盒车间', '条盒糊盒', '条盒糊盒工序汇总', '/', 'group'],
+            ['糊盒车间', '条盒糊盒', '条盒糊盒白班汇总', '/', 'shift'],
+            ['糊盒车间', '条盒糊盒', '条盒糊盒夜班汇总', '/', 'shift'],
+            ['糊盒车间', '皮壳组', '皮壳组工序汇总', '/', 'group'],
+            ['糊盒车间', '皮壳组', '皮壳组白班汇总', '/', 'shift'],
+            ['糊盒车间', '皮壳组', '皮壳组夜班汇总', '/', 'shift'],
+            ['糊盒车间', '小盒糊盒', '小盒糊盒工序汇总', '/', 'group'],
+            ['糊盒车间', '小盒糊盒', '小盒糊盒白班汇总', '/', 'shift'],
+            ['糊盒车间', '小盒糊盒', '小盒糊盒夜班汇总', '/', 'shift'],
+            ['糊盒车间', '小盒切割', '小盒切割工序汇总', '/', 'group'],
+            ['糊盒车间', '小盒切割', '小盒切割白班汇总', '/', 'shift'],
+            ['糊盒车间', '小盒切割', '小盒切割夜班汇总', '/', 'shift'],
+            ['糊盒车间', '贴硫酸纸', '贴硫酸纸工序汇总', '/', 'group'],
+            ['糊盒车间', '贴硫酸纸', '贴硫酸纸白班汇总', '/', 'shift'],
+            ['糊盒车间', '贴硫酸纸', '贴硫酸纸夜班汇总', '/', 'shift'],
+        ];
+    }
+
+    protected function buildLeftFixedData($sheet, array $rows)
+    {
+        $startRow = 5;
+        foreach ($rows as $i => $row) {
+            $r = $startRow + $i;
+            $sheet->setCellValue('A' . $r, $row[0]);
+            $sheet->setCellValue('B' . $r, $row[1]);
+            $sheet->setCellValue('C' . $r, $row[2]);
+            $sheet->setCellValue('D' . $r, $row[3]);
+            $sheet->setCellValue('E' . $r, '/');
+            $sheet->setCellValue('F' . $r, '/');
+            $sheet->setCellValue('G' . $r, '/');
+        }
+
+        $lastRow = $startRow + count($rows) - 1;
+        $this->mergeVerticalByValue($sheet, 'A', 7, $lastRow);
+        $this->mergeVerticalByValue($sheet, 'B', 7, $lastRow);
+
+        return $lastRow;
+    }
+
+    protected function mergeVerticalByValue($sheet, $col, $startRow, $endRow)
+    {
+        $mergeStart = $startRow;
+        $prev = (string)$sheet->getCell($col . $startRow)->getValue();
+
+        for ($r = $startRow + 1; $r <= $endRow + 1; $r++) {
+            $curr = $r <= $endRow ? (string)$sheet->getCell($col . $r)->getValue() : null;
+            if ($curr !== $prev) {
+                if ($r - 1 > $mergeStart) {
+                    $sheet->mergeCells($col . $mergeStart . ':' . $col . ($r - 1));
+                }
+                $mergeStart = $r;
+                $prev = $curr;
+            }
+        }
+    }
+
+    protected function applyCommonStyle($sheet, $lastRow)
+    {
+        $range = 'A1:AA' . $lastRow;
+        $sheet->getStyle($range)->applyFromArray([
+            'alignment' => [
+                'horizontal' => Alignment::HORIZONTAL_CENTER,
+                'vertical'   => Alignment::VERTICAL_CENTER,
+                'wrapText'   => true,
+            ],
+            'borders' => [
+                'allBorders' => [
+                    'borderStyle' => Border::BORDER_THIN,
+                    'color'       => ['rgb' => '000000'],
+                ],
+            ],
+            'font' => [
+                'name' => '宋体',
+                'size' => 10,
+            ],
+        ]);
+
+        $sheet->getStyle('A1:AA4')->getFont()->setBold(true);
+        $sheet->getRowDimension(1)->setRowHeight(22);
+        $sheet->getRowDimension(2)->setRowHeight(36);
+        $sheet->getRowDimension(3)->setRowHeight(30);
+        $sheet->getRowDimension(4)->setRowHeight(18);
+        for ($r = 5; $r <= $lastRow; $r++) {
+            $sheet->getRowDimension($r)->setRowHeight(20);
+        }
+    }
+
+    protected function setColumnWidths($sheet)
+    {
+        $widths = [
+            'A' => 16, 'B' => 14, 'C' => 28, 'D' => 10, 'E' => 10,
+            'F' => 12, 'G' => 12, 'H' => 10, 'I' => 10, 'J' => 10,
+            'K' => 12, 'L' => 10, 'M' => 10, 'N' => 10, 'O' => 10,
+            'P' => 10, 'Q' => 10, 'R' => 10, 'S' => 10, 'T' => 10,
+            'U' => 10, 'V' => 12, 'W' => 10, 'X' => 10, 'Y' => 10,
+            'Z' => 10, 'AA' => 12,
+        ];
+        foreach ($widths as $col => $w) {
+            $sheet->getColumnDimension($col)->setWidth($w);
+        }
+    }
+}