OrderService.php 40 KB

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