OrderService.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. <?php
  2. namespace App\Services\Coach;
  3. use App\Enums\OrderGrabRecordStatus;
  4. use App\Enums\OrderRecordStatus;
  5. use App\Enums\OrderStatus;
  6. use App\Enums\OrderType;
  7. use App\Enums\ProjectStatus;
  8. use App\Enums\TechnicianAuthStatus;
  9. use App\Enums\TechnicianLocationType;
  10. use App\Enums\TechnicianStatus;
  11. use App\Jobs\AutoFinishOrder;
  12. use App\Models\CoachUser;
  13. use App\Models\MemberUser;
  14. use App\Models\Order;
  15. use App\Models\OrderGrabRecord;
  16. use App\Models\OrderRecord;
  17. use App\Services\SettingItemService;
  18. use Illuminate\Support\Facades\DB;
  19. use Illuminate\Support\Facades\Log;
  20. use Illuminate\Support\Facades\Redis;
  21. class OrderService
  22. {
  23. private const DEFAULT_PER_PAGE = 10;
  24. private const MAX_DISTANCE = 40; // 最大距离限制(公里)
  25. private SettingItemService $settingService;
  26. public function __construct(SettingItemService $settingService)
  27. {
  28. $this->settingService = $settingService;
  29. }
  30. /**
  31. * 获取技师的订单列表
  32. *
  33. * @param int $userId 技师用户ID
  34. * @param array $params 请求参数
  35. * @return \Illuminate\Pagination\LengthAwarePaginator
  36. */
  37. public function getOrderList(int $userId, array $params)
  38. {
  39. return DB::transaction(function () use ($userId, $params) {
  40. try {
  41. // 加载技师用户信息
  42. $user = MemberUser::findOrFail($userId);
  43. $coach = $user->coach;
  44. abort_if(! $coach, 404, '技师不存在');
  45. // 构建订单查询
  46. $query = $coach->orders()->whereNotIn('state', [
  47. OrderStatus::CREATED->value,
  48. OrderStatus::ASSIGNED->value,
  49. ])
  50. ->orderBy('created_at', 'desc');
  51. // 分页获取数据
  52. $paginator = $query->paginate(
  53. $params['per_page'] ?? 10,
  54. ['*'],
  55. 'page',
  56. $params['page'] ?? 1
  57. );
  58. // 需要加载关联数据
  59. $items = collect($paginator->items())->map(function ($order) {
  60. $order->type_text = OrderType::from($order->type)->label();
  61. $order->state_text = OrderStatus::from($order->state)->label();
  62. return $order;
  63. });
  64. $query->with(['project']); // 加载项目信息以便返回project_name等字段
  65. return [
  66. 'items' => $items,
  67. 'total' => $paginator->total(),
  68. ];
  69. } catch (\Exception $e) {
  70. Log::error('获取技师订单列表失败', [
  71. 'user_id' => $userId,
  72. 'error_message' => $e->getMessage(),
  73. 'error_trace' => $e->getTraceAsString(),
  74. ]);
  75. throw $e;
  76. }
  77. });
  78. }
  79. /**
  80. * 获取可抢订单列表
  81. *
  82. * @param int $userId 技师用户ID
  83. * @param array $params 请求参数
  84. * @return \Illuminate\Pagination\LengthAwarePaginator
  85. */
  86. public function getGrabList(int $userId, array $params)
  87. {
  88. try {
  89. // 加载用户和技师信息
  90. $user = MemberUser::with([
  91. 'coach',
  92. 'coach.info',
  93. 'coach.real',
  94. 'coach.qual',
  95. 'coach.locations',
  96. 'coach.projects',
  97. ])->findOrFail($userId);
  98. // 验证技师信息
  99. [$coach, $location] = $this->validateCoach($user);
  100. // 获取技师项目信息
  101. $coachProjects = $coach->projects;
  102. $coachProjectIds = $coachProjects->pluck('project_id')->toArray();
  103. // 获取系统配置
  104. $settings = $this->getSystemSettings();
  105. // 获取附近订单
  106. $orderDistances = $this->getNearbyOrders($location);
  107. // 构建订单查询
  108. $query = $this->buildOrderQuery($coachProjectIds, $orderDistances);
  109. // 处理订单数据
  110. $this->processOrderData($query, $orderDistances, $coachProjects, $settings);
  111. // 分页获取数据
  112. return $query->paginate(
  113. $params['per_page'] ?? 10,
  114. ['*'],
  115. 'page',
  116. $params['page'] ?? 1
  117. );
  118. } catch (\Exception $e) {
  119. Log::error('获取可抢订单列表失败', [
  120. 'user_id' => $userId,
  121. 'params' => $params,
  122. 'error' => $e->getMessage(),
  123. 'trace' => $e->getTraceAsString(),
  124. ]);
  125. throw $e;
  126. }
  127. }
  128. /**
  129. * 验证技师信息
  130. */
  131. private function validateCoach(MemberUser $user): array
  132. {
  133. $coach = $user->coach;
  134. abort_if(! $coach, 404, '技师不存在');
  135. abort_if(! $coach->info, 404, '技师信息不存在');
  136. abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状态异常');
  137. abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过');
  138. abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过');
  139. $location = $coach->locations()
  140. ->where('type', TechnicianLocationType::COMMON->value)
  141. ->first();
  142. abort_if(! $location, 404, '技师定位地址不存在');
  143. return [$coach, $location];
  144. }
  145. /**
  146. * 获取系统配置
  147. */
  148. private function getSystemSettings(): array
  149. {
  150. return [
  151. 'coach_income' => $this->settingService->getItemDetail('coach_income')->default_value ?? 0,
  152. 'min_distance' => $this->settingService->getItemDetail('qibugongli')->default_value ?? 0,
  153. 'base_price' => $this->settingService->getItemDetail('qibujia')->default_value ?? 0,
  154. 'per_km_price' => $this->settingService->getItemDetail('meigonglijiage')->default_value ?? 0,
  155. ];
  156. }
  157. /**
  158. * 获取附近订单
  159. */
  160. private function getNearbyOrders($location, float $maxDistance = 40): array
  161. {
  162. $nearbyOrders = Redis::georadius(
  163. 'order_locations',
  164. $location->longitude,
  165. $location->latitude,
  166. $maxDistance,
  167. 'km',
  168. ['WITHCOORD', 'WITHDIST']
  169. );
  170. if (! $nearbyOrders) {
  171. Log::warning('获取附近订单失败', [
  172. 'location' => [
  173. 'longitude' => $location->longitude,
  174. 'latitude' => $location->latitude,
  175. ],
  176. ]);
  177. return [];
  178. }
  179. $orderDistances = [];
  180. foreach ($nearbyOrders as $order) {
  181. $orderId = str_replace('order:', '', $order[0]);
  182. $distance = round($order[1], 1);
  183. $orderDistances[$orderId] = $distance;
  184. }
  185. return $orderDistances;
  186. }
  187. /**
  188. * 构建订单查询
  189. */
  190. private function buildOrderQuery(array $coachProjectIds, array $orderDistances)
  191. {
  192. return Order::query()
  193. ->select([
  194. 'id',
  195. 'order_no',
  196. 'project_id',
  197. 'location',
  198. 'address',
  199. 'latitude',
  200. 'longitude',
  201. 'service_time',
  202. 'created_at',
  203. 'agent_id',
  204. DB::raw('0 as distance'),
  205. ])
  206. ->with(['project', 'agent.projects'])
  207. ->where('state', OrderStatus::CREATED)
  208. ->whereIn('project_id', $coachProjectIds)
  209. ->whereIn('id', array_keys($orderDistances))
  210. ->orderBy('created_at', 'desc');
  211. }
  212. /**
  213. * 处理订单数据
  214. */
  215. private function processOrderData($query, array $orderDistances, $coachProjects, array $settings): void
  216. {
  217. $query->get()->each(function ($order) use ($orderDistances, $coachProjects, $settings) {
  218. // 设置距离
  219. $order->distance = $orderDistances[$order->id];
  220. // 处理代理商项目价格
  221. $this->processAgentPrice($order);
  222. // 处理技师项目信息
  223. $this->processCoachProject($order, $coachProjects, $settings);
  224. });
  225. }
  226. /**
  227. * 处理代理商价格
  228. */
  229. private function processAgentPrice($order): void
  230. {
  231. if ($order->agent_id && $order->agent && $order->agent->projects) {
  232. $agentProject = $order->agent->projects
  233. ->where('id', $order->project_id)
  234. ->where('state', 1)
  235. ->first();
  236. if ($agentProject) {
  237. $order->project->price = $agentProject->agent_price;
  238. $order->project->duration = $agentProject->duration;
  239. }
  240. }
  241. }
  242. /**
  243. * 处理技师项目信息
  244. */
  245. private function processCoachProject($order, $coachProjects, array $settings): void
  246. {
  247. $coachProject = $coachProjects->where('project_id', $order->project_id)->first();
  248. if (! $coachProject) {
  249. return;
  250. }
  251. // 计算技师佣金
  252. $order->project->coach_income = round($order->project->price * $settings['coach_income'], 2);
  253. // 处理优惠金额
  254. if ($coachProject->discount_amount > 0) {
  255. $order->project->discount_amount = $coachProject->discount_amount;
  256. $order->project->final_price = $order->project->price - $coachProject->discount_amount;
  257. }
  258. // 处理路费
  259. if ($coachProject->traffic_fee > 0) {
  260. $this->calculateTrafficFee($order, $coachProject, $settings);
  261. }
  262. }
  263. /**
  264. * 计算路费
  265. */
  266. private function calculateTrafficFee($order, $coachProject, array $settings): void
  267. {
  268. $trafficFee = $settings['base_price'];
  269. if ($order->distance > $settings['min_distance']) {
  270. $trafficFee += ($order->distance - $settings['min_distance']) * $settings['per_km_price'];
  271. }
  272. $order->project->traffic_fee = round($trafficFee, 2);
  273. // 双程收费
  274. if ($coachProject->is_round_trip) {
  275. $order->project->traffic_fee *= 2;
  276. }
  277. // 计算最终价格
  278. $order->project->final_price = ($order->project->final_price ?? $order->project->price)
  279. + $order->project->traffic_fee;
  280. }
  281. /**
  282. * 技师抢单
  283. *
  284. * @param int $userId 技师用户ID
  285. * @param int $orderId 订单ID
  286. * @return array
  287. */
  288. public function grabOrder(int $userId, int $orderId)
  289. {
  290. return DB::transaction(function () use ($userId, $orderId) {
  291. try {
  292. // 加载用户和技师信息
  293. $user = MemberUser::with([
  294. 'coach',
  295. 'coach.info',
  296. 'coach.real',
  297. 'coach.qual',
  298. 'coach.locations',
  299. 'coach.projects',
  300. ])->findOrFail($userId);
  301. // 验证技师信息
  302. [$coach, $location] = $this->validateCoach($user);
  303. // 获取订单信息
  304. $order = Order::lockForUpdate()->findOrFail($orderId);
  305. // 验证订单状态
  306. abort_if($order->state !== OrderStatus::CREATED->value, 400, '订单状态异常,无法抢单');
  307. // 验证订单类型
  308. abort_if($order->type !== OrderType::GRAB->value, 400, '该订单不是抢单类型');
  309. // 检查技师是否已参与抢单
  310. $existingGrab = $coach->grabRecords()
  311. ->where('order_id', $orderId)
  312. ->first();
  313. abort_if($existingGrab, 400, '您已参与抢单,请勿重复操作');
  314. // 验证订单是否在技师服务范围内
  315. // 通过Redis GEO计算订单与技师的距离
  316. $distance = Redis::geodist(
  317. 'order_locations',
  318. 'order:'.$order->id,
  319. $location->longitude.','.$location->latitude,
  320. 'km'
  321. ) ?? PHP_FLOAT_MAX;
  322. abort_if($distance > self::MAX_DISTANCE, 400, '订单超出服务范围');
  323. // 验证技师是否具备该项目服务资格
  324. $coachProject = $coach->projects()
  325. ->where('project_id', $order->project_id)
  326. ->where('state', ProjectStatus::OPEN->value)
  327. ->first();
  328. abort_if(! $coachProject, 400, '未开通该项目服务资格');
  329. // 添加抢单记录
  330. OrderGrabRecord::create([
  331. 'order_id' => $order->id,
  332. 'coach_id' => $coach->id,
  333. 'distance' => $distance,
  334. 'state' => OrderGrabRecordStatus::JOINED->value,
  335. 'created_at' => now(),
  336. ]);
  337. // 记录日志
  338. Log::info('技师参与抢单', [
  339. 'user_id' => $userId,
  340. 'coach_id' => $coach->id,
  341. 'order_id' => $orderId,
  342. ]);
  343. return [
  344. 'message' => '已参与抢单',
  345. 'order_id' => $orderId,
  346. ];
  347. } catch (\Exception $e) {
  348. Log::error('技师参与抢单失败', [
  349. 'user_id' => $userId,
  350. 'order_id' => $orderId,
  351. 'error_message' => $e->getMessage(),
  352. 'error_trace' => $e->getTraceAsString(),
  353. ]);
  354. throw $e;
  355. }
  356. });
  357. }
  358. /**
  359. * 技师接单
  360. *
  361. * @param int $userId 技师用户ID
  362. * @param int $orderId 订单ID
  363. */
  364. public function acceptOrder(int $userId, int $orderId): array
  365. {
  366. return DB::transaction(function () use ($userId, $orderId) {
  367. try {
  368. // 加载用户和技师信息
  369. $user = MemberUser::with([
  370. 'coach',
  371. 'coach.info',
  372. 'coach.real',
  373. 'coach.qual',
  374. ])->findOrFail($userId);
  375. // 验证技师信息
  376. [$coach, $location] = $this->validateCoach($user);
  377. // 获取订单信息并加锁
  378. $order = Order::lockForUpdate()->findOrFail($orderId);
  379. // 验证订单状态
  380. abort_if($order->state !== OrderStatus::ASSIGNED->value, 400, '订单状态异常,无法接单');
  381. // 验证订单是否分配给该技师
  382. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  383. // 更新订单状态
  384. $order->update([
  385. 'state' => OrderStatus::ACCEPTED->value,
  386. 'accepted_at' => now(),
  387. ]);
  388. // 记录日志
  389. Log::info('技师接单成功', [
  390. 'user_id' => $userId,
  391. 'coach_id' => $coach->id,
  392. 'order_id' => $orderId,
  393. 'order_no' => $order->order_no,
  394. ]);
  395. return [
  396. 'message' => '接单成功',
  397. 'order_id' => $orderId,
  398. 'order_no' => $order->order_no,
  399. ];
  400. } catch (\Exception $e) {
  401. Log::error('技师接单失败', [
  402. 'user_id' => $userId,
  403. 'order_id' => $orderId,
  404. 'error' => $e->getMessage(),
  405. 'file' => $e->getFile(),
  406. 'line' => $e->getLine(),
  407. ]);
  408. throw $e;
  409. }
  410. });
  411. }
  412. /**
  413. * 技师拒单
  414. *
  415. * @param int $userId 用户ID
  416. * @param int $orderId 订单ID
  417. * @param string $reason 拒单原因
  418. */
  419. public function rejectOrder(int $userId, int $orderId, string $reason): array
  420. {
  421. return DB::transaction(function () use ($userId, $orderId, $reason) {
  422. try {
  423. // 获取技师信息(优化关联加载)
  424. $user = MemberUser::with([
  425. 'coach',
  426. 'coach.info',
  427. 'coach.real',
  428. 'coach.qual',
  429. ])->findOrFail($userId);
  430. // 验证技师信息
  431. [$coach, $location] = $this->validateCoach($user);
  432. // 获取订单信息并加锁
  433. $order = Order::lockForUpdate()->findOrFail($orderId);
  434. // 验证订单状态(修正状态判断)
  435. abort_if(! in_array($order->state, [
  436. OrderStatus::ASSIGNED->value,
  437. OrderStatus::PAID->value,
  438. ]), 400, '订单状态异常,无法拒单');
  439. // 验证订单是否分配给该技师
  440. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  441. // 检查拒单次数限制
  442. $rejectCount = OrderRecord::where('object_id', $coach->id)
  443. ->where('object_type', CoachUser::class)
  444. ->where('state', OrderRecordStatus::REJECTED->value)
  445. ->whereDate('created_at', today())
  446. ->count();
  447. // 更新订单状态
  448. $order->update([
  449. 'state' => OrderStatus::REJECTED->value,
  450. ]);
  451. // 创建订单记录
  452. OrderRecord::create([
  453. 'order_id' => $order->id,
  454. 'object_id' => $coach->id,
  455. 'object_type' => CoachUser::class,
  456. 'state' => OrderRecordStatus::REJECTED->value,
  457. 'remark' => $reason,
  458. ]);
  459. // 发送消息通知
  460. try {
  461. // event(new OrderRejectedEvent($order, $coach, $reason));
  462. } catch (\Exception $e) {
  463. Log::error('发送拒单通知失败', [
  464. 'order_id' => $orderId,
  465. 'coach_id' => $coach->id,
  466. 'error' => $e->getMessage(),
  467. ]);
  468. }
  469. // 记录日志
  470. Log::info('技师拒单成功', [
  471. 'user_id' => $userId,
  472. 'coach_id' => $coach->id,
  473. 'order_id' => $orderId,
  474. 'order_no' => $order->order_no,
  475. 'reason' => $reason,
  476. 'reject_count' => $rejectCount + 1,
  477. ]);
  478. return [
  479. 'message' => '拒单成功',
  480. 'order_id' => $orderId,
  481. 'order_no' => $order->order_no,
  482. 'reject_count' => $rejectCount + 1,
  483. 'max_reject_count' => 5,
  484. ];
  485. } catch (\Exception $e) {
  486. Log::error('技师拒单失败', [
  487. 'user_id' => $userId,
  488. 'order_id' => $orderId,
  489. 'reason' => $reason,
  490. 'error' => $e->getMessage(),
  491. 'file' => $e->getFile(),
  492. 'line' => $e->getLine(),
  493. ]);
  494. throw $e;
  495. }
  496. });
  497. }
  498. /**
  499. * 技师出发
  500. *
  501. * @param int $userId 技师用户ID
  502. * @param int $orderId 订单ID
  503. */
  504. public function depart(int $userId, int $orderId): array
  505. {
  506. try {
  507. return DB::transaction(function () use ($userId, $orderId) {
  508. // 获取技师信息
  509. $user = MemberUser::with(['coach'])->findOrFail($userId);
  510. // 获取订单信息
  511. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  512. // 检查订单是否存在
  513. abort_if(! $order, 404, '订单不存在');
  514. // 检查是否是该技师的订单
  515. abort_if($order->coach_id !== $user->coach->id, 403, '无权操作此订单');
  516. // 检查订单状态是否为已分配技师
  517. abort_if($order->status !== OrderStatus::ASSIGNED->value, 400, '订单状态不正确');
  518. // 更新订单状态为技师出发
  519. $order->state = OrderStatus::DEPARTED->value;
  520. $order->save();
  521. // 记录订单状态变更日志
  522. OrderRecord::create([
  523. 'order_id' => $orderId,
  524. 'state' => OrderRecordStatus::DEPARTED->value,
  525. 'operator_id' => $user->coach->id,
  526. 'operator_type' => CoachUser::class,
  527. 'remark' => '技师已出发',
  528. ]);
  529. // 发送通知给用户
  530. // TODO: 发送通知
  531. // event(new TechnicianDepartedEvent($order));
  532. return [
  533. 'status' => true,
  534. 'message' => '操作成功',
  535. 'data' => [
  536. 'order_id' => $orderId,
  537. 'status' => $order->status,
  538. 'created_at' => $order->created_at,
  539. ],
  540. ];
  541. });
  542. } catch (\Exception $e) {
  543. \Log::error('技师出发失败', [
  544. 'user_id' => $userId,
  545. 'order_id' => $orderId,
  546. 'error' => $e->getMessage(),
  547. ]);
  548. throw $e;
  549. }
  550. }
  551. /**
  552. * 技师到达
  553. *
  554. * @param int $userId 技师用户ID
  555. * @param int $orderId 订单ID
  556. */
  557. public function arrive(int $userId, int $orderId): array
  558. {
  559. return DB::transaction(function () use ($userId, $orderId) {
  560. try {
  561. // 获取技师信息
  562. $user = MemberUser::with(['coach'])->findOrFail($userId);
  563. $coach = $user->coach;
  564. abort_if(! $coach, 404, '技师信息不存在');
  565. // 获取订单信息
  566. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  567. abort_if(! $order, 404, '订单不存在');
  568. // 检查是否是该技师的订单
  569. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  570. // 检查订单状态
  571. abort_if(! in_array($order->state, [
  572. OrderStatus::DEPARTED->value,
  573. ]), 400, '订单状态不正确');
  574. $now = now();
  575. // 更新订单状态为技师到达
  576. $order->state = OrderStatus::ARRIVED->value;
  577. $order->save();
  578. // 记录订单状态变更日志
  579. OrderRecord::create([
  580. 'order_id' => $orderId,
  581. 'state' => OrderRecordStatus::ARRIVED->value,
  582. 'object_id' => $coach->id,
  583. 'object_type' => CoachUser::class,
  584. 'remark' => '技师已到达',
  585. ]);
  586. // 更新技师当前位置到Redis GEO
  587. try {
  588. Redis::geoadd(
  589. 'coach_locations',
  590. $order->longitude,
  591. $order->latitude,
  592. $coach->id.'_'.TechnicianLocationType::CURRENT->value
  593. );
  594. } catch (\Exception $e) {
  595. Log::error('更新技师位置失败', [
  596. 'coach_id' => $coach->id,
  597. 'order_id' => $orderId,
  598. 'error' => $e->getMessage(),
  599. ]);
  600. }
  601. // TODO: 发送通知给用户
  602. // event(new TechnicianArrivedEvent($order));
  603. Log::info('技师到达成功', [
  604. 'coach_id' => $coach->id,
  605. 'order_id' => $orderId,
  606. 'arrived_at' => $now,
  607. ]);
  608. return [
  609. 'status' => true,
  610. 'message' => '操作成功',
  611. 'data' => [
  612. 'order_id' => $orderId,
  613. 'status' => $order->state,
  614. 'arrived_at' => $now,
  615. ],
  616. ];
  617. } catch (\Exception $e) {
  618. Log::error('技师到达失败', [
  619. 'user_id' => $userId,
  620. 'order_id' => $orderId,
  621. 'error' => $e->getMessage(),
  622. 'trace' => $e->getTraceAsString(),
  623. ]);
  624. throw $e;
  625. }
  626. });
  627. }
  628. /**
  629. * 技师扫码开始服务
  630. *
  631. * @param int $userId 技师用户ID
  632. * @param int $orderId 订单ID
  633. * @param string $qrCode 客户二维码
  634. */
  635. public function startService(int $userId, int $orderId, string $qrCode): array
  636. {
  637. return DB::transaction(function () use ($userId, $orderId, $qrCode) {
  638. try {
  639. // 获取技师信息
  640. $user = MemberUser::with(['coach'])->findOrFail($userId);
  641. $coach = $user->coach;
  642. abort_if(! $coach, 404, '技师信息不存在');
  643. // 获取订单信息
  644. $order = Order::query()
  645. ->where('id', $orderId)
  646. ->lockForUpdate()
  647. ->first();
  648. abort_if(! $order, 404, '订单不存在');
  649. // 检查是否是该技师的订单
  650. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  651. // 检查订单状态
  652. abort_if(! in_array($order->state, [
  653. OrderStatus::ARRIVED->value,
  654. OrderStatus::PAID->value,
  655. ]), 400, '订单状态不正确');
  656. // 验证二维码
  657. $this->validateQrCode($order, $qrCode);
  658. $now = now();
  659. // 更新订单状态为服务中
  660. $order->state = OrderStatus::SERVING->value;
  661. $order->save();
  662. // 记录订单状态变更日志
  663. OrderRecord::create([
  664. 'order_id' => $orderId,
  665. 'state' => OrderRecordStatus::STARTED->value,
  666. 'object_id' => $coach->id,
  667. 'object_type' => CoachUser::class,
  668. 'remark' => '开始服务',
  669. ]);
  670. // 获取项目服务时长(分钟)
  671. $duration = $order->project->duration ?? 60;
  672. // 派发延迟任务,在服务时长到期后自动完成订单
  673. AutoFinishOrder::dispatch($order)
  674. ->delay(now()->addMinutes($duration));
  675. // TODO: 发送通知给用户
  676. // event(new ServiceStartedEvent($order));
  677. Log::info('技师开始服务', [
  678. 'coach_id' => $coach->id,
  679. 'order_id' => $orderId,
  680. 'service_start_time' => $now,
  681. ]);
  682. return [
  683. 'status' => true,
  684. 'message' => '开始服务成功',
  685. 'data' => [
  686. 'order_id' => $orderId,
  687. 'status' => $order->state,
  688. 'service_start_time' => $now,
  689. ],
  690. ];
  691. } catch (\Exception $e) {
  692. Log::error('开始服务失败', [
  693. 'user_id' => $userId,
  694. 'order_id' => $orderId,
  695. 'qr_code' => $qrCode,
  696. 'error' => $e->getMessage(),
  697. 'trace' => $e->getTraceAsString(),
  698. ]);
  699. throw $e;
  700. }
  701. });
  702. }
  703. /**
  704. * 验证客户二维码
  705. *
  706. * @param Order $order 订单对象
  707. * @param string $qrCode 扫描的二维码
  708. */
  709. private function validateQrCode(Order $order, string $qrCode): void
  710. {
  711. // 二维码格式: order_{order_id}_{timestamp}_{sign}
  712. $parts = explode('_', $qrCode);
  713. abort_if(count($parts) !== 4, 400, '二维码格式错误');
  714. [$prefix, $scanOrderId, $timestamp, $sign] = $parts;
  715. // 验证前缀
  716. abort_if($prefix !== 'order', 400, '无效的二维码');
  717. // 验证订单ID
  718. abort_if((int) $scanOrderId !== $order->id, 400, '二维码与订单不匹配');
  719. // 验证时间戳(二维码5分钟内有效)
  720. $qrTimestamp = (int) $timestamp;
  721. $now = time();
  722. abort_if($now - $qrTimestamp > 300, 400, '二维码已过期');
  723. // 验证签名
  724. $correctSign = md5("order_{$order->id}_{$timestamp}_".config('app.key'));
  725. abort_if($sign !== $correctSign, 400, '二维码签名错误');
  726. }
  727. }