OrderService.php 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426
  1. <?php
  2. namespace App\Services\Coach;
  3. use App\Models\Coach;
  4. use App\Models\Order;
  5. use App\Enums\OrderType;
  6. use App\Models\CoachUser;
  7. use App\Enums\OrderSource;
  8. use App\Enums\OrderStatus;
  9. use App\Models\MemberUser;
  10. use App\Models\OrderRecord;
  11. use App\Enums\PaymentMethod;
  12. use App\Enums\ProjectStatus;
  13. use App\Models\SettingGroup;
  14. use App\Jobs\AutoFinishOrder;
  15. use App\Enums\TechnicianStatus;
  16. use App\Models\OrderGrabRecord;
  17. use App\Enums\OrderRecordStatus;
  18. use App\Enums\TransactionStatus;
  19. use Illuminate\Support\Facades\DB;
  20. use App\Enums\TechnicianAuthStatus;
  21. use App\Enums\TechnicianWorkStatus;
  22. use Illuminate\Support\Facades\Log;
  23. use App\Enums\OrderGrabRecordStatus;
  24. use App\Services\SettingItemService;
  25. use App\Enums\TechnicianLocationType;
  26. use App\Models\SettingItem;
  27. use Illuminate\Support\Facades\Cache;
  28. use Illuminate\Support\Facades\Redis;
  29. use Exception;
  30. use Carbon\Carbon;
  31. class OrderService
  32. {
  33. private const DEFAULT_PER_PAGE = 10;
  34. private const MAX_DISTANCE = 40; // 最大距离限制(公里)
  35. private SettingItemService $settingService;
  36. private CommissionService $commissionService;
  37. public function __construct(SettingItemService $settingService, CommissionService $commissionService)
  38. {
  39. $this->settingService = $settingService;
  40. $this->commissionService = $commissionService;
  41. }
  42. /**
  43. * 根据标签页获取订单状态
  44. */
  45. private function getOrderStatesByTab(int $tab): array
  46. {
  47. return match ($tab) {
  48. 1 => [ // 待接单
  49. OrderStatus::PAID->value,
  50. ],
  51. 2 => [ // 进行中
  52. OrderStatus::ACCEPTED->value,
  53. OrderStatus::DEPARTED->value,
  54. OrderStatus::ARRIVED->value,
  55. OrderStatus::SERVICE_START->value,
  56. OrderStatus::SERVICING->value,
  57. OrderStatus::SERVICE_END->value,
  58. OrderStatus::LEAVING->value,
  59. ],
  60. 3 => [ // 已完成
  61. OrderStatus::COMPLETED->value,
  62. ],
  63. 4 => [ // 待评价
  64. OrderStatus::LEFT->value,
  65. ],
  66. 5 => [ // 已取消
  67. OrderStatus::CANCELLED->value,
  68. OrderStatus::REFUNDED->value,
  69. OrderStatus::REJECTED->value,
  70. OrderStatus::REFUNDING->value,
  71. OrderStatus::REFUND_FAILED->value,
  72. ],
  73. default => [], // 全部订单(已在基础查询中排除未支付和已分配状态)
  74. };
  75. }
  76. /**
  77. * 获取技师订单列表
  78. *
  79. * @param int $coachId 技师ID
  80. * @param array $params 查询参数
  81. * @return array
  82. */
  83. public function getOrderList(int $coachId, array $params = [])
  84. {
  85. // 1. 构建基础查询
  86. $query = Order::query()
  87. ->where('coach_id', $coachId)
  88. ->whereNull('coach_deleted_at') // 不显示技师已删除的订单
  89. ->whereNotIn('state', [
  90. OrderStatus::CREATED->value, // 排除未支付订单
  91. OrderStatus::ASSIGNED->value, // 排除已分配状态订单
  92. ])
  93. ->with(['project', 'user']);
  94. // 2. 获取状态过滤条件
  95. $states = $this->getOrderStatesByTab($params['tab'] ?? 0);
  96. if (!empty($states)) {
  97. $query->whereIn('state', $states);
  98. }
  99. // 3. 分页查询
  100. $perPage = $params['per_page'] ?? 10;
  101. $page = $params['page'] ?? 1;
  102. $paginatedOrders = $query->orderBy('created_at', 'desc')
  103. ->paginate($perPage, ['*'], 'page', $page);
  104. // 4. 格式化数据
  105. $formattedOrders = collect($paginatedOrders->items())->map(function ($order) {
  106. return $this->formatOrderListItem($order);
  107. });
  108. return [
  109. 'items' => $formattedOrders,
  110. 'total' => $paginatedOrders->total(),
  111. ];
  112. }
  113. /**
  114. * 获取可抢订单列表
  115. *
  116. * @param int $userId 技师用户ID
  117. * @param array $params 请求参数
  118. * @return \Illuminate\Pagination\LengthAwarePaginator
  119. */
  120. public function getGrabList(int $userId, array $params)
  121. {
  122. try {
  123. abort_if(empty($params['longitude']) || empty($params['latitude']), 422, '请提供当前位置');
  124. // 加载用户和技师信息
  125. $user = MemberUser::with([
  126. 'coach',
  127. 'coach.info',
  128. 'coach.real',
  129. 'coach.qual',
  130. 'coach.locations',
  131. 'coach.projects',
  132. ])->findOrFail($userId);
  133. // 验证技师信息并获取固定位置
  134. [$coach, $fixedLocation] = $this->validateCoach($user);
  135. // 获取技师项目信息
  136. $coachProjects = $coach->projects;
  137. $coachProjectIds = $coachProjects->pluck('project_id')->toArray();
  138. // 使用技师当前位置获取附近订单
  139. $currentLocation = [
  140. 'longitude' => $params['longitude'],
  141. 'latitude' => $params['latitude']
  142. ];
  143. $orderDistances = $this->getNearbyOrders($currentLocation);
  144. // 构建订单查询
  145. $query = $this->buildOrderQuery($coachProjectIds, $orderDistances);
  146. // 获取技师已抢单记录
  147. $grabRecords = OrderGrabRecord::where('coach_id', $coach->id)
  148. ->whereIn('order_id', array_keys($orderDistances))
  149. ->get()
  150. ->keyBy('order_id');
  151. // 分页查询并处理数据
  152. $paginatedOrders = $query->paginate(
  153. $params['per_page'] ?? 10,
  154. ['*'],
  155. 'page',
  156. $params['page'] ?? 1
  157. );
  158. // 处理订单数据
  159. $items = collect($paginatedOrders->items())->map(function ($order) use ($orderDistances, $coach, $fixedLocation, $grabRecords) {
  160. // 获取路费规则
  161. $settings = $this->getDeliveryFeeRules($coach->id, $order->agent_id);
  162. // 设置当前位置到订单的距离
  163. $order->distance = $orderDistances[$order->id];
  164. // 处理代理商项目价格
  165. $this->processAgentPrice($order);
  166. // 使用 Redis GEODIST 计算固定位置到订单的距离
  167. $fixedDistance = Redis::geodist(
  168. 'order_locations',
  169. 'order_' . $order->id,
  170. $fixedLocation->longitude . ',' . $fixedLocation->latitude,
  171. 'km'
  172. ) ?? 0;
  173. // 计算路费
  174. $trafficFee = $this->calculateFeeByRules($fixedDistance, $settings, $coach->id, $order->project_id);
  175. // 设置路费相关信息
  176. $order->traffic_amount = round($trafficFee, 2);
  177. $order->fixed_distance = round($fixedDistance, 1);
  178. // 计算技师分佣金额
  179. $order->coach_income = round($order->project_amount * 0.5, 2);
  180. // 设置抢单状态
  181. $order->is_grabbed = isset($grabRecords[$order->id]);
  182. $order->grab_state = $grabRecords[$order->id]->state ?? null;
  183. return $order;
  184. });
  185. return [
  186. 'items' => $items,
  187. 'total' => $paginatedOrders->total()
  188. ];
  189. } catch (\Exception $e) {
  190. Log::error('获取可抢订单列表失败', [
  191. 'user_id' => $userId,
  192. 'params' => $params,
  193. 'error' => $e->getMessage(),
  194. 'trace' => $e->getTraceAsString(),
  195. ]);
  196. throw $e;
  197. }
  198. }
  199. /**
  200. * 根据规则计算路费
  201. *
  202. * @param float $distance 距离(公里)
  203. * @param array{
  204. * min_distance: float, // 最小计费距离(公里)
  205. * min_fee: float, // 小路费(元)
  206. * per_km_fee: float, // 每公里费用(元)
  207. * max_distance: float, // 最大服务距离(公里)
  208. * max_fee: float, // 最大路费(元)
  209. * free_distance: float // 免费服务距离(公里)
  210. * } $rules 计费规则
  211. * @param int $coachId 技师ID
  212. * @param int $projectId 项目ID
  213. * @return float 路费金额(元)
  214. */
  215. private function calculateFeeByRules(float $distance, array $rules, int $coachId, int $projectId): float
  216. {
  217. // 1. 获取技师项目路费设置
  218. $coachProject = DB::table('coach_project')
  219. ->where('coach_id', $coachId)
  220. ->where('project_id', $projectId)
  221. ->where('state', 1) // 启用状态
  222. ->first();
  223. // 2. 如果技师设置了免路费,直接返回技师设置的固定路费
  224. if ($coachProject && (int)$coachProject->traffic_fee_type === 0) {
  225. return 0.00;
  226. }
  227. // 4. 检查是否在免费距离内
  228. if ($distance <= $rules['free_distance']) {
  229. return 0.00;
  230. }
  231. // 5. 计算实际计费距离
  232. $chargeDistance = max(0, $distance - $rules['free_distance']);
  233. // 6. 检查是否达到最小计费距离
  234. if ($chargeDistance < $rules['min_distance']) {
  235. return $rules['min_fee'];
  236. }
  237. // 7. 计算基础路费
  238. $fee = bcmul($chargeDistance, $rules['per_km_fee'], 2);
  239. // 8. 应用最小路费限制
  240. $fee = max($fee, $rules['min_fee']);
  241. // 9. 应用最大路费限制
  242. $fee = min($fee, $rules['max_fee']);
  243. // 10. 如果技师设置了固定路费,使用技师设置的路费
  244. if ($coachProject && $coachProject->traffic_fee > 0) {
  245. $fee = (float)$coachProject->traffic_fee;
  246. }
  247. // 11. 根据路费类型计算(单程/双程)
  248. if ($coachProject && $coachProject->traffic_fee_type === 2) { // 2表示双程
  249. $fee = bcmul($fee, '2', 2); // 双程路费翻倍
  250. }
  251. return $fee;
  252. }
  253. /**
  254. * 验证技师信息
  255. */
  256. private function validateCoach(MemberUser $user): array
  257. {
  258. $coach = $user->coach;
  259. abort_if(! $coach, 404, '技师不存在');
  260. abort_if(! $coach->info, 404, '技师信息不存在');
  261. abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状态异常');
  262. abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过');
  263. abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过');
  264. $location = $coach->locations()
  265. ->where('type', TechnicianLocationType::COMMON->value)
  266. ->first();
  267. abort_if(! $location, 404, '技师定位地址不存在');
  268. return [$coach, $location];
  269. }
  270. /**
  271. * 获取路费计算规则
  272. *
  273. * @return array{
  274. * min_distance: float,
  275. * min_fee: float,
  276. * per_km_fee: float,
  277. * max_distance: float,
  278. * max_fee: float,
  279. * free_distance: float
  280. * }
  281. */
  282. private function getDeliveryFeeRules(int $coachId, ?int $agentId): array
  283. {
  284. // 1. 获取系统默认规则
  285. $defaultRules = $this->getDefaultDeliveryFeeRules();
  286. // 2. 获取代理商规则(如果有)
  287. $agentRules = $agentId ? $this->getAgentDeliveryFeeRules($agentId) : [];
  288. // 3. 获取技师个性化规则
  289. // $coachRules = $this->getCoachDeliveryFeeRules($coachId);
  290. // 4. 合并规则,优先级:技师 > 代理商 > 系统默认
  291. return array_merge(
  292. $defaultRules,
  293. $agentRules
  294. );
  295. }
  296. /**
  297. * 获取系统默认路费规则
  298. */
  299. private function getDefaultDeliveryFeeRules(): array
  300. {
  301. // TODO: 从配置表中获取系统默认路费规则
  302. return [
  303. 'min_distance' => 3.0, // 最小计费距离(公里)
  304. 'base_price' => 10.0, // 最小路费(元)
  305. 'per_km_price' => 3.0, // 每公里费用(元)
  306. 'max_distance' => 120.0, // 最大服务距离(公里)
  307. 'max_fee' => 50.0, // 最大路费(元)
  308. 'free_distance' => 1.0, // 免费服务距离(公里)
  309. ];
  310. }
  311. /**
  312. * 获取代理商路费规则
  313. */
  314. private function getAgentDeliveryFeeRules(int $agentId): array
  315. {
  316. // TODO: 从配置表中获取代理商路费规则
  317. return [];
  318. }
  319. /**
  320. * 获取技师路费规则
  321. */
  322. private function getCoachDeliveryFeeRules(int $coachId): array
  323. {
  324. // TODO: 从配置表中获取技师个性化路费规则
  325. return [];
  326. }
  327. /**
  328. * 获取附近订单
  329. *
  330. * @param array $location 位置信息,包含 longitude 和 latitude
  331. * @param float $maxDistance 最大距离(公里)
  332. * @return array
  333. */
  334. private function getNearbyOrders(array $location, float $maxDistance = 40): array
  335. {
  336. $nearbyOrders = Redis::georadius(
  337. 'order_locations',
  338. $location['longitude'],
  339. $location['latitude'],
  340. $maxDistance,
  341. 'km',
  342. ['WITHCOORD', 'WITHDIST']
  343. );
  344. if (! $nearbyOrders) {
  345. Log::warning('获取附近订单失败', [
  346. 'location' => $location
  347. ]);
  348. return [];
  349. }
  350. $orderDistances = [];
  351. foreach ($nearbyOrders as $order) {
  352. $orderId = str_replace('order_', '', $order[0]);
  353. $distance = round($order[1], 1);
  354. $orderDistances[$orderId] = $distance;
  355. }
  356. return $orderDistances;
  357. }
  358. /**
  359. * 构建订单查询
  360. */
  361. private function buildOrderQuery(array $coachProjectIds, array $orderDistances)
  362. {
  363. return Order::query()
  364. ->select([
  365. 'id',
  366. 'order_no',
  367. 'project_id',
  368. 'location',
  369. 'address',
  370. 'latitude',
  371. 'longitude',
  372. 'service_time',
  373. 'created_at',
  374. 'agent_id',
  375. 'project_amount', // 项目金额
  376. DB::raw('0 as distance'),
  377. ])
  378. ->with(['project', 'agent.projects'])
  379. ->where('state', OrderStatus::CREATED)
  380. ->whereIn('project_id', $coachProjectIds)
  381. ->whereIn('id', array_keys($orderDistances))
  382. ->orderBy('created_at', 'desc');
  383. }
  384. /**
  385. * 处理代理商价格
  386. */
  387. private function processAgentPrice($order): void
  388. {
  389. if ($order->agent_id && $order->agent && $order->agent->projects) {
  390. $agentProject = $order->agent->projects
  391. ->where('id', $order->project_id)
  392. ->where('state', 1)
  393. ->first();
  394. if ($agentProject) {
  395. $order->project->price = $agentProject->agent_price;
  396. $order->project->duration = $agentProject->duration;
  397. }
  398. }
  399. }
  400. /**
  401. * 计算路费
  402. */
  403. private function calculateTrafficFee($order, $coachProject, array $settings): void
  404. {
  405. $trafficFee = $settings['base_price'];
  406. if ($order->distance > $settings['min_distance']) {
  407. $trafficFee += ($order->distance - $settings['min_distance']) * $settings['per_km_price'];
  408. }
  409. $order->project->traffic_fee = round($trafficFee, 2);
  410. // 程收费
  411. if ($coachProject->is_round_trip) {
  412. $order->project->traffic_fee *= 2;
  413. }
  414. // 计算最终价格
  415. $order->project->final_price = ($order->project->final_price ?? $order->project->price)
  416. + $order->project->traffic_fee;
  417. }
  418. /**
  419. * 技师抢单
  420. *
  421. * @param int $userId 技师用户ID
  422. * @param int $orderId 订单ID
  423. * @return array
  424. */
  425. public function grabOrder(int $userId, int $orderId)
  426. {
  427. return DB::transaction(function () use ($userId, $orderId) {
  428. try {
  429. // 加载用户和技师信息
  430. $user = MemberUser::with([
  431. 'coach',
  432. 'coach.info',
  433. 'coach.real',
  434. 'coach.qual',
  435. 'coach.locations',
  436. 'coach.projects',
  437. ])->findOrFail($userId);
  438. // 验证技师信息
  439. [$coach, $location] = $this->validateCoach($user);
  440. // 获取订单信息
  441. $order = Order::lockForUpdate()->findOrFail($orderId);
  442. // 验证订单状态
  443. abort_if($order->state !== OrderStatus::CREATED->value, 400, '订单状态异常,无法抢单');
  444. // 验证订单类型
  445. abort_if($order->type !== OrderType::GRAB->value, 400, '该订单不是抢单类型');
  446. // 检查技师是否已参与抢单
  447. $existingGrab = $coach->grabRecords()
  448. ->where('order_id', $orderId)
  449. ->first();
  450. abort_if($existingGrab, 400, '您已参与抢单,请勿重复操作');
  451. // 验证订单是否在技师服务范围内
  452. // 通过Redis GEO计算订单与技师的距离
  453. $distance = Redis::geodist(
  454. 'order_locations',
  455. 'order:' . $order->id,
  456. $location->longitude . ',' . $location->latitude,
  457. 'km'
  458. ) ?? PHP_FLOAT_MAX;
  459. abort_if($distance > self::MAX_DISTANCE, 400, '订单超出服务范围');
  460. // 验证技师是否具备该项目服务资格
  461. $coachProject = $coach->projects()
  462. ->where('project_id', $order->project_id)
  463. ->where('state', ProjectStatus::OPEN->value)
  464. ->first();
  465. abort_if(! $coachProject, 400, '未开通该项目服务资格');
  466. // 添加抢单记录
  467. OrderGrabRecord::create([
  468. 'order_id' => $order->id,
  469. 'coach_id' => $coach->id,
  470. 'distance' => $distance,
  471. 'state' => OrderGrabRecordStatus::JOINED->value,
  472. 'created_at' => now(),
  473. ]);
  474. // 记录日志
  475. Log::info('技师参与抢单', [
  476. 'user_id' => $userId,
  477. 'coach_id' => $coach->id,
  478. 'order_id' => $orderId,
  479. ]);
  480. return [
  481. 'message' => '已参与抢单',
  482. 'order_id' => $orderId,
  483. ];
  484. } catch (\Exception $e) {
  485. Log::error('技师参与抢单失败', [
  486. 'user_id' => $userId,
  487. 'order_id' => $orderId,
  488. 'error_message' => $e->getMessage(),
  489. 'error_trace' => $e->getTraceAsString(),
  490. ]);
  491. throw $e;
  492. }
  493. });
  494. }
  495. /**
  496. * 技师接单
  497. *
  498. * @param int $userId 用户ID
  499. * @param int $orderId 订单ID
  500. * @return array
  501. * @throws \Exception
  502. */
  503. public function acceptOrder(int $userId, int $orderId): array
  504. {
  505. return DB::transaction(function () use ($userId, $orderId) {
  506. try {
  507. // 获取技师信息
  508. $user = MemberUser::with(['coach'])->findOrFail($userId);
  509. $coach = $user->coach;
  510. abort_if(!$coach, 404, '技师信息不存在');
  511. // 1. 验证订单状态
  512. $order = Order::where('id', $orderId)
  513. ->where('coach_id', $coach->id)
  514. ->where('state', OrderStatus::PAID->value)
  515. ->lockForUpdate()
  516. ->firstOrFail();
  517. // 2. 更新订单状态为已接单
  518. $order->update([
  519. 'state' => OrderStatus::ACCEPTED->value,
  520. 'accept_time' => now()
  521. ]);
  522. // 3. 创建接单记录
  523. OrderRecord::create([
  524. 'order_id' => $order->id,
  525. 'object_id' => $coach->id,
  526. 'object_type' => CoachUser::class,
  527. 'state' => OrderRecordStatus::ACCEPTED->value,
  528. 'remark' => '技师接单'
  529. ]);
  530. // 4. 如果是加钟订单,需要特殊处理
  531. if ($order->type === OrderType::OVERTIME->value) {
  532. // 获取主订单
  533. $mainOrder = Order::findOrFail($order->parent_id);
  534. // 获取主订单的其他加钟订单(只查询已接单、开始服务、服务中、服务结束状态)
  535. $otherOrders = Order::where('parent_id', $mainOrder->id)
  536. ->where('id', '!=', $order->id)
  537. ->where('type', OrderType::OVERTIME->value)
  538. ->whereIn('state', [
  539. OrderStatus::ACCEPTED->value, // 已接单
  540. OrderStatus::SERVICE_START->value, // 开始服务
  541. OrderStatus::SERVICING->value, //服务中
  542. OrderStatus::SERVICE_END->value // 服务结束
  543. ])
  544. ->orderBy('service_end_time', 'desc')
  545. ->first();
  546. // 设置服务时间
  547. if ($otherOrders) {
  548. // 如果有其他加钟订单,使用最新加钟订单的结束时间作为开始时间
  549. $startTime = $otherOrders->service_end_time;
  550. } else {
  551. // 如果没有其他加钟订单,比较当前时间和主订单结束时间,取较大值
  552. $now = now();
  553. $mainOrderEndTime = Carbon::parse($mainOrder->service_end_time);
  554. $startTime = $now->gt($mainOrderEndTime) ? $now : $mainOrderEndTime;
  555. }
  556. // 计算结束时间
  557. $endTime = Carbon::parse($startTime)->addMinutes($order->project_duration);
  558. // 更新订单状态为开始服务
  559. $order->update([
  560. 'state' => OrderStatus::SERVICING->value,
  561. 'service_start_time' => $startTime,
  562. 'service_end_time' => $endTime
  563. ]);
  564. // 创建开始服务记录
  565. OrderRecord::create([
  566. 'order_id' => $order->id,
  567. 'object_id' => $coach->id,
  568. 'object_type' => CoachUser::class,
  569. 'state' => OrderRecordStatus::STARTED->value,
  570. 'remark' => '开始加钟服务'
  571. ]);
  572. // 派发延迟任务,在服务时长到期后自动完成订单
  573. AutoFinishOrder::dispatch($order)
  574. ->delay($endTime);
  575. }
  576. return ['message' => '接单成功'];
  577. } catch (Exception $e) {
  578. Log::error('接单失败', [
  579. 'user_id' => $userId,
  580. 'order_id' => $orderId,
  581. 'error' => $e->getMessage(),
  582. 'trace' => $e->getTraceAsString()
  583. ]);
  584. throw $e;
  585. }
  586. });
  587. }
  588. /**
  589. * 技师拒单
  590. *
  591. * @param int $userId 用户ID
  592. * @param int $orderId 订单ID
  593. * @param string $reason 拒单原因
  594. */
  595. public function rejectOrder(int $userId, int $orderId, string $reason): array
  596. {
  597. return DB::transaction(function () use ($userId, $orderId, $reason) {
  598. try {
  599. // 获取技师信息(优化关联加载)
  600. $user = MemberUser::with([
  601. 'coach',
  602. 'coach.info',
  603. 'coach.real',
  604. 'coach.qual',
  605. ])->findOrFail($userId);
  606. // 验证技师信息
  607. [$coach, $location] = $this->validateCoach($user);
  608. // 获取订单信息并加锁
  609. $order = Order::lockForUpdate()->findOrFail($orderId);
  610. // 验证订单状态(修正状态判断)
  611. abort_if(! in_array($order->state, [
  612. OrderStatus::ASSIGNED->value,
  613. OrderStatus::PAID->value,
  614. ]), 400, '订单状态异常,无法拒单');
  615. // 验证订单是否分配给该技师
  616. abort_if($order->coach_id !== $coach->id, 403, '该订单未分配给您');
  617. // 检查拒单次数限制
  618. $rejectCount = OrderRecord::where('object_id', $coach->id)
  619. ->where('object_type', CoachUser::class)
  620. ->where('state', OrderRecordStatus::REJECTED->value)
  621. ->whereDate('created_at', today())
  622. ->count();
  623. // 更新订单状态
  624. $order->update([
  625. 'state' => OrderStatus::REJECTED->value,
  626. ]);
  627. // 创建订单记录
  628. OrderRecord::create([
  629. 'order_id' => $order->id,
  630. 'object_id' => $coach->id,
  631. 'object_type' => CoachUser::class,
  632. 'state' => OrderRecordStatus::REJECTED->value,
  633. 'remark' => $reason,
  634. ]);
  635. // 发送消息通知
  636. try {
  637. // event(new OrderRejectedEvent($order, $coach, $reason));
  638. } catch (\Exception $e) {
  639. Log::error('发送拒单通知失败', [
  640. 'order_id' => $orderId,
  641. 'coach_id' => $coach->id,
  642. 'error' => $e->getMessage(),
  643. ]);
  644. }
  645. // 记录日志
  646. Log::info('技师拒单成功', [
  647. 'user_id' => $userId,
  648. 'coach_id' => $coach->id,
  649. 'order_id' => $orderId,
  650. 'order_no' => $order->order_no,
  651. 'reason' => $reason,
  652. 'reject_count' => $rejectCount + 1,
  653. ]);
  654. return [
  655. 'message' => '拒单成功',
  656. 'order_id' => $orderId,
  657. 'order_no' => $order->order_no,
  658. 'reject_count' => $rejectCount + 1,
  659. 'max_reject_count' => 5,
  660. ];
  661. } catch (\Exception $e) {
  662. Log::error('技师拒单失败', [
  663. 'user_id' => $userId,
  664. 'order_id' => $orderId,
  665. 'reason' => $reason,
  666. 'error' => $e->getMessage(),
  667. 'file' => $e->getFile(),
  668. 'line' => $e->getLine(),
  669. ]);
  670. throw $e;
  671. }
  672. });
  673. }
  674. /**
  675. * 技师出发
  676. *
  677. * @param int $userId 技师用户ID
  678. * @param int $orderId 订单ID
  679. */
  680. public function depart(int $userId, int $orderId): array
  681. {
  682. try {
  683. return DB::transaction(function () use ($userId, $orderId) {
  684. // 获取技师信息
  685. $user = MemberUser::with(['coach'])->findOrFail($userId);
  686. // 获取订单信息
  687. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  688. // 检查订单是否存在
  689. abort_if(! $order, 404, '订单不存在');
  690. // 检查是否是该技师的订单
  691. abort_if($order->coach_id !== $user->coach->id, 403, '无权操作此订单');
  692. // 根据订单类型判断订单状态
  693. abort_if($order->state != OrderStatus::ACCEPTED->value, 400, '订单状态不正确');
  694. // 更新订单状态为技师出发
  695. $order->state = OrderStatus::DEPARTED->value;
  696. $order->save();
  697. // 记录订单状态变更日志
  698. OrderRecord::create([
  699. 'order_id' => $orderId,
  700. 'state' => OrderRecordStatus::DEPARTED->value,
  701. 'object_id' => $user->coach->id,
  702. 'object_type' => CoachUser::class,
  703. 'remark' => '技师已出发',
  704. ]);
  705. // 发送通知给用户
  706. // TODO: 发送通知
  707. // event(new TechnicianDepartedEvent($order));
  708. return [
  709. 'status' => true,
  710. 'message' => '操作成功',
  711. 'data' => [
  712. 'order_id' => $orderId,
  713. 'status' => $order->state,
  714. 'created_at' => $order->created_at,
  715. ],
  716. ];
  717. });
  718. } catch (\Exception $e) {
  719. \Log::error('技师出发失败', [
  720. 'user_id' => $userId,
  721. 'order_id' => $orderId,
  722. 'error' => $e->getMessage(),
  723. ]);
  724. throw $e;
  725. }
  726. }
  727. /**
  728. * 技师到达
  729. *
  730. * @param int $userId 技师用户ID
  731. * @param int $orderId 订单ID
  732. */
  733. public function arrive(int $userId, int $orderId): array
  734. {
  735. return DB::transaction(function () use ($userId, $orderId) {
  736. try {
  737. // 获取技师信息
  738. $user = MemberUser::with(['coach'])->findOrFail($userId);
  739. $coach = $user->coach;
  740. abort_if(! $coach, 404, '技师信息不存在');
  741. // 获取订单信息
  742. $order = Order::query()->where('id', $orderId)->lockForUpdate()->first();
  743. abort_if(! $order, 404, '订单不存在');
  744. // 检查是否是该技师的订单
  745. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  746. // 检查订单状态
  747. abort_if(! in_array($order->state, [
  748. OrderStatus::DEPARTED->value,
  749. ]), 400, '订单状态不正确');
  750. $now = now();
  751. // 更新订单状态为技师到达
  752. $order->state = OrderStatus::ARRIVED->value;
  753. $order->save();
  754. // 记录订单状态变更日志
  755. OrderRecord::create([
  756. 'order_id' => $orderId,
  757. 'state' => OrderRecordStatus::ARRIVED->value,
  758. 'object_id' => $coach->id,
  759. 'object_type' => CoachUser::class,
  760. 'remark' => '技师已到达',
  761. ]);
  762. // 更新技师当前位置到Redis GEO
  763. try {
  764. Redis::geoadd(
  765. 'coach_locations',
  766. $order->longitude,
  767. $order->latitude,
  768. $coach->id . '_' . TechnicianLocationType::CURRENT->value
  769. );
  770. } catch (\Exception $e) {
  771. Log::error('更新技师位置失败', [
  772. 'coach_id' => $coach->id,
  773. 'order_id' => $orderId,
  774. 'error' => $e->getMessage(),
  775. ]);
  776. }
  777. // TODO: 发送通知给用户
  778. // event(new TechnicianArrivedEvent($order));
  779. Log::info('技师到达成功', [
  780. 'coach_id' => $coach->id,
  781. 'order_id' => $orderId,
  782. 'arrived_at' => $now,
  783. ]);
  784. return [
  785. 'status' => true,
  786. 'message' => '操作成功',
  787. 'data' => [
  788. 'order_id' => $orderId,
  789. 'status' => $order->state,
  790. 'arrived_at' => $now,
  791. ],
  792. ];
  793. } catch (\Exception $e) {
  794. Log::error('技师到达失败', [
  795. 'user_id' => $userId,
  796. 'order_id' => $orderId,
  797. 'error' => $e->getMessage(),
  798. 'trace' => $e->getTraceAsString(),
  799. ]);
  800. throw $e;
  801. }
  802. });
  803. }
  804. /**
  805. * 技师扫码开始服务
  806. *
  807. * @param int $userId 技师用户ID
  808. * @param int $orderId 订单ID
  809. * @param string $qrCode 客户二维码
  810. */
  811. public function startService(int $userId, int $orderId, string $qrCode): array
  812. {
  813. return DB::transaction(function () use ($userId, $orderId, $qrCode) {
  814. try {
  815. // 获取技师信息
  816. $user = MemberUser::with(['coach'])->findOrFail($userId);
  817. $coach = $user->coach;
  818. abort_if(! $coach, 404, '技师信息不存在');
  819. // 获取订单信息
  820. $order = Order::query()
  821. ->where('id', $orderId)
  822. ->lockForUpdate()
  823. ->first();
  824. abort_if(! $order, 404, '订单不存在');
  825. // 检查是否是该技师的订单
  826. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  827. // 检查订单状态
  828. abort_if(! in_array($order->state, [
  829. OrderStatus::ARRIVED->value,
  830. OrderStatus::PAID->value,
  831. ]), 400, '订单状态不正确');
  832. // 验证二维码
  833. $this->validateQrCode($order, $qrCode);
  834. $now = now();
  835. // 更新订单状态为服务中
  836. $order->state = OrderStatus::SERVICING->value;
  837. $order->service_start_time = $now;
  838. $order->service_end_time = $now->copy()->addMinutes($order->project_duration);
  839. $order->save();
  840. // 更新技师工作状态为忙碌中
  841. $coach->update([
  842. 'work_status' => TechnicianWorkStatus::BUSY->value
  843. ]);
  844. // 记录订单状态变更日志
  845. OrderRecord::create([
  846. 'order_id' => $orderId,
  847. 'state' => OrderRecordStatus::STARTED->value,
  848. 'object_id' => $coach->id,
  849. 'object_type' => CoachUser::class,
  850. 'remark' => '开始服务',
  851. ]);
  852. // 获取项目服务时长(分钟)
  853. $duration = $order->project->duration ?? 60;
  854. // 派发延迟任务,在服务时长到期后自动完成订单
  855. AutoFinishOrder::dispatch($order)
  856. ->delay(now()->addMinutes($duration));
  857. // TODO: 发送通知给用户
  858. // event(new ServiceStartedEvent($order));
  859. Log::info('技师开始服务', [
  860. 'coach_id' => $coach->id,
  861. 'order_id' => $orderId,
  862. 'service_start_time' => $now,
  863. ]);
  864. return [
  865. 'status' => true,
  866. 'message' => '开始服务成功',
  867. 'data' => [
  868. 'order_id' => $orderId,
  869. 'status' => $order->state,
  870. 'service_start_time' => $now,
  871. ],
  872. ];
  873. } catch (\Exception $e) {
  874. Log::error('开始服务失败', [
  875. 'user_id' => $userId,
  876. 'order_id' => $orderId,
  877. 'qr_code' => $qrCode,
  878. 'error' => $e->getMessage(),
  879. 'trace' => $e->getTraceAsString(),
  880. ]);
  881. throw $e;
  882. }
  883. });
  884. }
  885. /**
  886. * 验证客户二维码
  887. *
  888. * @param Order $order 订单对象
  889. * @param string $qrCode 扫描的二维码
  890. */
  891. private function validateQrCode(Order $order, string $qrCode): void
  892. {
  893. // 二维码格式: order_{order_id}_{timestamp}_{sign}
  894. $parts = explode('_', $qrCode);
  895. abort_if(count($parts) !== 4, 400, '二维码格式错误');
  896. [$prefix, $scanOrderId, $timestamp, $sign] = $parts;
  897. // 验证前缀
  898. abort_if($prefix !== 'order', 400, '无效的二维码');
  899. // 验证订单ID
  900. abort_if((int) $scanOrderId !== $order->id, 400, '二维码与订单不匹配');
  901. // 验证时间戳(二维码5分钟内有效)
  902. // $qrTimestamp = (int) $timestamp;
  903. // $now = time();
  904. // abort_if($now - $qrTimestamp > 300, 400, '二维码已过期');
  905. // 验证签名
  906. $correctSign = md5("order_{$order->id}_{$timestamp}_" . config('app.key'));
  907. abort_if($sign !== $correctSign, 400, '二维码签名错误');
  908. }
  909. /**
  910. * 技师撤离
  911. *
  912. * @param int $userId 技师户ID
  913. * @param int $orderId 订单ID
  914. *
  915. * @throws \Exception
  916. */
  917. public function leave(int $userId, int $orderId): array
  918. {
  919. return DB::transaction(function () use ($userId, $orderId) {
  920. try {
  921. // 获取技师信息,同时加载钱包关联
  922. $user = MemberUser::with(['coach', 'coach.wallet'])->findOrFail($userId);
  923. $coach = $user->coach;
  924. abort_if(! $coach, 404, '技师信息不存在');
  925. $coach->load('wallet');
  926. // 获取订单信息
  927. $order = Order::query()
  928. ->with(['coach', 'coach.wallet'])
  929. ->where('id', $orderId)
  930. ->lockForUpdate()
  931. ->firstOrFail();
  932. // 验证订单状态和权限
  933. $this->validateLeaveOrder($order, $coach);
  934. // 更新订单状态
  935. $this->updateOrderStatus($order);
  936. // 记录订单状态变更
  937. $this->createLeaveRecord($order, $coach);
  938. // 处理订单分佣
  939. $this->commissionService->handleOrderCommission($order);
  940. // 清理技师位置信息
  941. $this->cleanCoachLocation($coach);
  942. // TODO: 发送通知给用户
  943. // event(new ServiceCompletedEvent($order));
  944. return [
  945. 'status' => true,
  946. 'message' => '撤离成功',
  947. 'data' => [
  948. 'order_id' => $orderId,
  949. 'state' => $order->state,
  950. 'leave_time' => now(),
  951. ],
  952. ];
  953. } catch (\Exception $e) {
  954. Log::error('技师撤离失败', [
  955. 'user_id' => $userId,
  956. 'order_id' => $orderId,
  957. 'error' => $e->getMessage(),
  958. 'trace' => $e->getTraceAsString(),
  959. ]);
  960. throw $e;
  961. }
  962. });
  963. }
  964. /**
  965. * 验证撤离订单
  966. */
  967. private function validateLeaveOrder(Order $order, CoachUser $coach): void
  968. {
  969. // 检查是否是该技师的订单
  970. abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
  971. // 检查订单状态
  972. abort_if(! in_array($order->state, [
  973. OrderStatus::LEAVING->value,
  974. ]), 400, '订单状态不正确,无法撤离');
  975. // 检查钱包状态
  976. abort_if(! $coach->wallet, 400, '技师钱包信息不存在');
  977. }
  978. /**
  979. * 更新订单状态
  980. */
  981. private function updateOrderStatus(Order $order): void
  982. {
  983. $order->state = OrderStatus::LEFT->value;
  984. $order->save();
  985. }
  986. /**
  987. * 创建撤离记录
  988. */
  989. private function createLeaveRecord(Order $order, CoachUser $coach): void
  990. {
  991. OrderRecord::create([
  992. 'order_id' => $order->id,
  993. 'state' => OrderRecordStatus::COMPLETED->value,
  994. 'object_id' => $coach->id,
  995. 'object_type' => CoachUser::class,
  996. 'remark' => '技师已撤离,服务完成',
  997. ]);
  998. }
  999. /**
  1000. * 清理技师位置信息
  1001. */
  1002. private function cleanCoachLocation(CoachUser $coach): void
  1003. {
  1004. try {
  1005. Redis::zrem(
  1006. 'coach_locations',
  1007. $coach->id . '_' . TechnicianLocationType::CURRENT->value
  1008. );
  1009. } catch (\Exception $e) {
  1010. Log::error('删除技师位置失败', [
  1011. 'coach_id' => $coach->id,
  1012. 'error' => $e->getMessage(),
  1013. ]);
  1014. // 不抛出异常,继续执行
  1015. }
  1016. }
  1017. /**
  1018. * 清理技师相关缓存
  1019. *
  1020. * @param int $coachId 技师ID
  1021. * @param string|null $date 服务日期
  1022. */
  1023. private function clearCoachCache(int $coachId, ?string $date = null): void
  1024. {
  1025. try {
  1026. // 清理技师时间段缓存
  1027. $date = $date ?: now()->toDateString();
  1028. $cacheKey = "coach:timeslots:{$coachId}:{$date}";
  1029. Cache::forget($cacheKey);
  1030. Log::info('成功清理技师缓存', [
  1031. 'coach_id' => $coachId,
  1032. 'date' => $date,
  1033. ]);
  1034. } catch (\Exception $e) {
  1035. Log::error('清理技师缓存失败', [
  1036. 'coach_id' => $coachId,
  1037. 'date' => $date,
  1038. 'error' => $e->getMessage(),
  1039. ]);
  1040. // 缓存清理失败不影响主流程
  1041. }
  1042. }
  1043. /**
  1044. * 获取设置项
  1045. *
  1046. * @param string $groupCode 设置组编码
  1047. * @param string $itemCode 设置项编码
  1048. * @return object 设置项对象
  1049. * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  1050. */
  1051. private function getSettingItem(string $groupCode, string $itemCode): object
  1052. {
  1053. // 获取设置组
  1054. $settingGroup = SettingGroup::where('code', $groupCode)->first();
  1055. abort_if(!$settingGroup, 404, "设置组[{$groupCode}]不存在");
  1056. // 获取设置项
  1057. $settingItem = $settingGroup->items()->where('code', $itemCode)->first();
  1058. abort_if(!$settingItem, 404, "设置项[{$itemCode}]不存在");
  1059. return $settingItem;
  1060. }
  1061. /**
  1062. * 更新订单设置
  1063. *
  1064. * 业务流程:
  1065. * 1. 获取订单配置项
  1066. * 2. 验证服务距离范围
  1067. * 3. 更新技师设置
  1068. * 4. 返回最新设置
  1069. *
  1070. * @param CoachUser $coach 技师对象
  1071. * @param array $data 设置数据,包含:
  1072. * - distance: float 服务距离(公里)
  1073. * @return array 返回最新设置信息,包含:
  1074. * - distance: float 当前服务距离
  1075. * - distance_max: float 最大服务距离限制
  1076. * - distance_min: float 最小服务距离限制
  1077. * @throws \Exception 当更新失败时抛出异常
  1078. */
  1079. public function setOrder(CoachUser $coach, array $data): array
  1080. {
  1081. return DB::transaction(function () use ($coach, $data) {
  1082. // 获取订单距离配置项
  1083. $distanceItem = $this->getSettingItem('order', 'distance');
  1084. // 验证服务距离范围
  1085. $distance = (float)$data['distance'];
  1086. abort_if(
  1087. $distance > $distanceItem->max_value || $distance < $distanceItem->min_value,
  1088. 422,
  1089. sprintf('服务距离必须在 %.1f-%.1f 公里之间', $distanceItem->min_value, $distanceItem->max_value)
  1090. );
  1091. // 更新技师服务距离设置
  1092. $coach->settingValues()->updateOrCreate(
  1093. [
  1094. 'item_id' => $distanceItem->id,
  1095. 'object_type' => $coach::class,
  1096. 'object_id' => $coach->id,
  1097. ],
  1098. ['value' => $distance]
  1099. );
  1100. // 返回最新设置
  1101. return [
  1102. 'distance' => $distance,
  1103. 'distance_max' => (float)$distanceItem->max_value,
  1104. 'distance_min' => (float)$distanceItem->min_value,
  1105. ];
  1106. });
  1107. }
  1108. /**
  1109. * 获取订单设置
  1110. *
  1111. * 业务流程:
  1112. * 1. 获取技师的订单设置
  1113. * 2. 获取系统配置的限制值
  1114. * 3. 返回设置信息
  1115. *
  1116. * @param CoachUser $coach 技师对象
  1117. * @return array 返回设置信息,包含:
  1118. * - distance: float 当前服务距离
  1119. * - distance_max: float 最大服务距离
  1120. * - distance_min: float 最小服务距离
  1121. * @throws \Exception 当获取失败时抛出异常
  1122. */
  1123. public function getOrderSettings(CoachUser $coach): array
  1124. {
  1125. // 获取订单距离配置项
  1126. $distanceItem = $this->getSettingItem('order', 'distance');
  1127. // 获取技师的距离设置
  1128. $distanceSetting = $coach->settingValues()
  1129. ->where('item_id', $distanceItem->id)
  1130. ->first();
  1131. // 返回设置信息
  1132. return [
  1133. 'distance' => (float)($distanceSetting?->value ?? $distanceItem->default_value), // 当前服务距离
  1134. 'distance_max' => (float)$distanceItem->max_value, // 最大服务距离限制
  1135. 'distance_min' => (float)$distanceItem->min_value, // 最小服务距离限制
  1136. ];
  1137. }
  1138. /**
  1139. * 格式化订单列表项
  1140. */
  1141. private function formatOrderListItem($order): array
  1142. {
  1143. return [
  1144. 'id' => $order->id, // 订单ID
  1145. 'order_no' => $order->order_no, // 订单编号
  1146. 'state' => $order->state, // 订单状态值
  1147. 'state_text' => OrderStatus::fromValue($order->state)?->label(), // 订单状态文本
  1148. 'payment_type' => $order->payment_type, // 支付类型值
  1149. 'payment_type_text' => PaymentMethod::fromValue($order->payment_type)?->label(), // 支付类型文本
  1150. 'total_amount' => $order->total_amount, // 订单总金额
  1151. 'project_amount' => $order->project_amount, // 项目金额
  1152. 'traffic_amount' => $order->traffic_amount, // 交通费金额
  1153. 'discount_amount' => $order->discount_amount, // 优惠金额
  1154. 'pay_amount' => $order->pay_amount, // 实付金额
  1155. 'refund_amount' => $order->refund_amount, // 退款金额
  1156. 'type' => $order->type, // 订单类型值
  1157. 'type_text' => OrderType::fromValue($order->type)?->label(), // 订单类型文本
  1158. 'source' => $order->source, // 订单来源值
  1159. 'source_text' => OrderSource::fromValue($order->source)?->label(), // 订单来源文本
  1160. 'distance' => $order->distance, // 目的地距离(米)
  1161. 'service_time' => $order->service_time instanceof \Carbon\Carbon // 预约服务时间
  1162. ? $order->service_time->toDateTimeString()
  1163. : $order->service_time,
  1164. 'accept_time' => $order->accept_time instanceof \Carbon\Carbon // 接单时间
  1165. ? $order->accept_time->toDateTimeString()
  1166. : $order->accept_time,
  1167. 'service_start_time' => $order->service_start_time instanceof \Carbon\Carbon // 实际开始时间
  1168. ? $order->service_start_time->toDateTimeString()
  1169. : $order->service_start_time,
  1170. 'service_end_time' => $order->service_end_time instanceof \Carbon\Carbon // 实际结束时间
  1171. ? $order->service_end_time->toDateTimeString()
  1172. : $order->service_end_time,
  1173. 'created_at' => $order->created_at instanceof \Carbon\Carbon // 创建时间
  1174. ? $order->created_at->toDateTimeString()
  1175. : $order->created_at,
  1176. 'project' => [ // 项目信息
  1177. 'id' => $order->project->id, // 项目ID
  1178. 'title' => $order->project->title, // 项目标题
  1179. 'cover' => $order->project->cover, // 项目封面图
  1180. 'duration' => $order->project->duration // 项目时长(分钟)
  1181. ],
  1182. 'user' => [ // 用户信息
  1183. 'id' => $order->user->id, // 用户ID
  1184. 'nickname' => $order->user->nickname, // 用户昵称
  1185. 'avatar' => $order->user->avatar, // 用户头像
  1186. 'mobile' => $order->user->mobile, // 用户手机号
  1187. ],
  1188. 'address' => [ // 服务地址信息
  1189. 'location' => $order->location, // 定位地址
  1190. 'address' => $order->address, // 详细地址
  1191. 'latitude' => $order->latitude, // 纬度
  1192. 'longitude' => $order->longitude, // 经度
  1193. 'phone' => $order->dest_phone // 联系人电话
  1194. ],
  1195. ];
  1196. }
  1197. /**
  1198. * 技师删除订单
  1199. *
  1200. * 业务逻辑:
  1201. * 1. 验证订单是否存在且分配给当前技师
  1202. * 2. 验证订单状态是否允许删除(已完成、已取消等)
  1203. * 3. 更新订单的coach_deleted_at字段
  1204. * 4. 记录删除操作
  1205. *
  1206. * @param int $coachId 技师ID
  1207. * @param int $orderId 订单ID
  1208. * @return array
  1209. * @throws \Exception
  1210. */
  1211. public function deleteOrder(int $coachId, int $orderId): array
  1212. {
  1213. return DB::transaction(function () use ($coachId, $orderId) {
  1214. // 验证订单是否存在且分配给当前技师
  1215. $order = Order::where('id', $orderId)
  1216. ->where('coach_id', $coachId)
  1217. ->whereNull('coach_deleted_at') // 确保订单未被技师删除
  1218. ->first();
  1219. abort_if(!$order, 404, '订单不存在');
  1220. // 验证订单状态是否允许删除
  1221. // 允许删除已完成、已取消、已退款等状态的订单
  1222. $allowedStates = [
  1223. OrderStatus::COMPLETED->value, // 已完成
  1224. OrderStatus::CANCELLED->value, // 已取消
  1225. OrderStatus::REFUNDED->value, // 已退款
  1226. OrderStatus::REJECTED->value, // 已拒单
  1227. ];
  1228. abort_if(!in_array($order->state, $allowedStates), 422, '当前订单状态不允许删除');
  1229. // 标记订单为技师已删除
  1230. $order->update([
  1231. 'coach_deleted_at' => now()
  1232. ]);
  1233. // 创建订单记录
  1234. OrderRecord::create([
  1235. 'order_id' => $order->id,
  1236. 'object_id' => $coachId,
  1237. 'object_type' => CoachUser::class,
  1238. 'state' => OrderRecordStatus::DELETED->value,
  1239. 'remark' => '技师删除订单'
  1240. ]);
  1241. return [
  1242. 'message' => '订单删除成功'
  1243. ];
  1244. });
  1245. }
  1246. }