cqp 1 месяц назад
Родитель
Сommit
ab73317842

+ 13 - 0
app/Http/Controllers/Api/RDController.php

@@ -20,6 +20,19 @@ class RDController extends BaseController
         }
     }
 
+    public function rdCreate2(Request $request)
+    {
+        $service = new RDService();
+        $user = $request->userData;
+        list($status,$data) = $service->rdCreate2($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
     public function rdEdit(Request $request)
     {
         $service = new RDService();

+ 18 - 44
app/Jobs/ProcessDataJob.php

@@ -2,18 +2,14 @@
 
 namespace App\Jobs;
 
-use App\Model\RevenueCostMain;
-use App\Service\TPlusServerService;
+use App\Service\RdGenerateDeviceService;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Facades\Redis;
-use MongoDB\Driver\Exception\Exception;
-use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Console\Output\OutputInterface;
 
 class ProcessDataJob implements ShouldQueue
@@ -24,55 +20,33 @@ class ProcessDataJob implements ShouldQueue
     protected $user;
     protected $type;
 
-    public $timeout = 300;
+    // 重点 1: 设置最大尝试次数为 1,一旦失败立即进入 failed_jobs,不重试
+    public $tries = 1;
+    // 重点 2: 设置超时时间,防止进程死锁
+    public $timeout = 360;
 
-    public function __construct($data, $user, $type = 1)
+    public function __construct($data)
     {
         $this->data = $data;
-        $this->user = $user;
-        $this->type = $type;
     }
 
     public function handle()
     {
-        $service = new TPlusServerService();
+        $service = new RdGenerateDeviceService();
+        $lockKey = $this->data['lock_key'];
         try {
-            $data = $this->data;
-            $user = $this->user;
-            $type = $this->type;
-
-            if($type == 1){
-                //收付款单
-                list($status, $msg) = $service->synRevenueCostFromTPlus($data, $user);
-            }elseif($type == 2){
-                list($status, $msg) = $service->synSalaryEmployeeFromMine($data, $user);
-            }else{
-                list($status, $msg) = $service->synFreightFeeFromMine($data, $user);
-            }
-
-            $this->finalDo($msg, $service);
-        } catch (\Throwable $e) {
-            $this->finalDo("异常:" . $e->getMessage(), $service);
-            $this->delete();
+            DB::transaction(function () use($service) {
+                $service->doGenerate($this->data['month']);
+            });
+        } catch (\Exception $e) {
+            // 只有这里 throw 了,任务才会进入 failed_jobs 表
+            throw $e;
+
+        } finally {
+            $service->delLimitingSendRequest($lockKey);
         }
     }
 
-    private function finalDo($msg, $service){
-        $type = $this->type;
-        $service->delTableKey($type);
-        $service->clearTmpTable($type);
-
-        $user = $this->user;
-        $data = $this->data;
-
-        RevenueCostMain::insert([
-            'result' => $msg,
-            'crt_id' => $user['id'],
-            'crt_time' => $data['operation_time'],
-            'order_type' => $data['type'],
-        ]);
-    }
-
     protected function echoMessage(OutputInterface $output)
     {
         //输出消息

+ 5 - 0
app/Service/RDService.php

@@ -16,6 +16,11 @@ class RDService extends Service
         return [$status, $msg];
     }
 
+    public function rdCreate2($data, $user){
+        list($status, $msg) = (new RdGenerateDeviceService())->generate($data);
+        return [$status, $msg];
+    }
+
     public function rdEdit($data,$user){
         list($status,$msg) = $this->rdRule($data, $user, false);
         if(!$status) return [$status,$msg];

+ 254 - 0
app/Service/RdGenerateDeviceService.php

@@ -0,0 +1,254 @@
+<?php
+
+namespace App\Service;
+
+use App\Jobs\ProcessDataJob;
+use App\Model\RD;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Cache;
+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)
+            ->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, '当月无工作日,无法生成'];
+
+        $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;
+
+        $calendar = DB::table('calendar')->where('del_time', 0)->where('time', $monthStart)->first();
+        $workDays = DB::table('calendar_details')
+            ->where('del_time', 0)
+            ->where('calendar_id', $calendar->id)
+            ->where('is_work', 1)
+            ->orderBy('time')
+            ->pluck('time')
+            ->toArray();
+
+        // 获取当月启用的研发项目
+        $items = DB::table('item')
+            ->where('del_time', 0)
+            ->where('is_use', 1)
+            ->where('start_time', '<=', $monthEnd)
+            ->where('end_time', '>=', $monthStart)
+            ->get();
+
+        if ($items->isEmpty()) return;
+
+        $itemIds = $items->pluck('id')->toArray();
+
+        // 删除当月该设备类型(type=2)的老数据
+        $rds = DB::table('rd')
+            ->where('del_time', 0)
+            ->where('type', RD::type_two)
+            ->whereIn('item_id', $itemIds)
+            ->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();
+        }
+
+        // 直接获取所有有效的设备档案ID
+        $device_ids = DB::table('device')
+            ->where('del_time', 0)
+            ->pluck('id')
+            ->toArray();
+
+        foreach ($device_ids as $device_id) {
+            // 为每个设备生成月度工时单
+            $this->generateDeviceMonth($device_id, $items, $workDays, $calendar);
+        }
+    }
+
+    protected function generateDeviceMonth(int $device_id, $items, array $workDays, $calendar)
+    {
+        // 获取该设备在当月的总分配工时配置
+        $deviceMonth = DB::table('device_details')
+            ->where('del_time', 0)
+            ->where('device_id', $device_id)
+            ->where('time', $calendar->time)
+            ->first();
+
+        if (!$deviceMonth || $deviceMonth->total_hours_2 <= 0) return;
+
+        // 1. 月总工时取整为满小时 (60的倍数)
+        $totalMinutes = intval(round($deviceMonth->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;
+
+        // 2. 随机化每日总工时分布 (例如 184小时分到23天,每天8小时)
+        $daysCount = count($validDays);
+        $avgDailyMinutes = intval(floor($totalMinutes / $daysCount) / 60) * 60;
+        $remainderMinutes = $totalMinutes - ($avgDailyMinutes * $daysCount);
+
+        $dailyMinutesMap = [];
+        foreach (array_keys($validDays) as $day) {
+            $dailyMinutesMap[$day] = $avgDailyMinutes;
+        }
+
+        if ($remainderMinutes > 0) {
+            $extraHours = $remainderMinutes / 60;
+            $dayKeys = array_keys($validDays);
+            shuffle($dayKeys);
+            for ($i = 0; $i < $extraHours; $i++) {
+                $targetDay = $dayKeys[$i % $daysCount];
+                if ($dailyMinutesMap[$targetDay] < 480) { // 单日最高 8 小时
+                    $dailyMinutesMap[$targetDay] += 60;
+                } else {
+                    $extraHours++;
+                }
+            }
+        }
+
+        // 3. 遍历日期并“离散化”分配项目工时
+        foreach ($validDays as $day => $dayItems) {
+            $dailyMinutes = $dailyMinutesMap[$day];
+            if ($dailyMinutes <= 0) continue;
+
+            $totalHoursToday = $dailyMinutes / 60; // 今天要分配的总小时数
+
+            // 初始化每个项目的分钟数容器
+            $projectMinutes = [];
+            foreach ($dayItems as $idx => $item) {
+                $projectMinutes[$idx] = 0;
+            }
+
+            // 获取项目索引并打乱,增加公平性
+            $itemIndexes = array_keys($dayItems);
+
+            // --- 核心离散逻辑:随机发牌法 ---
+            for ($h = 0; $h < $totalHoursToday; $h++) {
+                // 如果项目很多(比如20个),这里每次随机选一个
+                // 结果会非常离散:可能8小时分给了8个不同的项目,每个项目1小时
+                $randomIndex = $itemIndexes[array_rand($itemIndexes)];
+                $projectMinutes[$randomIndex] += 60;
+            }
+
+            // 4. 执行生成
+            foreach ($dayItems as $idx => $item) {
+                if (isset($projectMinutes[$idx]) && $projectMinutes[$idx] > 0) {
+                    // 调用之前定义的 createRd 写入数据库
+                    $this->createRd($item->id, $day, $projectMinutes[$idx], $device_id);
+                }
+            }
+        }
+    }
+
+    protected function createRd(int $itemId, int $dayTime, int $minutes, int $device_id)
+    {
+        $dayStart   = Carbon::createFromTimestamp($dayTime)->setTime(8, 0);
+        $lunchStart = Carbon::createFromTimestamp($dayTime)->setTime(11, 30);
+        $lunchEnd   = Carbon::createFromTimestamp($dayTime)->setTime(12, 0);
+        $dayEnd     = Carbon::createFromTimestamp($dayTime)->setTime(16, 30);
+
+        $availableMinutes = 480;
+        $minutes = min($minutes, $availableMinutes);
+
+        $maxOffset = $availableMinutes - $minutes;
+        $randomOffset = (intval($maxOffset / 30) > 0) ? mt_rand(0, intval($maxOffset / 30)) * 30 : 0;
+
+        $start = $dayStart->copy()->addMinutes($randomOffset);
+        $end = $start->copy();
+        $remaining = $minutes;
+
+        if ($start->lt($lunchStart)) {
+            $morningLeft = $lunchStart->diffInMinutes($start);
+            if ($remaining <= $morningLeft) {
+                $end->addMinutes($remaining);
+            } else {
+                $remaining -= $morningLeft;
+                $end = $lunchEnd->copy()->addMinutes($remaining);
+            }
+        } else {
+            $end->addMinutes($remaining);
+        }
+
+        if ($end->gt($dayEnd)) {
+            $end = $dayEnd->copy();
+        }
+
+        $crtTime = Carbon::createFromTimestamp($dayTime)->setTime(16, 30)->addSeconds(mt_rand(0, 1800))->timestamp;
+        $orderNumber = 'DEV' . date('Ymd', $dayTime) . mt_rand(1000, 9999) . substr(uniqid(), -3);
+
+        $rdId = DB::table('rd')->insertGetId([
+            'crt_time' => $crtTime,
+            'order_time' => $dayTime,
+            'item_id' => $itemId,
+            'start_time_hour' => $start->hour,
+            'start_time_min' => $start->minute,
+            'end_time_hour' => $end->hour,
+            'end_time_min' => $end->minute,
+            'total_hours' => $minutes,
+            'order_number' => $orderNumber,
+            'type' => RD::type_two, // 设备工时单类型为 2
+            'del_time' => 0
+        ]);
+
+        DB::table('rd_details')->insert([
+            'rd_id' => $rdId,
+            'data_id' => $device_id, // 存储设备ID
+            'type' => RD::type_two,   // 类型为 2
+            'crt_time' => $crtTime,
+            'upd_time' => $crtTime,
+            'del_time' => 0
+        ]);
+    }
+}

+ 71 - 252
app/Service/RdGenerateService.php

@@ -2,6 +2,7 @@
 
 namespace App\Service;
 
+use App\Model\RD;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\DB;
 
@@ -29,42 +30,35 @@ class RdGenerateService extends Service
         $monthStart = Carbon::createFromFormat('Y-m', $month)->startOfMonth()->timestamp;
         $monthEnd   = Carbon::createFromFormat('Y-m', $month)->endOfMonth()->timestamp;
 
-        // 日历
         $calendar = DB::table('calendar')
-            ->where('del_time',0)
+            ->where('del_time', 0)
             ->where('time', $monthStart)
             ->first();
 
-        if (!$calendar) {
-            throw new \Exception('当月日历未配置');
-        }
+        if (!$calendar) throw new \Exception('当月日历未配置');
 
-        // 工作日
         $workDays = DB::table('calendar_details')
-            ->where('del_time',0)
+            ->where('del_time', 0)
             ->where('calendar_id', $calendar->id)
             ->where('is_work', 1)
             ->orderBy('time')
             ->pluck('time')
             ->toArray();
 
-        if (empty($workDays)) {
-            throw new \Exception('当月无工作日');
-        }
+        if (empty($workDays)) throw new \Exception('当月无工作日');
 
-        // 启用项目
         $items = DB::table('item')
-            ->where('del_time',0)
+            ->where('del_time', 0)
             ->where('is_use', 1)
             ->where('start_time', '<=', $monthEnd)
             ->where('end_time', '>=', $monthStart)
             ->get();
 
-        // 删除当月所有项目的老数据
         $itemIds = $items->pluck('id')->toArray();
         if (!empty($itemIds)) {
             $rds = DB::table('rd')
-                ->where('del_time',0)
+                ->where('del_time', 0)
+                ->where('type',RD::type_one)
                 ->whereIn('item_id', $itemIds)
                 ->whereBetween('order_time', [$monthStart, $monthEnd])
                 ->pluck('id')
@@ -76,59 +70,44 @@ class RdGenerateService extends Service
             }
         }
 
