Browse Source

feat:技师端-设置技师排班

刘学玺 4 months ago
parent
commit
2d8e7b6264

+ 58 - 0
app/Admin/Controllers/CoachScheduleController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Admin\Controllers;
+
+use App\Services\CoachScheduleService;
+use Slowlyo\OwlAdmin\Controllers\AdminController;
+
+/**
+ * 技师排班
+ *
+ * @property CoachScheduleService $service
+ */
+class CoachScheduleController extends AdminController
+{
+	protected string $serviceName = CoachScheduleService::class;
+
+	public function list()
+	{
+		$crud = $this->baseCRUD()
+			->filterTogglable(false)
+			->headerToolbar([
+				$this->createButton('dialog'),
+				...$this->baseHeaderToolBar()
+			])
+			->columns([
+				amis()->TableColumn('id', 'ID')->sortable(),
+				amis()->TableColumn('coach_id', '技师ID'),
+				amis()->TableColumn('time_ranges', '时间段JSON'),
+				amis()->TableColumn('state', '状态:1-启用 2-禁用'),
+				amis()->TableColumn('created_at', admin_trans('admin.created_at'))->type('datetime')->sortable(),
+				amis()->TableColumn('updated_at', admin_trans('admin.updated_at'))->type('datetime')->sortable(),
+				$this->rowActions('dialog')
+			]);
+
+		return $this->baseList($crud);
+	}
+
+	public function form($isEdit = false)
+	{
+		return $this->baseForm()->body([
+			amis()->TextControl('coach_id', '技师ID'),
+			amis()->TextControl('time_ranges', '时间段JSON'),
+			amis()->TextControl('state', '状态:1-启用 2-禁用'),
+		]);
+	}
+
+	public function detail()
+	{
+		return $this->baseDetail()->body([
+			amis()->TextControl('id', 'ID')->static(),
+			amis()->TextControl('coach_id', '技师ID')->static(),
+			amis()->TextControl('time_ranges', '时间段JSON')->static(),
+			amis()->TextControl('state', '状态:1-启用 2-禁用')->static(),
+			amis()->TextControl('created_at', admin_trans('admin.created_at'))->static(),
+			amis()->TextControl('updated_at', admin_trans('admin.updated_at'))->static(),
+		]);
+	}
+}

+ 70 - 0
app/Http/Controllers/Coach/AccountController.php

@@ -207,4 +207,74 @@ class AccountController extends Controller
 
         return $this->success($result);
     }
+
+    /**
+     * [账户]设置排班时间
+     *
+     * @description 设置技师每天通用的排班时间段
+     *
+     * @authenticated
+     *
+     * @bodyParam time_ranges array required 时间段数组
+     * @bodyParam time_ranges[].start_time string required 开始时间(HH:mm格式) Example: "09:00"
+     * @bodyParam time_ranges[].end_time string required 结束时间(HH:mm格式) Example: "12:00"
+     *
+     * @response {
+     *   "status": true,
+     *   "message": "排班设置成功",
+     *   "data": {
+     *     "coach_id": 1,
+     *     "time_ranges": [
+     *       {
+     *         "start_time": "09:00",
+     *         "end_time": "12:00"
+     *       },
+     *       {
+     *         "start_time": "14:00",
+     *         "end_time": "18:00"
+     *       }
+     *     ]
+     *   }
+     * }
+     * @response 400 {
+     *   "message": "时间段格式错误"
+     * }
+     * @response 400 {
+     *   "message": "时间格式错误,应为HH:mm格式"
+     * }
+     * @response 400 {
+     *   "message": "结束时间必须大于开始时间"
+     * }
+     * @response 400 {
+     *   "message": "时间段之间不能重叠"
+     * }
+     */
+    public function setSchedule(Request $request)
+    {
+        $validated = $request->validate([
+            'time_ranges' => 'required|array|min:1',
+            'time_ranges.*.start_time' => [
+                'required',
+                'string',
+                'regex:/^([01][0-9]|2[0-3]):[0-5][0-9]$/',
+            ],
+            'time_ranges.*.end_time' => [
+                'required',
+                'string',
+                'regex:/^([01][0-9]|2[0-3]):[0-5][0-9]$/',
+            ],
+        ], [
+            'time_ranges.required' => '必须设置时间段',
+            'time_ranges.array' => '时间段必须是数组格式',
+            'time_ranges.min' => '至少设置一个时间段',
+            'time_ranges.*.start_time.required' => '开始时间不能为空',
+            'time_ranges.*.start_time.regex' => '开始时间格式错误,应为HH:mm格式',
+            'time_ranges.*.end_time.required' => '结束时间不能为空',
+            'time_ranges.*.end_time.regex' => '结束时间格式错误,应为HH:mm格式',
+        ]);
+
+        return $this->success(
+            $this->service->setSchedule(Auth::user()->id, $validated['time_ranges'])
+        );
+    }
 }

+ 18 - 0
app/Models/CoachSchedule.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Slowlyo\OwlAdmin\Models\BaseModel as Model;
+
+/**
+ * 技师排班
+ */
+class CoachSchedule extends Model
+{
+    use SoftDeletes;
+
+    protected $table = 'coach_schedules';
+
+    protected $guarded = [];
+}

+ 177 - 0
app/Services/Coach/AccountService.php

@@ -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(),
+            ]);
+        }
+    }
 }

+ 17 - 0
app/Services/CoachScheduleService.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\CoachSchedule;
+use Slowlyo\OwlAdmin\Services\AdminService;
+
+/**
+ * 技师排班
+ *
+ * @method CoachSchedule getModel()
+ * @method CoachSchedule|\Illuminate\Database\Query\Builder query()
+ */
+class CoachScheduleService extends AdminService
+{
+	protected string $modelName = CoachSchedule::class;
+}

+ 7 - 5
routes/admin.php

@@ -8,13 +8,13 @@
 
 // =====================================================================
 
+use Slowlyo\OwlAdmin\Admin;
 use Illuminate\Routing\Router;
 use Illuminate\Support\Facades\Route;
-use Slowlyo\OwlAdmin\Admin;
 
 Route::group([
-    'domain' => Admin::config('admin.route.domain'),
-    'prefix' => Admin::config('admin.route.prefix'),
+    'domain'     => Admin::config('admin.route.domain'),
+    'prefix'     => Admin::config('admin.route.prefix'),
     'middleware' => Admin::config('admin.route.middleware'),
 ], function (Router $router) {
     // 用户
@@ -89,11 +89,13 @@ Route::group([
     $router->resource('coach_real_records', \App\Admin\Controllers\CoachRealRecordController::class);
     // 设置分组管理
     $router->resource('setting_groups', \App\Admin\Controllers\SettingGroupController::class);
-    // 设置项
+    // 设置项管理
     $router->resource('setting_items', \App\Admin\Controllers\SettingItemController::class);
     // 设置权限管理
     $router->resource('setting_permissions', \App\Admin\Controllers\SettingPermissionController::class);
     // 设置值管理
     $router->resource('setting_values', \App\Admin\Controllers\SettingValueController::class);
+    // 技师排班
+    $router->resource('coach_schedules', \App\Admin\Controllers\CoachScheduleController::class);
 
-});
+});