OrderService.php 33 KB

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