ValidatesServiceTime.php 7.4 KB

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