瀏覽代碼

胡庆余堂

cqp 3 月之前
父節點
當前提交
e79c9f6f65
共有 1 個文件被更改,包括 97 次插入41 次删除
  1. 97 41
      app/Service/RdGenerateService.php

+ 97 - 41
app/Service/RdGenerateService.php

@@ -87,7 +87,7 @@ class RdGenerateService extends Service
     }
 
     /**
-     * 按员工生成当月工时单,严格保证总工时匹配
+     * 按员工生成当月工时单,严格保证总工时匹配(通过将分配分钟数取整到 5 的倍数实现)
      */
     protected function generateEmployeeMonth(int $employeeId, $items, array $workDays, $calendar)
     {
@@ -100,8 +100,15 @@ class RdGenerateService extends Service
             return;
         }
 
-        $totalMinutes = (int)round($employeeMonth->total_hours_2 * 60); // 员工总分钟数
-        $dayMaxMinutes = 510; // 每天最大分钟数 (含午休)
+        // 员工总分钟数:这里保留了浮点数取整可能带来的微小误差,但这是上游数据决定的
+        $totalMinutes = (int)round($employeeMonth->total_hours_2 * 60);
+
+        // --- 核心修复 1: 确保总分钟数是 5 的倍数,否则损失这 $1-4$ 分钟的精度 ---
+        // 这是为了让后续的平均分配和余数计算能确保最终分配的总和是 5 的倍数
+        $totalMinutes = intval($totalMinutes / 5) * 5;
+        // --- 核心修复 1 结束 ---
+
+        $dayMaxMinutes = 510 - 30; // 每天最大分钟数(扣午休)
 
         // 员工参与的项目
         $employeeItems = [];
@@ -111,49 +118,86 @@ class RdGenerateService extends Service
                 ->where('type', 1)
                 ->where('data_id', $employeeId)
                 ->exists();
-            if ($exists) {
-                $employeeItems[] = $item;
-            }
+            if ($exists) $employeeItems[] = $item;
         }
 
         if (empty($employeeItems)) return;
 
