$mainId]); foreach($authPlatforms as $pt) { switch ($pt['platform']) { case 'shansong': $this->adapters['shansong'] = new ShansongAdapter($pt['accessToken']); break; case 'huolala': $this->adapters['huolala'] = new HuolalaAdapter($pt['accessToken']); break; case 'fengniao': $adapter = new FengniaoAdapter($pt['accessToken']); $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID $this->adapters['fengniao'] = $adapter; break; } } } else { $authPlatform = DeliveryAuthTokenClass::getByCondition(['mainId'=>$mainId, 'platform'=>$platform]); switch ($platform) { case 'shansong': $this->adapters['shansong'] = new ShansongAdapter($authPlatform['accessToken']); break; case 'huolala': $this->adapters['huolala'] = new HuolalaAdapter($authPlatform['accessToken']); break; case 'fengniao': //$this->adapters['fengniao'] = new FengniaoAdapter($authPlatform['accessToken']); $adapter = new FengniaoAdapter($authPlatform['accessToken']); $adapter->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID $this->adapters['fengniao'] = $adapter; break; } $this->platformName = $platform; } //$this->adapters = [ //'meituan' => new MeituanAdapter(), //'dada' => new DadaAdapter(), //'sf' => new SFAdapter(), //'uu' => new UUAdapter(), //'fengniao' => new FengniaoAdapter(), //]; } /** * 发单调度 */ public function createOrder($order, $shopId, $params) { $order = $order->toArray(); $shop = ShopClass::getById($shopId); $adapter = $this->getAdapter(); $orderData = $adapter->formatOrderData($order, $shop, $params); return $adapter->createOrder($orderData); } /** * @param $orderId 平台订单号 * @param string $thirdOrderNo xhGhsOrder.orderSn */ public function selectOrder($orderId, $thirdOrderNo='') { $adapter = $this->getAdapter(); if ($adapter instanceof ShansongAdapter) { return $adapter->selectOrder($orderId, $thirdOrderNo); } return $adapter->selectOrder($orderId); } public function cancelOrder($orderId, $reason) { $adapter = $this->getAdapter(); return $adapter->cancelOrder($orderId, $reason); } public function getCancelReasonList($orderId) { $adapter = $this->getAdapter(); return $adapter->getCancelReasonList($orderId); } public function getAdapter() { return $this->adapters[$this->platformName]; } /** * 获取所有平台的最佳报价(使用 Guzzle 并发请求) * * 流程说明: * 1. 同步准备各平台的前置数据(如城市信息、订单商品等) * 2. 使用 Guzzle Pool 并发发送各平台的报价请求(5秒超时) * 3. 某个平台失败/超时不影响其他平台,继续等待结果 * * @param array $order 订单信息 * @param array $shop 店铺信息 * @return array 返回所有平台的报价结果 */ public function getAllPlatformPrice($order, $shop, $orderTime) { // 第一步:准备前置数据(同步进行,因为某些平台需要这些数据) $preparedData = $this->preparePlatformData($order, $shop, $orderTime); if (empty($preparedData)) { return ['error' => '所有平台数据准备失败']; } // 第二步:并发调用各平台的报价接口 $results = $this->concurrentGetPrices($preparedData, $orderTime); if (empty($results['success']) && empty($results['failed'])) { return ['error' => '没有可用的物流平台']; } // 格式化返回结果 return [ 'quotes' => array_values($results['success'] ?? []), 'failed' => $results['failed'] ?? [], ]; } /** * 为各平台准备订单数据(前置数据同步获取) * * @param array $order 订单信息 * @param array $shop 店铺信息 * @return array 各平台的订单数据 */ private function preparePlatformData($order, $shop) { $preparedData = []; // 准备 Huolala 数据 if (isset($this->adapters['huolala'])) { try { $preparedData['huolala'] = $this->prepareHuolalaData($order, $shop); } catch (\Exception $e) { Yii::warning("Huolala 数据准备失败: {$e->getMessage()}"); } } // 准备 Fengniao 数据 if (isset($this->adapters['fengniao'])) { try { $preparedData['fengniao'] = $this->prepareFengniaoData($order, $shop); } catch (\Exception $e) { Yii::warning("Fengniao 数据准备失败: {$e->getMessage()}"); } } // 准备 Shansong 数据 if (isset($this->adapters['shansong'])) { try { $preparedData['shansong'] = $this->prepareShansongData($order, $shop); } catch (\Exception $e) { Yii::warning("Shansong 数据准备失败: {$e->getMessage()}"); } } return $preparedData; } /** * 准备货拉拉订单数据 */ private function prepareHuolalaData($order, $shop) { $formatCity = rtrim($order['city'], '市'); $cities = include Yii::getAlias('@common/components/delivery/platform/huolala/cities.php'); if (!isset($cities[$formatCity])) { throw new \Exception('城市编码表中没有找到城市: ' . $formatCity); } $cityId = $cities[$formatCity]['city_id']; $cityVehicleList = $this->adapters['huolala']->getCityVehicleList($cityId); // 产生外部请求 -- TODO 优化成不耗时等待 $cityInfoRevision = $cityVehicleList['city_info_revision']; // 获取所有车型列表 $vehicleList = $cityVehicleList['vehicle_list']; if (empty($vehicleList)) { throw new \Exception('没有找到可选车型'); } // 解析额外需求 $specReqItem = $cityVehicleList['spec_req_item'] ?? []; $specReq = []; foreach ($specReqItem as $item) { $specReq[] = $item['type'] ?? null; } $specReq = array_filter($specReq); // 构建所有车型数据 $vehicleTypeList = []; foreach ($vehicleList as $vehicle) { $vehicleStd = []; $vehicleStd[] = count($vehicle['vehicle_std_item']) > 0 ? $vehicle['vehicle_std_item'][0]['name'] : ''; $vehicleTypeList[$vehicle['order_vehicle_id']] = [ 'order_vehicle_id' => $vehicle['order_vehicle_id'], 'city_info_revision' => $cityInfoRevision, 'order_time' => time() + 600, 'addr_info' => [ [ 'name' => $shop['merchantName'], 'addr' => $shop['province'] . $shop['city'] . $shop['dist'] . $shop['address'], 'city_id' => $cityId, 'city_name' => $shop['city'], 'district_name' => $shop['dist'], 'house_number' => $shop['floor'], 'contacts_name' => $shop['mobile'], 'contacts_phone_no' => $shop['mobile'], 'lat_lon' => ['lat' => (float)$shop['lat'], 'lon' => (float)$shop['long']], ], [ 'name' => $order['customName'], 'addr' => $order['fullAddress'], 'city_id' => $cityId, 'city_name' => $order['city'], 'district_name' => $order['dist'], 'house_number' => $order['floor'], 'contacts_name' => $order['customName'], 'contacts_phone_no' => $order['customMobile'], 'lat_lon' => ['lat' => (float)$order['lat'], 'lon' => (float)$order['long']], ] ], 'vehicle_std' => $vehicleStd, 'spec_req' => array_values($specReq), 'coupon_id' => 123456, 'invoice_type' => 1, 'order_service_type' => 1, '_meta' => [ 'vehicle_type' => $vehicle['vehicle_name'], 'city_info_revision' => $cityInfoRevision, ] ]; } return [ 'platform' => 'huolala', 'city_id' => $cityId, 'vehicle_type_list' => $vehicleTypeList, ]; } /** * 准备蜂鸟订单数据 */ private function prepareFengniaoData($order, $shop) { $this->adapters['fengniao']->setMerchantId(14594092); // TODO: 确认是否需要使用动态的商户ID $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num'); $goodsItemList = []; foreach ($itemInfos as $item) { $goodsItemList[] = [ 'item_actual_amount_cent' => (int)($item['unitPrice'] * 100 * $item['num']), 'item_amount_cent' => (int)($item['unitPrice'] * 100), 'item_id' => $item['id'], 'item_name' => $item['name'], 'item_quantity' => $item['num'], ]; } return [ 'platform' => 'fengniao', 'partner_order_code' => $order['orderSn'], 'receiver_primary_phone' => $order['customMobile'], 'receiver_name' => $order['customName'], 'receiver_latitude' => (float)$order['lat'], 'receiver_longitude' => (float)$order['long'], 'receiver_address' => $order['fullAddress'], 'position_source' => 3, 'goods_count' => count($goodsItemList), 'goods_weight' => (float)$order['weight'], 'goods_total_amount_cent' => (int)($order['prePrice'] * 100), 'goods_actual_amount_cent' => (int)($order['actPrice'] * 100), 'goods_item_list' => $goodsItemList, 'order_type' => 1, 'chain_store_id' => 467788524, 'order_remark' => $order['remark'] ?? '', ]; } /** * 准备闪送订单数据 */ private function prepareShansongData($order, $shop) { return [ 'platform' => 'shansong', 'city_name' => $shop['city'], 'sender' => [ 'from_address' => $shop['address'], 'from_address_detail' => $shop['floor'], 'from_sender_name' => $shop['shopName'], 'from_mobile' => $shop['mobile'], 'from_latitude' => (float)$shop['lat'], 'from_longitude' => (float)$shop['long'], ], 'receiver_list' => [ [ 'order_no' => $order['orderSn'], 'to_address' => $order['address'], 'to_address_detail' => $order['floor'], 'to_receiver_name' => $order['customName'], 'to_mobile' => $order['customMobile'], 'to_latitude' => (float)$order['lat'], 'to_longitude' => (float)$order['long'], 'good_type' => 7, 'weight' => (int)$order['weight'], 'remarks' => $order['remark'] ?? '', ] ], 'appoint_type' => 0, 'appointment_date' => '', 'travel_way' => 0, 'delivery_type' => 1, 'expect_start_time' => null, 'expect_end_time' => null, ]; } /** * 并发获取各平台报价(核心实现) * * 使用 HttpClient::postConcurrent 实现真正的并发调用,每个平台请求 5 秒超时。 * 某个平台的超时或失败不会影响其他平台的执行。 * * @param array $preparedData 准备好的各平台数据 * @param string $orderTime 配送时间 * @return array 包含成功和失败结果 */ private function concurrentGetPrices($preparedData, $orderTime) { $results = [ 'success' => [], 'failed' => [], ]; // 第一步:收集所有平台的请求信息 $allRequests = []; $platformMapping = []; // 用于将请求标识映射回平台名称 foreach ($preparedData as $platform => $data) { try { if (!isset($this->adapters[$platform])) { $results['failed'][$platform] = '平台适配器未初始化'; continue; } $adapter = $this->adapters[$platform]; // 根据平台类型调用相应的 buildPriceRequest(s) 方法 if ($platform === 'huolala') { // 货拉拉返回多个请求(一个请求对应一个车型) $priceRequests = $adapter->buildPriceRequests($data, $orderTime); foreach ($priceRequests as $requestKey => $requestInfo) { $allRequests[$requestKey] = $requestInfo; $platformMapping[$requestKey] = ['platform' => 'huolala', 'data' => $data]; } } else { // 其他平台(蜂鸟、闪送)各返回一个请求 $requestInfo = $adapter->buildPriceRequest($data, $orderTime); $requestKey = "{$platform}_quote"; $allRequests[$requestKey] = $requestInfo; $platformMapping[$requestKey] = ['platform' => $platform, 'data' => $data]; } } catch (\Throwable $e) { $results['failed'][$platform] = "构建请求失败: {$e->getMessage()}"; Yii::error("[DispatchService] {$platform} 构建请求异常: {$e->getMessage()}"); } } if (empty($allRequests)) { return $results; } // 第二步:使用 postConcurrent 并发发送所有请求 $startTime = microtime(true); $concurrentResults = HttpClient::postConcurrent($allRequests, 3); $totalDuration = microtime(true) - $startTime; Yii::info("[DispatchService] 所有报价请求完成 (" . round($totalDuration * 1000) . "ms)"); // 第三步:处理并发请求的结果 foreach ($concurrentResults['success'] as $requestKey => $response) { if (!isset($platformMapping[$requestKey])) { continue; } $mapping = $platformMapping[$requestKey]; $platform = $mapping['platform']; $data = $mapping['data']; try { $adapter = $this->adapters[$platform]; $quote = null; if ($platform === 'huolala') { // 货拉拉的响应处理 $quote = $adapter->processPriceResponse($response); if ($quote && !isset($results['success']['huolala'])) { // 第一次处理货拉拉的结果,初始化 $results['success']['huolala'] = [ 'platform' => 'huolala', 'city_id' => $data['city_id'], 'vehicle_type_list' => $data['vehicle_type_list'], 'price_info_list' => [], ]; } $keyArr = explode('_', $requestKey); $vehicle_type_list_key = $keyArr[2]; // 将该车型的报价加入到列表中 if ($quote && isset($results['success']['huolala'])) { $results['success']['huolala']['price_info_list'][] = [ 'calculate_price_info' => isset($quote['calculate_price_info_list']) ? $quote['calculate_price_info_list'][0] : [], 'vehicle_type' => $data['vehicle_type_list'][$vehicle_type_list_key] ]; } } else { // 蜂鸟和闪送的响应处理 $quote = $adapter->processPriceResponse($response); if ($quote && !isset($quote['error'])) { $quote['_duration'] = $totalDuration; $results['success'][$platform] = $quote; Yii::info("[DispatchService] {$platform} 报价成功 (" . round($totalDuration * 1000) . "ms)"); } else { $errorMsg = $quote['error'] ?? '报价返回数据为空'; $results['failed'][$platform] = $errorMsg; Yii::warning("[DispatchService] {$platform} 报价返回错误: {$errorMsg}"); } } } catch (\Throwable $e) { $platform = $mapping['platform']; $results['failed'][$platform] = "处理响应失败: {$e->getMessage()}"; Yii::error("[DispatchService] {$platform} 处理响应异常: {$e->getMessage()}"); } } // 第四步:处理失败的请求 foreach ($concurrentResults['failed'] as $requestKey => $errorMsg) { if (!isset($platformMapping[$requestKey])) { continue; } $platform = $platformMapping[$requestKey]['platform']; // 对于货拉拉,只记录某个车型失败,不影响整体平台状态 if ($platform === 'huolala') { Yii::warning("[DispatchService] huolala 车型报价失败 ({$requestKey}): {$errorMsg}"); } else { if (!isset($results['failed'][$platform])) { $results['failed'][$platform] = $errorMsg; Yii::error("[DispatchService] {$platform} 报价请求失败: {$errorMsg}"); } } } // 为货拉拉添加元数据 if (isset($results['success']['huolala'])) { $results['success']['huolala']['_duration'] = $totalDuration; } return $results; } public function openCitiesLists($platform) { $ap = $this->adapters[$platform]; return $ap->cityList(); } }