RdGenerateService.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <?php
  2. namespace App\Service;
  3. use Carbon\Carbon;
  4. use Illuminate\Support\Facades\DB;
  5. class RdGenerateService extends Service
  6. {
  7. public function generate(array $data)
  8. {
  9. if (empty($data['month'])) {
  10. return [false, '请选择生成研发人员工时单年月'];
  11. }
  12. try {
  13. DB::transaction(function () use ($data) {
  14. $this->doGenerate($data['month']);
  15. });
  16. } catch (\Throwable $e) {
  17. return [false, $e->getMessage()];
  18. }
  19. return [true, ''];
  20. }
  21. protected function doGenerate(string $month)
  22. {
  23. $monthStart = Carbon::createFromFormat('Y-m', $month)->startOfMonth()->timestamp;
  24. $monthEnd = Carbon::createFromFormat('Y-m', $month)->endOfMonth()->timestamp;
  25. // 日历
  26. $calendar = DB::table('calendar')
  27. ->where('time', $monthStart)
  28. ->first();
  29. if (!$calendar) {
  30. throw new \Exception('当月日历未配置');
  31. }
  32. // 工作日
  33. $workDays = DB::table('calendar_details')
  34. ->where('calendar_id', $calendar->id)
  35. ->where('is_work', 1)
  36. ->orderBy('time')
  37. ->pluck('time')
  38. ->toArray();
  39. if (empty($workDays)) {
  40. throw new \Exception('当月无工作日');
  41. }
  42. // 启用项目
  43. $items = DB::table('item')
  44. ->where('is_use', 1)
  45. ->where('start_time', '<=', $monthEnd)
  46. ->where('end_time', '>=', $monthStart)
  47. ->get();
  48. // 删除当月所有项目的老数据
  49. $itemIds = $items->pluck('id')->toArray();
  50. if (!empty($itemIds)) {
  51. $rds = DB::table('rd')
  52. ->whereIn('item_id', $itemIds)
  53. ->whereBetween('order_time', [$monthStart, $monthEnd])
  54. ->pluck('id')
  55. ->toArray();
  56. if (!empty($rds)) {
  57. DB::table('rd_details')->whereIn('rd_id', $rds)->delete();
  58. DB::table('rd')->whereIn('id', $rds)->delete();
  59. }
  60. }
  61. // 获取所有员工id
  62. $employeeIds = DB::table('item_details')
  63. ->whereIn('item_id', $itemIds)
  64. ->where('type', 1)
  65. ->pluck('data_id')
  66. ->unique()
  67. ->toArray();
  68. // 按员工生成当月工时单
  69. foreach ($employeeIds as $employeeId) {
  70. $this->generateEmployeeMonth($employeeId, $items, $workDays, $calendar);
  71. }
  72. }
  73. /**
  74. * 按员工生成当月工时单,严格保证总工时匹配
  75. */
  76. protected function generateEmployeeMonth(int $employeeId, $items, array $workDays, $calendar)
  77. {
  78. $employeeMonth = DB::table('employee_details')
  79. ->where('employee_id', $employeeId)
  80. ->where('time', $calendar->time)
  81. ->first();
  82. if (!$employeeMonth || $employeeMonth->total_hours_2 <= 0) {
  83. return;
  84. }
  85. $totalMinutes = (int)round($employeeMonth->total_hours_2 * 60); // 员工总分钟数
  86. $dayMaxMinutes = 510; // 每天最大分钟数 (含午休)
  87. // 员工参与的项目
  88. $employeeItems = [];
  89. foreach ($items as $item) {
  90. $exists = DB::table('item_details')
  91. ->where('item_id', $item->id)
  92. ->where('type', 1)
  93. ->where('data_id', $employeeId)
  94. ->exists();
  95. if ($exists) {
  96. $employeeItems[] = $item;
  97. }
  98. }
  99. if (empty($employeeItems)) return;
  100. // 生成所有槽位:项目 × 工作日(有效周期内)
  101. $slots = [];
  102. foreach ($employeeItems as $item) {
  103. foreach ($workDays as $day) {
  104. if ($day >= $item->start_time && $day <= $item->end_time) {
  105. $slots[] = [
  106. 'item_id' => $item->id,
  107. 'day' => $day
  108. ];
  109. }
  110. }
  111. }
  112. if (empty($slots)) return;
  113. // 均分总分钟数
  114. $numSlots = count($slots);
  115. $baseMinutes = floor($totalMinutes / $numSlots);
  116. $remainder = $totalMinutes - $baseMinutes * $numSlots;
  117. foreach ($slots as $i => $slot) {
  118. $minutes = $baseMinutes;
  119. if ($i === $numSlots - 1) {
  120. // 最后 slot 补齐余数
  121. $minutes += $remainder;
  122. }
  123. // 确保单日不超过最大分钟数
  124. $minutes = min($minutes, $dayMaxMinutes - 30); // 扣掉午休
  125. $this->createRd($slot['item_id'], $slot['day'], $minutes, $employeeId);
  126. }
  127. }
  128. /**
  129. * 创建单条研发工时单
  130. */
  131. protected function createRd(int $itemId, int $dayTime, int $minutes, int $employeeId)
  132. {
  133. $dayStart = Carbon::createFromTimestamp($dayTime)->setTime(8, 0);
  134. $lunchStart = Carbon::createFromTimestamp($dayTime)->setTime(11, 30);
  135. $lunchEnd = Carbon::createFromTimestamp($dayTime)->setTime(12, 0);
  136. $dayEnd = Carbon::createFromTimestamp($dayTime)->setTime(16, 30);
  137. // 上午可用分钟
  138. $morningMinutes = $lunchStart->diffInMinutes($dayStart);
  139. // 下午可用分钟
  140. $afternoonMinutes = $dayEnd->diffInMinutes($lunchEnd);
  141. // 确保总工时不会超过全天可用分钟
  142. $minutes = min($minutes, $morningMinutes + $afternoonMinutes);
  143. // 决定工时分配到上午和下午
  144. if ($minutes <= $morningMinutes) {
  145. $start = $dayStart->copy()->addMinutes(mt_rand(0, $morningMinutes - $minutes));
  146. $end = $start->copy()->addMinutes($minutes);
  147. } elseif ($minutes <= $afternoonMinutes) {
  148. $start = $lunchEnd->copy()->addMinutes(mt_rand(0, $afternoonMinutes - $minutes));
  149. $end = $start->copy()->addMinutes($minutes);
  150. } else {
  151. // 分两段:上午尽量满,剩余在下午
  152. $start = $dayStart->copy();
  153. $end = $lunchEnd->copy()->addMinutes($minutes - $morningMinutes);
  154. }
  155. // 调整到整5分钟
  156. $start->minute = ceil($start->minute / 5) * 5;
  157. $end->minute = ceil($end->minute / 5) * 5;
  158. // crt_time 16:30 ~ 17:00
  159. $crtTime = Carbon::createFromTimestamp($dayTime)
  160. ->setTime(16, 30)
  161. ->addSeconds(mt_rand(0, 30*60))
  162. ->timestamp;
  163. $dayPrefix = date('Ymd', $dayTime);
  164. $orderNumber = 'RD' . $dayPrefix . mt_rand(1000, 9999) . substr(uniqid(), -3);
  165. $rdId = DB::table('rd')->insertGetId([
  166. 'crt_time' => $crtTime,
  167. 'order_time' => $dayTime,
  168. 'item_id' => $itemId,
  169. 'start_time_hour' => $start->hour,
  170. 'start_time_min' => $start->minute,
  171. 'end_time_hour' => $end->hour,
  172. 'end_time_min' => $end->minute,
  173. 'total_hours' => $minutes,
  174. 'order_number' => $orderNumber,
  175. 'type' => 1
  176. ]);
  177. DB::table('rd_details')->insert([
  178. 'rd_id' => $rdId,
  179. 'data_id' => $employeeId,
  180. 'type' => 1,
  181. 'crt_time' => $crtTime,
  182. 'upd_time' => $crtTime,
  183. ]);
  184. }
  185. }