|
|
@@ -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,
|
|
|
]);
|
|
|
}
|
|
|
-
|
|
|
-}
|
|
|
+}
|