OrderService.php 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. <?php
  2. namespace App\Services\Coach;
  3. use App\Models\Order;
  4. use App\Enums\OrderType;
  5. use App\Models\CoachUser;
  6. use App\Enums\OrderSource;
  7. use App\Enums\OrderStatus;
  8. use App\Models\MemberUser;
  9. use App\Models\OrderRecord;
  10. use App\Enums\ProjectStatus;
  11. use App\Models\SettingGroup;
  12. use App\Jobs\AutoFinishOrder;
  13. use App\Enums\TechnicianStatus;
  14. use App\Models\OrderGrabRecord;
  15. use App\Enums\OrderRecordStatus;
  16. use Illuminate\Support\Facades\DB;
  17. use App\Enums\TechnicianAuthStatus;
  18. use Illuminate\Support\Facades\Log;
  19. use App\Enums\OrderGrabRecordStatus;
  20. use App\Services\SettingItemService;
  21. use App\Enums\TechnicianLocationType;
  22. use Illuminate\Support\Facades\Cache;
  23. use Illuminate\Support\Facades\Redis;
  24. use App\Enums\PaymentMethod;
  25. use App\Models\Coach;
  26. class OrderService
  27. {
  28. private const DEFAULT_PER_PAGE = 10;
  29. private const MAX_DISTANCE = 40; // 最大距离限制(公里)
  30. private SettingItemService $settingService;
  31. private CommissionService $commissionService;
  32. public function __construct(SettingItemService $settingService, CommissionService $commissionService)
  33. {
  34. $this->settingService = $settingService;
  35. $this->commissionService = $commissionService;
  36. }
  37. /**
  38. * 根据标签页获取订单状态
  39. */
  40. private function getOrderStatesByTab(int $tab): array
  41. {
  42. return match ($tab) {
  43. 1 => [ // 待接单
  44. OrderStatus::PAID->value,
  45. ],
  46. 2 => [ // 进行中
  47. OrderStatus::ACCEPTED->value,
  48. OrderStatus::DEPARTED->value,
  49. OrderStatus::ARRIVED->value,
  50. OrderStatus::SERVICE_START->value,
  51. OrderStatus::SERVICING->value,
  52. OrderStatus::SERVICE_END->value,
  53. OrderStatus::LEAVING->value,
  54. ],
  55. 3 => [ // 已完成
  56. OrderStatus::COMPLETED->value,
  57. ],
  58. 4 => [ // 待评价
  59. OrderStatus::LEFT->value,
  60. ],
  61. 5 => [ // 已取消
  62. OrderStatus::CANCELLED->value,
  63. OrderStatus::REFUNDED->value,
  64. OrderStatus::REJECTED->value,
  65. OrderStatus::REFUNDING->value,
  66. OrderStatus::REFUND_FAILED->value,
  67. ],
  68. default => [], // 全部订单(已在基础查询中排除未支付和已分配状态)
  69. };
  70. }
  71. /**
  72. * 获取技师订单列表
  73. *
  74. * @param int $coachId 技师ID
  75. * @param array $params 查询参数
  76. * @return array
  77. */
  78. public function getOrderList(int $coachId, array $params = [])
  79. {
  80. // 1. 构建基础查询
  81. $query = Order::query()
  82. ->where('coach_id', $coachId)
  83. ->whereNull('coach_deleted_at') // 不显示技师已删除的订单
  84. ->whereNotIn('state', [
  85. OrderStatus::CREATED->value, // 排除未支付订单
  86. OrderStatus::ASSIGNED->value, // 排除已分配状态订单
  87. ])
  88. ->with(['project', 'user']);
  89. // 2. 获取状态过滤条件
  90. $states = $this->getOrderStatesByTab($params['tab'] ?? 0);
  91. if (!empty($states)) {
  92. $query->whereIn('state', $states);
  93. }
  94. // 3. 分页查询
  95. $perPage = $params['per_page'] ?? 10;
  96. $page = $params['page'] ?? 1;
  97. $paginatedOrders = $query->orderBy('created_at', 'desc')
  98. ->paginate($perPage, ['*'], 'page', $page);
  99. // 4. 格式化数据
  100. $formattedOrders = collect($paginatedOrders->items())->map(function ($order) {
  101. return $this->formatOrderListItem($order);
  102. });
  103. return [
  104. 'items' => $formattedOrders,
  105. 'total' => $paginatedOrders->total(),
  106. ];
  107. }
  108. /**
  109. * 获取可抢订单列表
  110. *
  111. * @param int $userId 技师用户ID
  112. * @param array $params 请求参数
  113. * @return \Illuminate\Pagination\LengthAwarePaginator
  114. */
  115. public function getGrabList(int $userId, array $params)
  116. {
  117. try {
  118. // 加载用户和技师信息
  119. $user = MemberUser::with([
  120. 'coach',
  121. 'coach.info',
  122. 'coach.real',
  123. 'coach.qual',
  124. 'coach.locations',
  125. 'coach.projects',
  126. ])->findOrFail($userId);
  127. // 验证技师信息
  128. [$coach, $location] = $this->validateCoach($user);
  129. // 获取技师项目信
  130. $coachProjects = $coach->projects;
  131. $coachProjectIds = $coachProjects->pluck('project_id')->toArray();
  132. // 获取系统配置
  133. $settings = $this->getSystemSettings();
  134. // 获取附近订单
  135. $orderDistances = $this->getNearbyOrders($location);
  136. // 构建订单查询
  137. $query = $this->buildOrderQuery($coachProjectIds, $orderDistances);
  138. // 处理订单数据
  139. $this->processOrderData($query, $orderDistances, $coachProjects, $settings);
  140. // 分页获取数据
  141. return $query->paginate(
  142. $params['per_page'] ?? 10,
  143. ['*'],
  144. 'page',
  145. $params['page'] ?? 1
  146. );
  147. } catch (\Exception $e) {
  148. Log::error('获取可抢订单列表失败', [
  149. 'user_id' => $userId,
  150. 'params' => $params,
  151. 'error' => $e->getMessage(),
  152. 'trace' => $e->getTraceAsString(),
  153. ]);
  154. throw $e;
  155. }
  156. }
  157. /**
  158. * 验证技师信息
  159. */
  160. private function validateCoach(MemberUser $user): array
  161. {
  162. $coach = $user->coach;
  163. abort_if(! $coach, 404, '技师不存在');
  164. abort_if(! $coach->info, 404, '技师信息不存在');
  165. abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状态异常');
  166. abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过');
  167. abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过');
  168. $location = $coach->locations()
  169. ->where('type', TechnicianLocationType::COMMON->value)
  170. ->first();
  171. abort_if(! $location, 404, '技师定位地址不存在');
  172. return [$coach, $location];
  173. }
  174. /**
  175. * 获取系统配置
  176. */
  177. private function getSystemSettings(): array
  178. {
  179. return [
  180. 'coach_income' => $this->settingService->getItemDetail('coach_income')->default_value ?? 0,
  181. 'min_distance' => $this->settingService->getItemDetail('qibugongli')->default_value ?? 0,
  182. 'base_price' => $this->settingService->getItemDetail('qibujia')->default_value ?? 0,
  183. 'per_km_price' => $this->settingService->getItemDetail('meigonglijiage')->default_value ?? 0,
  184. ];
  185. }
  186. /**
  187. * 获取附近订单
  188. */
  189. private function getNearbyOrders($location, float $maxDistance = 40): array
  190. {
  191. $nearbyOrders = Redis::georadius(
  192. 'order_locations',
  193. $location->longitude,
  194. $location->latitude,
  195. $maxDistance,
  196. 'km',
  197. ['WITHCOORD', 'WITHDIST']
  198. );
  199. if (! $nearbyOrders) {
  200. Log::warning('获取附近订单失败', [
  201. 'location' => [
  202. 'longitude' => $location->longitude,
  203. 'latitude' => $location->latitude,
  204. ],
  205. ]);
  206. return [];
  207. }
  208. $orderDistances = [];
  209. foreach ($nearbyOrders as $order) {
  210. $orderId = str_replace('order:', '', $order[0]);
  211. $distance = round($order[1], 1);
  212. $orderDistances[$orderId] = $distance;
  213. }
  214. return $orderDistances;
  215. }
  216. /**
  217. * 构建订单查询
  218. */
  219. private function buildOrderQuery(array $coachProjectIds, array $orderDistances)
  220. {
  221. return Order::query()
  222. ->select([
  223. 'id',
  224. 'order_no',
  225. 'project_id',
  226. 'location',
  227. 'address',
  228. 'latitude',
  229. 'longitude',
  230. 'service_time',
  231. 'created_at',
  232. 'agent_id',
  233. DB::raw('0 as distance'),
  234. ])
  235. ->with(['project', 'agent.projects'])
  236. ->where('state', OrderStatus::CREATED)
  237. ->whereIn('project_id', $coachProjectIds)
  238. ->whereIn('id', array_keys($orderDistances))
  239. ->orderBy('created_at', 'desc');
  240. }
  241. /**
  242. * 处理订单数据
  243. */
  244. private function processOrderData($query, array $orderDistances, $coachProjects, array $settings): void
  245. {
  246. $query->get()->each(function ($order) use ($orderDistances, $coachProjects, $settings) {
  247. // 设置距离
  248. $order->distance = $orderDistances[$order->id];
  249. // 处理代理商项目价格
  250. $this->processAgentPrice($order);
  251. // 处理技师项目信息
  252. $this->processCoachProject($order, $coachProjects, $settings);
  253. });
  254. }
  255. /**
  256. * 处理代理商价格
  257. */
  258. private function processAgentPrice($order): void
  259. {
  260. if ($order->agent_id && $order->agent && $order->agent->projects) {
  261. $agentProject = $order->agent->projects
  262. ->where('id', $order->project_id)
  263. ->where('state', 1)
  264. ->first();
  265. if ($agentProject) {
  266. $order->project->price = $agentProject->agent_price;
  267. $order->project->duration = $agentProject->duration;
  268. }
  269. }
  270. }
  271. /**
  272. * 处理技师项目信息
  273. */
  274. private function processCoachProject($order, $coachProjects, array $settings): void
  275. {
  276. $coachProject = $coachProjects->where('project_id', $order->project_id)->first();
  277. if (! $coachProject) {
  278. return;
  279. }
  280. // 计算技师佣金
  281. $order->project->coach_income = round($order->project->price * $settings['coach_income'], 2);
  282. // 处理优惠金额
  283. if ($coachProject->discount_amount > 0) {
  284. $order->project->discount_amount = $coachProject->discount_amount;
  285. $order->project->final_price = $order->project->price - $coachProject->discount_amount;
  286. }
  287. // 处理路费
  288. if ($coachProject->traffic_fee > 0) {
  289. $this->calculateTrafficFee($order, $coachProject, $settings);
  290. }
  291. }
  292. /**
  293. * 计算路费
  294. */
  295. private function calculateTrafficFee($order, $coachProject, array $settings): void
  296. {
  297. $trafficFee = $settings['base_price'];
  298. if ($order->distance > $settings['min_distance']) {
  299. $trafficFee += ($order->distance - $settings['min_distance']) * $settings['per_km_price'];
  300. }
  301. $order->project->traffic_fee = round($trafficFee, 2);
  302. // 程收费
  303. if ($coachProject->is_round_trip) {
  304. $order->project->traffic_fee *= 2;
  305. }
  306. // 计算最终价格
  307. $order->project->final_price = ($order->project->final_price ?? $order->project->price)
  308. + $order->project->traffic_fee;
  309. }
  310. /**
  311. * 技师抢单
  312. *
  313. * @param int $userId 技师用户ID
  314. * @param int $orderId 订单ID
  315. * @return array
  316. */
  317. public function grabOrder(int $userId, int $orderId)
  318. {
  319. return DB::transaction(function () use ($userId, $orderId) {
  320. try {
  321. // 加载用户和技师信息
  322. $user = MemberUser::with([
  323. 'coach',
  324. 'coach.info',
  325. 'coach.real',
  326. 'coach.qual',
  327. 'coach.locations',
  328. 'coach.projects',
  329. ])->findOrFail($userId);
  330. // 验证技师信息
  331. [$coach, $location] = $this->validateCoach($user);
  332. // 获取订单信息
  333. $order = Order::lockForUpdate()->findOrFail($orderId);
  334. // 验证订单状态
  335. abort_if($order->state !== OrderStatus::CREATED->value, 400, '订单状态异常,无法抢单');
  336. // 验证订单类型
  337. abort_if($order->type !== OrderType::GRAB->value, 400, '该订单不是抢单类型');
  338. // 检查技师是否已参与抢单
  339. $existingGrab = $coach->grabRecords()
  340. ->where('order_id', $orderId)
  341. ->first();
  342. abort_if($existingGrab, 400, '您已参与抢单,请勿重复操作');
  343. // 验证订单是否在技师服务范围内
  344. // 通过Redis GEO计算订单与技师的距离
  345. $distance = Redis::geodist(
  346. 'order_locations',
  347. 'order:' . $order->id,
  348. $location->longitude . ',' . $location->latitude,
  349. 'km'
  350. ) ?? PHP_FLOAT_MAX;
  351. abort_if($distance > self::MAX_DISTANCE, 400, '订单超出服务范围');
  352. // 验证技师是否具备该项目服务资格
  353. $coachProject = $coach->projects()
  354. ->where('project_id', $order->project_id)
  355. ->where('state', ProjectStatus::OPEN->value)
  356. ->first();
  357. abort_if(! $coachProject, 400, '未开通该项目服务资格');
  358. // 添加抢单记录
  359. OrderGrabRecord::create([
  360. 'order_id' => $order->id,
  361. 'coach_id' => $coach->id,
  362. 'distance' => $distance,
  363. 'state' => OrderGrabRecordStatus::JOINED->value,
  364. 'created_at' => now(),
  365. ]);
  366. // 记录日志
  367. Log::info('技师参与抢单', [
  368. 'user_id' => $userId,
  369. 'coach_id' => $coach->id,
  370. 'order_id' => $orderId,
  371. ]);
  372. return [
  373. 'message' => '已参与抢单',
  374. 'order_id' => $orderId,
  375. ];
  376. } catch (\Exception $e) {
  377. Log::error('技师参与抢单失败', [
  378. 'user_id' => $userId,
  379. 'order_id' => $orderId,
  380. 'error_message' => $e->getMessage(),
  381. 'error_trace' => $e->getTraceAsString(),
  382. ]);
  383. throw $e;
  384. }
  385. });
  386. }
  387. /**
  388. * 技师接单
  389. *
  390. * @param int $userId 技师用户ID
  391. * @param int $orderId 订单ID
  392. */
  393. public function acceptOrder(int $userId, int $orderId): array
  394. {
  395. return DB::transaction(function () use ($userId, $orderId) {
  396. try {
  397. // 加载用户和技师信息
  398. $user = MemberUser::with([
  399. 'coach',
  400. 'coach.info',
  401. 'coach.real',
  402. 'coach.qual',
  403. ])->findOrFail($userId);
  404. // 验证技师信息
  405. [$coach, $location] = $this->validateCoach($user);
  406. // 获取订单信息并加锁
  407. $order = Order::lockForUpdate()->findOrFail($orderId);
  408. // 验证订单状态
  409. abort_if($order->state !== OrderStatus::PAID->value, 400, '订单状态异常,无法接单');
  410. // 验证订单是否分配给该技师
  411. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  412. // 更新订单状态
  413. $order->state = OrderStatus::ACCEPTED->value;
  414. $order->save();
  415. // 清理技师相关缓存
  416. $this->clearCoachCache($coach->id, $order->service_time);
  417. // 记录日志
  418. Log::info('技师接单成功', [
  419. 'user_id' => $userId,
  420. 'coach_id' => $coach->id,
  421. 'order_id' => $orderId,
  422. 'order_no' => $order->order_no,
  423. ]);
  424. return [
  425. 'message' => '接单成功',
  426. 'order_id' => $orderId,
  427. 'order_no' => $order->order_no,
  428. ];
  429. } catch (\Exception $e) {
  430. Log::error('技师接单失败', [
  431. 'user_id' => $userId,
  432. 'order_id' => $orderId,
  433. 'error' => $e->getMessage(),
  434. 'file' => $e->getFile(),
  435. 'line' => $e->getLine(),
  436. ]);
  437. throw $e;
  438. }
  439. });
  440. }
  441. /**
  442. * 技师拒单
  443. *
  444. * @param int $userId 用户ID
  445. * @param int $orderId 订单ID
  446. * @param string $reason 拒单原因
  447. */
  448. public function rejectOrder(int $userId, int $orderId, string $reason): array
  449. {
  450. return DB::transaction(function () use ($userId, $orderId, $reason) {
  451. try {
  452. // 获取技师信息(优化关联加载)
  453. $user = MemberUser::with([
  454. 'coach',
  455. 'coach.info',
  456. 'coach.real',
  457. 'coach.qual',
  458. ])->findOrFail($userId);
  459. // 验证技师信息
  460. [$coach, $location] = $this->validateCoach($user);
  461. // 获取订单信息并加锁
  462. $order = Order::lockForUpdate()->findOrFail($orderId);
  463. // 验证订单状态(修正状态判断)
  464. abort_if(! in_array($order->state, [
  465. OrderStatus::ASSIGNED->value,
  466. OrderStatus::PAID->value,
  467. ]), 400, '订单状态异常,无法拒单');
  468. // 验证订单是否分配给该技师
  469. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  470. // 检查拒单次数限制
  471. $rejectCount = OrderRecord::where('object_id', $coach->id)
  472. ->where('object_type', CoachUser::class)
  473. ->where('state', OrderRecordStatus::REJECTED->value)
  474. ->whereDate('created_at', today())
  475. ->count();
  476. // 更新订单状态
  477. $order->update([
  478. 'state' => OrderStatus::REJECTED->value,
  479. ]);
  480. // 创建订单记录
  481. OrderRecord::create([
  482. 'order_id' => $order->id,
  483. 'object_id' => $coach->id,
  484. 'object_type' => CoachUser::class,
  485. 'state' => OrderRecordStatus::REJECTED->value,
  486. 'remark' => $reason,
  487. ]);
  488. // 发送消息通知
  489. try {
  490. // event(new OrderRejectedEvent($order, $coach, $reason));
  491. } catch (\Exception $e) {
  492. Log::error('发送拒单通知失败', [
  493. 'order_id' => $orderId,
  494. 'coach_id' => $coach->id,
  495. 'error' => $e->getMessage(),
  496. ]);
  497. }
  498. // 记录日志
  499. Log::info('技师拒单成功', [
  500. 'user_id' => $userId,
  501. 'coach_id' => $coach->id,
  502. 'order_id' => $orderId,
  503. 'order_no' => $order->order_no,
  504. 'reason' => $reason,
  505. 'reject_count' => $rejectCount + 1,
  506. ]);
  507. return [
  508. 'message' => '拒单成功',
  509. 'order_id' => $orderId,
  510. 'order_no' => $order->order_no,
  511. 'reject_count' => $rejectCount + 1,
  512. 'max_reject_count' => 5,
  513. ];
  514. } catch (\Exception $e) {
  515. Log::error('技师拒单失败', [
  516. 'user_id' => $userId,
  517. 'order_id' => $orderId,
  518. 'reason' => $reason,
  519. 'error' => $e->getMessage(),
  520. 'file' => $e->getFile(),
  521. 'line' => $e->getLine(),
  522. ]);
  523. throw $e;
  524. }
  525. });
  526. }
  527. /**
  528. * 技师出发
  529. *
  530. * @param int $userId 技师用户ID
  531. * @param int $orderId 订单ID
  532. */
  533. public function depart(int $userId, int $orderId): array
  534. {
  535. try {
  536. return DB::transaction(function () use ($userId, $orderId) {
  537. // 获取技师信息
  538. $user = MemberUser::with(['coach'])->findOrFail($userId);
  539. // 获取订单信息
  540. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  541. // 检查订单是否存在
  542. abort_if(! $order, 404, '订单不存在');
  543. // 检查是否是该技师的订单
  544. abort_if($order->coach_id !== $user->coach->id, 403, '无权操作此订单');
  545. // 根据订单类型判断订单状态
  546. if ($order->type == OrderType::VISIT->value) {
  547. abort_if($order->state != OrderStatus::ACCEPTED->value, 400, '订单状态不正确');
  548. } elseif ($order->type == OrderType::GRAB->value) {
  549. abort_if($order->state != OrderStatus::PAID->value, 400, '订单状态不正确');
  550. }
  551. // 更新订单状态为技师出发
  552. $order->state = OrderStatus::DEPARTED->value;
  553. $order->save();
  554. // 记录订单状态变更日志
  555. OrderRecord::create([
  556. 'order_id' => $orderId,
  557. 'state' => OrderRecordStatus::DEPARTED->value,
  558. 'object_id' => $user->coach->id,
  559. 'object_type' => CoachUser::class,
  560. 'remark' => '技师已出发',
  561. ]);
  562. // 发送通知给用户
  563. // TODO: 发送通知
  564. // event(new TechnicianDepartedEvent($order));
  565. return [
  566. 'status' => true,
  567. 'message' => '操作成功',
  568. 'data' => [
  569. 'order_id' => $orderId,
  570. 'status' => $order->state,
  571. 'created_at' => $order->created_at,
  572. ],
  573. ];
  574. });
  575. } catch (\Exception $e) {
  576. \Log::error('技师出发失败', [
  577. 'user_id' => $userId,
  578. 'order_id' => $orderId,
  579. 'error' => $e->getMessage(),
  580. ]);
  581. throw $e;
  582. }
  583. }
  584. /**
  585. * 技师到达
  586. *
  587. * @param int $userId 技师用户ID
  588. * @param int $orderId 订单ID
  589. */
  590. public function arrive(int $userId, int $orderId): array
  591. {
  592. return DB::transaction(function () use ($userId, $orderId) {
  593. try {
  594. // 获取技师信息
  595. $user = MemberUser::with(['coach'])->findOrFail($userId);
  596. $coach = $user->coach;
  597. abort_if(! $coach, 404, '技师信息不存在');
  598. // 获取订单信息
  599. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  600. abort_if(! $order, 404, '订单不存在');
  601. // 检查是否是该技师的订单
  602. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  603. // 检查订单状态
  604. abort_if(! in_array($order->state, [
  605. OrderStatus::DEPARTED->value,
  606. ]), 400, '订单状态不正确');
  607. $now = now();
  608. // 更新订单状态为技师到达
  609. $order->state = OrderStatus::ARRIVED->value;
  610. $order->save();
  611. // 记录订单状态变更日志
  612. OrderRecord::create([
  613. 'order_id' => $orderId,
  614. 'state' => OrderRecordStatus::ARRIVED->value,
  615. 'object_id' => $coach->id,
  616. 'object_type' => CoachUser::class,
  617. 'remark' => '技师已到达',
  618. ]);
  619. // 更新技师当前位置到Redis GEO
  620. try {
  621. Redis::geoadd(
  622. 'coach_locations',
  623. $order->longitude,
  624. $order->latitude,
  625. $coach->id . '_' . TechnicianLocationType::CURRENT->value
  626. );
  627. } catch (\Exception $e) {
  628. Log::error('更新技师位置失败', [
  629. 'coach_id' => $coach->id,
  630. 'order_id' => $orderId,
  631. 'error' => $e->getMessage(),
  632. ]);
  633. }
  634. // TODO: 发送通知给用户
  635. // event(new TechnicianArrivedEvent($order));
  636. Log::info('技师到达成功', [
  637. 'coach_id' => $coach->id,
  638. 'order_id' => $orderId,
  639. 'arrived_at' => $now,
  640. ]);
  641. return [
  642. 'status' => true,
  643. 'message' => '操作成功',
  644. 'data' => [
  645. 'order_id' => $orderId,
  646. 'status' => $order->state,
  647. 'arrived_at' => $now,
  648. ],
  649. ];
  650. } catch (\Exception $e) {
  651. Log::error('技师到达失败', [
  652. 'user_id' => $userId,
  653. 'order_id' => $orderId,
  654. 'error' => $e->getMessage(),
  655. 'trace' => $e->getTraceAsString(),
  656. ]);
  657. throw $e;
  658. }
  659. });
  660. }
  661. /**
  662. * 技师扫码开始服务
  663. *
  664. * @param int $userId 技师用户ID
  665. * @param int $orderId 订单ID
  666. * @param string $qrCode 客户二维码
  667. */
  668. public function startService(int $userId, int $orderId, string $qrCode): array
  669. {
  670. return DB::transaction(function () use ($userId, $orderId, $qrCode) {
  671. try {
  672. // 获取技师信息
  673. $user = MemberUser::with(['coach'])->findOrFail($userId);
  674. $coach = $user->coach;
  675. abort_if(! $coach, 404, '技师信息不存在');
  676. // 获取订单信息
  677. $order = Order::query()
  678. ->where('id', $orderId)
  679. ->lockForUpdate()
  680. ->first();
  681. abort_if(! $order, 404, '订单不存在');
  682. // 检查是否是该技师的订单
  683. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  684. // 检查订单状态
  685. abort_if(! in_array($order->state, [
  686. OrderStatus::ARRIVED->value,
  687. OrderStatus::PAID->value,
  688. ]), 400, '订单状态不正确');
  689. // 验证二维码
  690. $this->validateQrCode($order, $qrCode);
  691. $now = now();
  692. // 更新订单状态为服务中
  693. $order->state = OrderStatus::SERVICING->value;
  694. $order->service_start_time = $now;
  695. $order->service_end_time = $now->copy()->addMinutes($order->project_duration);
  696. $order->save();
  697. // 记录订单状态变更日志
  698. OrderRecord::create([
  699. 'order_id' => $orderId,
  700. 'state' => OrderRecordStatus::STARTED->value,
  701. 'object_id' => $coach->id,
  702. 'object_type' => CoachUser::class,
  703. 'remark' => '开始服务',
  704. ]);
  705. // 获取项目服务时长(分钟)
  706. $duration = $order->project->duration ?? 60;
  707. // 派发延迟任务,在服务时长到期后自动完成订单
  708. AutoFinishOrder::dispatch($order)
  709. ->delay(now()->addMinutes($duration));
  710. // TODO: 发送通知给用户
  711. // event(new ServiceStartedEvent($order));
  712. Log::info('技师开始服务', [
  713. 'coach_id' => $coach->id,
  714. 'order_id' => $orderId,
  715. 'service_start_time' => $now,
  716. ]);
  717. return [
  718. 'status' => true,
  719. 'message' => '开始服务成功',
  720. 'data' => [
  721. 'order_id' => $orderId,
  722. 'status' => $order->state,
  723. 'service_start_time' => $now,
  724. ],
  725. ];
  726. } catch (\Exception $e) {
  727. Log::error('开始服务失败', [
  728. 'user_id' => $userId,
  729. 'order_id' => $orderId,
  730. 'qr_code' => $qrCode,
  731. 'error' => $e->getMessage(),
  732. 'trace' => $e->getTraceAsString(),
  733. ]);
  734. throw $e;
  735. }
  736. });
  737. }
  738. /**
  739. * 验证客户二维码
  740. *
  741. * @param Order $order 订单对象
  742. * @param string $qrCode 扫描的二维码
  743. */
  744. private function validateQrCode(Order $order, string $qrCode): void
  745. {
  746. // 二维码格式: order_{order_id}_{timestamp}_{sign}
  747. $parts = explode('_', $qrCode);
  748. abort_if(count($parts) !== 4, 400, '二维码格式错误');
  749. [$prefix, $scanOrderId, $timestamp, $sign] = $parts;
  750. // 验证前缀
  751. abort_if($prefix !== 'order', 400, '无效的二维码');
  752. // 验证订单ID
  753. abort_if((int) $scanOrderId !== $order->id, 400, '二维码与订单不匹配');
  754. // 验证时间戳(二维码5分钟内有效)
  755. // $qrTimestamp = (int) $timestamp;
  756. // $now = time();
  757. // abort_if($now - $qrTimestamp > 300, 400, '二维码已过期');
  758. // 验证签名
  759. $correctSign = md5("order_{$order->id}_{$timestamp}_" . config('app.key'));
  760. abort_if($sign !== $correctSign, 400, '二维码签名错误');
  761. }
  762. /**
  763. * 技师撤离
  764. *
  765. * @param int $userId 技师户ID
  766. * @param int $orderId 订单ID
  767. *
  768. * @throws \Exception
  769. */
  770. public function leave(int $userId, int $orderId): array
  771. {
  772. return DB::transaction(function () use ($userId, $orderId) {
  773. try {
  774. // 获取技师信息,同时加载钱包关联
  775. $user = MemberUser::with(['coach', 'coach.wallet'])->findOrFail($userId);
  776. $coach = $user->coach;
  777. abort_if(! $coach, 404, '技师信息不存在');
  778. $coach->load('wallet');
  779. // 获取订单信息
  780. $order = Order::query()
  781. ->with(['coach', 'coach.wallet'])
  782. ->where('id', $orderId)
  783. ->lockForUpdate()
  784. ->firstOrFail();
  785. // 验证订单状态和权限
  786. $this->validateLeaveOrder($order, $coach);
  787. // 更新订单状态
  788. $this->updateOrderStatus($order);
  789. // 记录订单状态变更
  790. $this->createLeaveRecord($order, $coach);
  791. // 处理订单分佣
  792. $this->commissionService->handleOrderCommission($order);
  793. // 清理技师位置信息
  794. $this->cleanCoachLocation($coach);
  795. // TODO: 发送通知给用户
  796. // event(new ServiceCompletedEvent($order));
  797. return [
  798. 'status' => true,
  799. 'message' => '撤离成功',
  800. 'data' => [
  801. 'order_id' => $orderId,
  802. 'state' => $order->state,
  803. 'leave_time' => now(),
  804. ],
  805. ];
  806. } catch (\Exception $e) {
  807. Log::error('技师撤离失败', [
  808. 'user_id' => $userId,
  809. 'order_id' => $orderId,
  810. 'error' => $e->getMessage(),
  811. 'trace' => $e->getTraceAsString(),
  812. ]);
  813. throw $e;
  814. }
  815. });
  816. }
  817. /**
  818. * 验证撤离订单
  819. */
  820. private function validateLeaveOrder(Order $order, CoachUser $coach): void
  821. {
  822. // 检查是否是该技师的订单
  823. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  824. // 检查订单状态
  825. abort_if(! in_array($order->state, [
  826. OrderStatus::LEAVING->value,
  827. ]), 400, '订单状态不正确,无法撤离');
  828. // 检查钱包状态
  829. abort_if(! $coach->wallet, 400, '技师钱包信息不存在');
  830. }
  831. /**
  832. * 更新订单状态
  833. */
  834. private function updateOrderStatus(Order $order): void
  835. {
  836. $order->state = OrderStatus::LEFT->value;
  837. $order->save();
  838. }
  839. /**
  840. * 创建撤离记录
  841. */
  842. private function createLeaveRecord(Order $order, CoachUser $coach): void
  843. {
  844. OrderRecord::create([
  845. 'order_id' => $order->id,
  846. 'state' => OrderRecordStatus::COMPLETED->value,
  847. 'object_id' => $coach->id,
  848. 'object_type' => CoachUser::class,
  849. 'remark' => '技师已撤离,服务完成',
  850. ]);
  851. }
  852. /**
  853. * 清理技师位置信息
  854. */
  855. private function cleanCoachLocation(CoachUser $coach): void
  856. {
  857. try {
  858. Redis::zrem(
  859. 'coach_locations',
  860. $coach->id . '_' . TechnicianLocationType::CURRENT->value
  861. );
  862. } catch (\Exception $e) {
  863. Log::error('删除技师位置失败', [
  864. 'coach_id' => $coach->id,
  865. 'error' => $e->getMessage(),
  866. ]);
  867. // 不抛出异常,继续执行
  868. }
  869. }
  870. /**
  871. * 清理技师相关缓存
  872. *
  873. * @param int $coachId 技师ID
  874. * @param string|null $date 服务日期
  875. */
  876. private function clearCoachCache(int $coachId, ?string $date = null): void
  877. {
  878. try {
  879. // 清理技师时间段缓存
  880. $date = $date ?: now()->toDateString();
  881. $cacheKey = "coach:timeslots:{$coachId}:{$date}";
  882. Cache::forget($cacheKey);
  883. Log::info('成功清理技师缓存', [
  884. 'coach_id' => $coachId,
  885. 'date' => $date,
  886. ]);
  887. } catch (\Exception $e) {
  888. Log::error('清理技师缓存失败', [
  889. 'coach_id' => $coachId,
  890. 'date' => $date,
  891. 'error' => $e->getMessage(),
  892. ]);
  893. // 缓存清理失败不影响主流程
  894. }
  895. }
  896. /**
  897. * 获取设置项
  898. *
  899. * @param string $groupCode 设置组编码
  900. * @param string $itemCode 设置项编码
  901. * @return object 设置项对象
  902. * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  903. */
  904. private function getSettingItem(string $groupCode, string $itemCode): object
  905. {
  906. // 获取设置组
  907. $settingGroup = SettingGroup::where('code', $groupCode)->first();
  908. abort_if(!$settingGroup, 404, "设置组[{$groupCode}]不存在");
  909. // 获取设置项
  910. $settingItem = $settingGroup->items()->where('code', $itemCode)->first();
  911. abort_if(!$settingItem, 404, "设置项[{$itemCode}]不存在");
  912. return $settingItem;
  913. }
  914. /**
  915. * 更新订单设置
  916. *
  917. * 业务流程:
  918. * 1. 获取订单配置项
  919. * 2. 验证服务距离范围
  920. * 3. 更新技师设置
  921. * 4. 返回最新设置
  922. *
  923. * @param CoachUser $coach 技师对象
  924. * @param array $data 设置数据,包含:
  925. * - distance: float 服务距离(公里)
  926. * @return array 返回最新设置信息,包含:
  927. * - distance: float 当前服务距离
  928. * - distance_max: float 最大服务距离限制
  929. * - distance_min: float 最小服务距离限制
  930. * @throws \Exception 当更新失败时抛出异常
  931. */
  932. public function setOrder(CoachUser $coach, array $data): array
  933. {
  934. return DB::transaction(function () use ($coach, $data) {
  935. // 获取订单距离配置项
  936. $distanceItem = $this->getSettingItem('order', 'distance');
  937. // 验证服务距离范围
  938. $distance = (float)$data['distance'];
  939. abort_if(
  940. $distance > $distanceItem->max_value || $distance < $distanceItem->min_value,
  941. 422,
  942. sprintf('服务距离必须在 %.1f-%.1f 公里之间', $distanceItem->min_value, $distanceItem->max_value)
  943. );
  944. // 更新技师服务距离设置
  945. $coach->settingValues()->updateOrCreate(
  946. [
  947. 'item_id' => $distanceItem->id,
  948. 'object_type' => $coach::class,
  949. 'object_id' => $coach->id,
  950. ],
  951. ['value' => $distance]
  952. );
  953. // 返回最新设置
  954. return [
  955. 'distance' => $distance,
  956. 'distance_max' => (float)$distanceItem->max_value,
  957. 'distance_min' => (float)$distanceItem->min_value,
  958. ];
  959. });
  960. }
  961. /**
  962. * 获取订单设置
  963. *
  964. * 业务流程:
  965. * 1. 获取技师的订单设置
  966. * 2. 获取系统配置的限制值
  967. * 3. 返回设置信息
  968. *
  969. * @param CoachUser $coach 技师对象
  970. * @return array 返回设置信息,包含:
  971. * - distance: float 当前服务距离
  972. * - distance_max: float 最大服务距离
  973. * - distance_min: float 最小服务距离
  974. * @throws \Exception 当获取失败时抛出异常
  975. */
  976. public function getOrderSettings(CoachUser $coach): array
  977. {
  978. // 获取订单距离配置项
  979. $distanceItem = $this->getSettingItem('order', 'distance');
  980. // 获取技师的距离设置
  981. $distanceSetting = $coach->settingValues()
  982. ->where('item_id', $distanceItem->id)
  983. ->first();
  984. // 返回设置信息
  985. return [
  986. 'distance' => (float)($distanceSetting?->value ?? $distanceItem->default_value), // 当前服务距离
  987. 'distance_max' => (float)$distanceItem->max_value, // 最大服务距离限制
  988. 'distance_min' => (float)$distanceItem->min_value, // 最小服务距离限制
  989. ];
  990. }
  991. /**
  992. * 格式化订单列表项
  993. */
  994. private function formatOrderListItem($order): array
  995. {
  996. return [
  997. 'id' => $order->id, // 订单ID
  998. 'order_no' => $order->order_no, // 订单编号
  999. 'state' => $order->state, // 订单状态值
  1000. 'state_text' => OrderStatus::fromValue($order->state)?->label(), // 订单状态文本
  1001. 'payment_type' => $order->payment_type, // 支付类型值
  1002. 'payment_type_text' => PaymentMethod::fromValue($order->payment_type)?->label(), // 支付类型文本
  1003. 'total_amount' => $order->total_amount, // 订单总金额
  1004. 'project_amount' => $order->project_amount, // 项目金额
  1005. 'traffic_amount' => $order->traffic_amount, // 交通费金额
  1006. 'discount_amount' => $order->discount_amount, // 优惠金额
  1007. 'pay_amount' => $order->pay_amount, // 实付金额
  1008. 'refund_amount' => $order->refund_amount, // 退款金额
  1009. 'type' => $order->type, // 订单类型值
  1010. 'type_text' => OrderType::fromValue($order->type)?->label(), // 订单类型文本
  1011. 'source' => $order->source, // 订单来源值
  1012. 'source_text' => OrderSource::fromValue($order->source)?->label(), // 订单来源文本
  1013. 'distance' => $order->distance, // 目的地距离(米)
  1014. 'service_time' => $order->service_time instanceof \Carbon\Carbon // 服务时间
  1015. ? $order->service_time->toDateTimeString()
  1016. : $order->service_time,
  1017. 'service_start_time' => $order->service_start_time instanceof \Carbon\Carbon // 服务开始时间
  1018. ? $order->service_start_time->toDateTimeString()
  1019. : $order->service_start_time,
  1020. 'service_end_time' => $order->service_end_time instanceof \Carbon\Carbon // 服务结束时间
  1021. ? $order->service_end_time->toDateTimeString()
  1022. : $order->service_end_time,
  1023. 'created_at' => $order->created_at instanceof \Carbon\Carbon // 创建时间
  1024. ? $order->created_at->toDateTimeString()
  1025. : $order->created_at,
  1026. 'project' => [ // 项目信息
  1027. 'id' => $order->project->id, // 项目ID
  1028. 'title' => $order->project->title, // 项目标题
  1029. 'cover' => $order->project->cover, // 项目封面图
  1030. 'duration' => $order->project->duration // 项目时长(分钟)
  1031. ],
  1032. 'user' => [ // 用户信息
  1033. 'id' => $order->user->id, // 用户ID
  1034. 'nickname' => $order->user->nickname, // 用户昵称
  1035. 'avatar' => $order->user->avatar, // 用户头像
  1036. 'mobile' => $order->user->mobile, // 用户手机号
  1037. ],
  1038. 'address' => [ // 服务地址信息
  1039. 'location' => $order->location, // 定位地址
  1040. 'address' => $order->address, // 详细地址
  1041. 'latitude' => $order->latitude, // 纬度
  1042. 'longitude' => $order->longitude, // 经度
  1043. ],
  1044. ];
  1045. }
  1046. /**
  1047. * 技师删除订单
  1048. *
  1049. * 业务逻辑:
  1050. * 1. 验证订单是否存在且分配给当前技师
  1051. * 2. 验证订单状态是否允许删除(已完成、已取消等)
  1052. * 3. 更新订单的coach_deleted_at字段
  1053. * 4. 记录删除操作
  1054. *
  1055. * @param int $coachId 技师ID
  1056. * @param int $orderId 订单ID
  1057. * @return array
  1058. * @throws \Exception
  1059. */
  1060. public function deleteOrder(int $coachId, int $orderId): array
  1061. {
  1062. return DB::transaction(function () use ($coachId, $orderId) {
  1063. // 验证订单是否存在且分配给当前技师
  1064. $order = Order::where('id', $orderId)
  1065. ->where('coach_id', $coachId)
  1066. ->whereNull('coach_deleted_at') // 确保订单未被技师删除
  1067. ->first();
  1068. abort_if(!$order, 404, '订单不存在');
  1069. // 验证订单状态是否允许删除
  1070. // 允许删除已完成、已取消、已退款等状态的订单
  1071. $allowedStates = [
  1072. OrderStatus::COMPLETED->value, // 已完成
  1073. OrderStatus::CANCELLED->value, // 已取消
  1074. OrderStatus::REFUNDED->value, // 已退款
  1075. OrderStatus::REJECTED->value, // 已拒单
  1076. ];
  1077. abort_if(!in_array($order->state, $allowedStates), 422, '当前订单状态不允许删除');
  1078. // 标记订单为技师已删除
  1079. $order->update([
  1080. 'coach_deleted_at' => now()
  1081. ]);
  1082. // 创建订单记录
  1083. OrderRecord::create([
  1084. 'order_id' => $order->id,
  1085. 'object_id' => $coachId,
  1086. 'object_type' => CoachUser::class,
  1087. 'state' => OrderRecordStatus::DELETED->value,
  1088. 'remark' => '技师删除订单'
  1089. ]);
  1090. return [
  1091. 'message' => '订单删除成功'
  1092. ];
  1093. });
  1094. }
  1095. }