Browse Source

Merge branch 'master' of ssh://gogs.yinbin.ink:30004/didong/owl-admin

Yin Bin 4 months ago
parent
commit
a219c85b8c

+ 99 - 0
app/Enums/OrderGrabRecordStatus.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace App\Enums;
+
+/**
+ * 抢单记录状态枚举类
+ */
+enum OrderGrabRecordStatus: int
+{
+    /**
+     * 抢单记录状态:已参加
+     */
+    case JOINED = 1;
+
+    /**
+     * 抢单记录状态:已成功
+     */
+    case SUCCEEDED = 2;
+
+    /**
+     * 抢单记录状态:已取消
+     */
+    case CANCELLED = 3;
+
+    /**
+     * 获取状态的显示文本
+     *
+     * @return string 状态的中文描述
+     */
+    public function label(): string
+    {
+        return match ($this) {
+            self::JOINED => '已参加',
+            self::SUCCEEDED => '已成功',
+            self::CANCELLED => '已取消',
+        };
+    }
+
+    /**
+     * 获取状态的整数值
+     *
+     * @return int 状态值
+     */
+    public function value(): int
+    {
+        return $this->value;
+    }
+
+    /**
+     * 检查当前状态是否与指定状态相同
+     *
+     * @param  self  $status  要比较的状态
+     * @return bool 如果状态相同返回 true,否则返回 false
+     */
+    public function is(self $status): bool
+    {
+        return $this === $status;
+    }
+
+    /**
+     * 根据整数值创建对应的状态枚举实例
+     *
+     * @param  int  $value  状态值
+     * @return self|null 返回对应的状态枚举实例,如果值无效则返回 null
+     */
+    public static function fromValue(int $value): ?self
+    {
+        return match ($value) {
+            self::JOINED->value => self::JOINED,
+            self::SUCCEEDED->value => self::SUCCEEDED,
+            self::CANCELLED->value => self::CANCELLED,
+            default => null
+        };
+    }
+
+    /**
+     * 获取所有状态的值数组
+     *
+     * @return array 包含所有状态值的数组
+     */
+    public static function values(): array
+    {
+        return array_column(self::cases(), 'value');
+    }
+
+    /**
+     * 获取所有状态的键值对数组
+     *
+     * @return array 状态值作为键,显示文本作为值的关联数组
+     */
+    public static function all(): array
+    {
+        return [
+            self::JOINED->value => self::JOINED->label(),
+            self::SUCCEEDED->value => self::SUCCEEDED->label(),
+            self::CANCELLED->value => self::CANCELLED->label(),
+        ];
+    }
+}

+ 73 - 2
app/Http/Controllers/Coach/OrderController.php

@@ -22,7 +22,7 @@ class OrderController extends Controller
     }
 
     /**
-     * [技师端-订单]获取可抢订单列表
+     * [订单]获取可抢订单列表
      *
      * @description 获取当前技师40公里范围内的可抢订单列表,包含订单基本信息和距离
      *
@@ -56,8 +56,79 @@ class OrderController extends Controller
      */
     public function getGrabList(Request $request)
     {
-        $params = $request->only(['area_code', 'page', 'per_page']);
+        $params = $request->validate([
+            'area_code' => 'required|string',
+            'page' => 'nullable|integer|min:1',
+            'per_page' => 'nullable|integer|min:1|max:50',
+        ]);
 
         return $this->service->getGrabList(Auth::user()->id, $params);
     }
+
+    /**
+     * [订单]获取订单列表
+     *
+     * @description 获取当前技师的订单列表,不包含已创建和已分配状态的订单
+     *
+     * @authenticated
+     *
+     * @queryParam page int 页码 Example: 1
+     * @queryParam per_page int 每页数量 Example: 10
+     *
+     * @response {
+     *   "data": {
+     *     "items": [
+     *       {
+     *         "id": 1,
+     *         "order_no": "202403210001",
+     *         "project_name": "精油推拿",
+     *         "project_duration": 60,
+     *         "project_price": "188.00",
+     *         "address": "山东省烟台市芝罘区幸福小区1号楼",
+     *         "service_time": "2024-03-21 10:00:00",
+     *         "status": "completed",
+     *         "created_at": "2024-03-21 09:30:00"
+     *       }
+     *     ],
+     *     "total": 100
+     *   }
+     * }
+     */
+    public function getOrderList(Request $request)
+    {
+        $params = $request->validate([
+            'page' => 'nullable|integer|min:1',
+            'per_page' => 'nullable|integer|min:1|max:50',
+        ]);
+
+        return $this->service->getOrderList(Auth::user()->id, $params);
+    }
+
+    /**
+     * [订单]抢单
+     *
+     * @description 技师抢取指定订单
+     *
+     * @authenticated
+     *
+     * @urlParam order_id integer required 订单ID Example: 1
+     *
+     * @response {
+     *   "message": "抢单成功",
+     *   "order_id": 1
+     * }
+     * @response 400 {
+     *   "message": "订单状态异常,无法抢单"
+     * }
+     * @response 400 {
+     *   "message": "订单超出服务范围"
+     * }
+     * @response 400 {
+     *   "message": "未开通该项目服务资格"
+     * }
+     */
+    public function grabOrder(int $order_id)
+    {
+        return $this->service->grabOrder(Auth::user()->id, $order_id);
+    }
 }

