OrderService.php 28 KB

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