OrderService.php 44 KB

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