OrderService.php 38 KB

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