settingService = $settingService; $this->commissionService = $commissionService; } /** * 根据标签页获取订单状态 */ private function getOrderStatesByTab(int $tab): array { return match ($tab) { 1 => [ // 待接单 OrderStatus::PAID->value, ], 2 => [ // 进行中 OrderStatus::ACCEPTED->value, OrderStatus::DEPARTED->value, OrderStatus::ARRIVED->value, OrderStatus::SERVICE_START->value, OrderStatus::SERVICING->value, OrderStatus::SERVICE_END->value, OrderStatus::LEAVING->value, ], 3 => [ // 已完成 OrderStatus::COMPLETED->value, ], 4 => [ // 待评价 OrderStatus::LEFT->value, ], 5 => [ // 已取消 OrderStatus::CANCELLED->value, OrderStatus::REFUNDED->value, OrderStatus::REJECTED->value, OrderStatus::REFUNDING->value, OrderStatus::REFUND_FAILED->value, ], default => [], // 全部订单(已在基础查询中排除未支付和已分配状态) }; } /** * 获取技师订单列表 * * @param int $coachId 技师ID * @param array $params 查询参数 * @return array */ public function getOrderList(int $coachId, array $params = []) { // 1. 构建基础查询 $query = Order::query() ->where('coach_id', $coachId) ->whereNull('coach_deleted_at') // 不显示技师已删除的订单 ->whereNotIn('state', [ OrderStatus::CREATED->value, // 排除未支付订单 OrderStatus::ASSIGNED->value, // 排除已分配状态订单 ]) ->with(['project', 'user']); // 2. 获取状态过滤条件 $states = $this->getOrderStatesByTab($params['tab'] ?? 0); if (!empty($states)) { $query->whereIn('state', $states); } // 3. 分页查询 $perPage = $params['per_page'] ?? 10; $page = $params['page'] ?? 1; $paginatedOrders = $query->orderBy('created_at', 'desc') ->paginate($perPage, ['*'], 'page', $page); // 4. 格式化数据 $formattedOrders = collect($paginatedOrders->items())->map(function ($order) { return $this->formatOrderListItem($order); }); return [ 'items' => $formattedOrders, 'total' => $paginatedOrders->total(), ]; } /** * 获取可抢订单列表 * * @param int $userId 技师用户ID * @param array $params 请求参数 * @return \Illuminate\Pagination\LengthAwarePaginator */ public function getGrabList(int $userId, array $params) { try { abort_if(empty($params['longitude']) || empty($params['latitude']), 422, '请提供当前位置'); // 加载用户和技师信息 $user = MemberUser::with([ 'coach', 'coach.info', 'coach.real', 'coach.qual', 'coach.locations', 'coach.projects', ])->findOrFail($userId); // 验证技师信息并获取固定位置 [$coach, $fixedLocation] = $this->validateCoach($user); // 获取技师项目信息 $coachProjects = $coach->projects; $coachProjectIds = $coachProjects->pluck('project_id')->toArray(); // 使用技师当前位置获取附近订单 $currentLocation = [ 'longitude' => $params['longitude'], 'latitude' => $params['latitude'] ]; $orderDistances = $this->getNearbyOrders($currentLocation); // 构建订单查询 $query = $this->buildOrderQuery($coachProjectIds, $orderDistances); // 获取技师已抢单记录 $grabRecords = OrderGrabRecord::where('coach_id', $coach->id) ->whereIn('order_id', array_keys($orderDistances)) ->get() ->keyBy('order_id'); // 分页查询并处理数据 $paginatedOrders = $query->paginate( $params['per_page'] ?? 10, ['*'], 'page', $params['page'] ?? 1 ); // 处理订单数据 $items = collect($paginatedOrders->items())->map(function ($order) use ($orderDistances, $coach, $fixedLocation, $grabRecords) { // 获取路费规则 $settings = $this->getDeliveryFeeRules($coach->id, $order->agent_id); // 设置当前位置到订单的距离 $order->distance = $orderDistances[$order->id]; // 处理代理商项目价格 $this->processAgentPrice($order); // 使用 Redis GEODIST 计算固定位置到订单的距离 $fixedDistance = Redis::geodist( 'order_locations', 'order_' . $order->id, $fixedLocation->longitude . ',' . $fixedLocation->latitude, 'km' ) ?? 0; // 计算路费 $trafficFee = $this->calculateFeeByRules($fixedDistance, $settings, $coach->id, $order->project_id); // 设置路费相关信息 $order->traffic_amount = round($trafficFee, 2); $order->fixed_distance = round($fixedDistance, 1); // 计算技师分佣金额 $order->coach_income = round($order->project_amount * 0.5, 2); // 设置抢单状态 $order->is_grabbed = isset($grabRecords[$order->id]); $order->grab_state = $grabRecords[$order->id]->state ?? null; return $order; }); return [ 'items' => $items, 'total' => $paginatedOrders->total() ]; } catch (\Exception $e) { Log::error('获取可抢订单列表失败', [ 'user_id' => $userId, 'params' => $params, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } } /** * 根据规则计算路费 * * @param float $distance 距离(公里) * @param array{ * min_distance: float, // 最小计费距离(公里) * min_fee: float, // 小路费(元) * per_km_fee: float, // 每公里费用(元) * max_distance: float, // 最大服务距离(公里) * max_fee: float, // 最大路费(元) * free_distance: float // 免费服务距离(公里) * } $rules 计费规则 * @param int $coachId 技师ID * @param int $projectId 项目ID * @return float 路费金额(元) */ private function calculateFeeByRules(float $distance, array $rules, int $coachId, int $projectId): float { // 1. 获取技师项目路费设置 $coachProject = DB::table('coach_project') ->where('coach_id', $coachId) ->where('project_id', $projectId) ->where('state', 1) // 启用状态 ->first(); // 2. 如果技师设置了免路费,直接返回技师设置的固定路费 if ($coachProject && (int)$coachProject->traffic_fee_type === 0) { return 0.00; } // 4. 检查是否在免费距离内 if ($distance <= $rules['free_distance']) { return 0.00; } // 5. 计算实际计费距离 $chargeDistance = max(0, $distance - $rules['free_distance']); // 6. 检查是否达到最小计费距离 if ($chargeDistance < $rules['min_distance']) { return $rules['min_fee']; } // 7. 计算基础路费 $fee = bcmul($chargeDistance, $rules['per_km_fee'], 2); // 8. 应用最小路费限制 $fee = max($fee, $rules['min_fee']); // 9. 应用最大路费限制 $fee = min($fee, $rules['max_fee']); // 10. 如果技师设置了固定路费,使用技师设置的路费 if ($coachProject && $coachProject->traffic_fee > 0) { $fee = (float)$coachProject->traffic_fee; } // 11. 根据路费类型计算(单程/双程) if ($coachProject && $coachProject->traffic_fee_type === 2) { // 2表示双程 $fee = bcmul($fee, '2', 2); // 双程路费翻倍 } return $fee; } /** * 验证技师信息 */ private function validateCoach(MemberUser $user): array { $coach = $user->coach; abort_if(! $coach, 404, '技师不存在'); abort_if(! $coach->info, 404, '技师信息不存在'); abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状态异常'); abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过'); abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过'); $location = $coach->locations() ->where('type', TechnicianLocationType::COMMON->value) ->first(); abort_if(! $location, 404, '技师定位地址不存在'); return [$coach, $location]; } /** * 获取路费计算规则 * * @return array{ * min_distance: float, * min_fee: float, * per_km_fee: float, * max_distance: float, * max_fee: float, * free_distance: float * } */ private function getDeliveryFeeRules(int $coachId, ?int $agentId): array { // 1. 获取系统默认规则 $defaultRules = $this->getDefaultDeliveryFeeRules(); // 2. 获取代理商规则(如果有) $agentRules = $agentId ? $this->getAgentDeliveryFeeRules($agentId) : []; // 3. 获取技师个性化规则 // $coachRules = $this->getCoachDeliveryFeeRules($coachId); // 4. 合并规则,优先级:技师 > 代理商 > 系统默认 return array_merge( $defaultRules, $agentRules ); } /** * 获取系统默认路费规则 */ private function getDefaultDeliveryFeeRules(): array { // TODO: 从配置表中获取系统默认路费规则 return [ 'min_distance' => 3.0, // 最小计费距离(公里) 'base_price' => 10.0, // 最小路费(元) 'per_km_price' => 3.0, // 每公里费用(元) 'max_distance' => 120.0, // 最大服务距离(公里) 'max_fee' => 50.0, // 最大路费(元) 'free_distance' => 1.0, // 免费服务距离(公里) ]; } /** * 获取代理商路费规则 */ private function getAgentDeliveryFeeRules(int $agentId): array { // TODO: 从配置表中获取代理商路费规则 return []; } /** * 获取技师路费规则 */ private function getCoachDeliveryFeeRules(int $coachId): array { // TODO: 从配置表中获取技师个性化路费规则 return []; } /** * 获取附近订单 * * @param array $location 位置信息,包含 longitude 和 latitude * @param float $maxDistance 最大距离(公里) * @return array */ private function getNearbyOrders(array $location, float $maxDistance = 40): array { $nearbyOrders = Redis::georadius( 'order_locations', $location['longitude'], $location['latitude'], $maxDistance, 'km', ['WITHCOORD', 'WITHDIST'] ); if (! $nearbyOrders) { Log::warning('获取附近订单失败', [ 'location' => $location ]); return []; } $orderDistances = []; foreach ($nearbyOrders as $order) { $orderId = str_replace('order_', '', $order[0]); $distance = round($order[1], 1); $orderDistances[$orderId] = $distance; } return $orderDistances; } /** * 构建订单查询 */ private function buildOrderQuery(array $coachProjectIds, array $orderDistances) { return Order::query() ->select([ 'id', 'order_no', 'project_id', 'location', 'address', 'latitude', 'longitude', 'service_time', 'created_at', 'agent_id', 'project_amount', // 项目金额 DB::raw('0 as distance'), ]) ->with(['project', 'agent.projects']) ->where('state', OrderStatus::CREATED) ->whereIn('project_id', $coachProjectIds) ->whereIn('id', array_keys($orderDistances)) ->orderBy('created_at', 'desc'); } /** * 处理代理商价格 */ private function processAgentPrice($order): void { if ($order->agent_id && $order->agent && $order->agent->projects) { $agentProject = $order->agent->projects ->where('id', $order->project_id) ->where('state', 1) ->first(); if ($agentProject) { $order->project->price = $agentProject->agent_price; $order->project->duration = $agentProject->duration; } } } /** * 计算路费 */ private function calculateTrafficFee($order, $coachProject, array $settings): void { $trafficFee = $settings['base_price']; if ($order->distance > $settings['min_distance']) { $trafficFee += ($order->distance - $settings['min_distance']) * $settings['per_km_price']; } $order->project->traffic_fee = round($trafficFee, 2); // 程收费 if ($coachProject->is_round_trip) { $order->project->traffic_fee *= 2; } // 计算最终价格 $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, '订单状态异常,无法抢单'); // 验证订单类型 abort_if($order->type !== OrderType::GRAB->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; } }); } /** * 技师接单 * * @param int $userId 用户ID * @param int $orderId 订单ID * @return array * @throws \Exception */ public function acceptOrder(int $userId, int $orderId): array { return DB::transaction(function () use ($userId, $orderId) { try { // 获取技师信息 $user = MemberUser::with(['coach'])->findOrFail($userId); $coach = $user->coach; abort_if(!$coach, 404, '技师信息不存在'); // 1. 验证订单状态 $order = Order::where('id', $orderId) ->where('coach_id', $coach->id) ->where('state', OrderStatus::PAID->value) ->lockForUpdate() ->firstOrFail(); // 2. 更新订单状态为已接单 $order->update([ 'state' => OrderStatus::ACCEPTED->value, 'accept_time' => now() ]); // 3. 创建接单记录 OrderRecord::create([ 'order_id' => $order->id, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'state' => OrderRecordStatus::ACCEPTED->value, 'remark' => '技师接单' ]); // 4. 如果是加钟订单,需要特殊处理 if ($order->type === OrderType::OVERTIME->value) { // 获取主订单 $mainOrder = Order::findOrFail($order->parent_id); // 获取主订单的其他加钟订单(只查询已接单、开始服务、服务中、服务结束状态) $otherOrders = Order::where('parent_id', $mainOrder->id) ->where('id', '!=', $order->id) ->where('type', OrderType::OVERTIME->value) ->whereIn('state', [ OrderStatus::ACCEPTED->value, // 已接单 OrderStatus::SERVICE_START->value, // 开始服务 OrderStatus::SERVICING->value, //服务中 OrderStatus::SERVICE_END->value // 服务结束 ]) ->orderBy('service_end_time', 'desc') ->first(); // 设置服务时间 if ($otherOrders) { // 如果有其他加钟订单,使用最新加钟订单的结束时间作为开始时间 $startTime = $otherOrders->service_end_time; } else { // 如果没有其他加钟订单,比较当前时间和主订单结束时间,取较大值 $now = now(); $mainOrderEndTime = Carbon::parse($mainOrder->service_end_time); $startTime = $now->gt($mainOrderEndTime) ? $now : $mainOrderEndTime; } // 计算结束时间 $endTime = Carbon::parse($startTime)->addMinutes($order->project_duration); // 更新订单状态为开始服务 $order->update([ 'state' => OrderStatus::SERVICING->value, 'service_start_time' => $startTime, 'service_end_time' => $endTime ]); // 创建开始服务记录 OrderRecord::create([ 'order_id' => $order->id, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'state' => OrderRecordStatus::STARTED->value, 'remark' => '开始加钟服务' ]); // 派发延迟任务,在服务时长到期后自动完成订单 AutoFinishOrder::dispatch($order) ->delay($endTime); } return ['message' => '接单成功']; } catch (Exception $e) { Log::error('接单失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } }); } /** * 技师拒单 * * @param int $userId 用户ID * @param int $orderId 订单ID * @param string $reason 拒单原因 */ public function rejectOrder(int $userId, int $orderId, string $reason): array { return DB::transaction(function () use ($userId, $orderId, $reason) { try { // 获取技师信息(优化关联加载) $user = MemberUser::with([ 'coach', 'coach.info', 'coach.real', 'coach.qual', ])->findOrFail($userId); // 验证技师信息 [$coach, $location] = $this->validateCoach($user); // 获取订单信息并加锁 $order = Order::lockForUpdate()->findOrFail($orderId); // 验证订单状态(修正状态判断) abort_if(! in_array($order->state, [ OrderStatus::ASSIGNED->value, OrderStatus::PAID->value, ]), 400, '订单状态异常,无法拒单'); // 验证订单是否分配给该技师 abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您'); // 检查拒单次数限制 $rejectCount = OrderRecord::where('object_id', $coach->id) ->where('object_type', CoachUser::class) ->where('state', OrderRecordStatus::REJECTED->value) ->whereDate('created_at', today()) ->count(); // 更新订单状态 $order->update([ 'state' => OrderStatus::REJECTED->value, ]); // 创建订单记录 OrderRecord::create([ 'order_id' => $order->id, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'state' => OrderRecordStatus::REJECTED->value, 'remark' => $reason, ]); // 发送消息通知 try { // event(new OrderRejectedEvent($order, $coach, $reason)); } catch (\Exception $e) { Log::error('发送拒单通知失败', [ 'order_id' => $orderId, 'coach_id' => $coach->id, 'error' => $e->getMessage(), ]); } // 记录日志 Log::info('技师拒单成功', [ 'user_id' => $userId, 'coach_id' => $coach->id, 'order_id' => $orderId, 'order_no' => $order->order_no, 'reason' => $reason, 'reject_count' => $rejectCount + 1, ]); return [ 'message' => '拒单成功', 'order_id' => $orderId, 'order_no' => $order->order_no, 'reject_count' => $rejectCount + 1, 'max_reject_count' => 5, ]; } catch (\Exception $e) { Log::error('技师拒单失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'reason' => $reason, 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ]); throw $e; } }); } /** * 技师出发 * * @param int $userId 技师用户ID * @param int $orderId 订单ID */ public function depart(int $userId, int $orderId): array { try { return DB::transaction(function () use ($userId, $orderId) { // 获取技师信息 $user = MemberUser::with(['coach'])->findOrFail($userId); // 获取订单信息 $order = Order::query()->where('id', $orderId)->lockForUpdate()->first(); // 检查订单是否存在 abort_if(! $order, 404, '订单不存在'); // 检查是否是该技师的订单 abort_if($order->coach_id !== $user->coach->id, 403, '无权操作此订单'); // 根据订单类型判断订单状态 if ($order->type == OrderType::VISIT->value) { abort_if($order->state != OrderStatus::ACCEPTED->value, 400, '订单状态不正确'); } elseif ($order->type == OrderType::GRAB->value) { abort_if($order->state != OrderStatus::PAID->value, 400, '订单状态不正确'); } // 更新订单状态为技师出发 $order->state = OrderStatus::DEPARTED->value; $order->save(); // 记录订单状态变更日志 OrderRecord::create([ 'order_id' => $orderId, 'state' => OrderRecordStatus::DEPARTED->value, 'object_id' => $user->coach->id, 'object_type' => CoachUser::class, 'remark' => '技师已出发', ]); // 发送通知给用户 // TODO: 发送通知 // event(new TechnicianDepartedEvent($order)); return [ 'status' => true, 'message' => '操作成功', 'data' => [ 'order_id' => $orderId, 'status' => $order->state, 'created_at' => $order->created_at, ], ]; }); } catch (\Exception $e) { \Log::error('技师出发失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'error' => $e->getMessage(), ]); throw $e; } } /** * 技师到达 * * @param int $userId 技师用户ID * @param int $orderId 订单ID */ public function arrive(int $userId, int $orderId): array { return DB::transaction(function () use ($userId, $orderId) { try { // 获取技师信息 $user = MemberUser::with(['coach'])->findOrFail($userId); $coach = $user->coach; abort_if(! $coach, 404, '技师信息不存在'); // 获取订单信息 $order = Order::query()->where('id', $orderId)->lockForUpdate()->first(); abort_if(! $order, 404, '订单不存在'); // 检查是否是该技师的订单 abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单'); // 检查订单状态 abort_if(! in_array($order->state, [ OrderStatus::DEPARTED->value, ]), 400, '订单状态不正确'); $now = now(); // 更新订单状态为技师到达 $order->state = OrderStatus::ARRIVED->value; $order->save(); // 记录订单状态变更日志 OrderRecord::create([ 'order_id' => $orderId, 'state' => OrderRecordStatus::ARRIVED->value, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'remark' => '技师已到达', ]); // 更新技师当前位置到Redis GEO try { Redis::geoadd( 'coach_locations', $order->longitude, $order->latitude, $coach->id . '_' . TechnicianLocationType::CURRENT->value ); } catch (\Exception $e) { Log::error('更新技师位置失败', [ 'coach_id' => $coach->id, 'order_id' => $orderId, 'error' => $e->getMessage(), ]); } // TODO: 发送通知给用户 // event(new TechnicianArrivedEvent($order)); Log::info('技师到达成功', [ 'coach_id' => $coach->id, 'order_id' => $orderId, 'arrived_at' => $now, ]); return [ 'status' => true, 'message' => '操作成功', 'data' => [ 'order_id' => $orderId, 'status' => $order->state, 'arrived_at' => $now, ], ]; } catch (\Exception $e) { Log::error('技师到达失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } }); } /** * 技师扫码开始服务 * * @param int $userId 技师用户ID * @param int $orderId 订单ID * @param string $qrCode 客户二维码 */ public function startService(int $userId, int $orderId, string $qrCode): array { return DB::transaction(function () use ($userId, $orderId, $qrCode) { try { // 获取技师信息 $user = MemberUser::with(['coach'])->findOrFail($userId); $coach = $user->coach; abort_if(! $coach, 404, '技师信息不存在'); // 获取订单信息 $order = Order::query() ->where('id', $orderId) ->lockForUpdate() ->first(); abort_if(! $order, 404, '订单不存在'); // 检查是否是该技师的订单 abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单'); // 检查订单状态 abort_if(! in_array($order->state, [ OrderStatus::ARRIVED->value, OrderStatus::PAID->value, ]), 400, '订单状态不正确'); // 验证二维码 $this->validateQrCode($order, $qrCode); $now = now(); // 更新订单状态为服务中 $order->state = OrderStatus::SERVICING->value; $order->service_start_time = $now; $order->service_end_time = $now->copy()->addMinutes($order->project_duration); $order->save(); // 更新技师工作状态为忙碌中 $coach->update([ 'work_status' => TechnicianWorkStatus::BUSY->value ]); // 记录订单状态变更日志 OrderRecord::create([ 'order_id' => $orderId, 'state' => OrderRecordStatus::STARTED->value, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'remark' => '开始服务', ]); // 获取项目服务时长(分钟) $duration = $order->project->duration ?? 60; // 派发延迟任务,在服务时长到期后自动完成订单 AutoFinishOrder::dispatch($order) ->delay(now()->addMinutes($duration)); // TODO: 发送通知给用户 // event(new ServiceStartedEvent($order)); Log::info('技师开始服务', [ 'coach_id' => $coach->id, 'order_id' => $orderId, 'service_start_time' => $now, ]); return [ 'status' => true, 'message' => '开始服务成功', 'data' => [ 'order_id' => $orderId, 'status' => $order->state, 'service_start_time' => $now, ], ]; } catch (\Exception $e) { Log::error('开始服务失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'qr_code' => $qrCode, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } }); } /** * 验证客户二维码 * * @param Order $order 订单对象 * @param string $qrCode 扫描的二维码 */ private function validateQrCode(Order $order, string $qrCode): void { // 二维码格式: order_{order_id}_{timestamp}_{sign} $parts = explode('_', $qrCode); abort_if(count($parts) !== 4, 400, '二维码格式错误'); [$prefix, $scanOrderId, $timestamp, $sign] = $parts; // 验证前缀 abort_if($prefix !== 'order', 400, '无效的二维码'); // 验证订单ID abort_if((int) $scanOrderId !== $order->id, 400, '二维码与订单不匹配'); // 验证时间戳(二维码5分钟内有效) // $qrTimestamp = (int) $timestamp; // $now = time(); // abort_if($now - $qrTimestamp > 300, 400, '二维码已过期'); // 验证签名 $correctSign = md5("order_{$order->id}_{$timestamp}_" . config('app.key')); abort_if($sign !== $correctSign, 400, '二维码签名错误'); } /** * 技师撤离 * * @param int $userId 技师户ID * @param int $orderId 订单ID * * @throws \Exception */ public function leave(int $userId, int $orderId): array { return DB::transaction(function () use ($userId, $orderId) { try { // 获取技师信息,同时加载钱包关联 $user = MemberUser::with(['coach', 'coach.wallet'])->findOrFail($userId); $coach = $user->coach; abort_if(! $coach, 404, '技师信息不存在'); $coach->load('wallet'); // 获取订单信息 $order = Order::query() ->with(['coach', 'coach.wallet']) ->where('id', $orderId) ->lockForUpdate() ->firstOrFail(); // 验证订单状态和权限 $this->validateLeaveOrder($order, $coach); // 更新订单状态 $this->updateOrderStatus($order); // 记录订单状态变更 $this->createLeaveRecord($order, $coach); // 处理订单分佣 $this->commissionService->handleOrderCommission($order); // 清理技师位置信息 $this->cleanCoachLocation($coach); // TODO: 发送通知给用户 // event(new ServiceCompletedEvent($order)); return [ 'status' => true, 'message' => '撤离成功', 'data' => [ 'order_id' => $orderId, 'state' => $order->state, 'leave_time' => now(), ], ]; } catch (\Exception $e) { Log::error('技师撤离失败', [ 'user_id' => $userId, 'order_id' => $orderId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } }); } /** * 验证撤离订单 */ private function validateLeaveOrder(Order $order, CoachUser $coach): void { // 检查是否是该技师的订单 abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单'); // 检查订单状态 abort_if(! in_array($order->state, [ OrderStatus::LEAVING->value, ]), 400, '订单状态不正确,无法撤离'); // 检查钱包状态 abort_if(! $coach->wallet, 400, '技师钱包信息不存在'); } /** * 更新订单状态 */ private function updateOrderStatus(Order $order): void { $order->state = OrderStatus::LEFT->value; $order->save(); } /** * 创建撤离记录 */ private function createLeaveRecord(Order $order, CoachUser $coach): void { OrderRecord::create([ 'order_id' => $order->id, 'state' => OrderRecordStatus::COMPLETED->value, 'object_id' => $coach->id, 'object_type' => CoachUser::class, 'remark' => '技师已撤离,服务完成', ]); } /** * 清理技师位置信息 */ private function cleanCoachLocation(CoachUser $coach): void { try { Redis::zrem( 'coach_locations', $coach->id . '_' . TechnicianLocationType::CURRENT->value ); } catch (\Exception $e) { Log::error('删除技师位置失败', [ 'coach_id' => $coach->id, 'error' => $e->getMessage(), ]); // 不抛出异常,继续执行 } } /** * 清理技师相关缓存 * * @param int $coachId 技师ID * @param string|null $date 服务日期 */ private function clearCoachCache(int $coachId, ?string $date = null): void { try { // 清理技师时间段缓存 $date = $date ?: now()->toDateString(); $cacheKey = "coach:timeslots:{$coachId}:{$date}"; Cache::forget($cacheKey); Log::info('成功清理技师缓存', [ 'coach_id' => $coachId, 'date' => $date, ]); } catch (\Exception $e) { Log::error('清理技师缓存失败', [ 'coach_id' => $coachId, 'date' => $date, 'error' => $e->getMessage(), ]); // 缓存清理失败不影响主流程 } } /** * 获取设置项 * * @param string $groupCode 设置组编码 * @param string $itemCode 设置项编码 * @return object 设置项对象 * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ private function getSettingItem(string $groupCode, string $itemCode): object { // 获取设置组 $settingGroup = SettingGroup::where('code', $groupCode)->first(); abort_if(!$settingGroup, 404, "设置组[{$groupCode}]不存在"); // 获取设置项 $settingItem = $settingGroup->items()->where('code', $itemCode)->first(); abort_if(!$settingItem, 404, "设置项[{$itemCode}]不存在"); return $settingItem; } /** * 更新订单设置 * * 业务流程: * 1. 获取订单配置项 * 2. 验证服务距离范围 * 3. 更新技师设置 * 4. 返回最新设置 * * @param CoachUser $coach 技师对象 * @param array $data 设置数据,包含: * - distance: float 服务距离(公里) * @return array 返回最新设置信息,包含: * - distance: float 当前服务距离 * - distance_max: float 最大服务距离限制 * - distance_min: float 最小服务距离限制 * @throws \Exception 当更新失败时抛出异常 */ public function setOrder(CoachUser $coach, array $data): array { return DB::transaction(function () use ($coach, $data) { // 获取订单距离配置项 $distanceItem = $this->getSettingItem('order', 'distance'); // 验证服务距离范围 $distance = (float)$data['distance']; abort_if( $distance > $distanceItem->max_value || $distance < $distanceItem->min_value, 422, sprintf('服务距离必须在 %.1f-%.1f 公里之间', $distanceItem->min_value, $distanceItem->max_value) ); // 更新技师服务距离设置 $coach->settingValues()->updateOrCreate( [ 'item_id' => $distanceItem->id, 'object_type' => $coach::class, 'object_id' => $coach->id, ], ['value' => $distance] ); // 返回最新设置 return [ 'distance' => $distance, 'distance_max' => (float)$distanceItem->max_value, 'distance_min' => (float)$distanceItem->min_value, ]; }); } /** * 获取订单设置 * * 业务流程: * 1. 获取技师的订单设置 * 2. 获取系统配置的限制值 * 3. 返回设置信息 * * @param CoachUser $coach 技师对象 * @return array 返回设置信息,包含: * - distance: float 当前服务距离 * - distance_max: float 最大服务距离 * - distance_min: float 最小服务距离 * @throws \Exception 当获取失败时抛出异常 */ public function getOrderSettings(CoachUser $coach): array { // 获取订单距离配置项 $distanceItem = $this->getSettingItem('order', 'distance'); // 获取技师的距离设置 $distanceSetting = $coach->settingValues() ->where('item_id', $distanceItem->id) ->first(); // 返回设置信息 return [ 'distance' => (float)($distanceSetting?->value ?? $distanceItem->default_value), // 当前服务距离 'distance_max' => (float)$distanceItem->max_value, // 最大服务距离限制 'distance_min' => (float)$distanceItem->min_value, // 最小服务距离限制 ]; } /** * 格式化订单列表项 */ private function formatOrderListItem($order): array { return [ 'id' => $order->id, // 订单ID 'order_no' => $order->order_no, // 订单编号 'state' => $order->state, // 订单状态值 'state_text' => OrderStatus::fromValue($order->state)?->label(), // 订单状态文本 'payment_type' => $order->payment_type, // 支付类型值 'payment_type_text' => PaymentMethod::fromValue($order->payment_type)?->label(), // 支付类型文本 'total_amount' => $order->total_amount, // 订单总金额 'project_amount' => $order->project_amount, // 项目金额 'traffic_amount' => $order->traffic_amount, // 交通费金额 'discount_amount' => $order->discount_amount, // 优惠金额 'pay_amount' => $order->pay_amount, // 实付金额 'refund_amount' => $order->refund_amount, // 退款金额 'type' => $order->type, // 订单类型值 'type_text' => OrderType::fromValue($order->type)?->label(), // 订单类型文本 'source' => $order->source, // 订单来源值 'source_text' => OrderSource::fromValue($order->source)?->label(), // 订单来源文本 'distance' => $order->distance, // 目的地距离(米) 'service_time' => $order->service_time instanceof \Carbon\Carbon // 预约服务时间 ? $order->service_time->toDateTimeString() : $order->service_time, 'accept_time' => $order->accept_time instanceof \Carbon\Carbon // 接单时间 ? $order->accept_time->toDateTimeString() : $order->accept_time, 'service_start_time' => $order->service_start_time instanceof \Carbon\Carbon // 实际开始时间 ? $order->service_start_time->toDateTimeString() : $order->service_start_time, 'service_end_time' => $order->service_end_time instanceof \Carbon\Carbon // 实际结束时间 ? $order->service_end_time->toDateTimeString() : $order->service_end_time, 'created_at' => $order->created_at instanceof \Carbon\Carbon // 创建时间 ? $order->created_at->toDateTimeString() : $order->created_at, 'project' => [ // 项目信息 'id' => $order->project->id, // 项目ID 'title' => $order->project->title, // 项目标题 'cover' => $order->project->cover, // 项目封面图 'duration' => $order->project->duration // 项目时长(分钟) ], 'user' => [ // 用户信息 'id' => $order->user->id, // 用户ID 'nickname' => $order->user->nickname, // 用户昵称 'avatar' => $order->user->avatar, // 用户头像 'mobile' => $order->user->mobile, // 用户手机号 ], 'address' => [ // 服务地址信息 'location' => $order->location, // 定位地址 'address' => $order->address, // 详细地址 'latitude' => $order->latitude, // 纬度 'longitude' => $order->longitude, // 经度 'phone' => $order->dest_phone // 联系人电话 ], ]; } /** * 技师删除订单 * * 业务逻辑: * 1. 验证订单是否存在且分配给当前技师 * 2. 验证订单状态是否允许删除(已完成、已取消等) * 3. 更新订单的coach_deleted_at字段 * 4. 记录删除操作 * * @param int $coachId 技师ID * @param int $orderId 订单ID * @return array * @throws \Exception */ public function deleteOrder(int $coachId, int $orderId): array { return DB::transaction(function () use ($coachId, $orderId) { // 验证订单是否存在且分配给当前技师 $order = Order::where('id', $orderId) ->where('coach_id', $coachId) ->whereNull('coach_deleted_at') // 确保订单未被技师删除 ->first(); abort_if(!$order, 404, '订单不存在'); // 验证订单状态是否允许删除 // 允许删除已完成、已取消、已退款等状态的订单 $allowedStates = [ OrderStatus::COMPLETED->value, // 已完成 OrderStatus::CANCELLED->value, // 已取消 OrderStatus::REFUNDED->value, // 已退款 OrderStatus::REJECTED->value, // 已拒单 ]; abort_if(!in_array($order->state, $allowedStates), 422, '当前订单状态不允许删除'); // 标记订单为技师已删除 $order->update([ 'coach_deleted_at' => now() ]); // 创建订单记录 OrderRecord::create([ 'order_id' => $order->id, 'object_id' => $coachId, 'object_type' => CoachUser::class, 'state' => OrderRecordStatus::DELETED->value, 'remark' => '技师删除订单' ]); return [ 'message' => '订单删除成功' ]; }); } }