+ 4 - 0
app/Models/OrderGrabRecord.php

@@ -14,8 +14,11 @@ class OrderGrabRecord extends Model
 
     protected $table = 'order_grab_records';
 
+    protected $guarded = [];
+
     /**
      * @Author FelixYin
+     *
      * @description 抢单记录所属订单
      */
     public function order()
@@ -25,6 +28,7 @@ class OrderGrabRecord extends Model
 
     /**
      * @Author FelixYin
+     *
      * @description 抢单记录所属技师
      */
     public function coach()

+ 1 - 0
app/Services/Client/OrderService.php

@@ -1050,6 +1050,7 @@ class OrderService
                     'coach_id' => $coach->id,
                     'nickname' => $coach->info->nickname,
                     'avatar' => $coach->info->avatar,
+                    'distance' => $grab->distance,
                     'created_at' => $grab->created_at->format('Y-m-d H:i:s'),
                 ];
             }

+ 159 - 0
app/Services/Coach/OrderService.php

@@ -2,12 +2,15 @@
 
 namespace App\Services\Coach;
 
+use App\Enums\OrderGrabRecordStatus;
 use App\Enums\OrderStatus;
+use App\Enums\ProjectStatus;
 use App\Enums\TechnicianAuthStatus;
 use App\Enums\TechnicianLocationType;
 use App\Enums\TechnicianStatus;
 use App\Models\MemberUser;
 use App\Models\Order;
+use App\Models\OrderGrabRecord;
 use App\Services\SettingItemService;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -15,6 +18,10 @@ use Illuminate\Support\Facades\Redis;
 
 class OrderService
 {
+    private const DEFAULT_PER_PAGE = 10;
+
+    private const MAX_DISTANCE = 40; // 最大距离限制(公里)
+
     private SettingItemService $settingService;
 
     public function __construct(SettingItemService $settingService)
@@ -22,6 +29,57 @@ class OrderService
         $this->settingService = $settingService;
     }
 