-        // 生成所有槽位:项目 × 工作日(有效周期内)
-        $slots = [];
-        foreach ($employeeItems as $item) {
-            foreach ($workDays as $day) {
+        // 所有有效工作日(员工参与项目的周期内)
+        $validDays = [];
+        foreach ($workDays as $day) {
+            $dayItems = [];
+            foreach ($employeeItems as $item) {
                 if ($day >= $item->start_time && $day <= $item->end_time) {
-                    $slots[] = [
-                        'item_id' => $item->id,
-                        'day' => $day
-                    ];
+                    $dayItems[] = $item;
                 }
             }
+            if (!empty($dayItems)) {
+                $validDays[$day] = $dayItems;
+            }
         }
 
-        if (empty($slots)) return;
+        if (empty($validDays)) return;
 
-        // 均分总分钟数
-        $numSlots = count($slots);
-        $baseMinutes = floor($totalMinutes / $numSlots);
-        $remainder = $totalMinutes - $baseMinutes * $numSlots;
+        // 每天大约的分钟数
+        $avgDailyMinutes = floor($totalMinutes / count($validDays));
+        $remainder = $totalMinutes - $avgDailyMinutes * count($validDays);
 
-        foreach ($slots as $i => $slot) {
-            $minutes = $baseMinutes;
-            if ($i === $numSlots - 1) {
-                // 最后 slot 补齐余数
-                $minutes += $remainder;
+        foreach ($validDays as $day => $dayItems) {
+            $dailyMinutes = $avgDailyMinutes;
+            // 最后一天补齐余数
+            if ($day === array_key_last($validDays)) {
+                $dailyMinutes += $remainder;
             }
 
-            // 确保单日不超过最大分钟数
-            $minutes = min($minutes, $dayMaxMinutes - 30); // 扣掉午休
+            // 随机给当天项目分配分钟数
+            $slots = [];
+            $remaining = $dailyMinutes;
+            $numItems = count($dayItems);
+
+            for ($i = 0; $i < $numItems; $i++) {
+                if ($i === $numItems - 1) {
+                    // 最后一个项目:分配所有剩余时间
+                    $slotMinutes = $remaining;
+                } else {
+                    // 随机 ±20% 分配
+                    $max = floor($remaining * 0.8);
+                    $slotMinutes = mt_rand(1, max(1, $max));
+
+                    // --- 核心修复 2: 确保随机分配的工时是 5 的倍数 ---
+                    $slotMinutes = intval($slotMinutes / 5) * 5;
+                    // 确保至少是 5 分钟的倍数(如果剩余时间允许)
+                    $slotMinutes = max(0, $slotMinutes);
+                    // --- 核心修复 2 结束 ---
+                }
+
+                $slots[] = $slotMinutes;
+                $remaining -= $slotMinutes;
+            }
+
+            // 调整最后剩余:将所有因为取整损失的分钟数,都加给最后一个项目
+            if ($remaining > 0) {
+                $slots[$numItems - 1] += $remaining;
+            }
 
-            $this->createRd($slot['item_id'], $slot['day'], $minutes, $employeeId);
+            // 确保最后一个项目的工时也是 5 的倍数(因为 $dailyMinutes$ 是 5 的倍数,这一步理论上是确保的)
+            // 我们需要对最终的 $slots[$numItems - 1] 再次取整,以处理可能的小数取整误差
+            $slots[$numItems - 1] = intval($slots[$numItems - 1] / 5) * 5;
+
+
+            // 创建工时单
+            foreach ($dayItems as $idx => $item) {
+                // 仅对分配到的工时大于 0 的项目创建记录
+                if ($slots[$idx] > 0) {
+                    $this->createRd($item->id, $day, $slots[$idx], $employeeId);
+                }
+            }
         }
     }
 
     /**
      * 创建单条研发工时单
+     * 由于传入的 $minutes 已经是 5 的倍数,此方法保证 total_hours 与 $start/$end 严格匹配。
      */
     protected function createRd(int $itemId, int $dayTime, int $minutes, int $employeeId)
     {
@@ -162,30 +206,42 @@ class RdGenerateService extends Service
         $lunchEnd   = Carbon::createFromTimestamp($dayTime)->setTime(12, 0);
         $dayEnd = Carbon::createFromTimestamp($dayTime)->setTime(16, 30);
 
-        // 上午可用分钟
         $morningMinutes = $lunchStart->diffInMinutes($dayStart);
-        // 下午可用分钟
         $afternoonMinutes = $dayEnd->diffInMinutes($lunchEnd);
+        $availableMinutes = $morningMinutes + $afternoonMinutes;
 
-        // 确保总工时不会超过全天可用分钟
-        $minutes = min($minutes, $morningMinutes + $afternoonMinutes);
+        // 保证总分钟数不超过可用时间
+        $minutes = min($minutes, $availableMinutes);
 
-        // 决定工时分配到上午和下午
+        // 随机生成 start 时间,整5分钟
         if ($minutes <= $morningMinutes) {
-            $start = $dayStart->copy()->addMinutes(mt_rand(0, $morningMinutes - $minutes));
-            $end = $start->copy()->addMinutes($minutes);
+            $maxOffset = $morningMinutes - $minutes;
+            // $dayStart (8:00) 已经是 5 的倍数
+            $offset = 5 * mt_rand(0, intval($maxOffset / 5));
+            $start = $dayStart->copy()->addMinutes($offset);
         } elseif ($minutes <= $afternoonMinutes) {
-            $start = $lunchEnd->copy()->addMinutes(mt_rand(0, $afternoonMinutes - $minutes));
-            $end = $start->copy()->addMinutes($minutes);
+            $maxOffset = $afternoonMinutes - $minutes;
+            $offset = 5 * mt_rand(0, intval($maxOffset / 5));
+            $start = $lunchEnd->copy()->addMinutes($offset);
         } else {
             // 分两段:上午尽量满,剩余在下午
             $start = $dayStart->copy();
-            $end = $lunchEnd->copy()->addMinutes($minutes - $morningMinutes);
+        }
+
+        // 计算结束时间,跨午休自动跳过
+        $remaining = $minutes;
+        if ($start->lt($lunchStart) && $remaining > $morningMinutes - $start->diffInMinutes($dayStart)) {
+            $usedMorning = $morningMinutes - $start->diffInMinutes($dayStart);
+            $remaining -= $usedMorning;
+            $end = $lunchEnd->copy()->addMinutes($remaining);
+        } else {
+            $end = $start->copy()->addMinutes($remaining);
         }
 
         // 调整到整5分钟
-        $start->minute = ceil($start->minute / 5) * 5;
-        $end->minute = ceil($end->minute / 5) * 5;
+        // 🚨 这一步保留,但由于 $minutes 是 5 的倍数,这里的取整不会导致 $end - $start$ 发生变化
+        $start->minute = intval($start->minute / 5) * 5;
+        $end->minute = intval($end->minute / 5) * 5;
 
         // crt_time 16:30 ~ 17:00
         $crtTime = Carbon::createFromTimestamp($dayTime)
@@ -204,6 +260,7 @@ class RdGenerateService extends Service
             'start_time_min' => $start->minute,
             'end_time_hour' => $end->hour,
             'end_time_min' => $end->minute,
+            // 重点:使用分配好的 $minutes,现在它已经是 5 的倍数,与 $start/$end$ 匹配
             'total_hours' => $minutes,
             'order_number' => $orderNumber,
             'type' => 1
@@ -217,5 +274,4 @@ class RdGenerateService extends Service
             'upd_time' => $crtTime,
         ]);
     }
-
-}
+}