-        // 获取所有员工id
         $employeeIds = DB::table('item_details')
-            ->where('del_time',0)
+            ->where('del_time', 0)
             ->whereIn('item_id', $itemIds)
             ->where('type', 1)
             ->pluck('data_id')
             ->unique()
             ->toArray();
 
-        // 按员工生成当月工时单
         foreach ($employeeIds as $employeeId) {
             $this->generateEmployeeMonth($employeeId, $items, $workDays, $calendar);
         }
     }
 
-    /**
-     * 按员工生成当月工时单,严格保证总工时匹配(通过将分配分钟数取整到 5 的倍数实现)
-     */
-    protected function generateEmployeeMonth1(int $employeeId, $items, array $workDays, $calendar)
+    protected function generateEmployeeMonth(int $employeeId, $items, array $workDays, $calendar)
     {
         $employeeMonth = DB::table('employee_details')
+            ->where('del_time', 0)
             ->where('employee_id', $employeeId)
             ->where('time', $calendar->time)
             ->first();
 
-        if (!$employeeMonth || $employeeMonth->total_hours_2 <= 0) {
-            return;
-        }
-
-        // 员工总分钟数:这里保留了浮点数取整可能带来的微小误差,但这是上游数据决定的
-        $totalMinutes = (int)round($employeeMonth->total_hours_2 * 60);
-
-        // --- 核心修复 1: 确保总分钟数是 5 的倍数,否则损失这 $1-4$ 分钟的精度 ---
-        // 这是为了让后续的平均分配和余数计算能确保最终分配的总和是 5 的倍数
-        $totalMinutes = intval($totalMinutes / 5) * 5;
-        // --- 核心修复 1 结束 ---
+        if (!$employeeMonth || $employeeMonth->total_hours_2 <= 0) return;
 
-        $dayMaxMinutes = 510 - 30; // 每天最大分钟数(扣午休)
+        // 1. 月总工时取整为满小时 (60的倍数)
+        $totalMinutes = intval(round($employeeMonth->total_hours_2 * 60) / 60) * 60;
 
-        // 员工参与的项目
         $employeeItems = [];
         foreach ($items as $item) {
             $exists = DB::table('item_details')
+                ->where('del_time', 0)
                 ->where('item_id', $item->id)
                 ->where('type', 1)
                 ->where('data_id', $employeeId)
                 ->exists();
             if ($exists) $employeeItems[] = $item;
         }
-
         if (empty($employeeItems)) return;
 
-        // 所有有效工作日(员工参与项目的周期内)
         $validDays = [];
         foreach ($workDays as $day) {
             $dayItems = [];
@@ -137,265 +116,104 @@ class RdGenerateService extends Service
                     $dayItems[] = $item;
                 }
             }
