OrderService.php 47 KB

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