|
@@ -4,6 +4,7 @@ namespace App\Services\Coach;
|
|
|
|
|
|
use App\Enums\TechnicianAuthStatus;
|
|
|
use App\Enums\TechnicianLocationType;
|
|
|
+use App\Models\CoachSchedule;
|
|
|
use App\Models\MemberUser;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
@@ -409,4 +410,180 @@ class AccountService
|
|
|
throw $e;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置技师排班时间(每天通用)
|
|
|
+ *
|
|
|
+ * @param int $userId 技师用户ID
|
|
|
+ * @param array $timeRanges 时间段数组 格式: [
|
|
|
+ * ['start_time' => '09:00', 'end_time' => '12:00'],
|
|
|
+ * ['start_time' => '14:00', 'end_time' => '18:00']
|
|
|
+ * ]
|
|
|
+ *
|
|
|
+ * @throws \Exception
|
|
|
+ */
|
|
|
+ public function setSchedule(int $userId, array $timeRanges): array
|
|
|
+ {
|
|
|
+ return DB::transaction(function () use ($userId, $timeRanges) {
|
|
|
+ try {
|
|
|
+ // 获取技师信息
|
|
|
+ $user = MemberUser::with(['coach'])->findOrFail($userId);
|
|
|
+ $coach = $user->coach;
|
|
|
+ abort_if(! $coach, 404, '技师信息不存在');
|
|
|
+
|
|
|
+ // 验证并排序时间段
|
|
|
+ $sortedRanges = $this->validateAndSortTimeRanges($timeRanges);
|
|
|
+
|
|
|
+ // 创建或更新排班记录
|
|
|
+ $schedule = CoachSchedule::updateOrCreate(
|
|
|
+ [
|
|
|
+ 'coach_id' => $coach->id,
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ 'time_ranges' => json_encode($sortedRanges),
|
|
|
+ 'state' => 1,
|
|
|
+ ]
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新Redis缓存
|
|
|
+ $this->updateScheduleCache($coach->id, $sortedRanges);
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ Log::info('技师排班设置成功', [
|
|
|
+ 'coach_id' => $coach->id,
|
|
|
+ 'time_ranges' => $sortedRanges,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'status' => true,
|
|
|
+ 'message' => '排班设置成功',
|
|
|
+ 'data' => [
|
|
|
+ 'coach_id' => $coach->id,
|
|
|
+ 'time_ranges' => $sortedRanges,
|
|
|
+ 'updated_at' => $schedule->updated_at->toDateTimeString(),
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('技师排班设置失败', [
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'time_ranges' => $timeRanges,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString(),
|
|
|
+ ]);
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证并排序时间段
|
|
|
+ */
|
|
|
+ private function validateAndSortTimeRanges(array $timeRanges): array
|
|
|
+ {
|
|
|
+ // 验证时间段数组
|
|
|
+ abort_if(empty($timeRanges), 400, '必须至少设置一个时间段');
|
|
|
+
|
|
|
+ // 验证每个时间段格式并转换为分钟数进行比较
|
|
|
+ $ranges = collect($timeRanges)->map(function ($range) {
|
|
|
+ abort_if(! isset($range['start_time'], $range['end_time']),
|
|
|
+ 400, '时间段格式错误');
|
|
|
+
|
|
|
+ // 验证时间格式
|
|
|
+ foreach (['start_time', 'end_time'] as $field) {
|
|
|
+ abort_if(! preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $range[$field]),
|
|
|
+ 400, '时间格式错误,应为HH:mm格式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为分钟数便于比较
|
|
|
+ $startMinutes = $this->timeToMinutes($range['start_time']);
|
|
|
+ $endMinutes = $this->timeToMinutes($range['end_time']);
|
|
|
+
|
|
|
+ // 验证时间先后
|
|
|
+ abort_if($startMinutes >= $endMinutes,
|
|
|
+ 400, "时间段 {$range['start_time']}-{$range['end_time']} 结束时间必须大于开始时间");
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'start_time' => $range['start_time'],
|
|
|
+ 'end_time' => $range['end_time'],
|
|
|
+ 'start_minutes' => $startMinutes,
|
|
|
+ 'end_minutes' => $endMinutes,
|
|
|
+ ];
|
|
|
+ })
|
|
|
+ ->sortBy('start_minutes')
|
|
|
+ ->values();
|
|
|
+
|
|
|
+ // 验证时间段是否重叠
|
|
|
+ $ranges->each(function ($range, $index) use ($ranges) {
|
|
|
+ if ($index > 0) {
|
|
|
+ $prevRange = $ranges[$index - 1];
|
|
|
+ abort_if($range['start_minutes'] <= $prevRange['end_minutes'],
|
|
|
+ 400, "时间段 {$prevRange['start_time']}-{$prevRange['end_time']} 和 ".
|
|
|
+ "{$range['start_time']}-{$range['end_time']} 之间存在重叠");
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 返回排序后的时间段,只保留需要的字段
|
|
|
+ return $ranges->map(function ($range) {
|
|
|
+ return [
|
|
|
+ 'start_time' => $range['start_time'],
|
|
|
+ 'end_time' => $range['end_time'],
|
|
|
+ ];
|
|
|
+ })->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将时间转换为分钟数
|
|
|
+ */
|
|
|
+ private function timeToMinutes(string $time): int
|
|
|
+ {
|
|
|
+ [$hours, $minutes] = explode(':', $time);
|
|
|
+
|
|
|
+ return (int) $hours * 60 + (int) $minutes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新Redis缓存
|
|
|
+ */
|
|
|
+ private function updateScheduleCache(int $coachId, array $timeRanges): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $cacheKey = "coach:schedule:{$coachId}";
|
|
|
+ $cacheData = [
|
|
|
+ 'updated_at' => now()->toDateTimeString(),
|
|
|
+ 'time_ranges' => $timeRanges,
|
|
|
+ ];
|
|
|
+
|
|
|
+ Redis::setex($cacheKey, 86400, json_encode($cacheData));
|
|
|
+
|
|
|
+ // 清除相关的可预约时间段缓存
|
|
|
+ $this->clearTimeSlotCache($coachId);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('更新排班缓存失败', [
|
|
|
+ 'coach_id' => $coachId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ // 缓存更新失败不影响主流程
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清除可预约时间段缓存
|
|
|
+ */
|
|
|
+ private function clearTimeSlotCache(int $coachId): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $pattern = "coach:timeslots:{$coachId}:*";
|
|
|
+ $keys = Redis::keys($pattern);
|
|
|
+ if (! empty($keys)) {
|
|
|
+ Redis::del($keys);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('清除时间段缓存失败', [
|
|
|
+ 'coach_id' => $coachId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|