-            if (!empty($dayItems)) {
-                $validDays[$day] = $dayItems;
-            }
+            if (!empty($dayItems)) $validDays[$day] = $dayItems;
         }
-
         if (empty($validDays)) return;
 
-        // 每天大约的分钟数
-        $avgDailyMinutes = floor($totalMinutes / count($validDays));
-        $remainder = $totalMinutes - $avgDailyMinutes * count($validDays);
-
-        foreach ($validDays as $day => $dayItems) {
-            $dailyMinutes = $avgDailyMinutes;
-            // 最后一天补齐余数
-            if ($day === array_key_last($validDays)) {
-                $dailyMinutes += $remainder;
-            }
-
-            // 随机给当天项目分配分钟数
-            $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;
-            }
-
-            // 确保最后一个项目的工时也是 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);
-                }
-            }
-        }
-    }
-
-    // ... (保留前面的代码不变)
-
-    /**
-     * 按员工生成当月工时单,严格保证总工时匹配(通过将分配分钟数取整到 60 的倍数实现)
-     */
-    protected function generateEmployeeMonth(int $employeeId, $items, array $workDays, $calendar)
-    {
-        $employeeMonth = DB::table('employee_details')
-            ->where('del_time',0)
-            ->where('employee_id', $employeeId)
-            ->where('time', $calendar->time)
-            ->first();
-
-        if (!$employeeMonth || $employeeMonth->total_hours_2 <= 0) {
-            return;
-        }
-
-        // 员工总分钟数:这里保留了浮点数取整可能带来的微小误差,但这是上游数据决定的
-        $totalMinutes = (int)round($employeeMonth->total_hours_2 * 60);
-
-        // --- 核心修复 1: 确保总分钟数是 60 的倍数 ---
-        // 这是为了让后续的所有分配都基于满小时进行
-        $totalMinutes = intval($totalMinutes / 60) * 60; // 调整为最接近的 60 倍数
-        // --- 核心修复 1 结束 ---
-
-        $dayMaxMinutes = 510 - 30; // 每天最大分钟数(扣午休)
+        // 2. 随机化每日总工时分布
+        $daysCount = count($validDays);
+        $avgDailyMinutes = intval(floor($totalMinutes / $daysCount) / 60) * 60;
+        $remainderMinutes = $totalMinutes - ($avgDailyMinutes * $daysCount);
 
-        // ... (省略:员工参与项目过滤逻辑)
-        $employeeItems = [];
-        foreach ($items as $item) {
-            $exists = DB::table('item_details')
-                ->where('del_time',0)
-                ->where('item_id', $item->id)
-                ->where('type', 1)
-                ->where('data_id', $employeeId)
-                ->exists();
-            if ($exists) $employeeItems[] = $item;
+        $dailyMinutesMap = [];
+        foreach (array_keys($validDays) as $day) {
+            $dailyMinutesMap[$day] = $avgDailyMinutes;
         }
 
-        if (empty($employeeItems)) return;
-
-        // ... (省略:有效工作日过滤逻辑)
-        $validDays = [];
-        foreach ($workDays as $day) {
-            $dayItems = [];
-            foreach ($employeeItems as $item) {
-                if ($day >= $item->start_time && $day <= $item->end_time) {
-                    $dayItems[] = $item;
+        if ($remainderMinutes > 0) {
+            $extraHours = $remainderMinutes / 60;
+            $dayKeys = array_keys($validDays);
+            shuffle($dayKeys);
+            for ($i = 0; $i < $extraHours; $i++) {
+                $targetDay = $dayKeys[$i % $daysCount];
+                if ($dailyMinutesMap[$targetDay] < 480) { // 最高8小时 (480min)
+                    $dailyMinutesMap[$targetDay] += 60;
+                } else {
+                    $extraHours++;
                 }
             }
-            if (!empty($dayItems)) {
-                $validDays[$day] = $dayItems;
-            }
         }
 
-        if (empty($validDays)) return;
-
-        // --- 核心修改 2: 确保每天大约的分钟数是 60 的倍数 ---
-        $avgDailyMinutes = floor($totalMinutes / count($validDays));
-        // 将平均数向下取整到 60 的倍数
-        $avgDailyMinutes = intval($avgDailyMinutes / 60) * 60;
-
-        // 重新计算余数:总分钟数 - (60 的倍数的平均分钟数 * 天数)
-        $remainder = $totalMinutes - $avgDailyMinutes * count($validDays);
-        // --- 核心修改 2 结束 ---
-
-
+        // 3. 遍历日期并生成明细
         foreach ($validDays as $day => $dayItems) {
-            $dailyMinutes = $avgDailyMinutes;
-            // 补齐余数的天数:将所有余数都累加到这几天
-            // 优化:将余数按天分配,每次分配 60 分钟,直到余数为 0。
-            if ($remainder > 0 && ($remainder >= 60 || $day === array_key_last($validDays))) {
-                // 如果是最后一天,或者余数大于等于 60,就分配 60 分钟(一个满小时)
-                $addMinutes = ($remainder >= 60) ? 60 : $remainder;
-                $dailyMinutes += $addMinutes;
-                $remainder -= $addMinutes;
-            }
-
-            // 检查 $dailyMinutes 是否为 60 的倍数,若不是,则向下取整(尽管按逻辑这应该不需要,以防万一)
-            $dailyMinutes = intval($dailyMinutes / 60) * 60;
-            if ($dailyMinutes === 0) continue; // 如果分配的工时为 0,则跳过当天
+            $dailyMinutes = $dailyMinutesMap[$day];
+            if ($dailyMinutes <= 0) continue;
 
-            // 随机给当天项目分配分钟数
             $slots = [];
             $remaining = $dailyMinutes;
             $numItems = count($dayItems);
 
             for ($i = 0; $i < $numItems; $i++) {
                 if ($i === $numItems - 1) {
-                    // 最后一个项目:分配所有剩余时间
                     $slotMinutes = $remaining;
                 } else {
-                    // 随机 ±20% 分配,确保分配的随机数也是 60 的倍数
-                    // 至少分配 60 分钟
-                    $minSlot = 60;
-                    // 最大分配:剩余时间的 80%,且是 60 的倍数
-                    $max = floor($remaining * 0.8);
-                    $maxSlot = intval($max / 60) * 60;
-
-                    if ($maxSlot < $minSlot) {
-                        // 如果连 60 分钟都分配不了,则将剩余时间留给最后一个项目
-                        $slotMinutes = 0;
-                    } else {
-                        // 在 [60, maxSlot] 之间以 60 分钟为步长随机选择
-                        $possibleSlots = range($minSlot, $maxSlot, 60);
-                        if (!empty($possibleSlots)) {
-                            $slotMinutes = $possibleSlots[array_rand($possibleSlots)];
-                        } else {
-                            $slotMinutes = 0;
-                        }
-                    }
+                    // 随机分配项目工时
+                    $maxSlot = intval(floor($remaining * 0.7) / 60) * 60;
+                    $slotMinutes = ($maxSlot >= 60) ? mt_rand(1, $maxSlot / 60) * 60 : 0;
                 }
-
                 $slots[] = $slotMinutes;
                 $remaining -= $slotMinutes;
             }
 
-            // 最后一个项目分配所有剩余时间,它现在应该是 60 的倍数
-            if ($remaining > 0) {
-                // 仅分配 60 的倍数
-                $add = intval($remaining / 60) * 60;
-                $slots[$numItems - 1] += $add;
-                // 注意:这里将剩余不足 60 分钟的部分舍弃了,以保证所有 $slots 都是 60 的倍数
-            }
-
-            // 确保最后一个项目的工时是 60 的倍数
-            $slots[$numItems - 1] = intval($slots[$numItems - 1] / 60) * 60;
-
-
-            // 创建工时单
             foreach ($dayItems as $idx => $item) {
-                // 仅对分配到的工时大于 0 的项目创建记录
-                if ($slots[$idx] > 0) {
-                    // $slots[$idx] 已经是 60 的倍数 (满小时)
+                if (isset($slots[$idx]) && $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)
     {
-        $dayStart = Carbon::createFromTimestamp($dayTime)->setTime(8, 0);
+        $dayStart   = Carbon::createFromTimestamp($dayTime)->setTime(8, 0);
         $lunchStart = Carbon::createFromTimestamp($dayTime)->setTime(11, 30);
         $lunchEnd   = Carbon::createFromTimestamp($dayTime)->setTime(12, 0);
-        $dayEnd = Carbon::createFromTimestamp($dayTime)->setTime(16, 30);
+        $dayEnd     = Carbon::createFromTimestamp($dayTime)->setTime(16, 30);
 
-        $morningMinutes = $lunchStart->diffInMinutes($dayStart);
-        $afternoonMinutes = $dayEnd->diffInMinutes($lunchEnd);
-        $availableMinutes = $morningMinutes + $afternoonMinutes;
-
-        // 保证总分钟数不超过可用时间
+        $availableMinutes = 480; // 8:00-16:30 扣除 30min 午休
         $minutes = min($minutes, $availableMinutes);
 
-        // 随机生成 start 时间,整5分钟
-        if ($minutes <= $morningMinutes) {
-            $maxOffset = $morningMinutes - $minutes;
-            // $dayStart (8:00) 已经是 5 的倍数
-            $offset = 5 * mt_rand(0, intval($maxOffset / 5));
-            $start = $dayStart->copy()->addMinutes($offset);
-        } elseif ($minutes <= $afternoonMinutes) {
-            $maxOffset = $afternoonMinutes - $minutes;
-            $offset = 5 * mt_rand(0, intval($maxOffset / 5));
-            $start = $lunchEnd->copy()->addMinutes($offset);
-        } else {
-            // 分两段:上午尽量满,剩余在下午
-            $start = $dayStart->copy();
-        }
+        // 计算最大可偏移量(单位:分钟),确保不会超出 16:30
+        $maxOffset = $availableMinutes - $minutes;
+        // 随机偏移:以 30 分钟为步长进行随机 (0, 30, 60...)
+        $randomOffset = (intval($maxOffset / 30) > 0) ? mt_rand(0, intval($maxOffset / 30)) * 30 : 0;
 
-        // 计算结束时间,跨午休自动跳过
+        $start = $dayStart->copy()->addMinutes($randomOffset);
+        $end = $start->copy();
         $remaining = $minutes;
-        if ($start->lt($lunchStart) && $remaining > $morningMinutes - $start->diffInMinutes($dayStart)) {
-            $usedMorning = $morningMinutes - $start->diffInMinutes($dayStart);
-            $remaining -= $usedMorning;
-            $end = $lunchEnd->copy()->addMinutes($remaining);
+
+        // 逻辑:如果开始时间在午休前
+        if ($start->lt($lunchStart)) {
+            $morningLeft = $lunchStart->diffInMinutes($start);
+            if ($remaining <= $morningLeft) {
+                $end->addMinutes($remaining);
+            } else {
+                // 跨午休逻辑
+                $remaining -= $morningLeft;
+                $end = $lunchEnd->copy()->addMinutes($remaining);
+            }
         } else {
-            $end = $start->copy()->addMinutes($remaining);
+            // 开始时间就在午休后
+            $end->addMinutes($remaining);
         }
 
-        // 调整到整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)
-            ->setTime(16, 30)
-            ->addSeconds(mt_rand(0, 30*60))
-            ->timestamp;
+        // 最终校验防止越界(虽然 offset 已做限制)
+        if ($end->gt($dayEnd)) {
+            $end = $dayEnd->copy();
+        }
 
-        $dayPrefix = date('Ymd', $dayTime);
-        $orderNumber = 'RD' . $dayPrefix . mt_rand(1000, 9999) . substr(uniqid(), -3);
+        $crtTime = Carbon::createFromTimestamp($dayTime)->setTime(16, 30)->addSeconds(mt_rand(0, 1800))->timestamp;
+        $orderNumber = 'RD' . date('Ymd', $dayTime) . mt_rand(1000, 9999) . substr(uniqid(), -3);
 
         $rdId = DB::table('rd')->insertGetId([
             'crt_time' => $crtTime,
@@ -405,10 +223,10 @@ 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
+            'type' => 1,
+            'del_time' => 0
         ]);
 
         DB::table('rd_details')->insert([
@@ -417,6 +235,7 @@ class RdGenerateService extends Service
             'type' => 1,
             'crt_time' => $crtTime,
             'upd_time' => $crtTime,
+            'del_time' => 0
         ]);
     }
 }

+ 8 - 1
app/Service/StatisticsService.php

@@ -278,6 +278,7 @@ class StatisticsService extends Service
             $rd_total_hours = $device_map[$value['id']] ?? 0;
             $rd_total_hours = bcdiv($rd_total_hours,60,2);
             $device[$key]['rd_total_hours'] = $rd_total_hours;
+            $device[$key]['set_total_hours'] = $rd_total_hours;
             //每个项目的工时
             $every_item_hours = $device_map_2[$value['id']] ?? [];
 
@@ -299,12 +300,18 @@ class StatisticsService extends Service
 
             $device[$key]['my_item'] = $my_item;
 
-            $device[$key]['position'] = "研发人员";
             $device[$key]['type_title'] = Device::$type[$value['type']] ?? "";
             $device[$key]['type_2_title'] = Device::$type_2[$value['type_2']] ?? "";
             $device[$key]['rate_one'] = "100%";
             $device[$key]['rate_two'] = "100%";
             $device[$key]['rate_three'] = "100%";
+
+            $rate_one = "100";
+            $device[$key]['rate_one'] = $rate_one . "%";
+            $rate_two = bcdiv($rd_total_hours, $rd_total_hours);
+            $rate_two = bcmul($rate_two, 100);
+            $device[$key]['rate_two'] = $rate_two . "%";
+            $device[$key]['rate_three'] = $rate_two . "%";
         }
 
         return $device;

+ 1 - 0
routes/api.php

@@ -119,6 +119,7 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('rdDetail', 'Api\RDController@rdDetail');
 
     $route->any('rdCreate', 'Api\RDController@rdCreate');
+    $route->any('rdCreate2', 'Api\RDController@rdCreate2');
 
     //统计
     $route->any('statisticsEmployee', 'Api\StatisticsController@statisticsEmployee');