ValidatesServiceTime.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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. use Illuminate\Support\Facades\Log;
  11. /**
  12. * 服务时间验证 Trait
  13. *
  14. * 业务规则:
  15. * 1. 技师状态验证:
  16. * - 技师必须存在且状态为激活
  17. * - 技师必须通过认证
  18. *
  19. * 2. 服务时间验证:
  20. * - 服务时间不能早于当前时间
  21. * - 最多只能提前7天预约
  22. * - 必须在技师的工作时间内
  23. * - 不能与其他订单时间冲突
  24. *
  25. * 3. 工作时间规则:
  26. * - 默认工作时间为 09:00-21:00
  27. * - 可以通过排班设置自定义工作时间
  28. * - 支持设置特殊休息日
  29. *
  30. * 4. 订单冲突规则:
  31. * - 检查已支付、已接单、服务中的订单
  32. * - 考虑服务时长(默认2小时)
  33. * - 开始时间不能在其他订单的服务时间段内
  34. * - 结束时间不能在其他订单的服务时间段内
  35. */
  36. trait ValidatesServiceTime
  37. {
  38. /**
  39. * 验证服务时间
  40. *
  41. * @param int $coachId 技师ID
  42. * @param string $serviceTime 服务时间(格式:Y-m-d H:i:s)
  43. * @param int $duration 服务时长(分钟)
  44. * @return bool
  45. *
  46. * @throws \Exception 验证失败时抛出异常
  47. */
  48. public function validateServiceTime(int $coachId, string $serviceTime, int $duration): bool
  49. {
  50. try {
  51. Log::info('开始验证服务时间', [
  52. 'coach_id' => $coachId,
  53. 'service_time' => $serviceTime,
  54. 'duration' => $duration
  55. ]);
  56. // 验证基本参数
  57. Log::info('开始验证基本参数');
  58. $this->validateServiceTimeParams($coachId, $serviceTime, $duration);
  59. Log::info('基本参数验证通过');
  60. Log::info('服务时间验证通过');
  61. return true;
  62. } catch (\Exception $e) {
  63. Log::error('验证服务时间失败', [
  64. 'coach_id' => $coachId,
  65. 'service_time' => $serviceTime,
  66. 'duration' => $duration,
  67. 'error_message' => $e->getMessage(),
  68. 'error_code' => $e->getCode(),
  69. 'error_file' => $e->getFile(),
  70. 'error_line' => $e->getLine(),
  71. 'error_trace' => $e->getTraceAsString()
  72. ]);
  73. throw $e;
  74. }
  75. }
  76. /**
  77. * 验证服务时间参数
  78. *
  79. * @param int $coachId 技师ID
  80. * @param string $serviceTime 服务时间(格式:Y-m-d H:i:s)
  81. * @param int $duration 服务时长(分钟)
  82. *
  83. * @throws \Exception 验证失败时抛出异常
  84. */
  85. private function validateServiceTimeParams(int $coachId, string $serviceTime, int $duration): void
  86. {
  87. Log::info('开始验证技师状态', ['coach_id' => $coachId]);
  88. // 验证技师状态
  89. $coach = CoachUser::query()
  90. ->with(['info'])
  91. ->where('id', $coachId)
  92. ->where('state', TechnicianStatus::ACTIVE->value)
  93. ->first();
  94. abort_if(! $coach, 400, '技师不存在或未激活');
  95. Log::info('技师状态验证通过', ['coach' => $coach->toArray()]);
  96. // 验证技师认证状态
  97. Log::info('开始验证技师认证状态');
  98. abort_if(
  99. ! $coach->info || $coach->info->state !== TechnicianAuthStatus::PASSED->value,
  100. 400,
  101. '技师未通过认证'
  102. );
  103. Log::info('技师认证状态验证通过');
  104. // 验证服务时间基本参数
  105. Log::info('开始验证服务时间基本参数', ['service_time' => $serviceTime]);
  106. $this->validateBasicServiceTime($serviceTime);
  107. Log::info('服务时间基本参数验证通过');
  108. // 验证技师工作时间
  109. Log::info('开始验证技师工作时间');
  110. $workSchedule = $this->getCoachWorkSchedule($coachId);
  111. Log::info('获取到技师排班信息', ['work_schedule' => $workSchedule]);
  112. $serviceDateTime = Carbon::parse($serviceTime);
  113. $serviceEndTime = $serviceDateTime->copy()->addMinutes($duration);
  114. Log::info('计算服务时间', [
  115. 'service_start' => $serviceDateTime->format('Y-m-d H:i:s'),
  116. 'service_end' => $serviceEndTime->format('Y-m-d H:i:s'),
  117. 'duration' => $duration
  118. ]);
  119. $this->validateWorkingHours($serviceDateTime, $serviceEndTime, $workSchedule);
  120. Log::info('技师工作时间验证通过');
  121. // 验证时间冲突
  122. Log::info('开始验证时间冲突');
  123. $this->checkTimeConflicts($coachId, $serviceDateTime, $serviceEndTime);
  124. Log::info('时间冲突验证通过');
  125. }
  126. /**
  127. * 验证服务时间基本参数
  128. *
  129. * 业务规则:
  130. * 1. 服务时间不能早于当前时间
  131. * 2. 最多只能提前7天预约
  132. *
  133. * @param string $serviceTime 服务时间
  134. *
  135. * @throws \Exception 验证失败时抛出异常
  136. */
  137. private function validateBasicServiceTime(string $serviceTime): void
  138. {
  139. $serviceDateTime = Carbon::parse($serviceTime);
  140. // 验证是否过期
  141. abort_if(
  142. $serviceDateTime->isPast(),
  143. 400,
  144. '服务时间不能早于当前时间'
  145. );
  146. // 验证预约时间范围(最多提前7天)
  147. $maxAdvanceDays = config('business.max_advance_days', 7);
  148. abort_if(
  149. $serviceDateTime->diffInDays(now()) > $maxAdvanceDays,
  150. 400,
  151. "最多只能提前{$maxAdvanceDays}天预约"
  152. );
  153. }
  154. /**
  155. * 获取技师工作时间安排
  156. *
  157. * 业务规则:
  158. * 1. 优先使用技师的自定义排班
  159. * 2. 如果没有排班,使用默认时间(09:00-21:00)
  160. * 3. 默认每天都工作
  161. *
  162. * @param int $coachId 技师ID
  163. * @return array 工作时间安排
  164. */
  165. private function getCoachWorkSchedule(int $coachId): array
  166. {
  167. Log::info('开始获取技师排班信息', ['coach_id' => $coachId]);
  168. $schedule = CoachSchedule::where('coach_id', $coachId)
  169. ->where('state', 1)
  170. ->first();
  171. Log::info('查询技师排班记录', [
  172. 'coach_id' => $coachId,
  173. 'has_schedule' => !is_null($schedule),
  174. 'schedule' => $schedule ? $schedule->toArray() : null
  175. ]);
  176. if (! $schedule) {
  177. $defaultSchedule = [
  178. 'work_days' => range(1, 7),
  179. 'work_hours' => [
  180. [
  181. 'start' => '09:00',
  182. 'end' => '21:00',
  183. ]
  184. ],
  185. 'rest_dates' => [],
  186. ];
  187. Log::info('未找到排班记录,使用默认排班', ['default_schedule' => $defaultSchedule]);
  188. return $defaultSchedule;
  189. }
  190. $timeRanges = is_string($schedule->time_ranges)
  191. ? json_decode($schedule->time_ranges, true)
  192. : $schedule->time_ranges;
  193. Log::info('解析排班时间范围', [
  194. 'raw_time_ranges' => $schedule->time_ranges,
  195. 'parsed_time_ranges' => $timeRanges
  196. ]);
  197. // 从排班表中获取工作日
  198. $workDays = [];
  199. $workHours = [];
  200. foreach ($timeRanges as $index => $range) {
  201. Log::info('处理时间范围', [
  202. 'index' => $index,
  203. 'range' => $range
  204. ]);
  205. $workHours[] = [
  206. 'start' => $range['start_time'],
  207. 'end' => $range['end_time'],
  208. ];
  209. Log::info('更新工作时间', [
  210. 'current_work_hours' => $workHours
  211. ]);
  212. }
  213. $workDays = array_unique($workDays);
  214. Log::info('处理工作日', [
  215. 'initial_work_days' => $workDays
  216. ]);
  217. // 如果没有设置工作日,则默认每天都工作
  218. if (empty($workDays)) {
  219. $workDays = range(1, 7);
  220. Log::info('未设置工作日,使用默认每天工作', [
  221. 'default_work_days' => $workDays
  222. ]);
  223. }
  224. $result = [
  225. 'work_days' => $workDays,
  226. 'work_hours' => $workHours,
  227. ];
  228. Log::info('获取技师排班信息完成', [
  229. 'final_schedule' => $result
  230. ]);
  231. return $result;
  232. }
  233. /**
  234. * 验证是否在工作时间内
  235. */
  236. private function validateWorkingHours(Carbon $serviceDateTime, Carbon $serviceEndTime, array $workSchedule): void
  237. {
  238. $isInSchedule = false;
  239. foreach ($workSchedule['work_hours'] as $range) {
  240. $rangeStart = Carbon::parse($serviceDateTime->format('Y-m-d') . ' ' . $range['start']);
  241. $rangeEnd = Carbon::parse($serviceDateTime->format('Y-m-d') . ' ' . $range['end']);
  242. if (
  243. $serviceDateTime->between($rangeStart, $rangeEnd)
  244. ) {
  245. $isInSchedule = true;
  246. break;
  247. }
  248. }
  249. abort_if(!$isInSchedule, 400, '所选时间不在技师服务时间范围内');
  250. }
  251. /**
  252. * 检查订单时间冲突
  253. */
  254. private function checkTimeConflicts(int $coachId, Carbon $serviceDateTime, Carbon $serviceEndTime): void
  255. {
  256. // 检查是否与其他订单时间重叠
  257. $conflictingOrders = Order::where('coach_id', $coachId)
  258. ->where(function ($query) use ($serviceDateTime, $serviceEndTime) {
  259. $query->where(function ($q) use ($serviceDateTime, $serviceEndTime) {
  260. $q->where('service_start_time', '<=', $serviceEndTime->format('Y-m-d H:i:s'))
  261. ->where('service_end_time', '>=', $serviceDateTime->format('Y-m-d H:i:s'));
  262. });
  263. })
  264. ->whereIn('state', [
  265. OrderStatus::ACCEPTED->value,
  266. OrderStatus::DEPARTED->value,
  267. OrderStatus::ARRIVED->value,
  268. OrderStatus::SERVICING->value,
  269. ])
  270. ->exists();
  271. abort_if($conflictingOrders, 400, '该时间段技师已有其他预约');
  272. }
  273. }