OrderService.php 37 KB

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