1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426 |
- <?php
- namespace App\Services\Coach;
- use App\Models\Coach;
- use App\Models\Order;
- use App\Enums\OrderType;
- use App\Models\CoachUser;
- use App\Enums\OrderSource;
- use App\Enums\OrderStatus;
- use App\Models\MemberUser;
- use App\Models\OrderRecord;
- use App\Enums\PaymentMethod;
- use App\Enums\ProjectStatus;
- use App\Models\SettingGroup;
- use App\Jobs\AutoFinishOrder;
- use App\Enums\TechnicianStatus;
- use App\Models\OrderGrabRecord;
- use App\Enums\OrderRecordStatus;
- use App\Enums\TransactionStatus;
- use Illuminate\Support\Facades\DB;
- use App\Enums\TechnicianAuthStatus;
- use App\Enums\TechnicianWorkStatus;
- use Illuminate\Support\Facades\Log;
- use App\Enums\OrderGrabRecordStatus;
- use App\Services\SettingItemService;
- use App\Enums\TechnicianLocationType;
- use App\Models\SettingItem;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\Redis;
- use Exception;
- use Carbon\Carbon;
- class OrderService
- {
- private const DEFAULT_PER_PAGE = 10;
- private const MAX_DISTANCE = 40; // 最大距离限制(公里)
- private SettingItemService $settingService;
- private CommissionService $commissionService;
- public function __construct(SettingItemService $settingService, CommissionService $commissionService)
- {
- $this->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, '无权操作此订单');
- // 根据订单类型判断订单状态
- abort_if($order->state != OrderStatus::ACCEPTED->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' => '订单删除成功'
- ];
- });
- }
- }
|