OrderService.php 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241
  1. <?php
  2. namespace App\Services\Client;
  3. use App\Enums\OrderSource;
  4. use App\Enums\OrderStatus;
  5. use App\Enums\OrderType;
  6. use App\Enums\PaymentMethod;
  7. use App\Enums\ProjectStatus;
  8. use App\Enums\TechnicianAuthStatus;
  9. use App\Enums\TechnicianStatus;
  10. use App\Enums\UserStatus;
  11. use App\Models\AgentConfig;
  12. use App\Models\AgentInfo;
  13. use App\Models\CoachConfig;
  14. use App\Models\CoachUser;
  15. use App\Models\MemberUser;
  16. use App\Models\Order;
  17. use App\Models\OrderGrabRecord;
  18. use App\Models\OrderRecord;
  19. use App\Models\Project;
  20. use App\Models\SysConfig;
  21. use App\Models\User;
  22. use App\Models\WalletPaymentRecord;
  23. use App\Models\WalletRefundRecord;
  24. use Exception;
  25. use Illuminate\Support\Facades\Auth;
  26. use Illuminate\Support\Facades\DB;
  27. use Illuminate\Support\Facades\Log;
  28. use Illuminate\Support\Facades\Redis;
  29. class OrderService
  30. {
  31. protected AgentService $agentService;
  32. protected ProjectService $projectService;
  33. public function __construct(
  34. AgentService $agentService,
  35. ProjectService $projectService
  36. ) {
  37. $this->agentService = $agentService;
  38. $this->projectService = $projectService;
  39. }
  40. /**
  41. * 订单初始化
  42. *
  43. * 初始化订单信息,包括用户钱包、技师信息、项目信息、地址信息和订单金额等
  44. *
  45. * @param int $userId 用户ID
  46. * @param array $data 订单数据
  47. * @return array 返回初始化的订单信息
  48. *
  49. * @throws \Exception 初始化失败时抛出异常
  50. */
  51. public function initialize(int $userId, array $data): array
  52. {
  53. try {
  54. // 参数验证
  55. abort_if(empty($data['project_id']), 400, '项目ID不能为空');
  56. abort_if(empty($data['coach_id']), 400, '技师ID不能为空');
  57. return DB::transaction(function () use ($userId, $data) {
  58. $user = MemberUser::find($userId);
  59. abort_if(! $user || $user->state != 'enable', 400, '用户状态异常');
  60. // 查询用户钱包
  61. $wallet = $user->wallet;
  62. // 查询默认地址
  63. $address = $user->address;
  64. $areaCode = $address ? $address->area_code : ($data['area_code'] ?? null);
  65. abort_if(empty($areaCode), 400, '区域编码不能为空');
  66. // 查询技师数据
  67. $coach = $this->validateCoach($data['coach_id']);
  68. // 获取项目详情
  69. $project = $this->projectService->getProjectDetail($data['project_id'], $areaCode);
  70. abort_if(! $project, 400, '项目不存在');
  71. // 计算订单金额
  72. $amounts = $this->calculateOrderAmount(
  73. $userId,
  74. $address?->id ?? 0,
  75. $data['coach_id'],
  76. $data['project_id'],
  77. $project->agent_id,
  78. false,
  79. $data['latitude'],
  80. $data['longitude']
  81. );
  82. return [
  83. 'wallet' => $wallet,
  84. 'coach' => $coach,
  85. 'project' => $project,
  86. 'address' => $address,
  87. 'amounts' => $amounts,
  88. ];
  89. });
  90. } catch (Exception $e) {
  91. Log::error('订单初始化失败', [
  92. 'userId' => $userId,
  93. 'data' => $data,
  94. 'error' => $e->getMessage(),
  95. 'trace' => $e->getTraceAsString(),
  96. ]);
  97. throw $e;
  98. }
  99. }
  100. /**
  101. * 创建订单
  102. */
  103. public function createOrder(int $userId, array $data): array
  104. {
  105. return DB::transaction(function () use ($userId, $data) {
  106. // 1. 参数校验
  107. $user = MemberUser::where('id', $userId)
  108. ->where('state', UserStatus::OPEN->value())
  109. ->firstOrFail();
  110. $project = Project::where('id', $data['project_id'])
  111. ->where('state', ProjectStatus::OPEN->value())
  112. ->firstOrFail();
  113. // 2. 订单类型判断
  114. $orderType = $data['order_type'];
  115. // 关键操作:验证必要参数
  116. abort_if(empty($data['project_id']), 400, '项目ID不能为空');
  117. // 上门订单必须指定技师和地址
  118. abort_if($orderType == OrderType::VISIT->value && empty($data['coach_id']), 400, '技师ID不能为空');
  119. abort_if($orderType == OrderType::VISIT->value && empty($data['address_id']), 400, '地址ID不能为空');
  120. // 抢单订单必须指定地址
  121. abort_if($orderType == OrderType::GRAB->value && empty($data['address_id']), 400, '地址ID不能为空');
  122. // 加钟订单必须指定原订单
  123. abort_if($orderType == OrderType::OVERTIME->value && empty($data['order_id']), 400, '原订单ID不能为空');
  124. // 到店订单必须指定店铺
  125. abort_if($orderType == OrderType::SHOP->value && empty($data['shop_id']), 400, '店铺ID不能为空');
  126. // 应急订单必须指定地址
  127. abort_if($orderType == OrderType::EMERGENCY->value && empty($data['address_id']), 400, '地址ID不能为空');
  128. // 3. 验证技师
  129. // 4. 根据订单类型处理
  130. // 上门订单
  131. if ($orderType == OrderType::VISIT->value) {
  132. $this->validateCoach($data['coach_id']);
  133. }
  134. // 加钟订单
  135. if ($orderType == OrderType::OVERTIME->value) {
  136. $originalOrder = $this->getOriginalOrder($user, $data['order_id']);
  137. $data['address_id'] = $originalOrder->address_id;
  138. $this->validateCoach($originalOrder->coach_id);
  139. abort_if(! in_array($originalOrder->state, ['service_ing', 'service_end']), 400, '原订单状态不允许加钟');
  140. $data = $this->prepareAddTimeData($originalOrder, $data);
  141. }
  142. $address = $user->addresses()
  143. ->where('id', $data['address_id'])
  144. ->firstOrFail();
  145. // 5. 计算订单金额
  146. $data['use_balance'] = $data['use_balance'] ?? false;
  147. $amounts = $this->calculateOrderAmount(
  148. $userId,
  149. $address->id,
  150. $data['coach_id'],
  151. $data['project_id'],
  152. $project?->agent_id,
  153. $data['use_balance']
  154. );
  155. // 6. 验证金额和余额
  156. abort_if($amounts['total_amount'] <= 0, 400, '订单金额异常');
  157. if ($data['payment_type'] == PaymentMethod::BALANCE->value) {
  158. $wallet = $user->wallet;
  159. abort_if($wallet->available_balance < $amounts['balance_amount'], 400, '可用余额不足');
  160. }
  161. // 7. 创建订单记录
  162. $order = $this->createOrderRecord($userId, $data, $orderType, $address, $data['payment_type'], (object) $amounts);
  163. // 8. 余额支付处理
  164. if ($order->payment_type == PaymentMethod::BALANCE->value && $orderType != OrderType::GRAB->value) {
  165. $this->handleBalancePayment($user, $order, $orderType);
  166. }
  167. return [
  168. 'order_id' => $order->id,
  169. 'payment_type' => $order->payment_type,
  170. ];
  171. });
  172. }
  173. // 提取方法:验证技师
  174. public function validateCoach(int $coachId): CoachUser
  175. {
  176. $coach = CoachUser::where('id', $coachId)->first();
  177. $coach = CoachUser::where('id', $coachId)
  178. ->where('state', TechnicianStatus::ACTIVE->value)
  179. ->whereHas('info', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  180. ->whereHas('qual', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  181. ->whereHas('real', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  182. ->firstOrFail();
  183. return $coach;
  184. }
  185. // 提取方法:获取原始订单
  186. private function getOriginalOrder($user, $orderId): Order
  187. {
  188. $originalOrder = $user->orders->where('id', $orderId)
  189. ->whereIn('state', ['service_ing', 'service_end'])
  190. ->firstOrFail();
  191. return $originalOrder;
  192. }
  193. // 提取方法:准备加钟订单数据
  194. private function prepareAddTimeData($originalOrder, $data): array
  195. {
  196. if ($originalOrder->state == 'service_ing') {
  197. $startTime = now();
  198. } else {
  199. $startTime = now();
  200. }
  201. return [
  202. ...$data,
  203. 'order_id' => $data['order_id'],
  204. 'address_id' => $originalOrder->address_id,
  205. 'service_time' => $startTime,
  206. 'coach_id' => $originalOrder->coach_id,
  207. ];
  208. }
  209. // 提取方法:创建订单记录
  210. private function createOrderRecord($userId, $data, $orderType, $address, $payment_type, object $amounts): Order
  211. {
  212. $order = new Order;
  213. $order->user_id = $userId;
  214. $order->project_id = $data['project_id'];
  215. $order->coach_id = $data['coach_id'];
  216. $order->type = $orderType;
  217. $order->state = OrderStatus::CREATED->value;
  218. $order->source = OrderSource::PLATFORM->value;
  219. $order->total_amount = $amounts->total_amount;
  220. $order->balance_amount = $amounts->balance_amount;
  221. $order->pay_amount = $amounts->pay_amount;
  222. $order->project_amount = $amounts->project_amount;
  223. $order->traffic_amount = $orderType == OrderType::OVERTIME->value ? 0 : $amounts->delivery_fee;
  224. $order->payment_type = ($data['use_balance'] && $amounts->pay_amount == 0) ? PaymentMethod::BALANCE->value : $payment_type;
  225. $order->service_time = $data['service_time'];
  226. $order->address_id = $data['address_id'];
  227. $order->longitude = $address->longitude;
  228. $order->latitude = $address->latitude;
  229. $order->location = $address->location;
  230. $order->address = $address->address;
  231. $order->area_code = $address->area_code;
  232. $order->save();
  233. // 创建订单记录
  234. OrderRecord::create([
  235. 'order_id' => $order->id,
  236. 'object_id' => $userId,
  237. 'object_type' => MemberUser::class,
  238. 'state' => OrderStatus::CREATED->value,
  239. 'remark' => $orderType == OrderType::OVERTIME->value ? '加钟订单' : '创建订单',
  240. ]);
  241. if ($orderType == OrderType::GRAB->value) {
  242. // 将订单地址经纬度存入redis的geo类型
  243. Redis::geoadd(
  244. 'order_locations',
  245. $order->longitude,
  246. $order->latitude,
  247. 'order:'.$order->id
  248. );
  249. }
  250. return $order;
  251. }
  252. // 提取方法:处理余额支付
  253. private function handleBalancePayment($user, $order, $orderType): void
  254. {
  255. $order->state = $orderType == 'normal' ? 'wait_receive' : 'service_ing';
  256. $order->save();
  257. OrderRecord::create([
  258. 'order_id' => $order->id,
  259. 'object_id' => $user->id,
  260. 'object_type' => MemberUser::class,
  261. 'state' => 'pay',
  262. 'remark' => '余额支付',
  263. ]);
  264. $user->wallet->decrement('total_balance', $order->balance_amount);
  265. $user->wallet->decrement('available_balance', $order->balance_amount);
  266. }
  267. /**
  268. * 取消订单
  269. */
  270. public function cancelOrder(int $userId, int $orderId): array
  271. {
  272. return DB::transaction(function () use ($userId, $orderId) {
  273. try {
  274. // 1. 验证用户和订单
  275. $order = $this->validateOrderForCancel($userId, $orderId);
  276. abort_if($order->state == 'cancel', 400, '订单已取消');
  277. // 2. 处理退款
  278. if (in_array($order->state, ['wait_receive', 'on_the_way'])) {
  279. $this->handleCancelRefund($order);
  280. }
  281. // 3. 完成订单取消
  282. $this->completeCancel($order, $userId);
  283. // 4. 通知技师
  284. if ($order->coach_id) {
  285. // event(new OrderCancelledEvent($order));
  286. }
  287. return ['message' => '订单已取消'];
  288. } catch (Exception $e) {
  289. $this->logCancelOrderError($e, $userId, $orderId);
  290. throw $e;
  291. }
  292. });
  293. }
  294. /**
  295. * 验证订单取消条件
  296. */
  297. private function validateOrderForCancel(int $userId, int $orderId): Order
  298. {
  299. // 复用之前的用户验证逻辑
  300. $user = MemberUser::where('id', $userId)
  301. ->where('state', UserStatus::OPEN->value)
  302. ->firstOrFail();
  303. // 验证订单状态
  304. $order = Order::where('user_id', $userId)
  305. ->where('id', $orderId)
  306. ->whereIn('state', [OrderStatus::CREATED->value, OrderStatus::ASSIGNED->value, OrderStatus::PAID->value, OrderStatus::ACCEPTED->value, OrderStatus::DEPARTED->value])
  307. ->lockForUpdate()
  308. ->firstOrFail();
  309. return $order;
  310. }
  311. /**
  312. * 处理订单取消退款
  313. */
  314. private function handleCancelRefund(Order $order): void
  315. {
  316. $user = $order->user;
  317. switch ($order->state) {
  318. case OrderStatus::ACCEPTED->value: // 已接单
  319. // 扣除20%费用
  320. $deductAmount = ($order->payment_amount + $order->balance_amount - $order->traffic_amount) * 0.2;
  321. $this->handleRefund($user, $order, $deductAmount, false);
  322. break;
  323. case OrderStatus::DEPARTED->value: // 已出发
  324. // 扣除50%费用并扣除路费
  325. $deductAmount = ($order->payment_amount + $order->balance_amount - $order->traffic_amount) * 0.5;
  326. $this->handleRefund($user, $order, $deductAmount, true);
  327. break;
  328. case OrderStatus::CREATED->value:
  329. // 待支付状态直接取消,无需退款
  330. break;
  331. default:
  332. abort(400, '当前订单状态不允许取消');
  333. }
  334. }
  335. /**
  336. * 完成订单取消
  337. */
  338. private function completeCancel(Order $order, int $userId): void
  339. {
  340. // 添加订单取消记录
  341. OrderRecord::create([
  342. 'order_id' => $order->id,
  343. 'object_id' => $userId,
  344. 'object_type' => MemberUser::class,
  345. 'state' => 'cancel',
  346. 'remark' => '用户取消订单',
  347. ]);
  348. // 修改订单状态
  349. $order->state = 'cancel';
  350. $order->cancel_time = now(); // 添加取消时间
  351. $order->save();
  352. // 如果有技师,可能需要通知技师订单已取消
  353. if ($order->coach_id) {
  354. // TODO: 发送通知给技师
  355. // event(new OrderCancelledEvent($order));
  356. }
  357. }
  358. /**
  359. * 处理退款
  360. */
  361. private function handleRefund(MemberUser $user, Order $order, float $deductAmount, bool $deductTrafficFee): void
  362. {
  363. // 关键操作:计算实际退款金额
  364. $refundAmount = $order->payment_amount + $order->balance_amount;
  365. if ($deductTrafficFee) {
  366. $refundAmount -= $order->traffic_amount;
  367. // TODO: 记录技师路费收入
  368. }
  369. $refundAmount -= $deductAmount;
  370. // 优先从余额支付金额中扣除
  371. $balanceRefund = min($order->balance_amount, $refundAmount);
  372. if ($balanceRefund > 0) {
  373. $this->createRefundRecords($user, $order, $balanceRefund);
  374. }
  375. // 剩余退款金额从支付金额中退还
  376. $paymentRefund = $refundAmount - $balanceRefund;
  377. if ($paymentRefund > 0) {
  378. $this->createRefundRecords($user, $order, $paymentRefund, 'payment');
  379. }
  380. // 记录平台收入
  381. if ($deductAmount > 0) {
  382. // TODO: 添加平台收入记录
  383. // PlatformIncome::create([...]);
  384. }
  385. }
  386. /**
  387. * 创建退款记录
  388. */
  389. private function createRefundRecords($user, $order, $amount, $type = 'balance'): void
  390. {
  391. $refundMethod = $type;
  392. $remark = $type == 'balance' ? '订单取消退还余额' : '订单取消退还支付金额';
  393. // 创建退款记录
  394. $refundRecord = $user->wallet->refundRecords()->create([
  395. 'refund_method' => $refundMethod,
  396. 'total_refund_amount' => $order->payment_amount + $order->balance_amount,
  397. 'actual_refund_amount' => '0.00',
  398. 'wallet_balance_refund_amount' => $type == 'balance' ? $amount : '0.00',
  399. 'recharge_balance_refund_amount' => '0.00',
  400. 'remark' => $remark,
  401. 'order_id' => $order->id,
  402. ]);
  403. // 创建交易记录
  404. $user->wallet->transRecords()->create([
  405. 'amount' => $amount,
  406. 'owner_type' => get_class($refundRecord),
  407. 'owner_id' => $refundRecord->id,
  408. 'remark' => $remark,
  409. 'trans_type' => 'income',
  410. 'storage_type' => 'balance',
  411. 'before_balance' => $user->wallet->total_balance,
  412. 'after_balance' => $user->wallet->total_balance + $amount,
  413. 'before_recharge_balance' => '0.00',
  414. 'after_recharge_balance' => '0.00',
  415. 'trans_time' => now(),
  416. 'state' => 'success',
  417. ]);
  418. // 更新钱包余额
  419. $user->wallet->increment('total_balance', $amount);
  420. $user->wallet->increment('available_balance', $amount);
  421. $user->wallet->save();
  422. }
  423. /**
  424. * 记录订单取消错误日志
  425. */
  426. private function logCancelOrderError(Exception $e, int $userId, int $orderId): void
  427. {
  428. // 复用之前的日志记录方法
  429. Log::error('取消订单失败:', [
  430. 'message' => $e->getMessage(),
  431. 'user_id' => $userId,
  432. 'order_id' => $orderId,
  433. 'trace' => $e->getTraceAsString(),
  434. ]);
  435. }
  436. /**
  437. * 结束订单
  438. */
  439. public function finishOrder(int $userId, int $orderId): array
  440. {
  441. return DB::transaction(function () use ($userId, $orderId) {
  442. try {
  443. // 1. 验证用户和订单
  444. $order = $this->validateOrderForFinish($userId, $orderId);
  445. abort_if($order->state == 'service_end', 400, '订单已完成');
  446. // 2. 验证技师状态
  447. $coach = $this->validateCoach($order->coach_id);
  448. // 3. 验证服务时长
  449. $this->validateServiceDuration($order);
  450. // 4. 完成订单
  451. $this->completeOrder($order, $userId);
  452. // 5. 通知技师
  453. // event(new OrderFinishedEvent($order));
  454. return ['message' => '订单已完成'];
  455. } catch (Exception $e) {
  456. $this->logFinishOrderError($e, $userId, $orderId);
  457. throw $e;
  458. }
  459. });
  460. }
  461. /**
  462. * 验证订单完成条件
  463. */
  464. private function validateOrderForFinish(int $userId, int $orderId): Order
  465. {
  466. // 验证用户状态
  467. $user = MemberUser::where('id', $userId)
  468. ->where('state', 'enable')
  469. ->firstOrFail();
  470. // 验证订单状态
  471. $order = Order::where('user_id', $userId)
  472. ->where('id', $orderId)
  473. ->where('state', 'service_ing')
  474. ->lockForUpdate()
  475. ->firstOrFail();
  476. return $order;
  477. }
  478. /**
  479. * 验证服务时长
  480. */
  481. private function validateServiceDuration(Order $order): void
  482. {
  483. // 计算服务时长
  484. $serviceStartTime = $order->service_start_time ?? $order->created_at;
  485. $serviceDuration = now()->diffInMinutes($serviceStartTime);
  486. // 获取项目要求的最短服务时长
  487. $minDuration = $order->project->duration ?? 0;
  488. abort_if($serviceDuration < $minDuration, 400, "服务时长不足{$minDuration}分钟");
  489. }
  490. /**
  491. * 完成订单
  492. */
  493. private function completeOrder(Order $order, int $userId): void
  494. {
  495. // 1. 创建订单记录
  496. OrderRecord::create([
  497. 'order_id' => $order->id,
  498. 'object_id' => $userId,
  499. 'object_type' => MemberUser::class,
  500. 'state' => 'finish',
  501. 'remark' => '服务完成',
  502. ]);
  503. // 2. 更新订单状态
  504. $order->state = 'service_end';
  505. $order->finish_time = now();
  506. $order->save();
  507. }
  508. /**
  509. * 记录订单完成错误日志
  510. */
  511. private function logFinishOrderError(Exception $e, int $userId, int $orderId): void
  512. {
  513. Log::error('结束订单失败:', [
  514. 'message' => $e->getMessage(),
  515. 'user_id' => $userId,
  516. 'order_id' => $orderId,
  517. 'trace' => $e->getTraceAsString(),
  518. ]);
  519. }
  520. /**
  521. * 确认技师离开
  522. */
  523. public function confirmLeave(int $userId, int $orderId): array
  524. {
  525. return DB::transaction(function () use ($userId, $orderId) {
  526. try {
  527. // 1. 参数校验
  528. $order = Order::where('user_id', $userId)
  529. ->where('id', $orderId)
  530. ->where('state', 'service_end') // 订单状态必须是服务结束
  531. ->firstOrFail();
  532. if (! $order) {
  533. throw new Exception('订单不能撤离');
  534. }
  535. // 2. 添加订单撤离记录
  536. OrderRecord::create([
  537. 'order_id' => $orderId,
  538. 'object_id' => $userId,
  539. 'object_type' => MemberUser::class,
  540. 'state' => 'leave',
  541. 'remark' => '技师已离开',
  542. ]);
  543. // 3. 修改订单状态为撤离
  544. $order->state = 'leave';
  545. $order->save();
  546. return ['message' => '已确技师离开'];
  547. } catch (Exception $e) {
  548. Log::error('确认技师离开失败:', [
  549. 'message' => $e->getMessage(),
  550. 'user_id' => $userId,
  551. 'order_id' => $orderId,
  552. ]);
  553. throw $e;
  554. }
  555. });
  556. }
  557. /**
  558. * 获取订单列表
  559. */
  560. public function getOrderList(int $user_id): \Illuminate\Contracts\Pagination\LengthAwarePaginator
  561. {
  562. $user = MemberUser::find($user_id);
  563. return $user->orders()
  564. ->with([
  565. 'coach.info:id,nickname,avatar,gender',
  566. ])
  567. ->orderBy('created_at', 'desc')
  568. ->paginate(10);
  569. }
  570. /**
  571. * 获取订单详情
  572. */
  573. public function getOrderDetail(int $userId, int $orderId): Order
  574. {
  575. $user = MemberUser::find($userId);
  576. return $user->orders()
  577. ->where('id', $orderId) // 需要添加订单ID条件
  578. ->with([
  579. 'coach.info:id,nickname,avatar,gender',
  580. 'records' => function ($query) {
  581. $query->orderBy('created_at', 'asc');
  582. },
  583. ])
  584. ->firstOrFail();
  585. }
  586. /**
  587. * 订单退款
  588. */
  589. public function refundOrder(int $orderId): array
  590. {
  591. // 使用 Auth::user() 获取用户对象
  592. $user = Auth::user();
  593. return DB::transaction(function () use ($orderId, $user) {
  594. // 查询并锁定订单
  595. $order = Order::where('id', $orderId)
  596. ->where('user_id', $user->id)
  597. ->where('state', 'pending')
  598. ->lockForUpdate()
  599. ->firstOrFail();
  600. // 更新订单状态
  601. $order->state = 'refunded';
  602. $order->save();
  603. // 添加订单记录
  604. OrderRecord::create([
  605. 'order_id' => $orderId,
  606. 'object_id' => $user->id,
  607. 'object_type' => 'user',
  608. 'state' => 'refund',
  609. 'remark' => '订单退款',
  610. ]);
  611. // 创建退款记录
  612. WalletRefundRecord::create([
  613. 'order_id' => $orderId,
  614. 'user_id' => $user->id,
  615. 'amount' => $order->total_amount,
  616. 'state' => 'success',
  617. ]);
  618. return ['message' => '退款成功'];
  619. });
  620. }
  621. /**
  622. * 获取代理商配置
  623. */
  624. public function getAgentConfig(int $agentId): array
  625. {
  626. $agent = AgentInfo::where('id', $agentId)
  627. ->where('state', 'enable')
  628. ->firstOrFail();
  629. // $config = AgentConfig::where('agent_id', $agentId)->firstOrFail();
  630. return [
  631. // 'min_distance' => $config->min_distance,
  632. // 'min_fee' => $config->min_fee,
  633. // 'per_km_fee' => $config->per_km_fee
  634. ];
  635. }
  636. /**
  637. * 获取技师配置
  638. */
  639. public function getCoachConfig(int $coachId): array
  640. {
  641. $coach = CoachUser::where('id', $coachId)
  642. ->where('state', 'enable')
  643. ->where('auth_state', 'passed')
  644. ->firstOrFail();
  645. // $config = CoachConfig::where('coach_id', $coachId)->firstOrFail();
  646. return [
  647. // 'delivery_fee_type' => $config->delivery_fee_type,
  648. // 'charge_delivery_fee' => $config->charge_delivery_fee
  649. ];
  650. }
  651. /**
  652. * 计算路费金额
  653. *
  654. * @param int $coachId 技师ID
  655. * @param int $projectId 项目ID
  656. * @param int|null $agentId 代理商ID
  657. * @param float $distance 距离(公里)
  658. * @return float 路费金额
  659. *
  660. * @throws Exception
  661. */
  662. public function calculateDeliveryFee(
  663. int $coachId,
  664. int $projectId,
  665. ?int $agentId,
  666. float $distance
  667. ): float {
  668. try {
  669. // 1. 校验技师
  670. $coach = CoachUser::where('state', TechnicianStatus::ACTIVE->value)
  671. ->whereHas('info', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  672. ->whereHas('real', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  673. ->whereHas('qual', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  674. ->with(['projects' => fn ($q) => $q->where('project_id', $projectId)])
  675. ->find($coachId);
  676. abort_if(! $coach, 404, '技师不存在或状态异常');
  677. // 2. 校验技师项目
  678. $coachProject = $coach->projects->first();
  679. abort_if(! $coachProject, 404, '技师项目不存在');
  680. // 3. 判断是否免收路费
  681. if ($coachProject->traffic_fee_type == 'free') {
  682. return 0;
  683. }
  684. // 4. 获取路费配置
  685. $config = $this->getDeliveryFeeConfig($agentId);
  686. abort_if(! $config, 404, '路费配置不存在');
  687. // 5. 计算路费
  688. $fee = $this->calculateFee($distance, $config);
  689. // 6. 判断是否往返
  690. return $coachProject->delivery_fee_type == 'round_trip'
  691. ? bcmul($fee, '2', 2)
  692. : $fee;
  693. } catch (Exception $e) {
  694. Log::error(__CLASS__.'->'.__FUNCTION__.'计算路费失败:', [
  695. 'message' => $e->getMessage(),
  696. 'coach_id' => $coachId,
  697. 'project_id' => $projectId,
  698. 'agent_id' => $agentId,
  699. 'distance' => $distance,
  700. 'trace' => $e->getTraceAsString(),
  701. ]);
  702. throw $e;
  703. }
  704. }
  705. /**
  706. * 获取路费配置
  707. */
  708. private function getDeliveryFeeConfig(?int $agentId): ?object
  709. {
  710. // 优先获取代理商配置
  711. if ($agentId) {
  712. $agent = AgentInfo::where('state', 'enable')
  713. ->with(['projectConfig'])
  714. ->find($agentId);
  715. if ($agent && $agent->projectConfig) {
  716. return $agent->projectConfig;
  717. }
  718. }
  719. // 获取系统配置
  720. return SysConfig::where('key', 'delivery_fee')->first();
  721. }
  722. /**
  723. * 计算路费
  724. */
  725. private function calculateFee(float $distance, object $config): float
  726. {
  727. // 最小距离内按起步价计算
  728. if ($distance <= $config->min_distance) {
  729. return (float) $config->min_fee;
  730. }
  731. // 超出最小距离部分按每公里费用计算
  732. $extraDistance = bcsub($distance, $config->min_distance, 2);
  733. $extraFee = bcmul($extraDistance, $config->per_km_fee, 2);
  734. return bcadd($config->min_fee, $extraFee, 2);
  735. }
  736. /**
  737. * 计算订单金额
  738. *
  739. * @param int $userId 用户ID
  740. * @param int $addressId 地址ID
  741. * @param int $coachId 技师ID
  742. * @param int $projectId 项目ID
  743. * @param int $agentId 代理商ID
  744. * @param bool $useBalance 是否使用余额
  745. * @param float $distance 距离
  746. * @param int $lat 纬度
  747. * @param int $lng 经度
  748. *
  749. * @throws Exception
  750. */
  751. public function calculateOrderAmount(
  752. int $userId,
  753. int $addressId,
  754. int $coachId,
  755. int $projectId,
  756. ?int $agentId = null,
  757. bool $useBalance = false,
  758. float $distance = 0,
  759. int $lat = 0,
  760. int $lng = 0
  761. ): array {
  762. try {
  763. // 1. 参数校验
  764. $user = MemberUser::find($userId);
  765. abort_if(! $user || $user->state != UserStatus::OPEN->value, 404, '用户不存在或状态异常');
  766. // 2. 查询技师项目
  767. $coach = $this->validateCoach($coachId);
  768. abort_if(! $coach, 404, '技师不存在或状态异常');
  769. $coachProject = $coach->projects()
  770. ->where('state', ProjectStatus::OPEN->value)
  771. ->where('project_id', $projectId)
  772. ->first();
  773. abort_if(! $coachProject, 404, '技师项目不存在');
  774. // 3. 查询基础项目
  775. $project = Project::where('id', $projectId)
  776. ->where('state', ProjectStatus::OPEN->value())
  777. ->first();
  778. abort_if(! $project, 404, '项目不存在或状态异常');
  779. // 4. 计算距离
  780. if (floatval($distance) <= 0) {
  781. $address = $user->addresses()->find($addressId) ?? ['latitude' => $lat, 'longitude' => $lng];
  782. $coachService = app(CoachService::class);
  783. $coachDetail = $coachService->getCoachDetail($coachId, $address['latitude'], $address['longitude']);
  784. $distance = $coachDetail['distance'] ?? 0;
  785. }
  786. // 5. 获取项目价格
  787. $projectAmount = $this->getProjectPrice($project, $agentId, $projectId);
  788. // 6. 计算路费
  789. $deliveryFee = $this->calculateDeliveryFee($coachId, $projectId, $agentId, $distance);
  790. // 7. 计算优惠券金额
  791. $couponAmount = $this->calculateCouponAmount();
  792. // 8. 计算总金额
  793. $totalAmount = bcadd($projectAmount, $deliveryFee, 2);
  794. $totalAmount = bcsub($totalAmount, $couponAmount, 2);
  795. $totalAmount = max(0, $totalAmount);
  796. // 9. 计算余额支付金额
  797. $balanceAmount = 0;
  798. $payAmount = $totalAmount;
  799. if ($useBalance && $totalAmount > 0) {
  800. $wallet = $user->wallet;
  801. abort_if(! $wallet, 404, '用户钱包不存在');
  802. if ($wallet->available_balance >= $totalAmount) {
  803. $balanceAmount = $totalAmount;
  804. $payAmount = 0;
  805. } else {
  806. $balanceAmount = $wallet->available_balance;
  807. $payAmount = bcsub($totalAmount, $balanceAmount, 2);
  808. }
  809. }
  810. return [
  811. 'total_amount' => $totalAmount,
  812. 'balance_amount' => $balanceAmount,
  813. 'pay_amount' => $payAmount,
  814. 'coupon_amount' => $couponAmount,
  815. 'project_amount' => $projectAmount,
  816. 'delivery_fee' => $deliveryFee,
  817. ];
  818. } catch (Exception $e) {
  819. Log::error(__CLASS__.'->'.__FUNCTION__.'计算订单金额失败:', [
  820. 'message' => $e->getMessage(),
  821. 'user_id' => $userId,
  822. 'project_id' => $projectId,
  823. 'trace' => $e->getTraceAsString(),
  824. ]);
  825. throw $e;
  826. }
  827. }
  828. /**
  829. * 获取项目价格
  830. */
  831. private function getProjectPrice($project, ?int $agentId, int $projectId): float
  832. {
  833. $price = $project->price;
  834. if ($agentId) {
  835. $agent = AgentInfo::where('state', 'enable')->find($agentId);
  836. if ($agent) {
  837. $agentProject = $agent->projects()
  838. ->where('state', 'enable')
  839. ->where('project_id', $projectId)
  840. ->first();
  841. if ($agentProject) {
  842. $price = $agentProject->price;
  843. }
  844. }
  845. }
  846. return (float) $price;
  847. }
  848. /**
  849. * 计算优惠券金额
  850. */
  851. private function calculateCouponAmount(): float
  852. {
  853. $couponAmount = 0;
  854. if (request()->has('coupon_id')) {
  855. // TODO: 优惠券逻辑
  856. }
  857. return $couponAmount;
  858. }
  859. /**
  860. * 计算支付金额分配
  861. */
  862. private function calculatePaymentAmounts($user, float $totalAmount, bool $useBalance): array
  863. {
  864. $balanceAmount = 0;
  865. $payAmount = $totalAmount;
  866. if ($useBalance) {
  867. $wallet = $user->wallet;
  868. if (! $wallet) {
  869. throw new Exception('用户钱包不存在');
  870. }
  871. if ($wallet->available_balance >= $totalAmount) {
  872. $balanceAmount = $totalAmount;
  873. $payAmount = 0;
  874. } else {
  875. $balanceAmount = $wallet->available_balance;
  876. $payAmount = bcsub($totalAmount, $balanceAmount, 2);
  877. }
  878. }
  879. return [$balanceAmount, $payAmount];
  880. }
  881. /**
  882. * 获取订单抢单池列表
  883. *
  884. * @param int $orderId 订单ID
  885. * @return array 抢单池列表
  886. */
  887. public function getOrderGrabList(int $orderId): array
  888. {
  889. try {
  890. // 查询订单信息
  891. $order = Order::where('id', $orderId)
  892. ->whereIn('state', [OrderStatus::CREATED->value])
  893. ->firstOrFail();
  894. // 查询抢单池列表
  895. $grabList = $order->grabRecords()->with(['coach.info'])->get();
  896. // 格式化返回数据
  897. $result = [];
  898. foreach ($grabList as $grab) {
  899. $coach = $grab->coach;
  900. $result[] = [
  901. 'id' => $grab->id,
  902. 'coach_id' => $coach->id,
  903. 'nickname' => $coach->info->nickname,
  904. 'avatar' => $coach->info->avatar,
  905. 'distance' => $grab->distance,
  906. 'created_at' => $grab->created_at->format('Y-m-d H:i:s'),
  907. ];
  908. }
  909. return $result;
  910. } catch (\Exception $e) {
  911. Log::error('获取订单抢单池列表失败', [
  912. 'error' => $e->getMessage(),
  913. 'order_id' => $orderId,
  914. ]);
  915. throw $e;
  916. }
  917. }
  918. /**
  919. * 指定技师
  920. */
  921. public function assignCoach(int $userId, int $orderId, int $coachId): bool
  922. {
  923. return DB::transaction(function () use ($userId, $orderId, $coachId) {
  924. try {
  925. // 1. 验证参数
  926. $order = $this->validateAssignOrder($userId, $orderId);
  927. // 2. 验证技师
  928. $coach = $this->validateCoach($coachId);
  929. // 3. 更新订单信息
  930. $this->updateOrderForAssign($order, $coachId);
  931. // 4. 创建订单记录(指派)
  932. $this->createAssignRecord($order, $userId);
  933. // 5. 处理支付
  934. if ($order->payment_type == 'balance') {
  935. $this->handleBalancePaymentForAssign($order, $userId, $coachId);
  936. }
  937. return true;
  938. } catch (Exception $e) {
  939. $this->logAssignCoachError($e, $userId, $orderId, $coachId);
  940. throw $e;
  941. }
  942. });
  943. }
  944. /**
  945. * 创建指派记录
  946. */
  947. private function createAssignRecord(Order $order, int $userId): void
  948. {
  949. OrderRecord::create([
  950. 'order_id' => $order->id,
  951. 'object_id' => $userId,
  952. 'object_type' => MemberUser::class,
  953. 'state' => 'assigned',
  954. 'remark' => '指定技师',
  955. ]);
  956. }
  957. /**
  958. * 处理指派订单的余额支付
  959. */
  960. private function handleBalancePaymentForAssign(Order $order, int $userId, int $coachId): void
  961. {
  962. // 验证余额
  963. $user = MemberUser::find($userId);
  964. $wallet = $user->wallet;
  965. abort_if($wallet->available_balance < $order->balance_amount, 400, '可用余额不足');
  966. // 扣除余额
  967. $wallet->decrement('total_balance', $order->balance_amount);
  968. $wallet->decrement('available_balance', $order->balance_amount);
  969. // 更新订单状态
  970. $order->update(['state' => 'wait_service']);
  971. // 创建钱包支付记录
  972. WalletPaymentRecord::create([
  973. 'order_id' => $order->id,
  974. 'wallet_id' => $wallet->id,
  975. 'payment_no' => 'balance_'.$order->id,
  976. 'payment_method' => 'balance',
  977. 'total_amount' => $order->balance_amount,
  978. 'actual_amount' => 0,
  979. 'used_wallet_balance' => $order->balance_amount,
  980. 'used_recharge_balance' => 0,
  981. 'state' => 'success',
  982. ]);
  983. // 创建支付成功记录
  984. OrderRecord::create([
  985. 'order_id' => $order->id,
  986. 'object_id' => $userId,
  987. 'object_type' => MemberUser::class,
  988. 'state' => 'pay',
  989. 'remark' => '余额支付成功',
  990. ]);
  991. // 创建接单记录
  992. OrderRecord::create([
  993. 'order_id' => $order->id,
  994. 'object_type' => CoachUser::class,
  995. 'object_id' => $coachId,
  996. 'state' => 'accepted',
  997. 'remark' => '抢单成功',
  998. ]);
  999. // 更新抢单记录
  1000. $this->updateGrabRecords($order->id, $coachId);
  1001. }
  1002. /**
  1003. * 验证指定技师的订单条件
  1004. */
  1005. private function validateAssignOrder(int $userId, int $orderId): Order
  1006. {
  1007. // 验证用户状态
  1008. $user = MemberUser::where('id', $userId)
  1009. ->where('state', 'enable')
  1010. ->firstOrFail();
  1011. // 验证订单状态
  1012. $order = Order::where('user_id', $userId)
  1013. ->where('id', $orderId)
  1014. ->whereIn('state', [OrderStatus::CREATED->value])
  1015. ->lockForUpdate()
  1016. ->firstOrFail();
  1017. return $order;
  1018. }
  1019. /**
  1020. * 更新订单信息
  1021. */
  1022. private function updateOrderForAssign(Order $order, int $coachId): void
  1023. {
  1024. // 修改订单技师
  1025. $order->coach_id = $coachId;
  1026. // 待支付订单需要重新计算金额
  1027. if ($order->state == OrderStatus::CREATED->value) {
  1028. $amounts = $this->calculateOrderAmount(
  1029. $order->user_id,
  1030. $order->address_id,
  1031. $coachId,
  1032. $order->project_id,
  1033. $order->agent_id,
  1034. $order->payment_type === PaymentMethod::BALANCE->value
  1035. );
  1036. // 更新订单金额
  1037. $order->total_amount = $amounts['total_amount'];
  1038. $order->balance_amount = $amounts['balance_amount'];
  1039. $order->pay_amount = $amounts['pay_amount'];
  1040. $order->discount_amount = $amounts['coupon_amount'];
  1041. $order->project_amount = $amounts['project_amount'];
  1042. $order->traffic_amount = $amounts['delivery_fee'];
  1043. }
  1044. $order->save();
  1045. }
  1046. /**
  1047. * 更新抢单记录
  1048. */
  1049. private function updateGrabRecords(int $orderId, int $coachId): void
  1050. {
  1051. OrderGrabRecord::where('order_id', $orderId)
  1052. ->update([
  1053. 'state' => 'success',
  1054. 'coach_id' => $coachId,
  1055. ]);
  1056. }
  1057. /**
  1058. * 记录指派技师错误日志
  1059. */
  1060. private function logAssignCoachError(Exception $e, int $userId, int $orderId, int $coachId): void
  1061. {
  1062. Log::error('分配技师失败:', [
  1063. 'message' => $e->getMessage(),
  1064. 'user_id' => $userId,
  1065. 'order_id' => $orderId,
  1066. 'coach_id' => $coachId,
  1067. ]);
  1068. }
  1069. }