ValidatesServiceTime.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <?php
  2. namespace App\Services\Client\Traits;
  3. use App\Enums\OrderStatus;
  4. use App\Enums\TechnicianAuthStatus;
  5. use App\Enums\TechnicianStatus;
  6. use App\Models\CoachSchedule;
  7. use App\Models\CoachUser;
  8. use App\Models\Order;
  9. use Carbon\Carbon;
  10. /**
  11. * 服务时间验证 Trait
  12. *
  13. * 业务规则:
  14. * 1. 技师状态验证:
  15. * - 技师必须存在且状态为激活
  16. * - 技师必须通过认证
  17. *
  18. * 2. 服务时间验证:
  19. * - 服务时间不能早于当前时间
  20. * - 最多只能提前7天预约
  21. * - 必须在技师的工作时间内
  22. * - 不能与其他订单时间冲突
  23. *
  24. * 3. 工作时间规则:
  25. * - 默认工作时间为 09:00-21:00
  26. * - 可以通过排班设置自定义工作时间
  27. * - 支持设置特殊休息日
  28. *
  29. * 4. 订单冲突规则:
  30. * - 检查已支付、已接单、服务中的订单
  31. * - 考虑服务时长(默认2小时)
  32. * - 开始时间不能在其他订单的服务时间段内
  33. * - 结束时间不能在其他订单的服务时间段内
  34. */
  35. trait ValidatesServiceTime
  36. {
  37. /**
  38. * 验证服务时间参数
  39. *
  40. * @param int $coachId 技师ID
  41. * @param string $serviceTime 服务时间(格式:Y-m-d H:i:s)
  42. *
  43. * @throws \Exception 验证失败时抛出异常
  44. */
  45. private function validateServiceTimeParams(int $coachId, string $serviceTime): void
  46. {
  47. // 验证技师状态
  48. $coach = CoachUser::query()
  49. ->with(['info'])
  50. ->where('id', $coachId)
  51. ->where('state', TechnicianStatus::ACTIVE->value)
  52. ->first();
  53. abort_if(! $coach, 400, '技师不存在或未激活');
  54. // 验证技师认证状态
  55. abort_if(! $coach->info || $coach->info->state !== TechnicianAuthStatus::PASSED->value,
  56. 400, '技师未通过认证');
  57. // 验证服务时间基本参数
  58. $this->validateBasicServiceTime($serviceTime);
  59. // 验证技师工作时间
  60. $workSchedule = $this->getCoachWorkSchedule($coachId);
  61. $this->validateWorkingHours($serviceTime, $workSchedule);
  62. // 验证时间冲突
  63. $this->checkTimeConflicts($coachId, $serviceTime);
  64. }
  65. /**
  66. * 验证服务时间基本参数
  67. *
  68. * 业务规则:
  69. * 1. 服务时间不能早于当前时间
  70. * 2. 最多只能提前7天预约
  71. *
  72. * @param string $serviceTime 服务时间
  73. *
  74. * @throws \Exception 验证失败时抛出异常
  75. */
  76. private function validateBasicServiceTime(string $serviceTime): void
  77. {
  78. $serviceDateTime = Carbon::parse($serviceTime);
  79. // 验证是否过期
  80. abort_if(
  81. $serviceDateTime->isPast(),
  82. 400,
  83. '服务时间不能早于当前时间'
  84. );
  85. // 验证预约时间范围(最多提前7天)
  86. $maxAdvanceDays = config('business.max_advance_days', 7);
  87. abort_if(
  88. $serviceDateTime->diffInDays(now()) > $maxAdvanceDays,
  89. 400,
  90. "最多只能提前{$maxAdvanceDays}天预约"
  91. );
  92. }
  93. /**
  94. * 获取技师工作时间安排
  95. *
  96. * 业务规则:
  97. * 1. 优先使用技师的自定义排班
  98. * 2. 如果没有排班,使用默认时间(09:00-21:00)
  99. * 3. 默认每天都工作
  100. *
  101. * @param int $coachId 技师ID
  102. * @return array 工作时间安排
  103. */
  104. private function getCoachWorkSchedule(int $coachId): array
  105. {
  106. $schedule = CoachSchedule::where('coach_id', $coachId)
  107. ->where('state', 1)
  108. ->first();
  109. if (! $schedule || empty($schedule->time_ranges)) {
  110. return [
  111. 'work_days' => range(1, 7),
  112. 'work_hours' => [
  113. 'start' => '09:00',
  114. 'end' => '21:00',
  115. ],
  116. 'rest_dates' => [],
  117. ];
  118. }
  119. $timeRanges = json_decode($schedule->time_ranges, true);
  120. return [
  121. 'work_days' => range(1, 7), // 默认每天都工作
  122. 'work_hours' => [
  123. 'start' => $timeRanges[0]['start_time'],
  124. 'end' => end($timeRanges)['end_time'],
  125. ],
  126. 'rest_dates' => [],
  127. ];
  128. }
  129. /**
  130. * 验证工作时间
  131. *
  132. * 业务规则:
  133. * 1. 检查是否为工作日
  134. * 2. 检查是否为特殊休息日
  135. * 3. 检查是否在工作时间范围内
  136. *
  137. * @param string $serviceTime 服务时间
  138. * @param array $workSchedule 工作时间安排
  139. *
  140. * @throws \Exception 验证失败时抛出异常
  141. */
  142. private function validateWorkingHours(string $serviceTime, array $workSchedule): void
  143. {
  144. $serviceDateTime = Carbon::parse($serviceTime);
  145. // 检查工作日
  146. $dayOfWeek = $serviceDateTime->dayOfWeek;
  147. $workDays = $workSchedule['work_days'] ?? range(1, 7); // 如果未设置,默认每天都工作
  148. abort_if(
  149. ! in_array($dayOfWeek, $workDays),
  150. 400,
  151. '该时间不在技师工作日内'
  152. );
  153. // 检查特殊休息日
  154. $dateStr = $serviceDateTime->format('Y-m-d');
  155. $restDates = $workSchedule['rest_dates'] ?? []; // 如果未设置,默认没有休息日
  156. abort_if(
  157. in_array($dateStr, $restDates),
  158. 400,
  159. '技师该日期休息'
  160. );
  161. // 检查工作时间
  162. $timeStr = $serviceDateTime->format('H:i');
  163. $workHours = $workSchedule['work_hours'] ?? [
  164. 'start' => '09:00',
  165. 'end' => '21:00',
  166. ]; // 如果未设置,使用默认工作时间
  167. $startTime = Carbon::parse($workHours['start']);
  168. $endTime = Carbon::parse($workHours['end']);
  169. abort_if(
  170. $timeStr < $startTime->format('H:i') || $timeStr > $endTime->format('H:i'),
  171. 400,
  172. sprintf(
  173. '服务时间需在%s-%s之间',
  174. $startTime->format('H:i'),
  175. $endTime->format('H:i')
  176. )
  177. );
  178. }
  179. /**
  180. * 检查时间冲突
  181. *
  182. * 业务规则:
  183. * 1. 检查已支付、已接单、服务中的订单
  184. * 2. 考虑服务时长(默认2小时)
  185. * 3. 新订单的服务时间段不能与其他订单重叠
  186. *
  187. * @param int $coachId 技师ID
  188. * @param string $serviceTime 服务时间
  189. *
  190. * @throws \Exception 验证失败时抛出异常
  191. */
  192. private function checkTimeConflicts(int $coachId, string $serviceTime): void
  193. {
  194. $serviceDateTime = Carbon::parse($serviceTime);
  195. // 获取服务时长配置(默认2小时)
  196. $serviceDuration = config('business.default_service_duration', 120);
  197. // 计算本次服务的结束时间
  198. $serviceEndTime = $serviceDateTime->copy()->addMinutes($serviceDuration);
  199. // 检查是否与其他订单时间冲突
  200. $conflictingOrder = Order::query()
  201. ->where('coach_id', $coachId)
  202. ->where('service_time', '<=', $serviceEndTime)
  203. ->where('service_end_time', '>=', $serviceDateTime)
  204. ->whereIn('status', [
  205. OrderStatus::PAID->value,
  206. OrderStatus::ACCEPTED->value,
  207. OrderStatus::SERVING->value,
  208. ])
  209. ->first();
  210. abort_if($conflictingOrder, 400, '该时间段已被预约');
  211. }
  212. }