OrderService.php 34 KB

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