OrderService.php 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. <?php
  2. namespace App\Services\Coach;
  3. use App\Enums\OrderGrabRecordStatus;
  4. use App\Enums\OrderRecordStatus;
  5. use App\Enums\OrderStatus;
  6. use App\Enums\OrderType;
  7. use App\Enums\ProjectStatus;
  8. use App\Enums\TechnicianAuthStatus;
  9. use App\Enums\TechnicianLocationType;
  10. use App\Enums\TechnicianStatus;
  11. use App\Jobs\AutoFinishOrder;
  12. use App\Models\CoachUser;
  13. use App\Models\MemberUser;
  14. use App\Models\Order;
  15. use App\Models\OrderGrabRecord;
  16. use App\Models\OrderRecord;
  17. use App\Models\SettingGroup;
  18. use App\Services\SettingItemService;
  19. use Illuminate\Support\Facades\Cache;
  20. use Illuminate\Support\Facades\DB;
  21. use Illuminate\Support\Facades\Log;
  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::SERVING->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 $coachId 技师ID
  878. * @param string $serviceTime 服务时间
  879. */
  880. public function validateServiceTime(int $coachId, string $serviceTime): bool
  881. {
  882. try {
  883. // 获取技师可服务时间段
  884. $availableTimeSlots = app(CoachService::class)->getSchedule($coachId);
  885. abort_if(empty($availableTimeSlots), 400, '技师无可用时间段');
  886. // 检测订单服务时间是否在技师可服务时间段内
  887. foreach ($availableTimeSlots as $timeSlot) {
  888. if ($serviceTime >= $timeSlot['start_time'] && $serviceTime <= $timeSlot['end_time']) {
  889. return true;
  890. }
  891. }
  892. abort(400, '服务时间不在技师可服务时间段内');
  893. } catch (\Exception $e) {
  894. Log::error('验证技师服务时间失败', [
  895. 'coach_id' => $coachId,
  896. 'service_time' => $serviceTime,
  897. 'error' => $e->getMessage(),
  898. 'trace' => $e->getTraceAsString(),
  899. ]);
  900. throw $e;
  901. }
  902. }
  903. /**
  904. * 订单设置
  905. *
  906. * @param int $userId 技师用户ID
  907. * @param array $data 订单设置数据
  908. * @return array
  909. *
  910. * @throws \Exception
  911. */
  912. public function setOrder(int $userId, array $data)
  913. {
  914. return DB::transaction(function () use ($userId, $data) {
  915. try {
  916. // 获取技师信息
  917. $user = MemberUser::with(['coach'])->findOrFail($userId);
  918. $coach = $user->coach;
  919. abort_if(! $coach, 404, '技师信息不存在');
  920. // 获取订单配置分组
  921. $settingGroup = SettingGroup::where('code', 'order')->first();
  922. abort_if(! $settingGroup, 404, '订单配置分组不存在');
  923. // 获取距离配置项
  924. $distanceSettingItem = $settingGroup->items()->where('code', 'distance')->first();
  925. abort_if(! $distanceSettingItem, 404, '距离配置项不存在');
  926. // 检查服务距离是否存在,并且是否大于0
  927. if (isset($data['distance']) && $data['distance'] > 0) {
  928. // 从全局配置中验证服务距离的合理范围
  929. abort_if($data['distance'] > $distanceSettingItem->max_value, 422, '服务距离不能超过'.$distanceSettingItem->max_value.'公里');
  930. // 更新技师服务距离配置
  931. $coach->settingValues()->updateOrCreate(
  932. ['item_id' => $distanceSettingItem->id],
  933. ['value' => $data['distance']],
  934. ['object_type' => $coach::class],
  935. ['object_id' => $coach->id],
  936. );
  937. }
  938. // 记录日志
  939. Log::info('订单设置更新成功', [
  940. 'coach_id' => $coach->id,
  941. 'settings' => $data,
  942. ]);
  943. return [
  944. 'message' => '订单设置更新成功',
  945. 'coach_id' => $coach->id,
  946. 'settings' => $data,
  947. ];
  948. } catch (\Exception $e) {
  949. Log::error('订单设置更新失败', [
  950. 'user_id' => $userId,
  951. 'data' => $data,
  952. 'error' => $e->getMessage(),
  953. 'file' => $e->getFile(),
  954. 'line' => $e->getLine(),
  955. ]);
  956. throw $e;
  957. }
  958. });
  959. }
  960. }