OrderService.php 41 KB

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