startOfMonth()->timestamp; $calendar = DB::table('calendar') ->where('del_time', 0) ->where('time', $monthStart) ->first(); if (!$calendar) return [false, '当月日历未配置']; $hasWorkDays = DB::table('calendar_details') ->where('del_time', 0) ->where('calendar_id', $calendar->id) ->where('is_work', 1) ->exists(); if (!$hasWorkDays) return [false, '当月无工作日,无法生成']; // try { // DB::transaction(function () use($data) { // $this->doGenerate($data['month']); // }); // } catch (\Exception $e) { // // 只有这里 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, '该月设备数据已在后台处理,请勿重复操作']; ProcessDataJob::dispatch($data)->onQueue(self::job_name); return [true, '任务已放到后台运行']; } public function delTableKey($key) { $this->dellimitingSendRequest($key); } /** * 队列真正执行的逻辑 */ public function doGenerate(string $month) { $monthStart = Carbon::createFromFormat('Y-m', $month)->startOfMonth()->timestamp; $monthEnd = Carbon::createFromFormat('Y-m', $month)->endOfMonth()->timestamp; // 1. 获取日历和工作日 $calendar = DB::table('calendar')->where('del_time', 0)->where('time', $monthStart)->first(); if (!$calendar) throw new \Exception('当月日历未配置'); $workDays = DB::table('calendar_details') ->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(); if ($items->isEmpty() || empty($workDays)) return; // 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)) { 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(); // --- 核心:按槽位合并逻辑开始 --- // 5. 内存计算:将所有设备的工时分配到槽位中合并 // $slots[日期][项目ID][时间槽Key] = ['start'=>Carbon, 'end'=>Carbon, 'device_ids'=>[]] $slots = []; foreach ($deviceConfigs as $config) { $this->assignDeviceToSlots($config, $items, $workDays, $slots); } // 6. 构造主表批量插入数组 $pendingRds = []; $rdToDevicesMap = []; // order_number => ['device_ids' => [], 'crt_time' => ...] foreach ($slots as $day => $projectGroups) { // 1. 获取当月第一天的时间戳 $belongMonth = strtotime(date('Y-m-01', $day)); $baseCrtTime = $day + 59400; // 创建时间从 16:30 开始递增 $sequence = 0; foreach ($projectGroups as $itemId => $timeBlocks) { foreach ($timeBlocks as $timeKey => $data) { // ... 循环内部 ... $dateStr = date('Ymd', $day); // 结合项目ID、循环序列和4位随机字符,兼顾了短小和唯一性 $orderNumber = sprintf("RD%s%d%s%s", $dateStr, $itemId, dechex($sequence), substr(md5(mt_rand()), 0, 4) ); // $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, 'belong_month' => $belongMonth, ]; $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); } // 通过 order_number 找回生成的 ID $insertedRds = DB::table('rd')->whereIn('order_number', array_keys($rdToDevicesMap)) ->select('id', 'order_number')->get(); $pendingDetails = []; foreach ($insertedRds as $rd) { $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) { DB::table('rd_details')->insert($chunk); } } /** * 计算并分配每个设备的工时到公共槽位 */ protected function assignDeviceToSlots($config, $items, $workDays, &$slots) { $totalMinutes = intval(round($config->total_hours_2 * 60) / 60) * 60; // 筛选有效天 $validDays = []; foreach ($workDays as $day) { $dayItems = []; foreach ($items as $item) { if ($day >= $item->start_time && $day <= $item->end_time) $dayItems[] = $item; } if (!empty($dayItems)) $validDays[$day] = $dayItems; } 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 = $currentTimePointer->copy(); // 跳过午休起始点 if ($start->gte($lunchStart) && $start->lt($lunchEnd)) { $start = $lunchEnd->copy(); } $end = $start->copy()->addMinutes(60); // 跨午休处理 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; // 指针移动(接力) $currentTimePointer = $end->copy(); } } } private function distributeMinutes($daysCount, $totalMinutes, $dayKeys) { $avg = (intval(floor($totalMinutes / $daysCount) / 60)) * 60; $res = array_fill_keys($dayKeys, $avg); $rem = ($totalMinutes - ($avg * $daysCount)) / 60; for($i=0; $i<$rem; $i++) { $res[$dayKeys[$i % $daysCount]] += 60; } return $res; } }