OrderService.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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\Models\CoachUser;
  12. use App\Models\MemberUser;
  13. use App\Models\Order;
  14. use App\Models\OrderGrabRecord;
  15. use App\Models\OrderRecord;
  16. use App\Services\SettingItemService;
  17. use Illuminate\Support\Facades\DB;
  18. use Illuminate\Support\Facades\Log;
  19. use Illuminate\Support\Facades\Redis;
  20. class OrderService
  21. {
  22. private const DEFAULT_PER_PAGE = 10;
  23. private const MAX_DISTANCE = 40; // 最大距离限制(公里)
  24. private SettingItemService $settingService;
  25. public function __construct(SettingItemService $settingService)
  26. {
  27. $this->settingService = $settingService;
  28. }
  29. /**
  30. * 获取技师的订单列表
  31. *
  32. * @param int $userId 技师用户ID
  33. * @param array $params 请求参数
  34. * @return \Illuminate\Pagination\LengthAwarePaginator
  35. */
  36. public function getOrderList(int $userId, array $params)
  37. {
  38. return DB::transaction(function () use ($userId, $params) {
  39. try {
  40. // 加载技师用户信息
  41. $user = MemberUser::findOrFail($userId);
  42. $coach = $user->coach;
  43. abort_if(! $coach, 404, '技师不存在');
  44. // 构建订单查询
  45. $query = $coach->orders()->whereNotIn('state', [
  46. OrderStatus::CREATED->value,
  47. OrderStatus::ASSIGNED->value,
  48. ])
  49. ->orderBy('created_at', 'desc');
  50. // 分页获取数据
  51. $paginator = $query->paginate(
  52. $params['per_page'] ?? 10,
  53. ['*'],
  54. 'page',
  55. $params['page'] ?? 1
  56. );
  57. // 需要加载关联数据
  58. $items = $paginator->items();
  59. $query->with(['project']); // 加载项目信息以便返回project_name等字段
  60. return [
  61. 'items' => $items,
  62. 'total' => $paginator->total(),
  63. ];
  64. } catch (\Exception $e) {
  65. Log::error('获取技师订单列表失败', [
  66. 'user_id' => $userId,
  67. 'error_message' => $e->getMessage(),
  68. 'error_trace' => $e->getTraceAsString(),
  69. ]);
  70. throw $e;
  71. }
  72. });
  73. }
  74. /**
  75. * 获取可抢订单列表
  76. *
  77. * @param int $userId 技师用户ID
  78. * @param array $params 请求参数
  79. * @return \Illuminate\Pagination\LengthAwarePaginator
  80. */
  81. public function getGrabList(int $userId, array $params)
  82. {
  83. try {
  84. // 加载用户和技师信息
  85. $user = MemberUser::with([
  86. 'coach',
  87. 'coach.info',
  88. 'coach.real',
  89. 'coach.qual',
  90. 'coach.locations',
  91. 'coach.projects',
  92. ])->findOrFail($userId);
  93. // 验证技师信息
  94. [$coach, $location] = $this->validateCoach($user);
  95. // 获取技师项目信息
  96. $coachProjects = $coach->projects;
  97. $coachProjectIds = $coachProjects->pluck('project_id')->toArray();
  98. // 获取系统配置
  99. $settings = $this->getSystemSettings();
  100. // 获取附近订单
  101. $orderDistances = $this->getNearbyOrders($location);
  102. // 构建订单查询
  103. $query = $this->buildOrderQuery($coachProjectIds, $orderDistances);
  104. // 处理订单数据
  105. $this->processOrderData($query, $orderDistances, $coachProjects, $settings);
  106. // 分页获取数据
  107. return $query->paginate(
  108. $params['per_page'] ?? 10,
  109. ['*'],
  110. 'page',
  111. $params['page'] ?? 1
  112. );
  113. } catch (\Exception $e) {
  114. Log::error('获取可抢订单列表失败', [
  115. 'user_id' => $userId,
  116. 'params' => $params,
  117. 'error' => $e->getMessage(),
  118. 'trace' => $e->getTraceAsString(),
  119. ]);
  120. throw $e;
  121. }
  122. }
  123. /**
  124. * 验证技师信息
  125. */
  126. private function validateCoach(MemberUser $user): array
  127. {
  128. $coach = $user->coach;
  129. abort_if(! $coach, 404, '技师不存在');
  130. abort_if(! $coach->info, 404, '技师信息不存在');
  131. abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状态异常');
  132. abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过');
  133. abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过');
  134. $location = $coach->locations()
  135. ->where('type', TechnicianLocationType::COMMON->value)
  136. ->first();
  137. abort_if(! $location, 404, '技师定位地址不存在');
  138. return [$coach, $location];
  139. }
  140. /**
  141. * 获取系统配置
  142. */
  143. private function getSystemSettings(): array
  144. {
  145. return [
  146. 'coach_income' => $this->settingService->getItemDetail('coach_income')->default_value ?? 0,
  147. 'min_distance' => $this->settingService->getItemDetail('qibugongli')->default_value ?? 0,
  148. 'base_price' => $this->settingService->getItemDetail('qibujia')->default_value ?? 0,
  149. 'per_km_price' => $this->settingService->getItemDetail('meigonglijiage')->default_value ?? 0,
  150. ];
  151. }
  152. /**
  153. * 获取附近订单
  154. */
  155. private function getNearbyOrders($location, float $maxDistance = 40): array
  156. {
  157. $nearbyOrders = Redis::georadius(
  158. 'order_locations',
  159. $location->longitude,
  160. $location->latitude,
  161. $maxDistance,
  162. 'km',
  163. ['WITHCOORD', 'WITHDIST']
  164. );
  165. if (! $nearbyOrders) {
  166. Log::warning('获取附近订单失败', [
  167. 'location' => [
  168. 'longitude' => $location->longitude,
  169. 'latitude' => $location->latitude,
  170. ],
  171. ]);
  172. return [];
  173. }
  174. $orderDistances = [];
  175. foreach ($nearbyOrders as $order) {
  176. $orderId = str_replace('order:', '', $order[0]);
  177. $distance = round($order[1], 1);
  178. $orderDistances[$orderId] = $distance;
  179. }
  180. return $orderDistances;
  181. }
  182. /**
  183. * 构建订单查询
  184. */
  185. private function buildOrderQuery(array $coachProjectIds, array $orderDistances)
  186. {
  187. return Order::query()
  188. ->select([
  189. 'id',
  190. 'order_no',
  191. 'project_id',
  192. 'location',
  193. 'address',
  194. 'latitude',
  195. 'longitude',
  196. 'service_time',
  197. 'created_at',
  198. 'agent_id',
  199. DB::raw('0 as distance'),
  200. ])
  201. ->with(['project', 'agent.projects'])
  202. ->where('state', OrderStatus::CREATED)
  203. ->whereIn('project_id', $coachProjectIds)
  204. ->whereIn('id', array_keys($orderDistances))
  205. ->orderBy('created_at', 'desc');
  206. }
  207. /**
  208. * 处理订单数据
  209. */
  210. private function processOrderData($query, array $orderDistances, $coachProjects, array $settings): void
  211. {
  212. $query->get()->each(function ($order) use ($orderDistances, $coachProjects, $settings) {
  213. // 设置距离
  214. $order->distance = $orderDistances[$order->id];
  215. // 处理代理商项目价格
  216. $this->processAgentPrice($order);
  217. // 处理技师项目信息
  218. $this->processCoachProject($order, $coachProjects, $settings);
  219. });
  220. }
  221. /**
  222. * 处理代理商价格
  223. */
  224. private function processAgentPrice($order): void
  225. {
  226. if ($order->agent_id && $order->agent && $order->agent->projects) {
  227. $agentProject = $order->agent->projects
  228. ->where('id', $order->project_id)
  229. ->where('state', 1)
  230. ->first();
  231. if ($agentProject) {
  232. $order->project->price = $agentProject->agent_price;
  233. $order->project->duration = $agentProject->duration;
  234. }
  235. }
  236. }
  237. /**
  238. * 处理技师项目信息
  239. */
  240. private function processCoachProject($order, $coachProjects, array $settings): void
  241. {
  242. $coachProject = $coachProjects->where('project_id', $order->project_id)->first();
  243. if (! $coachProject) {
  244. return;
  245. }
  246. // 计算技师佣金
  247. $order->project->coach_income = round($order->project->price * $settings['coach_income'], 2);
  248. // 处理优惠金额
  249. if ($coachProject->discount_amount > 0) {
  250. $order->project->discount_amount = $coachProject->discount_amount;
  251. $order->project->final_price = $order->project->price - $coachProject->discount_amount;
  252. }
  253. // 处理路费
  254. if ($coachProject->traffic_fee > 0) {
  255. $this->calculateTrafficFee($order, $coachProject, $settings);
  256. }
  257. }
  258. /**
  259. * 计算路费
  260. */
  261. private function calculateTrafficFee($order, $coachProject, array $settings): void
  262. {
  263. $trafficFee = $settings['base_price'];
  264. if ($order->distance > $settings['min_distance']) {
  265. $trafficFee += ($order->distance - $settings['min_distance']) * $settings['per_km_price'];
  266. }
  267. $order->project->traffic_fee = round($trafficFee, 2);
  268. // 双程收费
  269. if ($coachProject->is_round_trip) {
  270. $order->project->traffic_fee *= 2;
  271. }
  272. // 计算最终价格
  273. $order->project->final_price = ($order->project->final_price ?? $order->project->price)
  274. + $order->project->traffic_fee;
  275. }
  276. /**
  277. * 技师抢单
  278. *
  279. * @param int $userId 技师用户ID
  280. * @param int $orderId 订单ID
  281. * @return array
  282. */
  283. public function grabOrder(int $userId, int $orderId)
  284. {
  285. return DB::transaction(function () use ($userId, $orderId) {
  286. try {
  287. // 加载用户和技师信息
  288. $user = MemberUser::with([
  289. 'coach',
  290. 'coach.info',
  291. 'coach.real',
  292. 'coach.qual',
  293. 'coach.locations',
  294. 'coach.projects',
  295. ])->findOrFail($userId);
  296. // 验证技师信息
  297. [$coach, $location] = $this->validateCoach($user);
  298. // 获取订单信息
  299. $order = Order::lockForUpdate()->findOrFail($orderId);
  300. // 验证订单状态
  301. abort_if($order->state !== OrderStatus::CREATED->value, 400, '订单状态异常,无法抢单');
  302. // 验证订单类型
  303. abort_if($order->type !== OrderType::GRAB->value, 400, '该订单不是抢单类型');
  304. // 检查技师是否已参与抢单
  305. $existingGrab = $coach->grabRecords()
  306. ->where('order_id', $orderId)
  307. ->first();
  308. abort_if($existingGrab, 400, '您已参与抢单,请勿重复操作');
  309. // 验证订单是否在技师服务范围内
  310. // 通过Redis GEO计算订单与技师的距离
  311. $distance = Redis::geodist(
  312. 'order_locations',
  313. 'order:'.$order->id,
  314. $location->longitude.','.$location->latitude,
  315. 'km'
  316. ) ?? PHP_FLOAT_MAX;
  317. abort_if($distance > self::MAX_DISTANCE, 400, '订单超出服务范围');
  318. // 验证技师是否具备该项目服务资格
  319. $coachProject = $coach->projects()
  320. ->where('project_id', $order->project_id)
  321. ->where('state', ProjectStatus::OPEN->value)
  322. ->first();
  323. abort_if(! $coachProject, 400, '未开通该项目服务资格');
  324. // 添加抢单记录
  325. OrderGrabRecord::create([
  326. 'order_id' => $order->id,
  327. 'coach_id' => $coach->id,
  328. 'distance' => $distance,
  329. 'state' => OrderGrabRecordStatus::JOINED->value,
  330. 'created_at' => now(),
  331. ]);
  332. // 记录日志
  333. Log::info('技师参与抢单', [
  334. 'user_id' => $userId,
  335. 'coach_id' => $coach->id,
  336. 'order_id' => $orderId,
  337. ]);
  338. return [
  339. 'message' => '已参与抢单',
  340. 'order_id' => $orderId,
  341. ];
  342. } catch (\Exception $e) {
  343. Log::error('技师参与抢单失败', [
  344. 'user_id' => $userId,
  345. 'order_id' => $orderId,
  346. 'error_message' => $e->getMessage(),
  347. 'error_trace' => $e->getTraceAsString(),
  348. ]);
  349. throw $e;
  350. }
  351. });
  352. }
  353. /**
  354. * 技师接单
  355. *
  356. * @param int $userId 技师用户ID
  357. * @param int $orderId 订单ID
  358. */
  359. public function acceptOrder(int $userId, int $orderId): array
  360. {
  361. return DB::transaction(function () use ($userId, $orderId) {
  362. try {
  363. // 加载用户和技师信息
  364. $user = MemberUser::with([
  365. 'coach',
  366. 'coach.info',
  367. 'coach.real',
  368. 'coach.qual',
  369. ])->findOrFail($userId);
  370. // 验证技师信息
  371. [$coach, $location] = $this->validateCoach($user);
  372. // 获取订单信息并加锁
  373. $order = Order::lockForUpdate()->findOrFail($orderId);
  374. // 验证订单状态
  375. abort_if($order->state !== OrderStatus::ASSIGNED->value, 400, '订单状态异常,无法接单');
  376. // 验证订单是否分配给该技师
  377. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  378. // 更新订单状态
  379. $order->update([
  380. 'state' => OrderStatus::ACCEPTED->value,
  381. 'accepted_at' => now(),
  382. ]);
  383. // 记录日志
  384. Log::info('技师接单成功', [
  385. 'user_id' => $userId,
  386. 'coach_id' => $coach->id,
  387. 'order_id' => $orderId,
  388. 'order_no' => $order->order_no,
  389. ]);
  390. return [
  391. 'message' => '接单成功',
  392. 'order_id' => $orderId,
  393. 'order_no' => $order->order_no,
  394. ];
  395. } catch (\Exception $e) {
  396. Log::error('技师接单失败', [
  397. 'user_id' => $userId,
  398. 'order_id' => $orderId,
  399. 'error' => $e->getMessage(),
  400. 'file' => $e->getFile(),
  401. 'line' => $e->getLine(),
  402. ]);
  403. throw $e;
  404. }
  405. });
  406. }
  407. /**
  408. * 技师拒单
  409. *
  410. * @param int $userId 用户ID
  411. * @param int $orderId 订单ID
  412. * @param string $reason 拒单原因
  413. */
  414. public function rejectOrder(int $userId, int $orderId, string $reason): array
  415. {
  416. return DB::transaction(function () use ($userId, $orderId, $reason) {
  417. try {
  418. // 获取技师信息(优化关联加载)
  419. $user = MemberUser::with([
  420. 'coach',
  421. 'coach.info',
  422. 'coach.real',
  423. 'coach.qual',
  424. ])->findOrFail($userId);
  425. // 验证技师信息
  426. [$coach, $location] = $this->validateCoach($user);
  427. // 获取订单信息并加锁
  428. $order = Order::lockForUpdate()->findOrFail($orderId);
  429. // 验证订单状态(修正状态判断)
  430. abort_if(! in_array($order->state, [
  431. OrderStatus::ASSIGNED->value,
  432. OrderStatus::PAID->value,
  433. ]), 400, '订单状态异常,无法拒单');
  434. // 验证订单是否分配给该技师
  435. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  436. // 检查拒单次数限制
  437. $rejectCount = OrderRecord::where('object_id', $coach->id)
  438. ->where('object_type', CoachUser::class)
  439. ->where('state', OrderRecordStatus::REJECTED->value)
  440. ->whereDate('created_at', today())
  441. ->count();
  442. // 更新订单状态
  443. $order->update([
  444. 'state' => OrderStatus::REJECTED->value,
  445. ]);
  446. // 创建订单记录
  447. OrderRecord::create([
  448. 'order_id' => $order->id,
  449. 'object_id' => $coach->id,
  450. 'object_type' => CoachUser::class,
  451. 'state' => OrderRecordStatus::REJECTED->value,
  452. 'remark' => $reason,
  453. ]);
  454. // 发送消息通知
  455. try {
  456. // event(new OrderRejectedEvent($order, $coach, $reason));
  457. } catch (\Exception $e) {
  458. Log::error('发送拒单通知失败', [
  459. 'order_id' => $orderId,
  460. 'coach_id' => $coach->id,
  461. 'error' => $e->getMessage(),
  462. ]);
  463. }
  464. // 记录日志
  465. Log::info('技师拒单成功', [
  466. 'user_id' => $userId,
  467. 'coach_id' => $coach->id,
  468. 'order_id' => $orderId,
  469. 'order_no' => $order->order_no,
  470. 'reason' => $reason,
  471. 'reject_count' => $rejectCount + 1,
  472. ]);
  473. return [
  474. 'message' => '拒单成功',
  475. 'order_id' => $orderId,
  476. 'order_no' => $order->order_no,
  477. 'reject_count' => $rejectCount + 1,
  478. 'max_reject_count' => 5,
  479. ];
  480. } catch (\Exception $e) {
  481. Log::error('技师拒单失败', [
  482. 'user_id' => $userId,
  483. 'order_id' => $orderId,
  484. 'reason' => $reason,
  485. 'error' => $e->getMessage(),
  486. 'file' => $e->getFile(),
  487. 'line' => $e->getLine(),
  488. ]);
  489. throw $e;
  490. }
  491. });
  492. }
  493. }