$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']); // 动态设置商户ID与门店ID $adapter->setMerchantId($pt['merchantId']); $adapter->setShopId($pt['shopId']); $this->adapters['fengniao'] = $adapter; break; case 'shunfeng': $this->adapters['shunfeng'] = new ShunfengAdapter($pt['accessToken'], $pt['shopId']); break; } //检测 token 是否即将过期 $tokenExpireTime = $pt['expiresAt']; if ($tokenExpireTime < time() + 86400 * 5) { Yii::info('刷新 token: ' . $pt['platform'] . ',refreshToken: ' . $pt['refreshToken'] . ',merchantId: ' . $pt['merchantId']); $this->refreshToken($pt['platform'], $pt['refreshToken'], ['merchantId' => $pt['merchantId']]); } } } 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']); // 动态设置商户ID与门店ID $adapter->setMerchantId($authPlatform['merchantId']); $adapter->setShopId($authPlatform['shopId']); $this->adapters['fengniao'] = $adapter; break; case 'shunfeng': $this->adapters['shunfeng'] = new ShunfengAdapter($authPlatform['accessToken'], $authPlatform['shopId']); break; } $this->platformName = $platform; if($authPlatform['expiresAt'] < time() + 86400 * 5) { Yii::info('刷新 token: ' . $platform . ',refreshToken: ' . $authPlatform['refreshToken'] . ',merchantId: ' . $authPlatform['merchantId']); $this->refreshToken($platform, $authPlatform['refreshToken'], ['merchantId' => $authPlatform['merchantId']]); } } } /** * 发单调度 */ public function createOrder($order, $shopId, $params) { $order = $order->toArray(); $shop = ShopClass::getById($shopId, false, 'id,merchantName,fullAddress,lat,long,city,dist,floor,mobile,merchantName'); $adapter = $this->getAdapter(); $orderData = $adapter->formatOrderData($order, $shop, $params); return $adapter->createOrder($orderData); } /** * */ public function addTip($data) { $adapter = $this->getAdapter(); return $adapter->addTip($data); } /** * @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); 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 { /** @var FengniaoAdapter $fengniaoAdapter */ $fengniaoAdapter = $this->adapters['fengniao']; $platformShopId = $fengniaoAdapter->getShopId(); $preparedData['fengniao'] = $this->prepareFengniaoData($order, $shop, $platformShopId); } 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()}"); } } // 准备顺丰订单数据 if (isset($this->adapters['shunfeng'])) { try { $preparedData['shunfeng'] = $this->prepareShunfengData($order, $shop); } catch (\Exception $e) { Yii::warning("Shunfeng 数据准备失败: {$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); // 产生外部请求 $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) { if(!in_array($vehicle['vehicle_name'], ['跑腿', '小面', '微面'])){ // && !$this->adapters['huolala']->getIsSandbox() continue; } $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, // TODO ??? '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, $platformShopId) { // $this->adapters['fengniao']->setMerchantId(14594092); // adapter 创建时已经设置了 $goodsItemList = []; $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num'); 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['actPrice'] * 100), 'goods_actual_amount_cent' => (int)($order['prePrice'] * 100), 'goods_item_list' => $goodsItemList, 'order_type' => 1, 'chain_store_id' => $platformShopId, // 蜂鸟平台 shopId 'order_remark' => $order['remark'] ?? '', ]; } /** * 准备闪送订单数据 */ private function prepareShansongData($order, $shop) { return [ 'platform' => 'shansong', 'cityName' => $shop['city'], 'sender' => [ 'fromAddress' => $shop['address'], 'fromAddressDetail' => $shop['floor'], 'fromSenderName' => $shop['shopName'], 'fromMobile' => $shop['mobile'], 'fromLatitude' => (float)$shop['lat'], 'fromLongitude' => (float)$shop['long'], ], 'receiverList' => [ [ 'orderNo' => $order['orderSn'], 'toAddress' => $order['address'], 'toAddressDetail' => $order['floor'], 'toReceiverName' => $order['customName'], 'toMobile' => $order['customMobile'], 'toLatitude' => (float)$order['lat'], 'toLongitude' => (float)$order['long'], 'goodType' => 7, 'weight' => (int)round($order['weight']), 'remarks' => $order['remark'] ?? '', ] ], 'appointType' => 0, 'appointmentDate' => '', 'travelWay' => 0, 'deliveryType' => 1, 'expectStartTime' => null, 'expectEndTime' => null, ]; } /** * 准备顺丰订单数据 */ private function prepareShunfengData($order, $shop) { // 获取订单商品信息 // $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num, unitWeight'); // 计算商品总重量(单位:克) // $totalWeight = 0; // foreach ($itemInfos as $item) { // $totalWeight += (int)($item['unitWeight'] * $item['num']) * 1000; // } // 如果没有商品重量,使用订单重量 // if ($totalWeight == 0 && isset($order['weight'])) { // $totalWeight = (int)$order['weight'] * 1000; // } // 默认重量为1000克(1公斤) // if ($totalWeight == 0) { // $totalWeight = 1000; // } return [ 'platform' => 'shunfeng', 'user_lng' => (string)$order['long'], // 用户地址经度 'user_lat' => (string)$order['lat'], // 用户地址纬度 'user_address' => $order['fullAddress'], // 用户详细地址 'weight' => $order['weight'] * 1000, // 物品重量(单位:克) 'product_type' => 4, // 物品类型(4:鲜花绿植) 'push_time' => time(), // 推单时间(秒级时间戳) 'shop' => [ // 发货店铺信息 'shop_name' => $shop['merchantName'], // 店铺名称 'shop_address' => $shop['address'], // 店铺地址 'shop_lng' => (string)$shop['long'], // 店铺经度 'shop_lat' => (string)$shop['lat'], // 店铺纬度 'shop_phone' => $shop['mobile'], // 店铺联系电话 ], //-------------------------- 非必填 ------------------------------- 'city_name' => $order['city'], // 发单城市 //'order_source' => '6', // 订单接入来源(6:京东秒送,可自定义) 'source_origin_order_id' => $order['orderSn'], // 商流订单号 'total_price' => (int)($order['actPrice'] * 100), // 用户订单总金额(单位:分) 'is_appoint' => 0, // 是否预约单(0:非预约单) 'appoint_type' => 0, // TODO 预约单类型(0:立即单) 'expect_time' => 0, // TODO 用户期望送达时间 //'expect_pickup_time' => 0, // TODO 用户期望上门时间 -- appoint_type=2时需必传,秒级时间戳 'shop_expect_time' => 0, // TODO 商家期望送达时间 -- 格式为:{timestamp}, 'lbs_type' => 2, // 坐标类型(2:高德坐标) 'is_insured' => 0, // 是否保价(0:非保价) 'is_person_direct' => 0, // 是否专人直送(0:否) 'vehicle' => 0, // 配送交通工具(0:否) //'four_wheeler_type' => 0, // 车型 -- 只有vehicle = 2,⻋型字段才有效。 'declared_value' => 0, // 保价金额(单位:分) 'gratuity_fee' => 0, // 订单小费(单位:分) 'rider_pick_method' => 1, // 物流流向(1:从门店取件送至用户) 'return_flag' => 511, // 返回字段控制标志位(511:全部返回) 'multi_pickup_info' => [], // 多点取货信息(暂不使用) ]; } /** * 并发获取各平台报价(核心实现) * * 使用 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->buildPriceRequest($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] ]; Yii::info("[DispatchService] {$platform} 报价成功: " . json_encode($quote)); } } else { // 蜂鸟和闪送的响应处理 $quote = $adapter->processPriceResponse($response); if ($quote && !isset($quote['error'])) { $quote['_duration'] = $totalDuration; $results['success'][$platform] = $quote; Yii::info("[DispatchService] {$platform} 报价成功: " . json_encode($quote)); } 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(); } /** * 刷新 token */ public function refreshToken($platform, $refreshToken, $data=[]) { switch($platform) { case 'shansong': $auth = new \common\components\delivery\platform\shansong\Auth(); $auth->refreshAccessToken($refreshToken); break; case 'huolala': $auth = new \common\components\delivery\platform\huolala\Auth(); $auth->refreshAccessToken($refreshToken); break; case 'fengniao': $auth = new \common\components\delivery\platform\fengniao\Auth(); $auth->refreshAccessToken($refreshToken, $data['merchantId']); break; case 'shunfeng': $auth = new \common\components\delivery\platform\shunfeng\Auth(); $auth->refreshAccessToken($refreshToken); break; } } /** * 格式化各平台报价为前端展示格式 * * @param array $platformQuotes 各平台报价结果(来自 getAllPlatformPrice) * @return array 格式化后的配送列表 */ public function formatPlatformQuotesForDisplay($platformQuotes) { $deliveryList = []; if (empty($platformQuotes['quotes'])) { return $deliveryList; } foreach ($platformQuotes['quotes'] as $item) { switch ($item['platform']) { case 'shunfeng': $deliveryList[] = [ 'name' => '顺丰', 'en_name' => 'shunfeng', 'price' => $item['real_pay_money'], 'distance' => $item['distance'], 'type' => '', 'isAble' => true ]; break; case 'shansong': $deliveryList[] = [ 'name' => '闪送', 'en_name' => 'shansong', 'price' => $item['total_amount'], 'distance' => $item['total_distance'], 'type' => '', 'issOrderNo' => $item['order_number'], 'isAble' => true ]; break; case 'fengniao': // 循环遍历 goods_infos 数组,提取有效项的信息 $validGoods = []; if (!empty($item['goods_infos']) && is_array($item['goods_infos'])) { foreach ($item['goods_infos'] as $good) { if (!empty($good['is_valid']) && $good['is_valid'] == 1) { $validGoods[] = [ 'actual_delivery_amount_cent' => $good['actual_delivery_amount_cent'] ?? 0, 'service_goods_id' => $good['service_goods_id'] ?? '' ]; $deliveryList[] = [ 'name' => '蜂鸟'. ' (' .$good['base_goods_id'] . ')', 'en_name' => 'fengniao', 'price' => $good['actual_delivery_amount_cent'], 'distance' => $item['distance'], 'valid_goods' => $validGoods, 'type' => $good['slogan'], 'isAble' => true ]; } else { $deliveryList[] = [ 'name' => '蜂鸟'. ' (' .$good['base_goods_id'] . ')', 'en_name' => 'fengniao', 'price' => 0, 'distance' => $item['distance'], 'valid_goods' => $good, 'type' => $good['disable_reason'], 'isAble' => false ]; } } } break; case 'huolala': foreach($item['price_info_list'] as $arr) { $priceInfo = $arr['calculate_price_info']; $vehicleType = $arr['vehicle_type']; $deliveryList[] = [ 'name' => '货拉拉' . ' (' . $vehicleType['_meta']['vehicle_type'] . ')', 'en_name' => 'huolala', 'price' => $priceInfo['price_conditions'][0]['price_info']['total_price'], 'distance' => $priceInfo['distance_info']['distance_total'], 'type' => $vehicleType['_meta']['vehicle_type'], 'isAble' => true, // --------------------------------------------- 'order_vehicle_id' => $vehicleType['order_vehicle_id'], 'city_id' => $item['city_id'], 'city_info_revision' => $vehicleType['city_info_revision'], // 下单需要透传的数据 'vehicle_std' => $vehicleType['vehicle_std'], 'spec_req' => $vehicleType['spec_req'], 'price_calculate_id' => $priceInfo['price_calculate_id'], 'price_item_encryption' => $priceInfo['price_conditions'][0]['price_item_encryption'], 'vehicle_attr' => $priceInfo['vehicle_info']['vehicle_attr'], 'commodity_info' => $priceInfo['price_conditions'][0]['commodity_item'], ]; } break; } } return $deliveryList; } }