$mainId]); foreach($authPlatforms as $pt) { switch ($pt['platform']) { case 'shansong': $this->adapters['shansong'] = new ShansongAdapter($pt['accessToken'], $pt['shopId']); 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, $pt['shopId']); break; case 'dada': $this->adapters['dada'] = new DadaAdapter($pt['accessToken'], $pt['shopId'], $pt['merchantId']); //$this->adapters['dada']->setShopNo($pt['shopId']); //$this->adapters['dada']->setSourceId($pt['merchantId']); // 用 merchanId 来保存 sourceId break; case 'didi': $this->adapters['didi'] = new DidiAdapter($pt['accessToken'], $pt['mainId']); 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'], $authPlatform['shopId']); 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, $authPlatform['shopId']); break; case 'dada': $this->adapters['dada'] = new DadaAdapter($authPlatform['accessToken'], $authPlatform['shopId'], $authPlatform['merchantId']); //$this->adapters['dada']->setShopNo($authPlatform['shopId']); //$this->adapters['dada']->setSourceId($authPlatform['merchantId']); // 用 merchanId 来保存 sourceId break; case 'didi': $this->adapters['didi'] = new DidiAdapter($authPlatform['accessToken'], $authPlatform['mainId']); 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 validateOrder($order) { $errors = []; if(empty($order['address'])){ $errors[] = '客户地址缺失'; } if(empty($order['long']) || empty($order['lat'])){ $errors[] = '客户经纬度缺失'; } if(empty($order['customName'])){ $errors[] = '客户姓名缺失'; } if(empty($order['city'])){ $errors[] = '客户城市信息缺失'; } if(empty($order['customMobile']) && empty($order['receiveMobile']) && empty($order['bookMobile'])){ $errors[] = '客户联系电话缺失'; } if(!empty($errors)){ util::fail(implode(', ', $errors)); } return true; } /** * @param $order * @param string $orderType 订单类型:xhOrder 或 xhGhsOrder * @param $shopId * @param $params * @return mixed */ public function createOrder($order, $orderType, $shopId, $params) { $order = $order->toArray(); if(!in_array($orderType, ['xhOrder', 'xhGhsOrder'])){ util::fail('订单类型错误'); } $shop = ShopClass::getById($shopId, false, 'id,merchantName,shopName,fullAddress,lat,long,city,dist,floor,mobile,merchantName'); $adapter = $this->getAdapter(); $orderData = $adapter->formatOrderData($order, $orderType, $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) { $deliveryOrder = $this->getDeliveryOrder($orderId, 'shansong'); return $adapter->selectOrder($deliveryOrder['orderId'], $thirdOrderNo); } if ($adapter instanceof HuolalaAdapter) { $deliveryOrder = $this->getDeliveryOrder($orderId, 'huolala'); return $adapter->selectOrder($deliveryOrder['orderId']); } if ($adapter instanceof FengniaoAdapter) { $deliveryOrder = $this->getDeliveryOrder($orderId, 'fengniao'); //order_id 和 partner_order_code 必填一个 return $adapter->selectOrder(['order_id'=>$deliveryOrder['orderId']]); } if ($adapter instanceof ShunfengAdapter) { $deliveryOrder = $this->getDeliveryOrder($orderId, 'shunfeng'); return $adapter->selectOrder($deliveryOrder['orderId']); } if ($adapter instanceof DadaAdapter) { $ghsOrder = OrderClass::getById($orderId, false, 'id,orderSn'); return $adapter->selectOrder($ghsOrder['orderSn']); } if ($adapter instanceof DidiAdapter) { $deliveryOrder = $this->getDeliveryOrder($orderId, 'didi', 'id,orderId,createdAt'); $timeStamp = strtoTime($deliveryOrder['createdAt']); return $adapter->selectOrder($deliveryOrder['orderId'], $thirdOrderNo, $timeStamp); } return $adapter->selectOrder($orderId); } /** * 根据订单号查找配送订单 * * @param string $orderId * @return object|null */ private function getDeliveryOrder($orderId, $deliveryId, $fields=null) { $fields = $fields ?? 'id,orderId'; // 查找批发配送订单 $deliveryOrder = GhsDeliveryOrderClass::getByCondition([ 'deliveryId' => $deliveryId, 'ghsOrderId' => $orderId, 'orderStatus' => ['not in', [-1, -2]], ], false, null, $fields); if ($deliveryOrder) { return $deliveryOrder; } // 查找零售配送订单 $deliveryOrder = HdDeliveryOrderClass::getByCondition([ 'deliveryId' => $deliveryId, 'hdOrderId' => $orderId, 'orderStatus' => ['not in', [-1, -2]], ], false, null, $fields); if ($deliveryOrder) { return $deliveryOrder; } util::fail('未找到对应的配送订单: ' . $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 店铺信息 * @param string $pickupTime 取件时间 * @return array 返回所有平台的报价结果 */ public function getAllPlatformPrice($order, $shop, $pickupTime) { // 第一步:准备前置数据(同步进行,因为某些平台需要这些数据) $preparedData = $this->preparePlatformData($order, $shop, $pickupTime); if (empty($preparedData)) { return ['error' => '所有平台数据准备失败']; } // 第二步:并发调用各平台的报价接口 $results = $this->concurrentGetPrices($preparedData, $pickupTime); if (empty($results['success']) && empty($results['failed'])) { return ['error' => '没有可用的物流平台']; } // 格式化返回结果 return [ 'quotes' => $results['success'] ?? [], 'failed' => $results['failed'] ?? [], ]; } /** * 为各平台准备订单数据(前置数据同步获取) * @param array $order 订单信息 * @param array $shop 店铺信息 * @param string $pickupTime 取件时间 * @return array 各平台的订单数据 */ private function preparePlatformData($order, $shop, $pickupTime) { $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, $pickupTime); } catch (\Exception $e) { Yii::warning("Shansong 数据准备失败: {$e->getMessage()}"); } } // 准备顺丰订单数据 if (isset($this->adapters['shunfeng'])) { try { /** @var ShunfengAdapter $adapter */ $adapter = $this->adapters['shunfeng']; // is_person_direct -- 店铺是否支持专人直送,0不支持 1支持 2只能发独享专送 $isPersonDirect = $adapter->getIsPersonDirect(); if($isPersonDirect != 2){ $customSettings = [];//自定义用户设置:默认不专人直送 $preparedData['shunfeng'] = $this->prepareShunfengData($order, $shop, $customSettings); } //添加专人直送的订单数据 if(in_array($isPersonDirect, [1, 2])){ // 支持专人直送 $customSettings = ['isPersonDirect' => 1];//自定义用户设置:专人直送 $preparedData['shunfeng_1v1'] = $this->prepareShunfengData($order, $shop, $customSettings); // Yii::info('预创建订单 shunfeng_1v1 request data: ' . json_encode($preparedData['shunfeng_1v1'])); } // else{ // $customSettings = [];//自定义用户设置:默认不专人直送 // $preparedData['shunfeng'] = $this->prepareShunfengData($order, $shop, $customSettings); // } } catch (\Exception $e) { Yii::warning("Shunfeng 数据准备失败: {$e->getMessage()}"); } } // 准备达达订单数据 if (isset($this->adapters['dada'])) { try { $dadaAdapter = $this->adapters['dada']; $shopNo = $dadaAdapter->getShopNo(); // 拼送 $customSettings = ['is_direct_delivery' => 0]; $preparedData['dada'] = $this->prepareDadaData($order, $shopNo, $customSettings); // 专送 //$customSettings = ['is_direct_delivery' => 1]; //$preparedData['dada_1v1'] = $this->prepareDadaData($order, $shopNo, $customSettings); } catch (\Exception $e) { Yii::warning("Dada 数据准备失败: {$e->getMessage()}"); } } // 准备滴滴订单数据 if (isset($this->adapters['didi'])) { try { $preparedData['didi'] = $this->prepareDidiData($order, $shop); } catch (\Exception $e) { Yii::warning("Didi 数据准备失败: {$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' => Functions::getMobile($order), 'lat_lon' => ['lat' => (float)$order['lat'], 'lon' => (float)$order['long']], ] ], 'vehicle_std' => $vehicleStd, 'spec_req' => array_values($specReq), 'coupon_id' => 123456, // TODO ??? 添加自动获取 coupon_id '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' => Functions::getMobile($order), 'receiver_name' => $order['customName'], 'receiver_latitude' => (float)$order['lat'], 'receiver_longitude' => (float)$order['long'], 'receiver_address' => $order['fullAddress'], 'position_source' => 3, //坐标经纬度来源(1:腾讯地图,2:百度地图, 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, $pickupTime) { $data = [ 'platform' => 'shansong', 'cityName' => $shop['city'], 'sender' => [ 'fromAddress' => $shop['address'], 'fromAddressDetail' => $shop['floor'], 'fromSenderName' => $shop['shopName'] == '首店' ? $shop['merchantName'] : $shop['merchantName'].'-'.$shop['shopName'], 'fromMobile' => $shop['mobile'], 'fromLatitude' => (string)$shop['lat'], 'fromLongitude' => (string)$shop['long'], ], 'receiverList' => [ [ 'orderNo' => $order['orderSn'], 'toAddress' => $order['address'], 'toAddressDetail' => $order['floor'], 'toReceiverName' => $order['customName'], 'toMobile' => Functions::getMobile($order), 'toLatitude' => (string)$order['lat'], 'toLongitude' => (string)$order['long'], 'goodType' => 7, //鲜花 'weight' => (int)round($order['weight']), 'remarks' => $order['remark'] ?? '', ] ], 'appointType' => 0, //预约类型: 0立即单,1预约单 'travelWay' => 0, 'deliveryType' => 1, //补充额外的订单数据 'sendNum' => $order['sendNum'] ?? '', ]; $timestamp = strtotime($pickupTime); $currentTime = time() + 3600; if($timestamp > $currentTime){ $data['appointType'] = 1; // 修改成 1预约单 $data['appointmentDate'] = $pickupTime; // 预约时间, yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后 //两天以内 if($timestamp > time() + 172800){ throw new \Exception('取件时间不能超过两天'); } // 'expectStartTime' 期望送达时间起始时间 -- 毫秒级时间戳。期望送达时间为时间点时,无需传本参数;期望送达时间为时间段时,期望送达时间为expectStartTime至expectEndTime // 'expectEndTime' 期望送达时间终止时间 -- 毫秒级时间戳。期望送达时间为时间点时,只需传本参数;期望送达时间为时间段时,期望送达时间为expectStartTime至expectEndTime $data['receiverList'][0]['expectStartTime'] = ($timestamp + 900) * 1000; $data['receiverList'][0]['expectEndTime'] = ($timestamp + 1800) * 1000; } return $data; } /** * 准备顺丰订单数据 */ private function prepareShunfengData($order, $shop, $customSettings) { // 获取订单商品信息 // $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' => 14, // 物品类型(14:鲜花) 'push_time' => time(), // 推单时间(秒级时间戳) //shop 平台级开发者需要传入 // 'shop' => [ // 发货店铺信息 // 'shop_name' => $shop['merchantName'], // 店铺名称 // 'shop_address' => $shop['fullAddress'], // 店铺地址 // '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, // 预约单的时候传入,1:预约送达单;2:预约上门单 //'expect_pickup_time' => 0, // TODO 用户期望上门时间 -- appoint_type=2时需必传,秒级时间戳 //'expect_time' => 0, // TODO 用户期望送达时间 //'shop_expect_time' => 0, // TODO 商家期望送达时间 -- 格式为:{timestamp}, 'lbs_type' => 2, // 坐标类型(2:高德坐标) 'is_insured' => 0, // 是否保价(0:非保价) 'is_person_direct' => $customSettings['isPersonDirect'] ?? 0, // 是否专人直送(0:否 1:是) 'vehicle' => 0, // 配送交通工具,0:否;1:电动车;2:小轿车 //'four_wheeler_type' => 0, // 车型 -- 只有vehicle = 2,⻋型字段才有效。 //'declared_value' => 0, // 保价金额(单位:分) //'gratuity_fee' => 0, // 订单小费(单位:分) 'rider_pick_method' => 1, // 物流流向(1:从门店取件送至用户) 'return_flag' => 511, // 返回字段控制标志位(511:全部返回) 'multi_pickup_info' => [], // 多点取货信息(暂不使用) ]; } /** * 准备达达订单数据 */ private function prepareDadaData($order, $shopNo, $customSettings) { $productList = []; $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num, bigNum, smallNum, xhUnitName'); foreach ($itemInfos as $item) { $productList[] = [ 'sku_name' => $item['name'], 'src_product_no' => (string)$item['id'], 'count' => (float)$item['num'], 'unit' => $item['xhUnitName'], ]; } // 回调地址 $callbackUrl = Yii::$app->params['ghsHost'] . '/delivery/dada-callback'; $request = [ 'platform' => 'dada', 'shop_no' => $shopNo, 'origin_id' => $order['orderSn'], 'cargo_price' => (float)$order['actPrice'], 'is_prepay' => 0, // 是否需要垫付 1:是 0:否 (垫付订单金额,非运费) 'receiver_name' => $order['customName'], 'receiver_address' => $order['fullAddress'], 'receiver_lat' => (float)$order['lat'], 'receiver_lng' => (float)$order['long'], 'callback' => $callbackUrl, 'cargo_weight' => isset($order['weight']) ? (float)$order['weight'] : 1.0, 'receiver_phone' => Functions::getMobile($order), //'tips' => 0, //小费(单位:元,精确小数点后一位,小费金额不能高于订单金额。) 'info' => $order['remark'] ?? '', 'product_list' => $productList, ]; if(isset($customSettings['is_direct_delivery']) && $customSettings['is_direct_delivery'] == 1){ // $request['origin_id'] = $order['orderSn'] . '_1v1'; // 根据达达的情况,必然会造成订单号出错 $request['is_direct_delivery'] = 1; //是否选择直拿直送(0:不需要;1:需要。选择直拿直送后,同一时间骑士只能配送此订单至完成,同时,也会相应的增加配送费用) } return $request; } /** * 准备滴滴订单数据 */ private function prepareDidiData($order, $shop) { return [ 'platform' => 'didi', 'sender' => [ 'fromSenderName' => $shop['merchantName'], 'fromMobile' => $shop['mobile'], 'fromAddress' => $shop['address'], 'fromAddressDetail' => $shop['floor'], 'fromLatitude' => (float)$shop['lat'], 'fromLongitude' => (float)$shop['long'], ], 'receiverList' => [ [ 'toReceiverName' => $order['customName'], 'toMobile' => Functions::getMobile($order), 'toAddress' => $order['address'], 'toAddressDetail' => $order['floor'], 'toLatitude' => (float)$order['lat'], 'toLongitude' => (float)$order['long'], 'weight' => isset($order['weight']) ? (float)$order['weight'] : 1.0, 'goodType' => 8, ] ] ]; } /** * 并发获取各平台报价(核心实现) * * 使用 HttpClient::postConcurrent 实现真正的并发调用,每个平台请求 10 秒超时。 * 某个平台的超时或失败不会影响其他平台的执行。 * * @param array $preparedData 准备好的各平台数据 * @param string $orderTime 配送时间 * @return array 包含成功和失败结果 */ private function concurrentGetPrices($preparedData, $orderTime) { $results = [ 'success' => [], 'failed' => [], ]; // 第一步:收集所有平台的请求信息 $allRequests = []; $platformMapping = []; // 用于将请求标识映射回平台名称 foreach ($preparedData as $platform => $data) { try { $adapterKey = $platform; //由于要支持特殊逻辑处理,把 $this->adapters[xxx] 的 xxx 单独取变量 if (!isset($this->adapters[$platform])) { if (substr($platform, -4) === '_1v1') { //特殊处理 -- 专人直送的订单,使用对应平台适配器 $adapterKey = substr($platform, 0, -4); } else { $results['failed'][$platform] = '平台适配器未初始化'; Yii::warning("[DispatchService] {$platform} 平台适配器未初始化"); continue; } } $adapter = $this->adapters[$adapterKey]; // 根据平台类型调用相应的 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 { $adapterKey = $platform; //由于要支持特殊逻辑处理,把 $this->adapters[xxx] 的 xxx 单独取变量 if (substr($platform, -4) === '_1v1') { //特殊处理 -- 专人直送的订单,使用对应平台适配器 $adapterKey = substr($platform, 0, -4); } $adapter = $this->adapters[$adapterKey]; $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 { $errorMsg = $quote['error'] ?? '报价返回数据为空'; $results['failed'][$platform] = $errorMsg; Yii::warning("[DispatchService] {$platform} 报价返回错误: {$errorMsg}"); } } 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}"); if(strncmp($platform, 'shunfeng', 8) === 0) { /** @var ShunfengAdapter $adapter */ if($errorMsg == '暂未开启专人直送服务') { $adapter->getShopInfo(); } if($errorMsg == '当前店铺只能发独享专送') { $adapter->getShopInfo(); } } } } } catch (\Throwable $e) { $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 $platformKey => $item) { if(!isset($item['platform'])){ Yii::error(json_encode($item)); continue; } switch ($item['platform']) { case 'shunfeng': $deliveryList[] = [ 'name' => '顺丰' . ($platformKey == 'shunfeng_1v1' ? ' (专送)' : ''), 'en_name' => 'shunfeng', 'price' => $item['real_pay_money'], 'distance' => $item['distance'], 'type' => '' . ($platformKey == 'shunfeng_1v1' ? '专人直送' : ''), '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'] . ')', 'base_goods_id' => $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'] . ')', 'base_goods_id' => $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 $key => $arr) { $priceInfo = $arr['calculate_price_info']; $priceConditions = $priceInfo['price_conditions'] ?? []; //价格明细 $distanceInfo = $priceInfo['distance_info'] ?? []; //距离信息 $vehicleType = $arr['vehicle_type']; if(empty($priceConditions)){ Yii::error('货拉拉 报价数据 priceConditions 为空:' . json_encode($item) . ' key: ' . $key); continue; } if(empty($distanceInfo)){ Yii::warning('货拉拉 报价数据 distanceInfo 为空:' . json_encode($item) . ' key: ' . $key); } $priceCalculateId = $priceInfo['price_calculate_id'] ?? 0; if(!isset($priceInfo['price_calculate_id'])){ Yii::warning('货拉拉 报价数据 priceInfo 缺少 price_calculate_id:' . json_encode($priceInfo) . ' key: ' . $key); } //车型信息 $vehicleAttr = $priceInfo['vehicle_info']['vehicle_attr'] ?? []; if(empty($vehicleAttr)){ Yii::info('货拉拉 报价数据 vehicleAttr 为空:' . ' key: ' . $key); //json_encode($priceInfo) . } $deliveryList[] = [ 'name' => '货拉拉' . ' (' . $vehicleType['_meta']['vehicle_type'] . ')', 'vehicle_type' => $vehicleType['_meta']['vehicle_type'], 'en_name' => 'huolala', 'price' => !empty($priceConditions) ? $priceConditions[0]['price_info']['total_price'] : 0, 'distance' => !empty($distanceInfo) ? $distanceInfo['distance_total'] : 0, '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' => $priceCalculateId, 'price_item_encryption' => !empty($priceConditions) ? $priceConditions[0]['price_item_encryption'] : '', 'vehicle_attr' => $vehicleAttr, 'commodity_info' => !empty($priceConditions) ? $priceConditions[0]['commodity_item'] : [], ]; } break; case 'dada': $list = [ 'name' => '达达'. ($platformKey == 'dada_1v1' ? ' (专送)' : ''), 'en_name' => 'dada', 'price' => $item['fee'], 'distance' => $item['distance'], 'type' => '', 'isAble' => true, // --------------------------------------------- 'deliverFee' => $item['deliverFee'], 'deliveryNo' => $item['deliveryNo'], ]; if(isset($item['couponName']) && isset($item['couponFee'])){ $list['couponName'] = $item['couponName']; $list['couponFee'] = $item['couponFee']; } $deliveryList[] = $list; break; case 'didi': if(!isset($item['estimateList'])){ Yii::error('didi 报价数据为空:' . json_encode($item)); continue; } $serviceType = [0 => '拼送', 1 => '直送']; foreach($item['estimateList'] as $arr) { $serType = $arr['serviceType'] ?? null; $type = empty($serType) ? '' : $serviceType[$serType]; //服务类型 $deliveryList[] = [ 'name' => '滴滴' . ' (' . $this->adapters['didi']->carType($arr['carType']) . $type . ')', 'en_name' => 'didi', 'price' => $arr['fee'], 'distance' => $arr['distance'], 'type' => $type, 'isAble' => true, // --------------------------------------------- 'estimateId' => $arr['estimateId'], 'bizType' => $arr['bizType'], 'discountFee' => $arr['discountFee'], 'icon' => $arr['icon'] ]; } break; } } return $deliveryList; } }