OrderService.php 14 KB

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