OrderService.php 15 KB

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