Browse Source

微信小程序

cqp 4 days ago
parent
commit
fb28c8d41b
1 changed files with 243 additions and 38 deletions
  1. 243 38
      app/Service/PersonWorkService.php

+ 243 - 38
app/Service/PersonWorkService.php

@@ -1113,6 +1113,11 @@ class PersonWorkService extends Service
             'batch_id' => $batchId,
             'list' => $previewData // 返回给前端展示
         ]];
+
+//        $total = 0;
+//        foreach ($previewData as $value){
+//            if($value['employee_id'] == 142 && $value['item_id'] == 122) $total += $value['total_work_min'];
+//        }dd($total);
     }
 
     /**
@@ -1285,9 +1290,9 @@ class PersonWorkService extends Service
     private function calculateDailyAllocation($monthStart, $topDepartId, $user)
     {
         $monthEnd = strtotime('+1 month', $monthStart) - 1;
-        $step = 5; // 5分钟对齐
+        $step = 5; // 尽量 5 分钟对齐;明细允许个别非整 5
 
-        // --- 1. 基础数据加载 (保持原版结构) ---
+        // --- 1. 基础数据加载 ---
         $monthlyOrder = DB::table('monthly_pw_order_details as d')
             ->join('monthly_pw_order as m', 'm.id', '=', 'd.main_id')
             ->leftJoin('employee as e', 'e.id', '=', 'd.employee_id')
@@ -1313,63 +1318,130 @@ class PersonWorkService extends Service
             ->whereBetween('m.order_time', [$monthStart, $monthEnd])->where('d.top_depart_id', $topDepartId)->where('m.del_time', 0)
             ->select('d.*', 'm.order_time', 'm.type as main_type')->get()->groupBy(['employee_id', 'order_time']);
 
-        // --- 2. 阶段一:计算预分配 (加入动态余数抵消) ---
+        // --- 2. 阶段一:先锁定「项目月目标分钟」,再按剩余目标往工作日摊 ---
+        // 保证:各项目月合计 = 月总 × 比例(末项吃余数);人员月总不变
         $finalAlloc = [];
-        $empBalance = []; // 记录每个人的“工时余额” [emp_id => minutes]
+        $dayBalance = []; // 按天 5 分钟取整时的滚动余额
 
         foreach ($monthlyOrder as $mDetail) {
             $empId = $mDetail->employee_id;
             $empRules = $ruleSet->get($empId);
-            if (!$empRules) continue;
+            if (!$empRules || $empRules->isEmpty()) continue;
+
+            $empTotalMin = (int)round((float)$mDetail->rd_total_hours * 60);
+            if ($empTotalMin <= 0) continue;
+
+            // 2.1 各项目月目标(非末项按比例取整,末项保证加总 = 月总)
+            $rules = $empRules->values();
+            $ruleCount = $rules->count();
+            $projTarget = [];
+            $assigned = 0;
+            foreach ($rules as $index => $rule) {
+                $itemId = (int)$rule->item_id;
+                if ($index === $ruleCount - 1) {
+                    $projTarget[$itemId] = ($projTarget[$itemId] ?? 0) + max(0, $empTotalMin - $assigned);
+                } else {
+                    $part = (int)round($empTotalMin * ((float)$rule->rate / 100));
+                    $projTarget[$itemId] = ($projTarget[$itemId] ?? 0) + $part;
+                    $assigned += $part;
+                }
+            }
+            // 极端情况:非末项 round 累加超过月总,压回末项
+            $sumTarget = array_sum($projTarget);
+            if ($sumTarget > $empTotalMin) {
+                $lastItemId = (int)$rules[$ruleCount - 1]->item_id;
+                $projTarget[$lastItemId] = max(0, $projTarget[$lastItemId] - ($sumTarget - $empTotalMin));
+            }
 
-            $empRemainingMin = (int)round((float)$mDetail->rd_total_hours * 60);
+            $projRem = $projTarget;
+            $empRemainingMin = $empTotalMin;
 
+            // 2.2 预计算有容量的工作日
+            $dayCaps = [];
             foreach ($allDays as $dayInfo) {
-                if ($empRemainingMin <= 0) break;
                 $dayTs = $dayInfo->time;
                 $tempPool = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
                 $dayCapacity = 0;
-                foreach ($tempPool as $p) { $dayCapacity += (int)($p['e'] - $p['s']); }
-                if ($dayCapacity <= 0) continue;
+                foreach ($tempPool as $p) {
+                    $dayCapacity += (int)($p['e'] - $p['s']);
+                }
+                if ($dayCapacity > 0) {
+                    $dayCaps[$dayTs] = $dayCapacity;
+                }
+            }
+            if (empty($dayCaps)) continue;
 
-                // 核心变动:计算今天理想分配量,并尝试 5 分钟取整
+            $dayTsList = array_keys($dayCaps);
+            $dayIndex = 0;
+            $dayCount = count($dayTsList);
+
+            foreach ($dayTsList as $dayTs) {
+                if ($empRemainingMin <= 0) break;
+                if (array_sum($projRem) <= 0) break;
+
+                $dayCapacity = $dayCaps[$dayTs];
                 $theoryAlloc = min($empRemainingMin, $dayCapacity);
+                $isLastDay = ($dayIndex === $dayCount - 1) || ($theoryAlloc >= $empRemainingMin);
 
-                // 如果还没到最后一天,强行把当天的分配量凑成 5 的倍数
-                if ($theoryAlloc < $empRemainingMin) {
-                    // 结合之前的余额进行取整
-                    $currentWithBalance = $theoryAlloc + ($empBalance[$empId] ?? 0);
+                // 当天总量尽量 5 对齐;最后一天(或最后一批)吃干净,允许非整 5
+                if (!$isLastDay) {
+                    $currentWithBalance = $theoryAlloc + ($dayBalance[$empId] ?? 0);
                     $roundedAlloc = (int)round($currentWithBalance / $step) * $step;
-
-                    // 不能超过当天池子上限
-                    $canAllocToday = min($roundedAlloc, $dayCapacity);
-                    $empBalance[$empId] = $currentWithBalance - $canAllocToday; // 剩下的留给明天
+                    $canAllocToday = min($roundedAlloc, $dayCapacity, $empRemainingMin);
+                    // 不能把某天抽成 0 导致后面更难摊完
+                    if ($canAllocToday <= 0 && $theoryAlloc > 0) {
+                        $canAllocToday = min($theoryAlloc, $empRemainingMin);
+                    }
+                    $dayBalance[$empId] = $currentWithBalance - $canAllocToday;
                 } else {
-                    // 最后一天,全部吃掉(包含之前剩下的所有余额)
-                    $canAllocToday = $empRemainingMin;
+                    $canAllocToday = min($empRemainingMin, $dayCapacity);
+                    $dayBalance[$empId] = 0;
                 }
 
-                $allocatedInDay = 0;
-                $ruleCount = count($empRules);
-                foreach ($empRules as $index => $rule) {
-                    $rate = (float)$rule->rate / 100;
-                    $projectMin = ($index === $ruleCount - 1) ? ($canAllocToday - $allocatedInDay) : (int)round($canAllocToday * $rate);
+                if ($canAllocToday <= 0) {
+                    $dayIndex++;
+                    continue;
+                }
 
-                    // 内部项目分配也尽量 5 分钟取整
-                    if ($index < $ruleCount - 1 && $projectMin > $step) {
-                        $projectMin = (int)round($projectMin / $step) * $step;
-                    }
+                // 按「项目剩余月目标」比例拆当天额度(最大余数法),保证月末项目合计贴近目标
+                $dayParts = $this->splitMinutesByRemaining($canAllocToday, $projRem, $step, $isLastDay);
+                foreach ($dayParts as $itemId => $projectMin) {
+                    if ($projectMin <= 0) continue;
+                    $finalAlloc[$dayTs][$itemId][$empId] = ($finalAlloc[$dayTs][$itemId][$empId] ?? 0) + $projectMin;
+                    $projRem[$itemId] = max(0, ($projRem[$itemId] ?? 0) - $projectMin);
+                }
 
-                    if ($projectMin > 0) {
-                        $finalAlloc[$dayTs][$rule->item_id][$empId] = ($finalAlloc[$dayTs][$rule->item_id][$empId] ?? 0) + $projectMin;
-                        $allocatedInDay += $projectMin;
+                $empRemainingMin -= $canAllocToday;
+                $dayIndex++;
+            }
+
+            // 2.3 若因取整/容量仍有项目剩余,尽量塞进后续仍有空闲的天
+            $stuck = array_sum($projRem);
+            if ($stuck > 0) {
+                foreach ($dayCaps as $dayTs => $dayCapacity) {
+                    if ($stuck <= 0) break;
+                    $used = 0;
+                    if (!empty($finalAlloc[$dayTs])) {
+                        foreach ($finalAlloc[$dayTs] as $emps) {
+                            $used += (int)($emps[$empId] ?? 0);
+                        }
+                    }
+                    $free = $dayCapacity - $used;
+                    if ($free <= 0) continue;
+
+                    foreach ($projRem as $itemId => $rem) {
+                        if ($rem <= 0 || $free <= 0) continue;
+                        $take = min($rem, $free);
+                        $finalAlloc[$dayTs][$itemId][$empId] = ($finalAlloc[$dayTs][$itemId][$empId] ?? 0) + $take;
+                        $projRem[$itemId] -= $take;
+                        $free -= $take;
+                        $stuck -= $take;
                     }
                 }
-                $empRemainingMin -= $canAllocToday;
             }
         }
 
-        // --- 3. 阶段二:打散到具体点 ---
+        // --- 3. 阶段二:打散到具体时间(起点尽量 5 对齐;单条时长允许非整 5) ---
         $previewList = [];
         $dailyEmpTimePools = [];
         $tempMainIdCounter = 1;
@@ -1381,22 +1453,36 @@ class PersonWorkService extends Service
                     $currentTempMainId = $tempMainIdCounter++;
 
                     if (!isset($dailyEmpTimePools[$dayTs][$empId])) {
-                        $dailyEmpTimePools[$dayTs][$empId] = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
+                        $dailyEmpTimePools[$dayTs][$empId] = $this->buildAvailablePool(
+                            $empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData
+                        );
                     }
 
                     $tempRem = (int)$toAllocMin;
                     foreach ($dailyEmpTimePools[$dayTs][$empId] as &$p) {
                         if ($tempRem <= 0) break;
 
-                        // 起点必须对齐 5 分钟
-                        $realStart = (int)ceil($p['s'] / $step) * $step;
+                        $rawStart = (int)$p['s'];
+                        $alignedStart = (int)ceil($rawStart / $step) * $step;
+                        // 对齐后仍够放下剩余(或至少还能放下一段)则对齐;否则用原始起点(允许个别不对齐)
+                        if ($alignedStart < $p['e'] && ($p['e'] - $alignedStart) >= min($tempRem, 1)) {
+                            $realStart = $alignedStart;
+                        } else {
+                            $realStart = $rawStart;
+                        }
                         if ($realStart >= $p['e']) continue;
 
                         $pMax = (int)($p['e'] - $realStart);
                         $take = min($tempRem, $pMax);
+                        // 本段还放不完时,优先按 5 截取,余数留给后续时段;最后一段(take==tempRem)保持精确
+                        if ($take < $tempRem && $take >= $step) {
+                            $alignedTake = (int)floor($take / $step) * $step;
+                            if ($alignedTake > 0) {
+                                $take = $alignedTake;
+                            }
+                        }
 
                         $realEnd = $realStart + $take;
-
                         $previewList[] = [
                             'temp_main_id'    => $currentTempMainId,
                             'order_time'      => date('Y-m-d', $dayTs),
@@ -1417,6 +1503,7 @@ class PersonWorkService extends Service
                         $tempRem -= $take;
                         $p['s'] = $realEnd;
                     }
+                    unset($p);
                 }
             }
         }
@@ -1424,6 +1511,124 @@ class PersonWorkService extends Service
         return ['status' => true, 'data' => $previewList];
     }
 
+    /**
+     * 按各项目剩余月目标,把当天额度拆开(最大余数法)
+     * - 非最后一天:尽量按 5 分钟块分配
+     * - 最后一天:允许 1 分钟精度,保证项目目标吃干净
+     *
+     * @param int $dayBudget 当天可分配总分钟
+     * @param array $projRem [item_id => 剩余目标分钟]
+     * @param int $step
+     * @param bool $forceExact 是否强制精确分完(最后一天)
+     * @return array [item_id => 当天分配分钟]
+     */
+    private function splitMinutesByRemaining($dayBudget, array $projRem, $step = 5, $forceExact = false)
+    {
+        $active = [];
+        foreach ($projRem as $itemId => $rem) {
+            if ($rem > 0) {
+                $active[(int)$itemId] = (int)$rem;
+            }
+        }
+        if (empty($active) || $dayBudget <= 0) {
+            return [];
+        }
+
+        $totalRem = array_sum($active);
+        $dayBudget = min($dayBudget, $totalRem);
+        $result = array_fill_keys(array_keys($active), 0);
+
+        if ($forceExact || $dayBudget < $step) {
+            // 精确拆:按比例 floor + 最大余数补齐
+            $order = [];
+            $used = 0;
+            foreach ($active as $itemId => $rem) {
+                $ideal = $dayBudget * $rem / $totalRem;
+                $base = (int)floor($ideal);
+                $base = min($base, $rem);
+                $result[$itemId] = $base;
+                $used += $base;
+                $order[] = [
+                    'item_id' => $itemId,
+                    'frac' => $ideal - $base,
+                    'rem' => $rem - $base,
+                ];
+            }
+            usort($order, function ($a, $b) {
+                if ($a['frac'] == $b['frac']) {
+                    return $b['rem'] <=> $a['rem'];
+                }
+                return $b['frac'] <=> $a['frac'];
+            });
+            $left = $dayBudget - $used;
+            foreach ($order as $row) {
+                if ($left <= 0) break;
+                if ($row['rem'] <= 0) continue;
+                $give = min($left, $row['rem']);
+                $result[$row['item_id']] += $give;
+                $left -= $give;
+            }
+            return array_filter($result);
+        }
+
+        // 非最后:先按 5 分钟块分配,余数分钟先留在余额逻辑里由后续天消化
+        $blockBudget = (int)floor($dayBudget / $step) * $step;
+        if ($blockBudget <= 0) {
+            // 当天不足 5 分钟,直接精确分掉
+            return $this->splitMinutesByRemaining($dayBudget, $active, $step, true);
+        }
+
+        $blocks = (int)($blockBudget / $step);
+        $blockRem = [];
+        foreach ($active as $itemId => $rem) {
+            $blockRem[$itemId] = (int)floor($rem / $step); // 项目剩余可占的整块数上限
+        }
+        // 权重仍用剩余分钟
+        $order = [];
+        $usedBlocks = 0;
+        foreach ($active as $itemId => $rem) {
+            $idealBlocks = $blocks * $rem / $totalRem;
+            $base = (int)floor($idealBlocks);
+            $maxBlocks = $blockRem[$itemId];
+            $base = min($base, $maxBlocks);
+            $result[$itemId] = $base * $step;
+            $usedBlocks += $base;
+            $order[] = [
+                'item_id' => $itemId,
+                'frac' => $idealBlocks - $base,
+                'rem_blocks' => $maxBlocks - $base,
+            ];
+        }
+        usort($order, function ($a, $b) {
+            if ($a['frac'] == $b['frac']) {
+                return $b['rem_blocks'] <=> $a['rem_blocks'];
+            }
+            return $b['frac'] <=> $a['frac'];
+        });
+        $leftBlocks = $blocks - $usedBlocks;
+        foreach ($order as $row) {
+            if ($leftBlocks <= 0) break;
+            if ($row['rem_blocks'] <= 0) continue;
+            $result[$row['item_id']] += $step;
+            $leftBlocks--;
+        }
+
+        // 当天 dayBudget 里不足一块的零头(1~4 分钟):补给剩余目标最大的项目(允许个别非整 5)
+        $odd = $dayBudget - $blockBudget;
+        if ($odd > 0) {
+            arsort($active);
+            foreach ($active as $itemId => $rem) {
+                $already = $result[$itemId] ?? 0;
+                if ($already >= $rem) continue;
+                $give = min($odd, $rem - $already);
+                $result[$itemId] = $already + $give;
+                break;
+            }
+        }
+
+        return array_filter($result);
+    }
+
     public function dailyPwOrderSave($data, $user)
     {
         $list = $data['list'] ?? [];