+    /**
+     * 获取技师的订单列表
+     *
+     * @param  int  $userId  技师用户ID
+     * @param  array  $params  请求参数
+     * @return \Illuminate\Pagination\LengthAwarePaginator
+     */
+    public function getOrderList(int $userId, array $params)
+    {
+        return DB::transaction(function () use ($userId, $params) {
+            try {
+                // 加载技师用户信息
+                $user = MemberUser::findOrFail($userId);
+                $coach = $user->coach;
+                abort_if(! $coach, 404, '技师不存在');
+
+                // 构建订单查询
+                $query = $coach->orders()->whereNotIn('state', [
+                    OrderStatus::CREATED->value,
+                    OrderStatus::ASSIGNED->value,
+                ])
+                    ->orderBy('created_at', 'desc');
+
+                // 分页获取数据
+                $paginator = $query->paginate(
+                    $params['per_page'] ?? 10,
+                    ['*'],
+                    'page',
+                    $params['page'] ?? 1
+                );
+
+                // 需要加载关联数据
+                $items = $paginator->items();
+                $query->with(['project']); // 加载项目信息以便返回project_name等字段
+
+                return [
+                    'items' => $items,
+                    'total' => $paginator->total(),
+                ];
+
+            } catch (\Exception $e) {
+                Log::error('获取技师订单列表失败', [
+                    'user_id' => $userId,
+                    'error_message' => $e->getMessage(),
+                    'error_trace' => $e->getTraceAsString(),
+                ]);
+                throw $e;
+            }
+        });
+    }
+
     /**
      * 获取可抢订单列表
      *
@@ -256,4 +314,105 @@ class OrderService
         $order->project->final_price = ($order->project->final_price ?? $order->project->price)
             + $order->project->traffic_fee;
     }
+
+    /**
+     * 技师抢单
+     *
+     * @param  int  $userId  技师用户ID
+     * @param  int  $orderId  订单ID
+     * @return array
+     */
+    public function grabOrder(int $userId, int $orderId)
+    {
+        return DB::transaction(function () use ($userId, $orderId) {
+            try {
+                // 加载用户和技师信息
+                $user = MemberUser::with([
+                    'coach',
+                    'coach.info',
+                    'coach.real',
+                    'coach.qual',
+                    'coach.locations',
+                    'coach.projects',
+                ])->findOrFail($userId);
+
+                // 验证技师信息
+                [$coach, $location] = $this->validateCoach($user);
+
+                // 获取订单信息
+                $order = Order::lockForUpdate()->findOrFail($orderId);
+
+                // 验证订单状态
+                abort_if($order->state !== OrderStatus::CREATED->value, 400, '订单状态异常,无法抢单');
+
+                // 检查技师是否已参与抢单
+                $existingGrab = $coach->grabRecords()
+                    ->where('order_id', $orderId)
+                    ->first();
+                abort_if($existingGrab, 400, '您已参与抢单,请勿重复操作');
+
+                // 验证订单是否在技师服务范围内
+                // 通过Redis GEO计算订单与技师的距离
+                $distance = Redis::geodist(
+                    'order_locations',
+                    'order:'.$order->id,
+                    $location->longitude.','.$location->latitude,
+                    'km'
+                ) ?? PHP_FLOAT_MAX;
+                abort_if($distance > self::MAX_DISTANCE, 400, '订单超出服务范围');
+
+                // 验证技师是否具备该项目服务资格
+                $coachProject = $coach->projects()
+                    ->where('project_id', $order->project_id)
+                    ->where('state', ProjectStatus::OPEN->value)
+                    ->first();
+                abort_if(! $coachProject, 400, '未开通该项目服务资格');
+
+                // 添加抢单记录
+                OrderGrabRecord::create([
+                    'order_id' => $order->id,
+                    'coach_id' => $coach->id,
+                    'distance' => $distance,
+                    'state' => OrderGrabRecordStatus::JOINED->value,
+                    'created_at' => now(),
+                ]);
+
+                // 记录日志
+                Log::info('技师参与抢单', [
+                    'user_id' => $userId,
+                    'coach_id' => $coach->id,
+                    'order_id' => $orderId,
+                ]);
+
+                return [
+                    'message' => '已参与抢单',
+                    'order_id' => $orderId,
+                ];
+
+            } catch (\Exception $e) {
+                Log::error('技师参与抢单失败', [
+                    'user_id' => $userId,
+                    'order_id' => $orderId,
+                    'error_message' => $e->getMessage(),
+                    'error_trace' => $e->getTraceAsString(),
+                ]);
+                throw $e;
+            }
+        });
+    }
+
+    /**
+     * 计算两点之间的距离(公里)
+     */
+    private function calculateDistance($lat1, $lon1, $lat2, $lon2): float
+    {
+        $theta = $lon1 - $lon2;
+        $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1))
+            * cos(deg2rad($lat2)) * cos(deg2rad($theta));
+        $dist = acos($dist);
+        $dist = rad2deg($dist);
+        $miles = $dist * 60 * 1.1515;
+
+        return round($miles * 1.609344, 1);  // 转换为公里并保留一位小数
+    }
 }

+ 7 - 2
routes/api.php

@@ -124,10 +124,11 @@ Route::prefix('client')->group(function () {
 });
 
 // 技师端路由组
-Route::prefix('coach')->middleware(['auth:sanctum', 'throttle:6,1'])->group(function () {
+Route::prefix('coach')->middleware(['auth:sanctum'])->group(function () {
     // 账户相关路由组
     Route::prefix('account')->group(function () {
         Route::post('base-info', [App\Http\Controllers\Coach\AccountController::class, 'submitBaseInfo'])
+            ->middleware(['throttle:6,1'])
             ->name('coach.account.base-info');
         Route::post('qualification', [App\Http\Controllers\Coach\AccountController::class, 'submitQualification']);
         Route::post('real-name', [App\Http\Controllers\Coach\AccountController::class, 'submitRealName'])
@@ -136,5 +137,9 @@ Route::prefix('coach')->middleware(['auth:sanctum', 'throttle:6,1'])->group(func
     });
 
     // 订单相关路由
-    Route::get('orders/grab-list', [CoachOrderController::class, 'getGrabList']);
+    Route::prefix('orders')->group(function () {
+        Route::get('/', [CoachOrderController::class, 'getOrderList']);
+        Route::get('/grab', [CoachOrderController::class, 'getGrabList']);
+        Route::post('/grab/{order_id}', [CoachOrderController::class, 'grabOrder']);
+    });
 });