OrderService.php 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  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. if ($order->type == OrderType::VISIT->value) {
  521. abort_if($order->state != OrderStatus::ACCEPTED->value, 400, '订单状态不正确');
  522. } elseif ($order->type == OrderType::GRAB->value) {
  523. abort_if($order->state != OrderStatus::PAID->value, 400, '订单状态不正确');
  524. }
  525. // 更新订单状态为技师出发
  526. $order->state = OrderStatus::DEPARTED->value;
  527. $order->save();
  528. // 记录订单状态变更日志
  529. OrderRecord::create([
  530. 'order_id' => $orderId,
  531. 'state' => OrderRecordStatus::DEPARTED->value,
  532. 'object_id' => $user->coach->id,
  533. 'object_type' => CoachUser::class,
  534. 'remark' => '技师已出发',
  535. ]);
  536. // 发送通知给用户
  537. // TODO: 发送通知
  538. // event(new TechnicianDepartedEvent($order));
  539. return [
  540. 'status' => true,
  541. 'message' => '操作成功',
  542. 'data' => [
  543. 'order_id' => $orderId,
  544. 'status' => $order->state,
  545. 'created_at' => $order->created_at,
  546. ],
  547. ];
  548. });
  549. } catch (\Exception $e) {
  550. \Log::error('技师出发失败', [
  551. 'user_id' => $userId,
  552. 'order_id' => $orderId,
  553. 'error' => $e->getMessage(),
  554. ]);
  555. throw $e;
  556. }
  557. }
  558. /**
  559. * 技师到达
  560. *
  561. * @param int $userId 技师用户ID
  562. * @param int $orderId 订单ID
  563. */
  564. public function arrive(int $userId, int $orderId): array
  565. {
  566. return DB::transaction(function () use ($userId, $orderId) {
  567. try {
  568. // 获取技师信息
  569. $user = MemberUser::with(['coach'])->findOrFail($userId);
  570. $coach = $user->coach;
  571. abort_if(! $coach, 404, '技师信息不存在');
  572. // 获取订单信息
  573. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  574. abort_if(! $order, 404, '订单不存在');
  575. // 检查是否是该技师的订单
  576. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  577. // 检查订单状态
  578. abort_if(! in_array($order->state, [
  579. OrderStatus::DEPARTED->value,
  580. ]), 400, '订单状态不正确');
  581. $now = now();
  582. // 更新订单状态为技师到达
  583. $order->state = OrderStatus::ARRIVED->value;
  584. $order->save();
  585. // 记录订单状态变更日志
  586. OrderRecord::create([
  587. 'order_id' => $orderId,
  588. 'state' => OrderRecordStatus::ARRIVED->value,
  589. 'object_id' => $coach->id,
  590. 'object_type' => CoachUser::class,
  591. 'remark' => '技师已到达',
  592. ]);
  593. // 更新技师当前位置到Redis GEO
  594. try {
  595. Redis::geoadd(
  596. 'coach_locations',
  597. $order->longitude,
  598. $order->latitude,
  599. $coach->id.'_'.TechnicianLocationType::CURRENT->value
  600. );
  601. } catch (\Exception $e) {
  602. Log::error('更新技师位置失败', [
  603. 'coach_id' => $coach->id,
  604. 'order_id' => $orderId,
  605. 'error' => $e->getMessage(),
  606. ]);
  607. }
  608. // TODO: 发送通知给用户
  609. // event(new TechnicianArrivedEvent($order));
  610. Log::info('技师到达成功', [
  611. 'coach_id' => $coach->id,
  612. 'order_id' => $orderId,
  613. 'arrived_at' => $now,
  614. ]);
  615. return [
  616. 'status' => true,
  617. 'message' => '操作成功',
  618. 'data' => [
  619. 'order_id' => $orderId,
  620. 'status' => $order->state,
  621. 'arrived_at' => $now,
  622. ],
  623. ];
  624. } catch (\Exception $e) {
  625. Log::error('技师到达失败', [
  626. 'user_id' => $userId,
  627. 'order_id' => $orderId,
  628. 'error' => $e->getMessage(),
  629. 'trace' => $e->getTraceAsString(),
  630. ]);
  631. throw $e;
  632. }
  633. });
  634. }
  635. /**
  636. * 技师扫码开始服务
  637. *
  638. * @param int $userId 技师用户ID
  639. * @param int $orderId 订单ID
  640. * @param string $qrCode 客户二维码
  641. */
  642. public function startService(int $userId, int $orderId, string $qrCode): array
  643. {
  644. return DB::transaction(function () use ($userId, $orderId, $qrCode) {
  645. try {
  646. // 获取技师信息
  647. $user = MemberUser::with(['coach'])->findOrFail($userId);
  648. $coach = $user->coach;
  649. abort_if(! $coach, 404, '技师信息不存在');
  650. // 获取订单信息
  651. $order = Order::query()
  652. ->where('id', $orderId)
  653. ->lockForUpdate()
  654. ->first();
  655. abort_if(! $order, 404, '订单不存在');
  656. // 检查是否是该技师的订单
  657. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  658. // 检查订单状态
  659. abort_if(! in_array($order->state, [
  660. OrderStatus::ARRIVED->value,
  661. OrderStatus::PAID->value,
  662. ]), 400, '订单状态不正确');
  663. // 验证二维码
  664. $this->validateQrCode($order, $qrCode);
  665. $now = now();
  666. // 更新订单状态为服务中
  667. $order->state = OrderStatus::SERVING->value;
  668. $order->save();
  669. // 记录订单状态变更日志
  670. OrderRecord::create([
  671. 'order_id' => $orderId,
  672. 'state' => OrderRecordStatus::STARTED->value,
  673. 'object_id' => $coach->id,
  674. 'object_type' => CoachUser::class,
  675. 'remark' => '开始服务',
  676. ]);
  677. // 获取项目服务时长(分钟)
  678. $duration = $order->project->duration ?? 60;
  679. // 派发延迟任务,在服务时长到期后自动完成订单
  680. AutoFinishOrder::dispatch($order)
  681. ->delay(now()->addMinutes($duration));
  682. // TODO: 发送通知给用户
  683. // event(new ServiceStartedEvent($order));
  684. Log::info('技师开始服务', [
  685. 'coach_id' => $coach->id,
  686. 'order_id' => $orderId,
  687. 'service_start_time' => $now,
  688. ]);
  689. return [
  690. 'status' => true,
  691. 'message' => '开始服务成功',
  692. 'data' => [
  693. 'order_id' => $orderId,
  694. 'status' => $order->state,
  695. 'service_start_time' => $now,
  696. ],
  697. ];
  698. } catch (\Exception $e) {
  699. Log::error('开始服务失败', [
  700. 'user_id' => $userId,
  701. 'order_id' => $orderId,
  702. 'qr_code' => $qrCode,
  703. 'error' => $e->getMessage(),
  704. 'trace' => $e->getTraceAsString(),
  705. ]);
  706. throw $e;
  707. }
  708. });
  709. }
  710. /**
  711. * 验证客户二维码
  712. *
  713. * @param Order $order 订单对象
  714. * @param string $qrCode 扫描的二维码
  715. */
  716. private function validateQrCode(Order $order, string $qrCode): void
  717. {
  718. // 二维码格式: order_{order_id}_{timestamp}_{sign}
  719. $parts = explode('_', $qrCode);
  720. abort_if(count($parts) !== 4, 400, '二维码格式错误');
  721. [$prefix, $scanOrderId, $timestamp, $sign] = $parts;
  722. // 验证前缀
  723. abort_if($prefix !== 'order', 400, '无效的二维码');
  724. // 验证订单ID
  725. abort_if((int) $scanOrderId !== $order->id, 400, '二维码与订单不匹配');
  726. // 验证时间戳(二维码5分钟内有效)
  727. $qrTimestamp = (int) $timestamp;
  728. $now = time();
  729. abort_if($now - $qrTimestamp > 300, 400, '二维码已过期');
  730. // 验证签名
  731. $correctSign = md5("order_{$order->id}_{$timestamp}_".config('app.key'));
  732. abort_if($sign !== $correctSign, 400, '二维码签名错误');
  733. }
  734. /**
  735. * 技师撤离
  736. *
  737. * @param int $userId 技师用户ID
  738. * @param int $orderId 订单ID
  739. *
  740. * @throws \Exception
  741. */
  742. public function leave(int $userId, int $orderId): array
  743. {
  744. return DB::transaction(function () use ($userId, $orderId) {
  745. try {
  746. // 获取技师信息
  747. $user = MemberUser::with(['coach'])->findOrFail($userId);
  748. $coach = $user->coach;
  749. abort_if(! $coach, 404, '技师信息不存在');
  750. // 获取订单信息
  751. $order = Order::query()
  752. ->with(['coach', 'coach.wallet'])
  753. ->where('id', $orderId)
  754. ->lockForUpdate()
  755. ->firstOrFail();
  756. // 验证订单状态和权限
  757. $this->validateLeaveOrder($order, $coach);
  758. // 更新订单状态
  759. $this->updateOrderStatus($order);
  760. // 记录订单状态变更
  761. $this->createLeaveRecord($order, $coach);
  762. // 处理订单分佣
  763. $this->commissionService->handleOrderCommission($order);
  764. // 清理技师位置信息
  765. $this->cleanCoachLocation($coach);
  766. // 记录日志
  767. Log::info('技师撤离成功', [
  768. 'coach_id' => $coach->id,
  769. 'order_id' => $orderId,
  770. 'leave_time' => now(),
  771. ]);
  772. // TODO: 发送通知给用户
  773. // event(new ServiceCompletedEvent($order));
  774. return [
  775. 'status' => true,
  776. 'message' => '撤离成功',
  777. 'data' => [
  778. 'order_id' => $orderId,
  779. 'state' => $order->state,
  780. 'leave_time' => now(),
  781. ],
  782. ];
  783. } catch (\Exception $e) {
  784. Log::error('技师撤离失败', [
  785. 'user_id' => $userId,
  786. 'order_id' => $orderId,
  787. 'error' => $e->getMessage(),
  788. 'trace' => $e->getTraceAsString(),
  789. ]);
  790. throw $e;
  791. }
  792. });
  793. }
  794. /**
  795. * 验证撤离订单
  796. */
  797. private function validateLeaveOrder(Order $order, CoachUser $coach): void
  798. {
  799. // 检查是否是该技师的订单
  800. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  801. // 检查订单状态
  802. abort_if(! in_array($order->state, [
  803. OrderStatus::LEFT->value,
  804. ]), 400, '订单状态不正确,无法撤离');
  805. // 检查钱包状态
  806. abort_if(! $coach->wallet, 400, '技师钱包信息不存在');
  807. }
  808. /**
  809. * 更新订单状态
  810. */
  811. private function updateOrderStatus(Order $order): void
  812. {
  813. $order->state = OrderStatus::COMPLETED->value;
  814. $order->save();
  815. }
  816. /**
  817. * 创建撤离记录
  818. */
  819. private function createLeaveRecord(Order $order, CoachUser $coach): void
  820. {
  821. OrderRecord::create([
  822. 'order_id' => $order->id,
  823. 'state' => OrderRecordStatus::COMPLETED->value,
  824. 'object_id' => $coach->id,
  825. 'object_type' => CoachUser::class,
  826. 'remark' => '技师已撤离,服务完成',
  827. ]);
  828. }
  829. /**
  830. * 清理技师位置信息
  831. */
  832. private function cleanCoachLocation(CoachUser $coach): void
  833. {
  834. try {
  835. Redis::zrem(
  836. 'coach_locations',
  837. $coach->id.'_'.TechnicianLocationType::CURRENT->value
  838. );
  839. } catch (\Exception $e) {
  840. Log::error('删除技师位置失败', [
  841. 'coach_id' => $coach->id,
  842. 'error' => $e->getMessage(),
  843. ]);
  844. // 不抛出异常,继续执行
  845. }
  846. }
  847. /**
  848. * 清理技师相关缓存
  849. *
  850. * @param int $coachId 技师ID
  851. * @param string|null $date 服务日期
  852. */
  853. private function clearCoachCache(int $coachId, ?string $date = null): void
  854. {
  855. try {
  856. // 清理技师时间段缓存
  857. $date = $date ?: now()->toDateString();
  858. $cacheKey = "coach:timeslots:{$coachId}:{$date}";
  859. Cache::forget($cacheKey);
  860. Log::info('成功清理技师缓存', [
  861. 'coach_id' => $coachId,
  862. 'date' => $date,
  863. ]);
  864. } catch (\Exception $e) {
  865. Log::error('清理技师缓存失败', [
  866. 'coach_id' => $coachId,
  867. 'date' => $date,
  868. 'error' => $e->getMessage(),
  869. ]);
  870. // 缓存清理失败不影响主流程
  871. }
  872. }
  873. }