OrderService.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. <?php
  2. namespace App\Services\Client;
  3. use App\Enums\OrderGrabRecordStatus;
  4. use App\Enums\OrderRecordStatus;
  5. use App\Enums\OrderSource;
  6. use App\Enums\OrderStatus;
  7. use App\Enums\OrderType;
  8. use App\Enums\PaymentMethod;
  9. use App\Enums\ProjectStatus;
  10. use App\Enums\TechnicianAuthStatus;
  11. use App\Enums\TechnicianStatus;
  12. use App\Enums\UserStatus;
  13. use App\Models\AgentConfig;
  14. use App\Models\AgentInfo;
  15. use App\Models\CoachConfig;
  16. use App\Models\CoachUser;
  17. use App\Models\MemberUser;
  18. use App\Models\Order;
  19. use App\Models\OrderGrabRecord;
  20. use App\Models\OrderRecord;
  21. use App\Models\Project;
  22. use App\Models\SysConfig;
  23. use App\Models\User;
  24. use App\Models\WalletPaymentRecord;
  25. use App\Models\WalletRefundRecord;
  26. use Exception;
  27. use Illuminate\Support\Facades\Auth;
  28. use Illuminate\Support\Facades\DB;
  29. use Illuminate\Support\Facades\Log;
  30. use Illuminate\Support\Facades\Redis;
  31. class OrderService
  32. {
  33. protected AgentService $agentService;
  34. protected ProjectService $projectService;
  35. public function __construct(
  36. AgentService $agentService,
  37. ProjectService $projectService
  38. ) {
  39. $this->agentService = $agentService;
  40. $this->projectService = $projectService;
  41. }
  42. /**
  43. * 订单初始化
  44. *
  45. * 初始化订单信息,包括用户钱包、技师信息、项目信息、地址信息和订单金额等
  46. *
  47. * @param int $userId 用户ID
  48. * @param array $data 订单数据
  49. * @return array 返回初始化的订单信息
  50. *
  51. * @throws \Exception 初始化失败时抛出异常
  52. */
  53. public function initialize(int $userId, array $data): array
  54. {
  55. try {
  56. // 参数验证
  57. abort_if(empty($data['project_id']), 400, '项目ID不能为空');
  58. abort_if(empty($data['coach_id']), 400, '技师ID不能为空');
  59. return DB::transaction(function () use ($userId, $data) {
  60. $user = MemberUser::find($userId);
  61. abort_if(! $user || $user->state != 'enable', 400, '用户状态异常');
  62. // 查询用户钱包
  63. $wallet = $user->wallet;
  64. // 查询默认地址
  65. $address = $user->address;
  66. $areaCode = $address ? $address->area_code : ($data['area_code'] ?? null);
  67. abort_if(empty($areaCode), 400, '区域编码不能为空');
  68. // 查询技师数据
  69. $coach = $this->validateCoach($data['coach_id']);
  70. // 获取项目详情
  71. $project = $this->projectService->getProjectDetail($data['project_id'], $areaCode);
  72. abort_if(! $project, 400, '项目不存在');
  73. // 计算订单金额
  74. $amounts = $this->calculateOrderAmount(
  75. $userId,
  76. $address?->id ?? 0,
  77. $data['coach_id'],
  78. $data['project_id'],
  79. $project->agent_id,
  80. false,
  81. $data['latitude'],
  82. $data['longitude']
  83. );
  84. return [
  85. 'wallet' => $wallet,
  86. 'coach' => $coach,
  87. 'project' => $project,
  88. 'address' => $address,
  89. 'amounts' => $amounts,
  90. ];
  91. });
  92. } catch (Exception $e) {
  93. Log::error('订单初始化失败', [
  94. 'userId' => $userId,
  95. 'data' => $data,
  96. 'error' => $e->getMessage(),
  97. 'trace' => $e->getTraceAsString(),
  98. ]);
  99. throw $e;
  100. }
  101. }
  102. /**
  103. * 创建订单
  104. */
  105. public function createOrder(int $userId, array $data): array
  106. {
  107. return DB::transaction(function () use ($userId, $data) {
  108. // 1. 参数校验
  109. $user = MemberUser::where('id', $userId)
  110. ->where('state', UserStatus::OPEN->value())
  111. ->firstOrFail();
  112. $project = Project::where('id', $data['project_id'])
  113. ->where('state', ProjectStatus::OPEN->value())
  114. ->firstOrFail();
  115. // 2. 订单类型判断
  116. $orderType = $data['order_type'];
  117. // 关键操作:验证必要参数
  118. abort_if(empty($data['project_id']), 400, '项目ID不能为空');
  119. // 上门订单必须指定技师和地址
  120. abort_if($orderType == OrderType::VISIT->value && empty($data['coach_id']), 400, '技师ID不能为空');
  121. abort_if($orderType == OrderType::VISIT->value && empty($data['address_id']), 400, '地址ID不能为空');
  122. // 抢单订单必须指定地址
  123. abort_if($orderType == OrderType::GRAB->value && empty($data['address_id']), 400, '地址ID不能为空');
  124. // 加钟订单必须指定原订单
  125. abort_if($orderType == OrderType::OVERTIME->value && empty($data['order_id']), 400, '原订单ID不能为空');
  126. // 到店订单必须指定店铺
  127. abort_if($orderType == OrderType::SHOP->value && empty($data['shop_id']), 400, '店铺ID不能为空');
  128. // 应急订单必须指定地址
  129. abort_if($orderType == OrderType::EMERGENCY->value && empty($data['address_id']), 400, '地址ID不能为空');
  130. // 3. 验证技师
  131. // 4. 根据订单类型处理
  132. // 上门订单
  133. if ($orderType == OrderType::VISIT->value) {
  134. $this->validateCoach($data['coach_id']);
  135. }
  136. // 加钟订单
  137. if ($orderType == OrderType::OVERTIME->value) {
  138. $originalOrder = $this->getOriginalOrder($user, $data['order_id']);
  139. $data['address_id'] = $originalOrder->address_id;
  140. $this->validateCoach($originalOrder->coach_id);
  141. abort_if(! in_array($originalOrder->state, ['service_ing', 'service_end']), 400, '原订单状态不允许加钟');
  142. $data = $this->prepareAddTimeData($originalOrder, $data);
  143. }
  144. $address = $user->addresses()
  145. ->where('id', $data['address_id'])
  146. ->firstOrFail();
  147. // 5. 计算订单金额
  148. $data['use_balance'] = $data['use_balance'] ?? false;
  149. $amounts = $this->calculateOrderAmount(
  150. $userId,
  151. $address->id,
  152. $data['coach_id'],
  153. $data['project_id'],
  154. $project?->agent_id,
  155. $data['use_balance']
  156. );
  157. // 6. 验证金额和余额
  158. abort_if($amounts['total_amount'] <= 0, 400, '订单金额异常');
  159. if ($data['payment_type'] == PaymentMethod::BALANCE->value) {
  160. $wallet = $user->wallet;
  161. abort_if($wallet->available_balance < $amounts['balance_amount'], 400, '可用余额不足');
  162. }
  163. // 7. 创建订单记录
  164. $order = $this->createOrderRecord($userId, $data, $orderType, $address, $data['payment_type'], (object) $amounts);
  165. // 8. 余额支付处理
  166. if ($order->payment_type == PaymentMethod::BALANCE->value && $orderType != OrderType::GRAB->value) {
  167. $this->handleBalancePayment($user, $order, $orderType);
  168. }
  169. return [
  170. 'order_id' => $order->id,
  171. 'payment_type' => $order->payment_type,
  172. ];
  173. });
  174. }
  175. // 提取方法:验证技师
  176. public function validateCoach(int $coachId): CoachUser
  177. {
  178. return CoachUser::where('id', $coachId)
  179. ->where('state', TechnicianStatus::ACTIVE->value)
  180. ->whereHas('info', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  181. ->whereHas('qual', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  182. ->whereHas('real', fn ($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  183. ->firstOrFail();
  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. $existsGrabSuccess = $order->grabRecords()
  931. ->where('state', OrderGrabRecordStatus::SUCCEEDED->value)
  932. ->exists();
  933. abort_if($existsGrabSuccess, 400, '该订单已抢单完成');
  934. // 4. 更新订单信息
  935. $this->updateOrderForAssign($order, $coachId);
  936. // 5. 创建订单记录(指派)
  937. $this->createAssignRecord($order, $userId);
  938. // 6. 处理支付
  939. if ($order->payment_type === PaymentMethod::BALANCE->value) {
  940. $this->handleBalancePaymentForAssign($order, $userId, $coachId);
  941. }
  942. return true;
  943. } catch (Exception $e) {
  944. $this->logAssignCoachError($e, $userId, $orderId, $coachId);
  945. throw $e;
  946. }
  947. });
  948. }
  949. /**
  950. * 创建指派记录
  951. */
  952. private function createAssignRecord(Order $order, int $userId): void
  953. {
  954. OrderRecord::create([
  955. 'order_id' => $order->id,
  956. 'object_id' => $userId,
  957. 'object_type' => MemberUser::class,
  958. 'state' => OrderRecordStatus::ASSIGNED->value,
  959. 'remark' => '指定技师',
  960. ]);
  961. }
  962. /**
  963. * 处理指派订单的余额支付
  964. */
  965. private function handleBalancePaymentForAssign(Order $order, int $userId, int $coachId): void
  966. {
  967. // 验证余额
  968. $user = MemberUser::find($userId);
  969. $wallet = $user->wallet;
  970. abort_if($wallet->available_balance < $order->balance_amount, 400, '可用余额不足');
  971. // 扣除余额
  972. $wallet->decrement('total_balance', $order->balance_amount);
  973. $wallet->decrement('available_balance', $order->balance_amount);
  974. // 更新订单状态
  975. $order->update(['state' => OrderStatus::PAID->value]);
  976. // 创建钱包支付记录
  977. WalletPaymentRecord::create([
  978. 'order_id' => $order->id,
  979. 'wallet_id' => $wallet->id,
  980. 'payment_no' => 'balance_'.$order->id,
  981. 'payment_method' => 'balance',
  982. 'total_amount' => $order->balance_amount,
  983. 'actual_amount' => 0,
  984. 'used_wallet_balance' => $order->balance_amount,
  985. 'used_recharge_balance' => 0,
  986. 'state' => 'success',
  987. ]);
  988. // 创建接单记录
  989. // OrderRecord::create([
  990. // 'order_id' => $order->id,
  991. // 'object_id' => $userId,
  992. // 'object_type' => MemberUser::class,
  993. // 'state' => OrderRecordStatus::ASSIGNED->value,
  994. // 'remark' => '已分配技师',
  995. // ]);
  996. // 创建支付成功记录
  997. OrderRecord::create([
  998. 'order_id' => $order->id,
  999. 'object_id' => $userId,
  1000. 'object_type' => MemberUser::class,
  1001. 'state' => OrderRecordStatus::PAID->value,
  1002. 'remark' => '余额支付成功',
  1003. ]);
  1004. // 更新抢单记录
  1005. $this->updateGrabRecords($order->id, $coachId);
  1006. }
  1007. /**
  1008. * 验证指定技师的订单条件
  1009. */
  1010. private function validateAssignOrder(int $userId, int $orderId): Order
  1011. {
  1012. // 验证用户状态
  1013. $user = MemberUser::where('id', $userId)
  1014. ->where('state', UserStatus::OPEN->value)
  1015. ->firstOrFail();
  1016. // 验证订单状态
  1017. $order = $user->orders()
  1018. ->where('id', $orderId)
  1019. ->whereIn('state', [OrderStatus::CREATED->value, OrderStatus::PAID->value])
  1020. ->lockForUpdate()
  1021. ->firstOrFail();
  1022. return $order;
  1023. }
  1024. /**
  1025. * 更新订单信息
  1026. */
  1027. private function updateOrderForAssign(Order $order, int $coachId): void
  1028. {
  1029. // 修改订单技师
  1030. $order->coach_id = $coachId;
  1031. // 待支付订单需要重新计算金额
  1032. if ($order->state == OrderStatus::CREATED->value) {
  1033. $amounts = $this->calculateOrderAmount(
  1034. $order->user_id,
  1035. $order->address_id,
  1036. $coachId,
  1037. $order->project_id,
  1038. $order->agent_id,
  1039. $order->payment_type === PaymentMethod::BALANCE->value
  1040. );
  1041. // 更新订单金额
  1042. $order->total_amount = $amounts['total_amount'];
  1043. $order->balance_amount = $amounts['balance_amount'];
  1044. $order->pay_amount = $amounts['pay_amount'];
  1045. $order->discount_amount = $amounts['coupon_amount'];
  1046. $order->project_amount = $amounts['project_amount'];
  1047. $order->traffic_amount = $amounts['delivery_fee'];
  1048. }
  1049. $order->save();
  1050. }
  1051. /**
  1052. * 更新抢单记录
  1053. */
  1054. private function updateGrabRecords(int $orderId, int $coachId): void
  1055. {
  1056. OrderGrabRecord::where('order_id', $orderId)
  1057. ->update([
  1058. 'state' => OrderGrabRecordStatus::SUCCEEDED->value,
  1059. 'coach_id' => $coachId,
  1060. ]);
  1061. }
  1062. /**
  1063. * 记录指派技师错误日志
  1064. */
  1065. private function logAssignCoachError(Exception $e, int $userId, int $orderId, int $coachId): void
  1066. {
  1067. Log::error('分配技师失败:', [
  1068. 'message' => $e->getMessage(),
  1069. 'user_id' => $userId,
  1070. 'order_id' => $orderId,
  1071. 'coach_id' => $coachId,
  1072. ]);
  1073. }
  1074. }