OrderService.php 41 KB

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