소스 검색

胡庆余堂

cqp 2 달 전
부모
커밋
dd3745cb14
2개의 변경된 파일162개의 추가작업 그리고 98개의 파일을 삭제
  1. 110 65
      app/Service/RdGenerateDeviceService.php
  2. 52 33
      app/Service/RdGenerateService.php

+ 110 - 65
app/Service/RdGenerateDeviceService.php

@@ -10,19 +10,18 @@ use Illuminate\Support\Facades\DB;
 class RdGenerateDeviceService extends Service
 {
     const job_name = "rd_device_set";
+
     /**
-     * 外部调用的入口
+     * 外部调用的入口 (保持原样,负责分发队列)
      */
     public function generate(array $data)
     {
-        // 1. 基础校验
         if (empty($data['month'])) {
             return [false, '请选择生成研发设备工时单年月'];
         }
 
         $monthStart = Carbon::createFromFormat('Y-m', $data['month'])->startOfMonth()->timestamp;
 
-        // 2. 业务状态校验 (提出来的校验逻辑)
         $calendar = DB::table('calendar')
             ->where('del_time', 0)
             ->where('time', $monthStart)
@@ -46,32 +45,32 @@ class RdGenerateDeviceService extends Service
 //            // 只有这里 throw 了,任务才会进入 failed_jobs 表
 //            return [false, $e->getMessage()];
 //
-//        }
+//        }dd(2);
 
         $key = self::job_name . $data['month'];
         $data['lock_key'] = $key;
 
-        list($status,$msg) = $this->limitingSendRequest($key);
-        if(! $status) return [false, '该月设备数据已在后台处理,请勿重复操作'];
+        list($status, $msg) = $this->limitingSendRequest($key);
+        if (!$status) return [false, '该月设备数据已在后台处理,请勿重复操作'];
 
         ProcessDataJob::dispatch($data)->onQueue(self::job_name);
 
-        return [true, '任'];
+        return [true, '任务已放到后台运行'];
     }
 
-    public function delTableKey($key){
+    public function delTableKey($key) {
         $this->dellimitingSendRequest($key);
     }
 
-    // 内存缓冲区
-    protected $pendingDetails = [];
-
+    /**
+     * 队列真正执行的逻辑
+     */
     public function doGenerate(string $month)
     {
         $monthStart = Carbon::createFromFormat('Y-m', $month)->startOfMonth()->timestamp;
         $monthEnd   = Carbon::createFromFormat('Y-m', $month)->endOfMonth()->timestamp;
 
-        // 1. 基础数据准备
+        // 1. 获取日历和工作日
         $calendar = DB::table('calendar')->where('del_time', 0)->where('time', $monthStart)->first();
         if (!$calendar) throw new \Exception('当月日历未配置');
 
@@ -79,56 +78,96 @@ class RdGenerateDeviceService extends Service
             ->where('del_time', 0)->where('calendar_id', $calendar->id)->where('is_work', 1)
             ->orderBy('time')->pluck('time')->toArray();
 
+        // 2. 获取当月有效项目
         $items = DB::table('item')->where('del_time', 0)->where('is_use', 1)
             ->where('start_time', '<=', $monthEnd)->where('end_time', '>=', $monthStart)
-            ->orderBy('id','desc')->get();
+            ->orderBy('id', 'desc')->get();
 
         if ($items->isEmpty() || empty($workDays)) return;
 
-        // 2. 清理旧数据 (保持原样)
+        // 3. 清理旧数据
         $rds = DB::table('rd')->where('del_time', 0)->where('type', RD::type_two)
             ->whereBetween('order_time', [$monthStart, $monthEnd])->pluck('id')->toArray();
         if (!empty($rds)) {
-            DB::table('rd_details')->whereIn('rd_id', $rds)->delete();
-            DB::table('rd')->whereIn('id', $rds)->delete();
+            foreach (array_chunk($rds, 1000) as $chunk) {
+                DB::table('rd_details')->whereIn('rd_id', $chunk)->delete();
+                DB::table('rd')->whereIn('id', $chunk)->delete();
+            }
         }
 
+        // 4. 获取设备配置
         $deviceConfigs = DB::table('device_details')->where('del_time', 0)
-            ->where('time', $calendar->time)->where('total_hours_2', '>', 0)->get()->keyBy('device_id');
+            ->where('time', $calendar->time)->where('total_hours_2', '>', 0)->get();
 
-        // 3. 内存计算数据
-        $pendingRds = [];
-        $rdToDeviceMap = []; // 用于记录 order_number 对应哪个 device_id
+        // --- 核心:按槽位合并逻辑开始 ---
 
-        foreach ($deviceConfigs as $deviceId => $config) {
-            $this->calculateData($deviceId, $items, $workDays, $config, $pendingRds, $rdToDeviceMap);
+        // 5. 内存计算:将所有设备的工时分配到槽位中合并
+        // $slots[日期][项目ID][时间槽Key] = ['start'=>Carbon, 'end'=>Carbon, 'device_ids'=>[]]
+        $slots = [];
+        foreach ($deviceConfigs as $config) {
+            $this->assignDeviceToSlots($config, $items, $workDays, $slots);
         }
 
-        // 4. 批量插入主表 (分批写入,防止 SQL 过长)
+        // 6. 构造主表批量插入数组
+        $pendingRds = [];
+        $rdToDevicesMap = []; // order_number => ['device_ids' => [], 'crt_time' => ...]
+
+        foreach ($slots as $day => $projectGroups) {
+            $baseCrtTime = $day + 59400; // 创建时间从 16:30 开始递增
+            $sequence = 0;
+
+            foreach ($projectGroups as $itemId => $timeBlocks) {
+                foreach ($timeBlocks as $timeKey => $data) {
+                    $orderNumber = 'RD' . $day . 'P' . $itemId . 'S' . $sequence . uniqid();
+                    $crtTime = $baseCrtTime + ($sequence * 10) + mt_rand(0, 5);
+
+                    $pendingRds[] = [
+                        'crt_time' => $crtTime,
+                        'order_time' => $day,
+                        'item_id' => $itemId,
+                        'start_time_hour' => $data['start']->hour,
+                        'start_time_min' => $data['start']->minute,
+                        'end_time_hour' => $data['end']->hour,
+                        'end_time_min' => $data['end']->minute,
+                        'total_hours' => 60,
+                        'order_number' => $orderNumber,
+                        'type' => RD::type_two,
+                        'del_time' => 0
+                    ];
+
+                    $rdToDevicesMap[$orderNumber] = [
+                        'device_ids' => $data['device_ids'],
+                        'crt_time' => $crtTime
+                    ];
+                    $sequence++;
+                }
+            }
+        }
+
+        // 7. 批量插入并同步详情
         if (empty($pendingRds)) return;
 
         foreach (array_chunk($pendingRds, 1000) as $chunk) {
             DB::table('rd')->insert($chunk);
         }
 
-        // 5. 核心:通过 order_number 批量找回生成的 ID
-        $allOrderNumbers = array_keys($rdToDeviceMap);
-        $insertedRds = DB::table('rd')
-            ->whereIn('order_number', $allOrderNumbers)
-            ->select('id', 'order_number', 'crt_time')
-            ->get();
+        // 通过 order_number 找回生成的 ID
+        $insertedRds = DB::table('rd')->whereIn('order_number', array_keys($rdToDevicesMap))
+            ->select('id', 'order_number')->get();
 
-        // 6. 构造并批量插入详情表
         $pendingDetails = [];
         foreach ($insertedRds as $rd) {
-            $pendingDetails[] = [
-                'rd_id'    => $rd->id,
-                'data_id'  => $rdToDeviceMap[$rd->order_number], // 从映射表找回设备ID
-                'type'     => RD::type_two,
-                'crt_time' => $rd->crt_time,
-                'upd_time' => $rd->crt_time,
-                'del_time' => 0
-            ];
+            $mapData = $rdToDevicesMap[$rd->order_number];
+            foreach ($mapData['device_ids'] as $deviceId) {
+                $pendingDetails[] = [
+                    'rd_id'    => $rd->id,
+                    'data_id'  => $deviceId,
+                    'type'     => RD::type_two,
+                    'crt_time' => $mapData['crt_time'],
+                    'upd_time' => $mapData['crt_time'],
+                    'del_time' => 0
+                ];
+            }
         }
 
         foreach (array_chunk($pendingDetails, 1000) as $chunk) {
@@ -136,11 +175,14 @@ class RdGenerateDeviceService extends Service
         }
     }
 
-    protected function calculateData($device_id, $items, $workDays, $config, &$pendingRds, &$rdToDeviceMap)
+    /**
+     * 计算并分配每个设备的工时到公共槽位
+     */
+    protected function assignDeviceToSlots($config, $items, $workDays, &$slots)
     {
         $totalMinutes = intval(round($config->total_hours_2 * 60) / 60) * 60;
 
-        // 筛选有效天
+        // 筛选有效天
         $validDays = [];
         foreach ($workDays as $day) {
             $dayItems = [];
@@ -152,44 +194,47 @@ class RdGenerateDeviceService extends Service
 
         if (empty($validDays)) return;
 
-        // 计算每日工时分配 (略,保持您原本的分配逻辑...)
         $dailyMinutesMap = $this->distributeMinutes(count($validDays), $totalMinutes, array_keys($validDays));
 
         foreach ($validDays as $day => $dayItems) {
             $dailyMinutes = $dailyMinutesMap[$day];
             if ($dailyMinutes <= 0) continue;
 
-            // 简单的随机项目分配
+            // 为了让不同设备能合并,使用固定的 8:00 作为参考起点
+            $currentTimePointer = Carbon::createFromTimestamp($day)->setTime(8, 0);
+            $lunchStart = Carbon::createFromTimestamp($day)->setTime(11, 30);
+            $lunchEnd   = Carbon::createFromTimestamp($day)->setTime(12, 0);
+
             $totalHoursToday = $dailyMinutes / 60;
             for ($h = 0; $h < $totalHoursToday; $h++) {
+                // 如果是多个设备同一时间做同一个项目,这里最好保证选择项目的随机种子在同一天内是对齐的,
+                // 或者业务允许随机。这里按设备随机选一个。
                 $item = $dayItems[array_rand($dayItems)];
 
-                // 生成时间点逻辑
-                $start = Carbon::createFromTimestamp($day)->setTime(8, 0)->addMinutes(mt_rand(0, 7) * 60);
+                $start = $currentTimePointer->copy();
+
+                // 跳过午休起始点
+                if ($start->gte($lunchStart) && $start->lt($lunchEnd)) {
+                    $start = $lunchEnd->copy();
+                }
+
                 $end = $start->copy()->addMinutes(60);
 
-                // 生成绝对唯一的单号:时间戳 + 毫秒 + 设备ID + 随机
-                // 这样既解决了速度问题,又保证了 order_number 的唯一索引不会崩
-                $orderNumber = 'RD' . $day . 'D' . $device_id . 'S' . uniqid();
-
-                $crtTime = $day + 59400 + mt_rand(0, 1800);
-
-                $pendingRds[] = [
-                    'crt_time' => $crtTime,
-                    'order_time' => $day,
-                    'item_id' => $item->id,
-                    'start_time_hour' => $start->hour,
-                    'start_time_min' => $start->minute,
-                    'end_time_hour' => $end->hour,
-                    'end_time_min' => $end->minute,
-                    'total_hours' => 60,
-                    'order_number' => $orderNumber,
-                    'type' => RD::type_two,
-                    'del_time' => 0
-                ];
+                // 跨午休处理
+                if ($start->lt($lunchStart) && $end->gt($lunchStart)) {
+                    $end->addMinutes(30);
+                }
+
+                // 生成用于合并的槽位 Key
+                $timeKey = $start->format('Hi') . '-' . $end->format('Hi');
+
+                // 存入合并缓冲区
+                $slots[$day][$item->id][$timeKey]['start'] = $start;
+                $slots[$day][$item->id][$timeKey]['end'] = $end;
+                $slots[$day][$item->id][$timeKey]['device_ids'][] = $config->device_id;
 
-                // 记录这个单号属于哪个设备
-                $rdToDeviceMap[$orderNumber] = $device_id;
+                // 指针移动(接力)
+                $currentTimePointer = $end->copy();
             }
         }
     }

+ 52 - 33
app/Service/RdGenerateService.php

@@ -15,6 +15,9 @@ class RdGenerateService extends Service
         }
 
         try {
+            // 设置脚本执行时间,防止大数据量时 PHP 超时
+            set_time_limit(300);
+
             DB::transaction(function () use ($data) {
                 $this->doGenerate($data['month']);
             });
@@ -68,7 +71,6 @@ class RdGenerateService extends Service
                 ->toArray();
 
             if (!empty($rds)) {
-                // 分片删除,防止 SQL 过长
                 foreach (array_chunk($rds, 1000) as $chunk) {
                     DB::table('rd_details')->whereIn('rd_id', $chunk)->delete();
                     DB::table('rd')->whereIn('id', $chunk)->delete();
@@ -76,13 +78,13 @@ class RdGenerateService extends Service
             }
         }
 
-        // 3. 预加载所有员工关联关系 (核心优化:内存化)
+        // 3. 预加载所有员工关联关系
         $itemDetails = DB::table('item_details')
             ->where('del_time', 0)
             ->whereIn('item_id', $itemIds)
             ->where('type', 1)
             ->get()
-            ->groupBy('data_id'); // 以员工ID分组
+            ->groupBy('data_id');
 
         $employeeIds = $itemDetails->keys()->toArray();
 
@@ -94,19 +96,17 @@ class RdGenerateService extends Service
             ->get()
             ->keyBy('employee_id');
 
-        $pendingRds = []; // 待插入的主表数据
-        $rdToEmployeeMap = []; // 临时映射:order_number => employee_id
+        $pendingRds = [];
+        $rdToEmployeeMap = [];
 
         // 5. 内存循环处理
         foreach ($employeeIds as $employeeId) {
             $empMonthConfig = $employeeMonths->get($employeeId);
             if (!$empMonthConfig || $empMonthConfig->total_hours_2 <= 0) continue;
 
-            // 获取该员工参与的项目集合
             $myProjectIds = $itemDetails->get($employeeId)->pluck('item_id')->toArray();
             $employeeItems = $items->whereIn('id', $myProjectIds);
 
-            // 计算该员工全月分布并填充到待插入数组
             $this->buildEmployeeRds(
                 $employeeId,
                 $employeeItems,
@@ -117,14 +117,12 @@ class RdGenerateService extends Service
             );
         }
 
-        // 6. 批量插入主表并同步明细表
+        // 6. 批量写入
         if (!empty($pendingRds)) {
-            // 分批插入主表 (MySQL 默认限制一次约 10MB)
             foreach (array_chunk($pendingRds, 1000) as $chunk) {
                 DB::table('rd')->insert($chunk);
             }
 
-            // 通过 order_number 反查回生成的 ID
             $insertedRds = DB::table('rd')
                 ->whereIn('order_number', array_keys($rdToEmployeeMap))
                 ->select('id', 'order_number', 'crt_time')
@@ -152,7 +150,6 @@ class RdGenerateService extends Service
     {
         $totalMinutes = intval(round($config->total_hours_2 * 60) / 60) * 60;
 
-        // 过滤有效日期
         $validDays = [];
         foreach ($workDays as $day) {
             $dayItems = [];
@@ -166,7 +163,6 @@ class RdGenerateService extends Service
 
         if (empty($validDays)) return;
 
-        // 分配每日时长
         $daysCount = count($validDays);
         $avgDailyMinutes = intval(floor($totalMinutes / $daysCount) / 60) * 60;
         $remainderMinutes = $totalMinutes - ($avgDailyMinutes * $daysCount);
@@ -190,11 +186,21 @@ class RdGenerateService extends Service
             }
         }
 
-        // 生成具体数据行
         foreach ($validDays as $day => $dayItems) {
             $dailyMinutes = $dailyMinutesMap[$day];
             if ($dailyMinutes <= 0) continue;
 
+            // --- 接力逻辑开始:计算当天统一的随机起点 ---
+            $availableMinutes = 480;
+            $maxOffset = $availableMinutes - $dailyMinutes;
+            $randomOffset = (intval($maxOffset / 30) > 0) ? mt_rand(0, intval($maxOffset / 30)) * 30 : 0;
+
+            // 当前时间指针指向当天的 08:00 + 随机偏移
+            $currentTimePointer = Carbon::createFromTimestamp($day)->setTime(8, 0)->addMinutes($randomOffset);
+            $lunchStart = Carbon::createFromTimestamp($day)->setTime(11, 30);
+            $lunchEnd   = Carbon::createFromTimestamp($day)->setTime(12, 0);
+            // --- 接力逻辑结束 ---
+
             $remaining = $dailyMinutes;
             $numItems = count($dayItems);
 
@@ -207,44 +213,52 @@ class RdGenerateService extends Service
                 }
 
                 if ($slotMinutes > 0) {
-                    $rowData = $this->calculateTimeSlots($item->id, $day, $slotMinutes, $employeeId);
-                    $pendingRds[] = $rowData;
-                    $rdToEmployeeMap[$rowData['order_number']] = $employeeId;
+                    // 使用接力计算函数
+                    $res = $this->calculateSequentialTime(
+                        $item->id,
+                        $slotMinutes,
+                        $employeeId,
+                        $currentTimePointer,
+                        $lunchStart,
+                        $lunchEnd,
+                        $day
+                    );
+
+                    $pendingRds[] = $res['rowData'];
+                    $rdToEmployeeMap[$res['rowData']['order_number']] = $employeeId;
+
+                    // 指针移动:下一个项目的开始时间是当前项目的结束时间
+                    $currentTimePointer = $res['nextPointer'];
                 }
                 $remaining -= $slotMinutes;
             }
         }
     }
 
-    protected function calculateTimeSlots($itemId, $dayTime, $minutes,$employeeId)
+    protected function calculateSequentialTime($itemId, $minutes, $employeeId, $startPointer, $lunchStart, $lunchEnd, $dayTime)
     {
-        $availableMinutes = 480;
-        $minutes = min($minutes, $availableMinutes);
-
-        $maxOffset = $availableMinutes - $minutes;
-        $randomOffset = (intval($maxOffset / 30) > 0) ? mt_rand(0, intval($maxOffset / 30)) * 30 : 0;
-
-        $dayStart   = Carbon::createFromTimestamp($dayTime)->setTime(8, 0);
-        $lunchStart = Carbon::createFromTimestamp($dayTime)->setTime(11, 30);
-        $lunchEnd   = Carbon::createFromTimestamp($dayTime)->setTime(12, 0);
-
-        $start = $dayStart->copy()->addMinutes($randomOffset);
+        $start = $startPointer->copy();
         $end = $start->copy();
+        $remaining = $minutes;
 
+        // 跨午休逻辑处理
         if ($start->lt($lunchStart)) {
             $morningLeft = $lunchStart->diffInMinutes($start);
-            if ($minutes <= $morningLeft) {
-                $end->addMinutes($minutes);
+            if ($remaining <= $morningLeft) {
+                $end->addMinutes($remaining);
             } else {
-                $end = $lunchEnd->copy()->addMinutes($minutes - $morningLeft);
+                // 扣除上午干掉的时间,剩下的从下午开始
+                $remaining -= $morningLeft;
+                $end = $lunchEnd->copy()->addMinutes($remaining);
             }
         } else {
-            $end->addMinutes($minutes);
+            // 如果起点已经在午休后(比如随机偏移很大)
+            $end->addMinutes($remaining);
         }
 
         $crtTime = Carbon::createFromTimestamp($dayTime)->setTime(16, 30)->addSeconds(mt_rand(0, 1800))->timestamp;
 
-        return [
+        $rowData = [
             'crt_id' => $employeeId,
             'crt_time' => $crtTime,
             'order_time' => $dayTime,
@@ -258,5 +272,10 @@ class RdGenerateService extends Service
             'type' => 1,
             'del_time' => 0
         ];
+
+        return [
+            'rowData' => $rowData,
+            'nextPointer' => $end // 返回结束指针
+        ];
     }
 }